GCC Code Coverage Report


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