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