GCC Code Coverage Report


Directory: ./
File: ecapp/SubDevice.h
Date: 2026-09-03 16:05:57
Exec Total Coverage
Lines: 54 54 100.0%
Branches: 21 40 52.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_export.h"
29
30 #include <cstddef>
31 #include <cstdint>
32 #include <string>
33 #include <variant>
34 #include <vector>
35
36 /****************************************************************************/
37
38 namespace EcApp {
39
40 /** Runtime-tagged value for the generic (string-factory) access path.
41 * Wrong-type access via std::get<T>() throws std::bad_variant_access. */
42 using ChannelValue = std::
43 variant<bool, uint16_t, int16_t, uint32_t, int32_t, float, double>;
44
45 /****************************************************************************/
46
47 /** A channel's identifier is a (kind, index) pair rather than one flat
48 * name: many devices have several channels of the same kind (e.g. 8
49 * "Input" channels on a digital input terminal, or "Input"+"Status" per
50 * analog channel, or -- for an IO-Link master -- one "Flow" channel
51 * per port). ChannelKind describes one such kind: its name, how many
52 * indices it has (0..count-1), and the type shared by every one of
53 * them.
54 *
55 * kind names are never hardcoded or special-cased by the base class --
56 * the single-argument inputIndex()/bindInput()/etc. overloads just
57 * require that exactly one kind is currently registered, whatever it
58 * is named. ecapp's own device adapters follow the convention of
59 * naming a device's sole/primary input kind "Input" and its
60 * sole/primary output kind "Output", reserving a more specific name
61 * (e.g. "Diagnosis", "Status", or an IO-Link variable name like
62 * "Flow") for a secondary or domain-specific kind. New adapters
63 * should follow that convention too, but it is not enforced anywhere. */
64
1/2
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
84 struct ChannelKind
65 {
66 std::string name;
67 unsigned int count; // valid indices are 0..count-1
68 ChannelValue type; // holds a default-constructed T only to tag the type
69 };
70
71 /****************************************************************************/
72
73 class SubDevice;
74
75 /** Returned by SubDevice::bindInput(). Resolved once (name lookup, type
76 * check); operator() then reads the channel with no lookup and no
77 * possibility of throwing. Deliberately not std::function: this is a
78 * plain, trivially-copyable value (pointer + index + function pointer),
79 * with no heap allocation and no indirection through a type-erased
80 * vtable. */
81 template <typename T>
82 class BoundInput
83 {
84 public:
85 3 T operator()() const { return get_(device_, index_); }
86
87 private:
88 friend class SubDevice;
89
90 3 BoundInput(
91 const SubDevice *device,
92 std::size_t position,
93 T (*get)(const SubDevice *, std::size_t)) :
94 3 device_(device), index_(position), get_(get)
95 3 {}
96
97 const SubDevice *device_;
98 std::size_t index_;
99 T (*get_)(const SubDevice *, std::size_t);
100 };
101
102 /** Returned by SubDevice::bindOutput(). See BoundInput. */
103 template <typename T>
104 class BoundOutput
105 {
106 public:
107 3 void operator()(T value) const { set_(device_, index_, value); }
108
109 private:
110 friend class SubDevice;
111
112 3 BoundOutput(
113 SubDevice *device,
114 std::size_t position,
115 void (*set)(SubDevice *, std::size_t, T)) :
116 3 device_(device), index_(position), set_(set)
117 3 {}
118
119 SubDevice *device_;
120 std::size_t index_;
121 void (*set_)(SubDevice *, std::size_t, T);
122 };
123
124 /****************************************************************************/
125
126 /** Generic, network-agnostic interface to an EtherCAT SubDevice (slave).
127 *
128 * PDO layout and vendor/product knowledge stay hidden behind actual
129 * device implementations; this interface is what createSubDevice()
130 * returns. Input and output channels are two independent sets, each
131 * organized as a small list of kinds (see ChannelKind); a channel is
132 * addressed by (kind, index).
133 *
134 * Three ways to reach a channel's value, in increasing order of setup
135 * cost and decreasing order of per-call cost:
136 * - getInput(kind, index)/setOutput(kind, index, value): resolves
137 * the identifier and returns/takes a type-erased ChannelValue,
138 * every call. For reflective/generic tooling that doesn't know a
139 * channel's type at compile time (it got kind/count/type from
140 * inputKinds()/outputKinds() while walking every kind, say).
141 * - readInput<T>(kind, index)/writeOutput<T>(kind, index, value):
142 * same, but returns/takes T directly (std::get<T> on the
143 * ChannelValue). Convenient for setup code or rarely-touched
144 * channels; re-does the lookup every call, so avoid it in a hot
145 * loop.
146 * - bindInput<T>(kind, index)/bindOutput<T>(kind, index): resolves
147 * (kind, index) once and returns a small callable for the hot
148 * loop; see BoundInput/BoundOutput. This is what a control loop
149 * should use for anything read/written every cycle.
150 * Every one of the above also has a single-argument (index-only)
151 * overload, valid when the device has exactly one input/output kind.
152 *
153 * inputKinds()/outputKinds() are the enumeration primitive for code
154 * that walks every kind without knowing kind names in advance -- e.g.
155 * a diagnostics tool listing every channel a device has.
156 */
157 12 class ECAPP_EXPORT SubDevice
158 {
159 public:
160 12 virtual ~SubDevice() = default;
161
162 /** Reads process data into the device's cached input values.
163 * Call once per cycle before querying any input channel. */
164 virtual void updateInputs() = 0;
165
166 /** Writes the device's cached output values to process data.
167 * Call once per cycle after all output channel writes. */
168 virtual void updateOutputs() = 0;
169
170 virtual const std::vector<ChannelKind> &inputKinds() const = 0;
171 virtual const std::vector<ChannelKind> &outputKinds() const = 0;
172
173 /** Resolves a (kind, index) identifier to a raw position. Throws
174 * UnknownChannel if no such channel exists. The single-argument
175 * overload additionally throws UnknownChannel if more than one
176 * kind is registered (ambiguous without naming one). */
177 std::size_t inputIndex(const std::string &kind, unsigned int index) const;
178 std::size_t inputIndex(unsigned int index) const;
179 std::size_t
180 outputIndex(const std::string &kind, unsigned int index) const;
181 std::size_t outputIndex(unsigned int index) const;
182
183 /** Type-erased, one-shot identifier-based access. */
184 9 ChannelValue getInput(const std::string &kind, unsigned int index) const
185 {
186 9 return getInputAt(inputIndex(kind, index));
187 }
188
189 5 ChannelValue getInput(unsigned int index) const
190 {
191 5 return getInputAt(inputIndex(index));
192 }
193
194 4 void setOutput(
195 const std::string &kind,
196 unsigned int index,
197 const ChannelValue &value)
198 {
199 4 setOutputAt(outputIndex(kind, index), value);
200 4 }
201
202 7 void setOutput(unsigned int index, const ChannelValue &value)
203 {
204 7 setOutputAt(outputIndex(index), value);
205 3 }
206
207 /** Same as getInput()/setOutput(), but returns/takes T
208 * directly. */
209 template <typename T>
210 9 T readInput(const std::string &kind, unsigned int index) const
211 {
212
3/6
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
✓ Branch 10 taken 2 times.
✗ Branch 11 not taken.
9 return std::get<T>(getInput(kind, index));
213 }
214
215 template <typename T>
216 3 T readInput(unsigned int index) const
217 {
218
1/2
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
3 return std::get<T>(getInput(index));
219 }
220
221 template <typename T>
222 2 void writeOutput(const std::string &kind, unsigned int index, T value)
223 {
224
2/4
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
2 setOutput(kind, index, ChannelValue(value));
225 2 }
226
227 template <typename T>
228 3 void writeOutput(unsigned int index, T value)
229 {
230
1/2
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 setOutput(index, ChannelValue(value));
231 3 }
232
233 /** Resolves (kind, index) once -- lookup and type check both
234 * happen here, not in the returned callable. */
235 template <typename T>
236 3 BoundInput<T> bindInput(const std::string &kind, unsigned int index) const
237 {
238 3 std::size_t position = inputIndex(kind, index);
239
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()) {
240
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 times.
4 if (k.name == kind) {
241
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 std::get<T>(k.type); // type check now, not every call
242 3 break;
243 }
244 }
245 3 return BoundInput<T>(this, position, &readAt<T>);
246 }
247
248 template <typename T>
249 1 BoundInput<T> bindInput(unsigned int index) const
250 {
251
1/2
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 return bindInput<T>(soleInputKind(), index);
252 }
253
254 template <typename T>
255 3 BoundOutput<T> bindOutput(const std::string &kind, unsigned int index)
256 {
257 3 std::size_t position = outputIndex(kind, index);
258
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()) {
259
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 if (k.name == kind) {
260
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 std::get<T>(k.type);
261 3 break;
262 }
263 }
264 3 return BoundOutput<T>(this, position, &writeAt<T>);
265 }
266
267 template <typename T>
268 2 BoundOutput<T> bindOutput(unsigned int index)
269 {
270
1/2
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 return bindOutput<T>(soleOutputKind(), index);
271 }
272
273 protected:
274 /** The actual per-device value access, keyed by the position
275 * an adapter chose internally (kind blocks concatenated in the
276 * order inputKinds()/outputKinds() lists them) -- not part of
277 * the public identifier scheme above. */
278 virtual ChannelValue getInputAt(std::size_t position) const = 0;
279 virtual void
280 setOutputAt(std::size_t position, const ChannelValue &value) = 0;
281
282 private:
283 template <typename T>
284 3 static T readAt(const SubDevice *device, std::size_t position)
285 {
286
1/2
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 return std::get<T>(device->getInputAt(position));
287 }
288
289 template <typename T>
290 3 static void writeAt(SubDevice *device, std::size_t position, T value)
291 {
292
1/2
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 device->setOutputAt(position, ChannelValue(value));
293 3 }
294
295 /** Returns the one kind shared by every registered input/output
296 * channel. Throws UnknownChannel if there are none or more than
297 * one (ambiguous -- name one explicitly instead). */
298 std::string soleInputKind() const;
299 std::string soleOutputKind() const;
300 };
301
302 } // namespace EcApp
303
304 #endif // ECAPP_SUBDEVICE_H
305
306 /****************************************************************************/
307