GCC Code Coverage Report


Directory: ./
File: ecapp/SubDevice.h
Date: 2026-09-23 16:22:15
Exec Total Coverage
Lines: 60 68 88.2%
Branches: 46 156 29.5%

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 #ifndef ECAPP_SUBDEVICE_H
24 #define ECAPP_SUBDEVICE_H
25
26 /****************************************************************************/
27
28 #include "ecapp/Exceptions.h"
29 #include "ecapp_export.h"
30
31 #include <cstddef>
32 #include <cstdint>
33 #include <memory>
34 #include <string>
35 #include <variant>
36 #include <vector>
37
38 /****************************************************************************/
39
40 struct pdserv;
41 struct pdtask;
42
43 namespace EcApp {
44
45 /** Runtime-tagged value for the generic (string-factory) access path.
46 *
47 * These are all the supported datatypes for inputs and outputs.
48 *
49 * Wrong-type access via std::get<T>() throws std::bad_variant_access.
50 */
51 using ChannelValue = std::variant<
52 bool,
53 uint8_t,
54 int8_t,
55 uint16_t,
56 int16_t,
57 uint32_t,
58 int32_t,
59 float,
60 double>;
61
62 /****************************************************************************/
63
64 /** Identifier for a kind of device channel.
65 *
66 * A channel's identifier is a (kind, index) pair rather than one flat name:
67 * many devices have several channels of the same kind (e. g. 8 "Input"
68 * channels on a digital input terminal, or "Input" and "Status" per analog
69 * channel, or -- for an IO-Link master -- one "Flow" channel per port).
70 * ChannelKind describes one such kind: its name, how many indices it has
71 * (0..count-1), and the type shared by every one of them.
72 */
73
1/4
✓ Branch 2 taken 43 times.
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
250 struct ChannelKind
74 {
75 std::string name;
76 unsigned int count; // valid indices are 0..count-1
77 ChannelValue type; // holds a default-constructed T only to tag the type
78 };
79
80 /****************************************************************************/
81
82 class Domain;
83 class SubDevice;
84
85 /** Returned by SubDevice::bindInput().
86 *
87 * Resolved once (name lookup, type check); operator() then reads the channel
88 * with no lookup and no possibility of throwing. Deliberately not
89 * std::function: this is a plain, trivially-copyable value (pointer + index +
90 * function pointer), with no heap allocation and no indirection through a
91 * type-erased vtable.
92 */
93 template <typename T>
94 class BoundInput
95 {
96 public:
97 3 T operator()() const { return get_(device_, index_); }
98
99 private:
100 friend class SubDevice;
101
102 3 BoundInput(
103 const SubDevice *device,
104 std::size_t position,
105 T (*get)(const SubDevice *, std::size_t)) :
106 3 device_(device), index_(position), get_(get)
107 3 {}
108
109 const SubDevice *device_;
110 std::size_t index_;
111 T (*get_)(const SubDevice *, std::size_t);
112 };
113
114 /** Returned by SubDevice::bindOutput().
115 *
116 * See BoundInput.
117 */
118 template <typename T>
119 class BoundOutput
120 {
121 public:
122 3 void operator()(T value) const { set_(device_, index_, value); }
123
124 private:
125 friend class SubDevice;
126
127 3 BoundOutput(
128 SubDevice *device,
129 std::size_t position,
130 void (*set)(SubDevice *, std::size_t, T)) :
131 3 device_(device), index_(position), set_(set)
132 3 {}
133
134 SubDevice *device_;
135 std::size_t index_;
136 void (*set_)(SubDevice *, std::size_t, T);
137 };
138
139 /****************************************************************************/
140
141 /** Generic interface to an EtherCAT SubDevice (slave).
142 *
143 * PDO layout and vendor/product knowledge stay hidden behind actual
144 * device implementations; this interface is what createSubDevice()
145 * returns. Input and output channels are two independent sets, each
146 * organized as a small list of kinds (see ChannelKind); a channel is
147 * addressed by (kind, index).
148 *
149 * Three ways to reach a channel's value, in increasing order of setup
150 * cost and decreasing order of per-call cost:
151 *
152 * - getInput(kind, index)/setOutput(kind, index, value): resolves
153 * the identifier and returns/takes a type-erased ChannelValue,
154 * every call. For reflective/generic tooling that doesn't know a
155 * channel's type at compile time (it got kind/count/type from
156 * inputKinds()/outputKinds() while walking every kind, say).
157 *
158 * - readInput<T>(kind, index)/writeOutput<T>(kind, index, value):
159 * same, but returns/takes T directly (std::get<T> on the
160 * ChannelValue). Convenient for setup code or rarely-touched
161 * channels; re-does the lookup every call, so avoid it in a hot
162 * loop.
163 *
164 * - bindInput<T>(kind, index)/bindOutput<T>(kind, index): resolves
165 * (kind, index) once and returns a small callable for the hot
166 * loop; see BoundInput/BoundOutput. This is what a control loop
167 * should use for anything read/written every cycle.
168 *
169 * Every one of the above also has a single-argument (index-only)
170 * overload, valid when the device has exactly one input/output kind.
171 *
172 * inputKinds()/outputKinds() are the enumeration primitive for code
173 * that walks every kind without knowing kind names in advance -- e. g.
174 * a diagnostics tool listing every channel a device has.
175 */
176 class ECAPP_EXPORT SubDevice
177 {
178 public:
179 /** Unregisters this device from whatever domain(s) it was
180 * registered with at construction (see the protected
181 * constructor below). */
182 virtual ~SubDevice();
183
184 /** Reads process data into the device's cached input values.
185 *
186 * Call once per cycle before querying any input channel -- either
187 * directly, or (if this device was constructed with a non-null domainIn)
188 * via that Domain's own updateInputs(), which calls this on every device
189 * registered with it.
190 *
191 * \warning Pick one path per device and stick to it: calling both would
192 * run this twice a cycle, which some devices (e.g. anything with edge
193 * detection or toggle logic) do not tolerate.
194 */
195 virtual void updateInputs() = 0;
196
197 /** Writes the device's cached output values to process data.
198 *
199 * Call once per cycle after all output channel writes -- either
200 * directly, or (if this device was constructed with a non-null
201 * domainOut) via that Domain's own updateOutputs().
202 *
203 * \warning See updateInputs() above: pick one path per device, not both.
204 */
205 virtual void updateOutputs() = 0;
206
207 virtual const std::vector<ChannelKind> &inputKinds() const = 0;
208 virtual const std::vector<ChannelKind> &outputKinds() const = 0;
209
210 /** Resolves a (kind, index) identifier to a raw position.
211 *
212 * Throws UnknownChannel if no such channel exists. The single-argument
213 * overload additionally throws UnknownChannel if more than one kind is
214 * registered (ambiguous without naming one).
215 */
216 std::size_t inputIndex(const std::string &kind, unsigned int index) const;
217 std::size_t inputIndex(unsigned int index) const;
218 std::size_t
219 outputIndex(const std::string &kind, unsigned int index) const;
220 std::size_t outputIndex(unsigned int index) const;
221
222 /** Type-erased, one-shot identifier-based access. */
223 18 ChannelValue getInput(const std::string &kind, unsigned int index) const
224 {
225 18 return getInputAt(inputIndex(kind, index));
226 }
227
228 4 ChannelValue getInput(unsigned int index) const
229 {
230 4 return getInputAt(inputIndex(index));
231 }
232
233 /** Wraps whatever setOutputAt() throws for a mismatched value type
234 * (every adapter's own std::get<T>(value), e.g. Ex4xxx's
235 * std::get<double>(value)) in ChannelTypeMismatch, naming this
236 * channel -- covers writeOutput<T>() too, since it calls this. */
237 16 void setOutput(
238 const std::string &kind,
239 unsigned int index,
240 const ChannelValue &value)
241 {
242 try {
243
3/4
✓ Branch 1 taken 16 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 14 times.
✓ Branch 5 taken 2 times.
16 setOutputAt(outputIndex(kind, index), value);
244 }
245 4 catch (const std::bad_variant_access &) {
246 throw ChannelTypeMismatch(
247
2/4
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
4 "Wrong type written to output channel (\"" + kind + "\", "
248
3/6
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 2 times.
✗ Branch 6 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
6 + std::to_string(index) + ").");
249 }
250 14 }
251
252 7 void setOutput(unsigned int index, const ChannelValue &value)
253 {
254
2/2
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 2 times.
12 std::string kind = soleOutputKind();
255 try {
256
3/4
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 2 times.
5 setOutputAt(outputIndex(index), value);
257 }
258 4 catch (const std::bad_variant_access &) {
259 throw ChannelTypeMismatch(
260
2/4
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
4 "Wrong type written to output channel (\"" + kind + "\", "
261
3/6
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 2 times.
✗ Branch 6 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
6 + std::to_string(index) + ").");
262 }
263 3 }
264
265 /** Same as getInput()/setOutput(), but returns/takes T directly.
266 * A mismatched T throws ChannelTypeMismatch, naming this
267 * channel. */
268 template <typename T>
269 18 T readInput(const std::string &kind, unsigned int index) const
270 {
271 try {
272
6/18
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 10 taken 6 times.
✗ Branch 11 not taken.
✓ Branch 13 taken 6 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✓ Branch 19 taken 2 times.
✗ Branch 20 not taken.
✓ Branch 22 taken 2 times.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
18 return std::get<T>(getInput(kind, index));
273 }
274 ✗ catch (const std::bad_variant_access &) {
275 throw ChannelTypeMismatch(
276 "Wrong type requested for input channel (\"" + kind
277 ✗ + "\", " + std::to_string(index) + ").");
278 }
279 }
280
281 template <typename T>
282 3 T readInput(unsigned int index) const
283 {
284
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 2 times.
4 std::string kind = soleInputKind();
285 try {
286
2/6
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
2 return std::get<T>(getInput(index));
287 }
288 ✗ catch (const std::bad_variant_access &) {
289 throw ChannelTypeMismatch(
290 "Wrong type requested for input channel (\"" + kind
291 ✗ + "\", " + std::to_string(index) + ").");
292 }
293 }
294
295 template <typename T>
296 12 void writeOutput(const std::string &kind, unsigned int index, T value)
297 {
298
3/6
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 10 taken 2 times.
✗ Branch 11 not taken.
12 setOutput(kind, index, ChannelValue(value));
299 12 }
300
301 template <typename T>
302 3 void writeOutput(unsigned int index, T value)
303 {
304
1/2
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 setOutput(index, ChannelValue(value));
305 3 }
306
307 /** Resolves (kind, index) once -- lookup and type check both
308 * happen here, not in the returned callable. A mismatched T
309 * throws ChannelTypeMismatch, naming this channel. */
310 template <typename T>
311 3 BoundInput<T> bindInput(const std::string &kind, unsigned int index) const
312 {
313 3 std::size_t position = inputIndex(kind, index);
314
2/4
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
4 for (const auto &k : inputKinds()) {
315
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 times.
4 if (k.name == kind) {
316 try {
317
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 std::get<T>(k.type); // type check now, not every call
318 }
319 ✗ catch (const std::bad_variant_access &) {
320 throw ChannelTypeMismatch(
321 "Wrong type bound to input channel (\"" + kind
322 ✗ + "\", " + std::to_string(index) + ").");
323 }
324 3 break;
325 }
326 }
327
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
3 return BoundInput<T>(this, position, &readAt<T>);
328 }
329
330 template <typename T>
331 1 BoundInput<T> bindInput(unsigned int index) const
332 {
333
1/2
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 return bindInput<T>(soleInputKind(), index);
334 }
335
336 template <typename T>
337 3 BoundOutput<T> bindOutput(const std::string &kind, unsigned int index)
338 {
339 3 std::size_t position = outputIndex(kind, index);
340
2/4
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
3 for (const auto &k : outputKinds()) {
341
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 if (k.name == kind) {
342 try {
343
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 std::get<T>(k.type);
344 }
345 ✗ catch (const std::bad_variant_access &) {
346 throw ChannelTypeMismatch(
347 "Wrong type bound to output channel (\"" + kind
348 ✗ + "\", " + std::to_string(index) + ").");
349 }
350 3 break;
351 }
352 }
353
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
3 return BoundOutput<T>(this, position, &writeAt<T>);
354 }
355
356 template <typename T>
357 2 BoundOutput<T> bindOutput(unsigned int index)
358 {
359
1/2
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 return bindOutput<T>(soleOutputKind(), index);
360 }
361
362 /** Alias-based counterparts of the (kind, index) identifier scheme.
363 *
364 * Resolved through the table set by setAlias() instead of a raw index,
365 * otherwise identical.
366 *
367 * Throw UnknownChannel if no alias by that name was ever set for kind (on
368 * the input or output side, respectively).
369 */
370 std::size_t
371 inputIndex(const std::string &kind, const std::string &alias) const;
372 std::size_t
373 outputIndex(const std::string &kind, const std::string &alias) const;
374
375 ChannelValue
376 getInput(const std::string &kind, const std::string &alias) const
377 {
378 return getInputAt(inputIndex(kind, alias));
379 }
380
381 void setOutput(
382 const std::string &kind,
383 const std::string &alias,
384 const ChannelValue &value)
385 {
386 try {
387 setOutputAt(outputIndex(kind, alias), value);
388 }
389 catch (const std::bad_variant_access &) {
390 throw ChannelTypeMismatch(
391 "Wrong type written to output channel (\"" + kind
392 + "\", \"" + alias + "\").");
393 }
394 }
395
396 template <typename T>
397 T readInput(const std::string &kind, const std::string &alias) const
398 {
399 try {
400 return std::get<T>(getInput(kind, alias));
401 }
402 catch (const std::bad_variant_access &) {
403 throw ChannelTypeMismatch(
404 "Wrong type requested for input channel (\"" + kind
405 + "\", \"" + alias + "\").");
406 }
407 }
408
409 template <typename T>
410 void
411 writeOutput(const std::string &kind, const std::string &alias, T value)
412 {
413 setOutput(kind, alias, ChannelValue(value));
414 }
415
416 template <typename T>
417 BoundInput<T>
418 bindInput(const std::string &kind, const std::string &alias) const
419 {
420 std::size_t position = inputIndex(kind, alias);
421 for (const auto &k : inputKinds()) {
422 if (k.name == kind) {
423 try {
424 std::get<T>(k.type);
425 }
426 catch (const std::bad_variant_access &) {
427 throw ChannelTypeMismatch(
428 "Wrong type bound to input channel (\"" + kind
429 + "\", \"" + alias + "\").");
430 }
431 break;
432 }
433 }
434 return BoundInput<T>(this, position, &readAt<T>);
435 }
436
437 template <typename T>
438 BoundOutput<T>
439 bindOutput(const std::string &kind, const std::string &alias)
440 {
441 std::size_t position = outputIndex(kind, alias);
442 for (const auto &k : outputKinds()) {
443 if (k.name == kind) {
444 try {
445 std::get<T>(k.type);
446 }
447 catch (const std::bad_variant_access &) {
448 throw ChannelTypeMismatch(
449 "Wrong type bound to output channel (\"" + kind
450 + "\", \"" + alias + "\").");
451 }
452 break;
453 }
454 }
455 return BoundOutput<T>(this, position, &writeAt<T>);
456 }
457
458 /** Set a list of string aliases for an input/output variable.
459 *
460 * Assigns aliases[i] as channel i's alias for `kind` (an entry of
461 * inputKinds() or outputKinds()), so it can be used in place of a raw
462 * index everywhere a (kind, index) pair is accepted -- see the (kind,
463 * alias) overloads above.
464 *
465 * If this SubDevice was constructed with pdserv/task, one extra pdserv
466 * signal (or parameter) is published per non-empty alias, at prefix + "/"
467 * + kind + "/" + aliases[i] (or prefix + "/" + paramName + "/" +
468 * aliases[i]) -- in addition to, not instead of, the existing bulk vector
469 * signal/parameter.
470 *
471 * aliases.size() must equal kind's channel count exactly; throws
472 * AliasCountMismatch otherwise. An empty string at index i within that
473 * full-length vector still just means "no alias for this one channel" --
474 * only the vector's overall length is required to match. Throws
475 * UnknownChannel if kind names neither an input nor an output kind.
476 * Because pdserv has no way to unregister a signal/parameter again, this
477 * can be called at most once per kind; a second call for the same kind
478 * throws std::logic_error rather than silently leaving the first call's
479 * signals stale. */
480 void setAlias(
481 const std::string &kind,
482 const std::vector<std::string> &aliases);
483
484 protected:
485 /** Registers this device with the given domain(s) -- either may
486 * be null (pass nullptr explicitly) for a device that has no
487 * outputs, no inputs, or (for an IoLinkDevice port, whose PDOs
488 * live in its hosting terminal's own domain registration)
489 * neither. domainOut/domainIn may be the same Domain.
490 * Registration lets Domain::updateOutputs()/updateInputs() reach
491 * this device without the application having to keep its own
492 * list of SubDevices; ~SubDevice() unregisters again, so a
493 * device's lifetime alone determines how long it stays reachable
494 * from its domain(s).
495 *
496 * No default arguments and no default constructor on purpose for
497 * domainOut/domainIn -- every adapter must say explicitly which
498 * domain(s) it does or does not use, rather than silently
499 * registering with neither. pdServ/task/prefix default to unset
500 * (null/empty) for a device that never intends to call
501 * registerInputChannel()/registerOutputChannel()/
502 * registerParameter()/setAlias() below -- silently doing without
503 * pdserv publishing is the existing, already-established
504 * convention throughout ecapp (see e.g. createSubDevice()).
505 */
506 SubDevice(
507 Domain *domainOut,
508 Domain *domainIn,
509 pdserv *pdServ = nullptr,
510 pdtask *task = nullptr,
511 std::string prefix = {});
512
513 /** The actual per-device value access, keyed by the position
514 * an adapter chose internally (kind blocks concatenated in the
515 * order inputKinds()/outputKinds() lists them) -- not part of
516 * the public identifier scheme above. */
517 virtual ChannelValue getInputAt(std::size_t position) const = 0;
518 virtual void
519 setOutputAt(std::size_t position, const ChannelValue &value) = 0;
520
521 /** Tag marking a uint8_t-backed channel/parameter that should be
522 * published as pd_boolean_T instead of the pd_uint8_T its element
523 * type would otherwise imply -- the one legitimate case where the
524 * published wire type doesn't match T one-to-one (a boolean
525 * channel is stored one byte per element, since std::vector<bool>
526 * has no contiguous storage to publish, but should still read as
527 * "boolean" rather than "byte" in the HMI). Pass as the last
528 * argument to the std::vector<uint8_t>/uint8_t overloads of
529 * registerInputChannel()/registerOutputChannel()/
530 * registerParameter() below; there is no escape hatch for any
531 * other T -- passing AsBoolean for, say, a std::vector<double>
532 * simply does not compile, so a wrong wire type can't be
533 * requested by mistake the way a raw type-code parameter would
534 * allow. */
535 struct AsBoolean
536 {};
537
538 /** Registers `data` as this kind's pdserv signal.
539 *
540 * Equivalent to the device calling pdserv_signal() directly for it,
541 * but additionally remembers its address so a later setAlias()
542 * call can publish per-channel signals for it, without this device
543 * exposing anything further. A device should call this instead of
544 * pdserv_signal() wherever it publishes one of its own
545 * ChannelKind vectors; existing direct pdserv_signal() calls can
546 * migrate to it one at a time -- a kind that hasn't been migrated
547 * yet simply doesn't support setAlias()'s per-channel pdserv
548 * publishing (alias-based lookup in code still works regardless,
549 * see setAlias()). `data` must stay valid and keep its address for
550 * as long as this SubDevice does.
551 *
552 * T must be one of ChannelValue's alternatives other than bool
553 * (every existing device stores a boolean-valued channel as
554 * std::vector<uint8_t>, one byte per element -- see AsBoolean
555 * above for publishing one of those as pd_boolean_T); explicitly
556 * instantiated for the other eight in SubDevice.cpp. */
557 template <typename T>
558 void registerInputChannel(const std::string &kind, std::vector<T> &data);
559
560 /** Same as above, but published as pd_boolean_T -- see AsBoolean. */
561 void registerInputChannel(
562 const std::string &kind,
563 std::vector<uint8_t> &data,
564 AsBoolean);
565
566 template <typename T>
567 void registerOutputChannel(const std::string &kind, std::vector<T> &data);
568
569 void registerOutputChannel(
570 const std::string &kind,
571 std::vector<uint8_t> &data,
572 AsBoolean);
573
574 /** Same idea for pdserv_parameter() -- a per-channel tuning value
575 * that isn't itself one of this device's (kind, index) channels
576 * (e.g. Ex3xxx's per-channel "Gain"), but shares an existing
577 * kind's channel count and, once setAlias() is called for that
578 * kind, its aliases too (e.g. "Gain/TI-0100" alongside
579 * "Input/TI-0100"). `kind` need not itself have been registered
580 * through registerInputChannel()/registerOutputChannel() -- only
581 * its channel count is used, for validation by whatever later
582 * calls setAlias(kind, ...). */
583 template <typename T>
584 void registerParameter(
585 const std::string &name,
586 const std::string &kind,
587 std::vector<T> &data,
588 unsigned int mode = 0666);
589
590 /** Same as above, but published as pd_boolean_T -- see AsBoolean. */
591 void registerParameter(
592 const std::string &name,
593 const std::string &kind,
594 std::vector<uint8_t> &data,
595 AsBoolean,
596 unsigned int mode = 0666);
597
598 /** Scalar counterparts of the three above, for a kind whose count
599 * is 1 and that a device therefore stores as a plain T member
600 * instead of a one-element std::vector<T> (e.g. ifm IO-Link
601 * sensors, one member per physical quantity: "float flow;" rather
602 * than "std::vector<double> flow {1, 0.0};"). Identical otherwise,
603 * including the AsBoolean overloads -- a scalar bool has no
604 * packed-storage problem (only std::vector<bool> does), but
605 * existing devices still store a single boolean-valued channel as
606 * a plain uint8_t for consistency with the vector case, so the
607 * same AsBoolean escape hatch applies here too. */
608 template <typename T>
609 void registerInputChannel(const std::string &kind, T &data);
610
611 void
612 registerInputChannel(const std::string &kind, uint8_t &data, AsBoolean);
613
614 template <typename T>
615 void registerOutputChannel(const std::string &kind, T &data);
616
617 void
618 registerOutputChannel(const std::string &kind, uint8_t &data, AsBoolean);
619
620 template <typename T>
621 void registerParameter(
622 const std::string &name,
623 const std::string &kind,
624 T &data,
625 unsigned int mode = 0666);
626
627 void registerParameter(
628 const std::string &name,
629 const std::string &kind,
630 uint8_t &data,
631 AsBoolean,
632 unsigned int mode = 0666);
633
634 /** ChannelValue counterparts of the scalar overloads above, for a
635 * device whose channel set/types aren't fixed at compile time
636 * (e.g. EL6692's "Bridge", whose channels are declared by the
637 * application at runtime through VariablePdo::configure() -- each
638 * one is stored as a plain ChannelValue, one per channel, rather
639 * than as a T or std::vector<T> member of a known type). The
640 * active alternative decides the pdserv type, same as the T
641 * overloads' T does; no AsBoolean escape hatch is needed here --
642 * a ChannelValue's bool alternative is one plain bool, with no
643 * std::vector<bool> packed-storage problem to work around. `value`
644 * must keep the same active alternative and address for as long
645 * as this SubDevice does (same address-stability contract
646 * std::variant's same-index assignment already gives same-index
647 * writes -- see e.g. EL6692::setOutput()). */
648 void registerInputChannel(const std::string &kind, ChannelValue &value);
649 void registerOutputChannel(const std::string &kind, ChannelValue &value);
650 void registerParameter(
651 const std::string &name,
652 const std::string &kind,
653 ChannelValue &value,
654 unsigned int mode = 0666);
655
656 private:
657 // Everything this base class needs to store privately (domain
658 // registration, pdserv/task/prefix, alias tables, ...) lives in
659 // Impl (defined in SubDevice.cpp) instead of directly here, same
660 // as Domain/Master -- SubDevice is meant to be subclassed by every
661 // device adapter, in-tree and third-party alike, so a change to
662 // its private bookkeeping must not change its size/layout and
663 // force every such adapter to recompile.
664 struct Impl;
665 std::unique_ptr<Impl> impl;
666
667 template <typename T>
668 3 static T readAt(const SubDevice *device, std::size_t position)
669 {
670
1/2
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 return std::get<T>(device->getInputAt(position));
671 }
672
673 template <typename T>
674 3 static void writeAt(SubDevice *device, std::size_t position, T value)
675 {
676
1/2
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 device->setOutputAt(position, ChannelValue(value));
677 3 }
678
679 /** Returns the one kind shared by every registered input/output
680 * channel. Throws UnknownChannel if there are none or more than
681 * one (ambiguous -- name one explicitly instead). */
682 std::string soleInputKind() const;
683 std::string soleOutputKind() const;
684 };
685
686 } // namespace EcApp
687
688 #endif // ECAPP_SUBDEVICE_H
689
690 /****************************************************************************/
691