GCC Code Coverage Report


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