GCC Code Coverage Report


Directory: ./
File: src/GenericSubDevice.cpp
Date: 2026-09-23 16:22:15
Exec Total Coverage
Lines: 61 141 43.3%
Branches: 61 380 16.1%

Line Branch Exec Source
1 /*****************************************************************************
2 *
3 * This file is part of the ecapp library (EtherCAT application devices).
4 *
5 * Copyright (C) 2026 Florian Pose <fp@igh.de>
6 *
7 * The ecapp library is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public License
9 * as published by the Free Software Foundation, version 3 of the
10 * License.
11 *
12 * The ecapp library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with the ecapp library. If not, see
19 * <https://www.gnu.org/licenses/>.
20 *
21 ****************************************************************************/
22
23 #include "ecapp/GenericSubDevice.h"
24
25 #include "ecapp/Domain.h"
26 #include "ecapp/Master.h"
27 #include "ecapp/SwappedSync.h"
28
29 #include <cstring>
30 #include <memory>
31 #include <sstream>
32 #include <stdexcept>
33 #include <type_traits>
34 #include <variant>
35
36 /****************************************************************************/
37
38 using std::make_unique;
39 using std::runtime_error;
40 using std::size_t;
41 using std::stringstream;
42 using std::unique_ptr;
43
44 namespace EcApp {
45
46 namespace {
47
48 /** Bit length of a channel's type on the wire -- bool is packed into a
49 * single bit like every other digital signal in this library; every
50 * other alternative gets its natural memory width. Same helper as
51 * src/devices/EL6692.cpp's bitLenOf(); duplicated rather than shared
52 * since neither file is meant to depend on the other. */
53 ✗ unsigned int bitLenOf(const ChannelValue &v)
54 {
55 ✗ return std::visit(
56 ✗ [](auto &&value) -> unsigned int {
57 using T = std::decay_t<decltype(value)>;
58 if constexpr (std::is_same_v<T, bool>) {
59 ✗ return 1;
60 }
61 else {
62 ✗ return sizeof(T) * 8;
63 }
64 },
65 ✗ v);
66 }
67
68 /** Raw little-endian bytes of a ChannelValue's active alternative, for
69 * ecrt_slave_config_sdo() (see GenericSdo) -- bool is written as a
70 * single 0/1 byte (matching ecrt_slave_config_sdo8()'s uint8_t; plain
71 * `bool`'s own size is implementation-defined, so it is not memcpy'd
72 * directly), every other alternative as its natural in-memory
73 * representation, which is already little-endian on every platform this
74 * library targets (same assumption the EC_READ_.../EC_WRITE_... macros
75 * already make). */
76 ✗ std::vector<uint8_t> sdoBytes(const ChannelValue &v)
77 {
78 return std::visit(
79 ✗ [](auto &&value) -> std::vector<uint8_t> {
80 using T = std::decay_t<decltype(value)>;
81 if constexpr (std::is_same_v<T, bool>) {
82 ✗ return {static_cast<uint8_t>(value ? 1 : 0)};
83 }
84 else {
85 ✗ std::vector<uint8_t> bytes(sizeof(T));
86 ✗ std::memcpy(bytes.data(), &value, sizeof(T));
87 ✗ return bytes;
88 }
89 },
90 ✗ v);
91 }
92
93 /** Reads the domain data at (offset, bit) into a ChannelValue holding the
94 * same alternative as `prototype`. */
95 ChannelValue
96 ✗ readRaw(const uint8_t *data,
97 int offset,
98 unsigned int bit,
99 const ChannelValue &prototype)
100 {
101 return std::visit(
102 ✗ [&](auto &&proto) -> ChannelValue {
103 using T = std::decay_t<decltype(proto)>;
104 if constexpr (std::is_same_v<T, bool>) {
105 ✗ return static_cast<bool>(EC_READ_BIT(data + offset, bit));
106 }
107 else if constexpr (std::is_same_v<T, uint8_t>) {
108 ✗ return EC_READ_U8(data + offset);
109 }
110 else if constexpr (std::is_same_v<T, int8_t>) {
111 ✗ return EC_READ_S8(data + offset);
112 }
113 else if constexpr (std::is_same_v<T, uint16_t>) {
114 ✗ return EC_READ_U16(data + offset);
115 }
116 else if constexpr (std::is_same_v<T, int16_t>) {
117 ✗ return EC_READ_S16(data + offset);
118 }
119 else if constexpr (std::is_same_v<T, uint32_t>) {
120 ✗ return EC_READ_U32(data + offset);
121 }
122 else if constexpr (std::is_same_v<T, int32_t>) {
123 ✗ return EC_READ_S32(data + offset);
124 }
125 else if constexpr (std::is_same_v<T, float>) {
126 ✗ return EC_READ_REAL(data + offset);
127 }
128 else { // double
129 ✗ return EC_READ_LREAL(data + offset);
130 }
131 },
132 ✗ prototype);
133 }
134
135 /** Writes `value` to the domain data at (offset, bit). */
136 ✗ void writeRaw(
137 uint8_t *data,
138 int offset,
139 unsigned int bit,
140 const ChannelValue &value)
141 {
142 ✗ std::visit(
143 ✗ [&](auto &&v) {
144 using T = std::decay_t<decltype(v)>;
145 if constexpr (std::is_same_v<T, bool>) {
146 ✗ EC_WRITE_BIT(data + offset, bit, v);
147 }
148 else if constexpr (std::is_same_v<T, uint8_t>) {
149 ✗ EC_WRITE_U8(data + offset, v);
150 }
151 else if constexpr (std::is_same_v<T, int8_t>) {
152 ✗ EC_WRITE_S8(data + offset, v);
153 }
154 else if constexpr (std::is_same_v<T, uint16_t>) {
155 ✗ EC_WRITE_U16(data + offset, v);
156 }
157 else if constexpr (std::is_same_v<T, int16_t>) {
158 ✗ EC_WRITE_S16(data + offset, v);
159 }
160 else if constexpr (std::is_same_v<T, uint32_t>) {
161 ✗ EC_WRITE_U32(data + offset, v);
162 }
163 else if constexpr (std::is_same_v<T, int32_t>) {
164 ✗ EC_WRITE_S32(data + offset, v);
165 }
166 else if constexpr (std::is_same_v<T, float>) {
167 ✗ EC_WRITE_REAL(data + offset, v);
168 }
169 else { // double
170 ✗ EC_WRITE_LREAL(data + offset, v);
171 }
172 ✗ },
173 value);
174 }
175
176 /** Throws if two entries in `entries` share a name -- ecapp cannot
177 * address them unambiguously afterwards (see
178 * SubDevice::inputIndex()/outputIndex()). ecapp-generate already
179 * deduplicates names before emitting GenericEntry tables, so this should
180 * never trigger for generated code; kept as a defense-in-depth check for
181 * hand-written callers too, same as EL6692's checkUniqueNames(). */
182 9 void checkUniqueNames(
183 const std::string &direction,
184 const std::vector<GenericEntry> &entries)
185 {
186
2/2
✓ Branch 1 taken 7 times.
✓ Branch 2 taken 8 times.
15 for (size_t i = 0; i < entries.size(); i++) {
187
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 6 times.
9 for (size_t j = i + 1; j < entries.size(); j++) {
188
2/2
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 2 times.
3 if (entries[i].name == entries[j].name) {
189
1/2
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 stringstream err;
190 err << "GenericSubDevice: " << direction << " channel '"
191
5/10
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 1 times.
✗ Branch 8 not taken.
✓ Branch 11 taken 1 times.
✗ Branch 12 not taken.
✓ Branch 14 taken 1 times.
✗ Branch 15 not taken.
1 << entries[i].name << "' declared more than once.";
192
2/4
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
1 throw runtime_error(err.str());
193 }
194 }
195 }
196 8 }
197
198 /** Throws if an input and an output entry share a name -- both would
199 * register the same pdserv signal path (see EL6692's
200 * checkNoCrossDirectionCollision(), same rationale). */
201 4 void checkNoCrossDirectionCollision(
202 const std::vector<GenericEntry> &outputEntries,
203 const std::vector<GenericEntry> &inputEntries)
204 {
205
2/2
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 3 times.
6 for (const auto &out : outputEntries) {
206
2/2
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 2 times.
7 for (const auto &in : inputEntries) {
207
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 4 times.
5 if (in.name == out.name) {
208
1/2
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 stringstream err;
209 err << "GenericSubDevice: channel '" << in.name
210
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 << "' declared as both an input and an output -- "
211 "both would register the same pdserv signal "
212
2/4
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 "path.";
213
2/4
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
1 throw runtime_error(err.str());
214 }
215 }
216 }
217 3 }
218
219 } // namespace
220
221 /****************************************************************************/
222
223 template <Mode mode>
224 5 GenericSubDevice<mode>::GenericSubDevice(
225 Master *master,
226 uint16_t alias,
227 uint16_t position,
228 uint32_t vendorId,
229 uint32_t productCode,
230 Domain *domainOut,
231 Domain *domainIn,
232 const ec_sync_info_t *syncs,
233 const std::vector<GenericEntry> &outputEntries,
234 const std::vector<GenericEntry> &inputEntries,
235 const std::string &prefix,
236 pdserv *pdServ,
237 pdtask *task,
238 const std::vector<GenericSdo> &sdos) :
239 SubDevice(domainOut, domainIn, pdServ, task, prefix),
240 vendorId_(vendorId),
241 productCode_(productCode),
242 domainOut_(domainOut),
243
1/4
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
7 domainIn_(domainIn)
244 {
245
3/8
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 4 times.
✓ Branch 6 taken 1 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
6 checkUniqueNames("output", outputEntries);
246
2/8
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 4 times.
✗ Branch 6 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
4 checkUniqueNames("input", inputEntries);
247
2/4
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
4 checkNoCrossDirectionCollision(outputEntries, inputEntries);
248
249
2/4
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 3 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
5 for (const auto &e : outputEntries) {
250
3/12
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
2 outputKinds_.push_back({e.name, 1, e.type});
251 2 OutChannel ch;
252
1/4
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
2 ch.value = e.type;
253
1/4
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
2 outputs_.push_back(ch);
254 }
255
2/4
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 3 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
5 for (const auto &e : inputEntries) {
256
3/12
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
2 inputKinds_.push_back({e.name, 1, e.type});
257 2 InChannel ch;
258
1/4
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
2 ch.value = e.type;
259
1/4
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
2 inputs_.push_back(ch);
260 }
261
262 // ChannelValue-typed registration (channel types aren't fixed at
263 // compile time here, same situation as EL6692's VariablePdo
264 // channels) -- also what makes every channel alias-able through
265 // SubDevice::setAlias().
266
2/4
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 3 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
5 for (size_t i = 0; i < outputEntries.size(); i++) {
267
1/4
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
2 registerOutputChannel(outputEntries[i].name, outputs_[i].value);
268 }
269
2/4
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 3 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
5 for (size_t i = 0; i < inputEntries.size(); i++) {
270
1/4
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
2 registerInputChannel(inputEntries[i].name, inputs_[i].value);
271 }
272
273
1/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
3 if (!master) {
274 3 return; // no bus, e.g. unit tests without hardware
275 }
276
277 ✗ sc_ = ecrt_master_slave_config(
278 master->getMaster(), alias, position, vendorId, productCode);
279 ✗ if (!sc_) {
280 ✗ throw runtime_error("Failed to get slave configuration.");
281 }
282
283 // SDOs are configured before the PDO mapping -- some slaves need an
284 // SDO write (e.g. an operating mode) before their PDO layout even
285 // makes sense, and this matches the order typical hand-written
286 // device classes use (SDO setup, then ecrt_slave_config_pdos()).
287 // Same call application code uses later, from outside the
288 // constructor (see configureSdo() below) -- sdos passed here is
289 // just a convenience for values already known at construction time.
290 ✗ for (const auto &sdo : sdos) {
291 ✗ configureSdo(sdo.index, sdo.subindex, sdo.value);
292 }
293
294 // SwappedSync<Mode::Control> only ever stores `syncs` itself (never
295 // mutates it); SwappedSync<Mode::Simulation> copies it into its own
296 // heap array before swapping directions there -- either way `syncs`
297 // itself is never written through, so the const_cast here is safe
298 // (see ecapp/SwappedSync.h/.cpp).
299 ✗ if (ecrt_slave_config_pdos(
300 sc_, EC_END,
301 SwappedSync<mode>(const_cast<ec_sync_info_t *>(syncs))
302 .getSync())) {
303 ✗ throw runtime_error("Failed to configure PDOs.");
304 }
305
306 ✗ for (size_t i = 0; i < outputEntries.size(); i++) {
307 ✗ const auto &e = outputEntries[i];
308 ✗ unsigned int bitLen = bitLenOf(e.type);
309 ✗ outputs_[i].offset = ecrt_slave_config_reg_pdo_entry(
310 ✗ sc_, e.index, e.subindex, domainOut->getDomain(),
311 ✗ bitLen == 1 ? &outputs_[i].bit : NULL);
312 ✗ if (outputs_[i].offset < 0) {
313 ✗ stringstream err;
314 ✗ err << "Failed to register generic output PDO entry '" << e.name
315 ✗ << "'.";
316 ✗ throw runtime_error(err.str());
317 }
318 }
319 ✗ for (size_t i = 0; i < inputEntries.size(); i++) {
320 ✗ const auto &e = inputEntries[i];
321 ✗ unsigned int bitLen = bitLenOf(e.type);
322 ✗ inputs_[i].offset = ecrt_slave_config_reg_pdo_entry(
323 ✗ sc_, e.index, e.subindex, domainIn->getDomain(),
324 ✗ bitLen == 1 ? &inputs_[i].bit : NULL);
325 ✗ if (inputs_[i].offset < 0) {
326 ✗ stringstream err;
327 ✗ err << "Failed to register generic input PDO entry '" << e.name
328 ✗ << "'.";
329 ✗ throw runtime_error(err.str());
330 }
331 }
332 }
333
334 /****************************************************************************/
335
336 template <Mode mode>
337 1 void GenericSubDevice<mode>::configureSdo(
338 uint16_t index,
339 uint8_t subindex,
340 const ChannelValue &value)
341 {
342
1/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1 if (!sc_) {
343 1 return; // no bus, e.g. unit tests without hardware
344 }
345 ✗ std::vector<uint8_t> bytes = sdoBytes(value);
346 ✗ if (ecrt_slave_config_sdo(
347 ✗ sc_, index, subindex, bytes.data(), bytes.size())) {
348 ✗ stringstream err;
349 ✗ err << "Failed to configure SDO 0x" << std::hex << index << ":"
350 ✗ << static_cast<unsigned int>(subindex) << ".";
351 ✗ throw runtime_error(err.str());
352 }
353 }
354
355 /****************************************************************************/
356
357 template <Mode mode>
358 2 void GenericSubDevice<mode>::updateInputs()
359 {
360 2 const uint8_t *data = domainIn_->getData();
361
1/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
2 if (!data) {
362 2 return;
363 }
364 ✗ for (auto &ch : inputs_) {
365 ✗ ch.value = readRaw(data, ch.offset, ch.bit, ch.value);
366 }
367 }
368
369 /****************************************************************************/
370
371 template <Mode mode>
372 3 void GenericSubDevice<mode>::updateOutputs()
373 {
374 3 uint8_t *data = domainOut_->getData();
375
1/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
3 if (!data) {
376 3 return;
377 }
378 ✗ for (auto &ch : outputs_) {
379 ✗ writeRaw(data, ch.offset, ch.bit, ch.value);
380 }
381 }
382
383 /****************************************************************************/
384
385 template <Mode mode>
386 1 ChannelValue GenericSubDevice<mode>::getInputAt(size_t position) const
387 {
388 1 return inputs_.at(position).value;
389 }
390
391 /****************************************************************************/
392
393 template <Mode mode>
394 3 void GenericSubDevice<mode>::setOutputAt(
395 size_t position,
396 const ChannelValue &value)
397 {
398 3 auto &ch = outputs_.at(position);
399 // Same same-index-assignment contract as EL6692::setOutput() -- see
400 // its comment for why: preserves the address already handed to
401 // pdserv for this channel.
402
2/4
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
3 if (value.index() != ch.value.index()) {
403 1 throw std::bad_variant_access();
404 }
405 2 ch.value = value;
406 2 }
407
408 /****************************************************************************/
409
410 template class GenericSubDevice<Mode::Control>;
411 template class GenericSubDevice<Mode::Simulation>;
412
413 /****************************************************************************/
414
415 5 unique_ptr<SubDevice> makeGenericSubDevice(
416 Mode mode,
417 Master *master,
418 uint16_t alias,
419 uint16_t position,
420 uint32_t vendorId,
421 uint32_t productCode,
422 Domain *domainOut,
423 Domain *domainIn,
424 const ec_sync_info_t *syncs,
425 const std::vector<GenericEntry> &outputEntries,
426 const std::vector<GenericEntry> &inputEntries,
427 const std::string &prefix,
428 pdserv *pdServ,
429 pdtask *task,
430 const std::vector<GenericSdo> &sdos)
431 {
432
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (mode == Mode::Control) {
433 ✗ return make_unique<GenericSubDevice<Mode::Control>>(
434 master, alias, position, vendorId, productCode, domainOut,
435 domainIn, syncs, outputEntries, inputEntries, prefix, pdServ,
436 ✗ task, sdos);
437 }
438 else {
439 8 return make_unique<GenericSubDevice<Mode::Simulation>>(
440 master, alias, position, vendorId, productCode, domainOut,
441 domainIn, syncs, outputEntries, inputEntries, prefix, pdServ,
442 3 task, sdos);
443 }
444 }
445
446 } // namespace EcApp
447
448 /****************************************************************************/
449