| Directory: | ./ |
|---|---|
| File: | ecapp/IoLinkDevice.h |
| Date: | 2026-09-23 16:22:15 |
| Exec | Total | Coverage | |
|---|---|---|---|
| Lines: | 12 | 19 | 63.2% |
| Branches: | 2 | 4 | 50.0% |
| 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_IOLINKDEVICE_H | ||
| 24 | #define ECAPP_IOLINKDEVICE_H | ||
| 25 | |||
| 26 | /****************************************************************************/ | ||
| 27 | |||
| 28 | #include "ecapp/SubDevice.h" | ||
| 29 | #include "ecapp_export.h" | ||
| 30 | |||
| 31 | #include <array> | ||
| 32 | #include <cstdint> | ||
| 33 | #include <ecrt.h> // EtherCAT realtime interface | ||
| 34 | #include <functional> | ||
| 35 | #include <memory> | ||
| 36 | #include <pdserv.h> | ||
| 37 | #include <string> | ||
| 38 | #include <utility> | ||
| 39 | #include <vector> | ||
| 40 | |||
| 41 | /****************************************************************************/ | ||
| 42 | |||
| 43 | namespace EcApp { | ||
| 44 | |||
| 45 | class Domain; | ||
| 46 | |||
| 47 | /** Everything a concrete IoLinkDevice needs to state about itself for | ||
| 48 | * configurePort() to identify/validate it to the hosting IO-Link | ||
| 49 | * master port (e.g. an EL6224's CoE object 0x80n0, "IO Settings" -- | ||
| 50 | * see configurePort()'s definition for the byte-level derivation). | ||
| 51 | * Values come straight from the device's IODD/datasheet: Vendor ID, | ||
| 52 | * Device ID and minimum cycle time from its "Kommunikation" chapter, | ||
| 53 | * process data lengths and SIO support from its process data chapter | ||
| 54 | * and "SIO Mode unterstuetzt" line. The remaining fields default to | ||
| 55 | * what every ifm device handled so far agrees on (IO-Link V1.1, ISDU | ||
| 56 | * supported, data storage active) and only need overriding for a | ||
| 57 | * device that actually differs. */ | ||
| 58 | 8 | struct IoLinkPortIdentity | |
| 59 | { | ||
| 60 | uint32_t vendorId; | ||
| 61 | uint32_t deviceId; | ||
| 62 | double minCycleTimeMs; | ||
| 63 | unsigned int processDataInBits = 0; | ||
| 64 | unsigned int processDataOutBits = 0; | ||
| 65 | bool sioSupported = false; | ||
| 66 | uint8_t revisionMajor = 1; | ||
| 67 | uint8_t revisionMinor = 1; | ||
| 68 | bool isduSupported = true; | ||
| 69 | bool dataStorage = true; | ||
| 70 | }; | ||
| 71 | |||
| 72 | /** Base class for a driver plugged into one port of an IO-Link master | ||
| 73 | * terminal (e.g. EL6224). An IO-Link master's ports are themselves | ||
| 74 | * tiny independent devices, each with its own fixed set of channels | ||
| 75 | * that only becomes known once a concrete driver is assigned to the | ||
| 76 | * port -- so rather than inventing a second generic-access mechanism | ||
| 77 | * for them, IoLinkDevice simply *is* a SubDevice; the hosting | ||
| 78 | * terminal's IoLinkMaster::port() hands one back as a plain SubDevice. | ||
| 79 | * | ||
| 80 | * A fresh instance (from createIoLinkDevice(), which is also where | ||
| 81 | * pdServ/task/prefix come from -- the hosting terminal already knows | ||
| 82 | * its own pdServ/task and can compute this port's prefix the moment a | ||
| 83 | * driver name is assigned to it, same as any other SubDevice) is | ||
| 84 | * unconfigured; the hosting terminal drives the one-time setup | ||
| 85 | * sequence per port, in this order: addConfig() (identifies the | ||
| 86 | * device to the master via SDO), then addOutputs()/addInputs() | ||
| 87 | * (dynamically maps this port's slot into the terminal's PDOs -- | ||
| 88 | * returning false leaves that direction unused), then | ||
| 89 | * registerOutputs()/registerInputs() (resolves the mapped PDO entries | ||
| 90 | * against the terminal's shared domains). */ | ||
| 91 | 1 | class ECAPP_EXPORT IoLinkDevice : public SubDevice | |
| 92 | { | ||
| 93 | public: | ||
| 94 | /** A port device's PDOs live in its hosting terminal's own domain | ||
| 95 | * registration (see registerOutputs()/registerInputs() below) -- | ||
| 96 | * it never registers with a domain itself, hence nullptr/nullptr | ||
| 97 | * rather than taking domains as constructor parameters. pdServ/ | ||
| 98 | * task may be null (same convention as every other ecapp device) | ||
| 99 | * for a device that isn't meant to publish itself to pdserv. */ | ||
| 100 | 1 | IoLinkDevice(pdserv *pdServ, pdtask *task, const std::string &prefix) : | |
| 101 |
1/2✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | SubDevice(nullptr, nullptr, pdServ, task, prefix) |
| 102 | 1 | {} | |
| 103 | |||
| 104 | // Declared here but defined out-of-line in IoLinkDevice.cpp -- key | ||
| 105 | // function for this class's vtable, same reasoning as | ||
| 106 | // Error::~Error() in Exceptions.h. | ||
| 107 | ~IoLinkDevice() override; | ||
| 108 | |||
| 109 | virtual void addConfig(ec_slave_config_t *, uint16_t sdoIndex) = 0; | ||
| 110 | |||
| 111 | protected: | ||
| 112 | /** Pure byte-assembly half of configurePort() below, split out so | ||
| 113 | * it can be unit-tested without a real ec_slave_config_t/EtherCAT | ||
| 114 | * master (a test-only subclass can reach this protected member | ||
| 115 | * via a `using IoLinkDevice::buildIoLinkPortConfigSdo;` | ||
| 116 | * declaration, the same way a real device subclass would) -- see | ||
| 117 | * the .cpp for the byte-level derivation of the 22-byte CoE "IO | ||
| 118 | * Settings" (0x80n0) complete-access block this returns. */ | ||
| 119 | static std::array<uint8_t, 22> | ||
| 120 | buildIoLinkPortConfigSdo(const IoLinkPortIdentity &identity); | ||
| 121 | |||
| 122 | /** Assembles and writes the port-configuration SDO described by | ||
| 123 | * `identity`, for use from a concrete addConfig() override -- | ||
| 124 | * turns identification of a new device into stating its | ||
| 125 | * datasheet parameters instead of hand-assembling/decoding an | ||
| 126 | * opaque SDO byte blob. See the .cpp for the byte-level | ||
| 127 | * derivation (from the EL6224 manual's CoE object 0x80n0 "IO | ||
| 128 | * Settings" together with the ifm device blobs already in ecapp | ||
| 129 | * when this was introduced). Throws on failure, same as a | ||
| 130 | * hand-written addConfig() would. */ | ||
| 131 | static void configurePort( | ||
| 132 | ec_slave_config_t *sc, | ||
| 133 | uint16_t sdoIndex, | ||
| 134 | const IoLinkPortIdentity &identity); | ||
| 135 | |||
| 136 | public: | ||
| 137 | ✗ | virtual bool addOutputs( | |
| 138 | ec_slave_config_t *, | ||
| 139 | uint16_t pdoIndex, | ||
| 140 | uint16_t pdoEntryIndex, | ||
| 141 | Domain *domainOut) | ||
| 142 | { | ||
| 143 | ✗ | return false; | |
| 144 | } | ||
| 145 | |||
| 146 | ✗ | virtual bool addInputs( | |
| 147 | ec_slave_config_t *, | ||
| 148 | uint16_t pdoIndex, | ||
| 149 | uint16_t pdoEntryIndex, | ||
| 150 | Domain *domainIn) | ||
| 151 | { | ||
| 152 | ✗ | return false; | |
| 153 | } | ||
| 154 | |||
| 155 | ✗ | virtual void registerOutputs(ec_slave_config_t *, Domain *domainOut) {} | |
| 156 | |||
| 157 | ✗ | virtual void registerInputs(ec_slave_config_t *, Domain *domainIn) {} | |
| 158 | }; | ||
| 159 | |||
| 160 | /****************************************************************************/ | ||
| 161 | |||
| 162 | using IoLinkDeviceFactory = std::function<std::unique_ptr< | ||
| 163 | IoLinkDevice>(pdserv *, pdtask *, const std::string &)>; | ||
| 164 | |||
| 165 | /** One registered IO-Link driver's identity (its factory itself is not | ||
| 166 | * exposed here -- see IoLinkDeviceRegistry::find()/createIoLinkDevice()). */ | ||
| 167 | ✗ | struct IoLinkDriverDescriptor | |
| 168 | { | ||
| 169 | std::string name; // e.g. "ifm-SU6020" | ||
| 170 | std::string sourceFile; // __FILE__ of the registering translation | ||
| 171 | // unit, e.g. for a "see the source" link | ||
| 172 | }; | ||
| 173 | |||
| 174 | /** Process-wide table of known IO-Link port drivers, populated by | ||
| 175 | * self-registration (see ECAPP_REGISTER_IOLINK_DEVICE) -- the same | ||
| 176 | * pattern as the bus-level Registry in Factory.h, just scoped to what | ||
| 177 | * can be plugged into an IO-Link master's port instead of what can be | ||
| 178 | * plugged into the EtherCAT bus itself. */ | ||
| 179 | 12 | class ECAPP_EXPORT IoLinkDeviceRegistry | |
| 180 | { | ||
| 181 | public: | ||
| 182 | static IoLinkDeviceRegistry &instance(); | ||
| 183 | |||
| 184 | void | ||
| 185 | add(std::string name, | ||
| 186 | std::string sourceFile, | ||
| 187 | IoLinkDeviceFactory factory); | ||
| 188 | |||
| 189 | /** Throws UnknownDevice if no driver is registered under this | ||
| 190 | * name. */ | ||
| 191 | const IoLinkDeviceFactory &find(const std::string &name) const; | ||
| 192 | |||
| 193 | /** Every registered driver, in registration order. For | ||
| 194 | * introspection (e.g. generating a list of supported devices) -- | ||
| 195 | * createIoLinkDevice()/find() remain the way to actually | ||
| 196 | * instantiate one. */ | ||
| 197 | std::vector<IoLinkDriverDescriptor> all() const; | ||
| 198 | |||
| 199 | private: | ||
| 200 | 12 | IoLinkDeviceRegistry() = default; | |
| 201 | |||
| 202 | 900 | struct Entry | |
| 203 | { | ||
| 204 | std::string name; | ||
| 205 | std::string sourceFile; | ||
| 206 | IoLinkDeviceFactory factory; | ||
| 207 | }; | ||
| 208 | std::vector<Entry> factories; | ||
| 209 | }; | ||
| 210 | |||
| 211 | /** Throws UnknownDevice if no driver is registered under this name. | ||
| 212 | * pdServ/task may be null (same convention as every other ecapp | ||
| 213 | * device) for a device that isn't meant to publish itself to pdserv; | ||
| 214 | * prefix is unused in that case. */ | ||
| 215 | ECAPP_EXPORT std::unique_ptr<IoLinkDevice> createIoLinkDevice( | ||
| 216 | const std::string &name, | ||
| 217 | pdserv *pdServ, | ||
| 218 | pdtask *task, | ||
| 219 | const std::string &prefix); | ||
| 220 | |||
| 221 | /** Registers one IoLinkDeviceFactory as a static-init side effect; see | ||
| 222 | * ECAPP_REGISTER_IOLINK_DEVICE. */ | ||
| 223 | struct IoLinkRegistrar | ||
| 224 | { | ||
| 225 | 180 | IoLinkRegistrar( | |
| 226 | std::string name, | ||
| 227 | std::string sourceFile, | ||
| 228 | IoLinkDeviceFactory factory) | ||
| 229 | { | ||
| 230 |
1/2✓ Branch 5 taken 180 times.
✗ Branch 6 not taken.
|
720 | IoLinkDeviceRegistry::instance().add( |
| 231 | 540 | std::move(name), std::move(sourceFile), std::move(factory)); | |
| 232 | 180 | } | |
| 233 | }; | ||
| 234 | |||
| 235 | } // namespace EcApp | ||
| 236 | |||
| 237 | /****************************************************************************/ | ||
| 238 | |||
| 239 | #define ECAPP_IOLINK_CONCAT_(a, b) a##b | ||
| 240 | #define ECAPP_IOLINK_CONCAT(a, b) ECAPP_IOLINK_CONCAT_(a, b) | ||
| 241 | |||
| 242 | /** Place at namespace scope in an IO-Link driver's .cpp to make it | ||
| 243 | * discoverable via createIoLinkDevice(), e.g. | ||
| 244 | * ECAPP_REGISTER_IOLINK_DEVICE("ifm-SU6020", IfmSU6020); -- Type must | ||
| 245 | * be constructible from (pdserv *, pdtask *, const std::string &). */ | ||
| 246 | #define ECAPP_REGISTER_IOLINK_DEVICE(name, Type) \ | ||
| 247 | static const ::EcApp::IoLinkRegistrar ECAPP_IOLINK_CONCAT( \ | ||
| 248 | ecappIoLinkRegistrar_, __LINE__)( \ | ||
| 249 | name, __FILE__, \ | ||
| 250 | [](pdserv *pdServ, pdtask *task, const std::string &prefix) { \ | ||
| 251 | return std::make_unique<Type>(pdServ, task, prefix); \ | ||
| 252 | }) | ||
| 253 | |||
| 254 | #endif // ECAPP_IOLINKDEVICE_H | ||
| 255 | |||
| 256 | /****************************************************************************/ | ||
| 257 |