| 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 |