GCC Code Coverage Report


Directory: ./
File: src/devices/Ex4xxx.cpp
Date: 2026-09-23 16:22:15
Exec Total Coverage
Lines: 65 101 64.4%
Branches: 42 270 15.6%

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 #include "devices/Ex4xxx.h"
24
25 #include "SlaveContext.h"
26 #include "ecapp/ConfigurableScale.h"
27 #include "ecapp/Domain.h"
28 #include "ecapp/Exceptions.h"
29 #include "ecapp/Factory.h"
30 #include "ecapp/Master.h"
31 #include "ecapp/SwappedSync.h"
32
33 #include <memory>
34 #include <sstream>
35 #include <stdexcept>
36
37 /****************************************************************************/
38
39 using std::make_unique;
40 using std::runtime_error;
41 using std::size_t;
42 using std::stringstream;
43 using std::unique_ptr;
44
45 namespace EcApp {
46
47 /* Master 0, Slave 2, "EL4022"
48 * Vendor ID: 0x00000002
49 * Product code: 0x0fb63052
50 * Revision number: 0x00140000
51 */
52
53 ec_pdo_entry_info_t el40x2_pdo_entries[] = {
54 {0x7000, 0x01, 16}, /* Analog output */
55 {0x7010, 0x01, 16}, /* Analog output */
56 };
57
58 ec_pdo_info_t el40x2_pdos[] = {
59 {0x1600, 1, el40x2_pdo_entries + 0}, /* AO RxPDO-Map Outputs Ch.1 */
60 {0x1601, 1, el40x2_pdo_entries + 1}, /* AO RxPDO-Map Outputs Ch.2 */
61 };
62
63 ec_sync_info_t el40x2_syncs[] = {
64 {0, EC_DIR_OUTPUT, 0, NULL, EC_WD_DISABLE},
65 {1, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE},
66 {2, EC_DIR_OUTPUT, 2, el40x2_pdos + 0, EC_WD_DISABLE},
67 {3, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE},
68 {0xff}};
69
70 /* Master 0, Slave 2, "EL4028"
71 * Vendor ID: 0x00000002
72 * Product code: 0x0fbc3052
73 * Revision number: 0x00140000
74 */
75
76 ec_pdo_entry_info_t el4028_pdo_entries[] = {
77 {0x7000, 0x01, 16}, /* Analog output */
78 {0x7010, 0x01, 16}, /* Analog output */
79 {0x7020, 0x01, 16}, /* Analog output */
80 {0x7030, 0x01, 16}, /* Analog output */
81 {0x7040, 0x01, 16}, /* Analog output */
82 {0x7050, 0x01, 16}, /* Analog output */
83 {0x7060, 0x01, 16}, /* Analog output */
84 {0x7070, 0x01, 16}, /* Analog output */
85 };
86
87 ec_pdo_info_t el4028_pdos[] = {
88 {0x1600, 1, el4028_pdo_entries + 0}, /* AO RxPDO-Map Outputs Ch.1 */
89 {0x1601, 1, el4028_pdo_entries + 1}, /* AO RxPDO-Map Outputs Ch.2 */
90 {0x1602, 1, el4028_pdo_entries + 2}, /* AO RxPDO-Map Outputs Ch.3 */
91 {0x1603, 1, el4028_pdo_entries + 3}, /* AO RxPDO-Map Outputs Ch.4 */
92 {0x1604, 1, el4028_pdo_entries + 4}, /* AO RxPDO-Map Outputs Ch.5 */
93 {0x1605, 1, el4028_pdo_entries + 5}, /* AO RxPDO-Map Outputs Ch.6 */
94 {0x1606, 1, el4028_pdo_entries + 6}, /* AO RxPDO-Map Outputs Ch.7 */
95 {0x1607, 1, el4028_pdo_entries + 7}, /* AO RxPDO-Map Outputs Ch.8 */
96 };
97
98 ec_sync_info_t el4028_syncs[] = {
99 {0, EC_DIR_OUTPUT, 0, NULL, EC_WD_DISABLE},
100 {1, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE},
101 {2, EC_DIR_OUTPUT, 8, el4028_pdos + 0, EC_WD_DISABLE},
102 {3, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE},
103 {0xff}};
104
105 /* Master 0, Slave 18, "EL4034"
106 * Vendor ID: 0x00000002
107 * Product code: 0x0fc23052
108 * Revision number: 0x00140000
109 */
110
111 ec_pdo_entry_info_t el4034_pdo_entries[] = {
112 {0x7000, 0x01, 16}, /* Analog output */
113 {0x7010, 0x01, 16}, /* Analog output */
114 {0x7020, 0x01, 16}, /* Analog output */
115 {0x7030, 0x01, 16}, /* Analog output */
116 };
117
118 ec_pdo_info_t el4034_pdos[] = {
119 {0x1600, 1, el4034_pdo_entries + 0}, /* RxPDO-Map OutputsCh.1 */
120 {0x1601, 1, el4034_pdo_entries + 1}, /* RxPDO-Map OutputsCh.2 */
121 {0x1602, 1, el4034_pdo_entries + 2}, /* RxPDO-Map OutputsCh.3 */
122 {0x1603, 1, el4034_pdo_entries + 3}, /* RxPDO-Map OutputsCh.4 */
123 };
124
125 ec_sync_info_t el4034_syncs[] = {
126 {0, EC_DIR_OUTPUT, 0, NULL, EC_WD_DISABLE},
127 {1, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE},
128 {2, EC_DIR_OUTPUT, 4, el4034_pdos + 0, EC_WD_DISABLE},
129 {3, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE},
130 {0xff}};
131
132 /* Master 0, Slave 15, "EL4102"
133 * Vendor ID: 0x00000002
134 * Product code: 0x10063052
135 * Revision number: 0x03fc0000
136 */
137
138 ec_pdo_entry_info_t el4102_pdo_entries[] = {
139 {0x3001, 0x01, 16}, /* Output */
140 {0x3002, 0x01, 16}, /* Output */
141 };
142
143 ec_pdo_info_t el4102_pdos[] = {
144 {0x1600, 1, el4102_pdo_entries + 0}, /* RxPDO 01 mapping */
145 {0x1601, 1, el4102_pdo_entries + 1}, /* RxPDO 02 mapping */
146 };
147
148 ec_sync_info_t el4102_syncs[] = {
149 {0, EC_DIR_OUTPUT, 0, NULL, EC_WD_DISABLE},
150 {1, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE},
151 {2, EC_DIR_OUTPUT, 2, el4102_pdos + 0, EC_WD_DISABLE},
152 {3, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE},
153 {0xff}};
154
155 /* Master 0, Slave 5, "EL4134"
156 * Vendor ID: 0x00000002
157 * Product code: 0x10263052
158 * Revision number: 0x03fc0000
159 */
160
161 ec_pdo_entry_info_t el4134_pdo_entries[] = {
162 {0x7000, 0x01, 16}, /* Analog output */
163 {0x7010, 0x01, 16}, /* Analog output */
164 {0x7020, 0x01, 16}, /* Analog output */
165 {0x7030, 0x01, 16}, /* Analog output */
166 };
167
168 ec_pdo_info_t el4134_pdos[] = {
169 {0x1600, 1, el4134_pdo_entries + 0}, /* AO RxPDO-Map OutputsCh.1 */
170 {0x1601, 1, el4134_pdo_entries + 1}, /* AO RxPDO-Map OutputsCh.2 */
171 {0x1602, 1, el4134_pdo_entries + 2}, /* AO RxPDO-Map OutputsCh.3 */
172 {0x1603, 1, el4134_pdo_entries + 3}, /* AO RxPDO-Map OutputsCh.4 */
173 };
174
175 ec_sync_info_t el4134_syncs[] = {
176 {0, EC_DIR_OUTPUT, 0, NULL, EC_WD_DISABLE},
177 {1, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE},
178 {2, EC_DIR_OUTPUT, 4, el4134_pdos + 0, EC_WD_DISABLE},
179 {3, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE},
180 {0xff}};
181
182 /****************************************************************************/
183
184 struct Ex4xxxType
185 {
186 const char *name;
187 uint32_t product_code;
188 unsigned int num_channels;
189 uint16_t entry_index;
190 uint16_t index_inc;
191 ec_sync_info_t *sync;
192 };
193
194 const static Ex4xxxType ex4xxxType[Ex4xxx<Mode::Control>::NumTypes] = {
195 {"EL4002", 0x0fa23052, 2, 0x7000, 0x0010, el40x2_syncs},
196 {"EL4012", 0x0fac3052, 2, 0x7000, 0x0010, el40x2_syncs},
197 {"EL4022", 0x0fb63052, 2, 0x7000, 0x0010, el40x2_syncs},
198 {"EL4028", 0x0fbc3052, 8, 0x7000, 0x0010, el4028_syncs},
199 {"EL4034", 0x0fc23052, 4, 0x7000, 0x0010, el4034_syncs},
200 {"EL4102", 0x10063052, 2, 0x3001, 0x0001, el4102_syncs},
201 {"EL4134", 0x10263052, 4, 0x7000, 0x0010, el4134_syncs},
202 };
203
204 /****************************************************************************/
205
206 template <Mode mode>
207 1 Ex4xxx<mode>::Ex4xxx(
208 Master *master,
209 uint16_t alias,
210 uint16_t position,
211 Domain *domainOut,
212 Type typeIdx) :
213 1 type(ex4xxxType[typeIdx]),
214 sc(NULL),
215 domainOut(domainOut),
216 1 off_out(type.num_channels, 0),
217 1 fullScale(type.num_channels, 1.0),
218 1 input(type.num_channels, 0.0),
219 1 enableForcing(type.num_channels, 0),
220 1 forcedValue(type.num_channels, 0.0),
221
6/24
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 7 taken 1 times.
✗ Branch 8 not taken.
✓ Branch 12 taken 1 times.
✗ Branch 13 not taken.
✓ Branch 17 taken 1 times.
✗ Branch 18 not taken.
✓ Branch 22 taken 1 times.
✗ Branch 23 not taken.
✓ Branch 27 taken 1 times.
✗ Branch 28 not taken.
✗ Branch 45 not taken.
✗ Branch 46 not taken.
✗ Branch 50 not taken.
✗ Branch 51 not taken.
✗ Branch 55 not taken.
✗ Branch 56 not taken.
✗ Branch 60 not taken.
✗ Branch 61 not taken.
✗ Branch 65 not taken.
✗ Branch 66 not taken.
✗ Branch 70 not taken.
✗ Branch 71 not taken.
6 output(type.num_channels, 0.0)
222 {
223 // pdserv publishing happens in Ex4xxxAdapter's constructor below,
224 // via SubDevice::registerOutputChannel()/registerParameter() --
225 // that is also what makes "Output" alias-able through
226 // SubDevice::setAlias().
227
228
1/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1 if (!master) {
229 1 return;
230 }
231
232 ✗ sc = ecrt_master_slave_config(
233 master->getMaster(), alias, position, 0x00000002,
234 ✗ type.product_code);
235 ✗ if (!sc) {
236 ✗ stringstream err;
237 ✗ err << "Failed to get slave configuration.";
238 ✗ throw runtime_error(err.str());
239 }
240
241 ✗ if (ecrt_slave_config_pdos(
242 ✗ sc, EC_END, SwappedSync<mode>(type.sync).getSync())) {
243 ✗ stringstream err;
244 ✗ err << "Failed to configure PDOs.";
245 ✗ throw runtime_error(err.str());
246 }
247
248 ✗ for (unsigned int c = 0; c < type.num_channels; c++) {
249 ✗ unsigned int entry_index = type.entry_index + type.index_inc * c;
250 ✗ off_out[c] = ecrt_slave_config_reg_pdo_entry(
251 sc, entry_index, 0x01, domainOut->getDomain(), NULL);
252 ✗ if (off_out[c] < 0) {
253 ✗ stringstream err;
254 ✗ err << "Failed to register Ex4xxx value PDO entry.";
255 ✗ throw runtime_error(err.str());
256 }
257 }
258 }
259
260 /****************************************************************************/
261
262 template <Mode mode>
263 1 void Ex4xxx<mode>::setFullScale(unsigned int channel, double newScale)
264 {
265
1/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1 if (channel < type.num_channels) {
266 1 fullScale[channel] = newScale;
267 }
268 1 }
269
270 /****************************************************************************/
271
272 template <Mode mode>
273 3 void Ex4xxx<mode>::setOutput(unsigned int channel, double extValue)
274 {
275
1/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
3 if (channel < type.num_channels) {
276 3 input[channel] = extValue;
277 }
278 3 }
279
280 /****************************************************************************/
281
282 template <Mode mode>
283 ✗ double Ex4xxx<mode>::getOutput(unsigned int channel)
284 {
285 ✗ if (domainOut->getData() && channel < type.num_channels) {
286 ✗ int16_t raw = EC_READ_S16(domainOut->getData() + off_out[channel]);
287 ✗ return raw * fullScale[channel] / 32768.0;
288 }
289 else {
290 ✗ return 0.0;
291 }
292 }
293
294 /****************************************************************************/
295
296 template <Mode mode>
297 1 void Ex4xxx<mode>::update()
298 {
299
2/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
3 for (unsigned int channel = 0; channel < type.num_channels; channel++) {
300
1/4
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
2 output[channel] = enableForcing[channel] ? forcedValue[channel]
301 : input[channel];
302
303
1/4
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
2 if (domainOut->getData()) {
304 double raw;
305
306 ✗ if (fullScale[channel]) {
307 ✗ raw = output[channel] / fullScale[channel] * 32768.0;
308 }
309 else {
310 ✗ raw = 0;
311 }
312
313 int16_t out;
314 ✗ if (raw > 32767.0) {
315 ✗ out = 32767;
316 }
317 ✗ else if (raw < -32768) {
318 ✗ out = -32768;
319 }
320 else {
321 ✗ out = raw;
322 }
323
324 ✗ EC_WRITE_S16(domainOut->getData() + off_out[channel], out);
325 }
326 }
327 1 }
328
329 /****************************************************************************/
330
331 template <Mode mode>
332 1 unsigned int Ex4xxx<mode>::numChannels() const
333 {
334 1 return type.num_channels;
335 }
336
337 /****************************************************************************/
338
339 template class Ex4xxx<Mode::Control>;
340 template class Ex4xxx<Mode::Simulation>;
341
342 /****************************************************************************/
343 // Generic SubDevice adapter: pure analog output, no inputs. Application
344 // code never names Ex4xxx, only e.g. "EL4022" -- the factory resolves
345 // it.
346 /****************************************************************************/
347
348 template <Mode mode>
349 2 class Ex4xxxAdapter : public SubDevice, public ConfigurableScale
350 {
351 public:
352 1 Ex4xxxAdapter(const SlaveContext &ctx, typename Ex4xxx<mode>::Type t) :
353 1 SubDevice(ctx.domainOut, nullptr, ctx.pdServ, ctx.task, ctx.prefix),
354 3 device(ctx.master, ctx.alias, ctx.position, ctx.domainOut, t),
355
7/28
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 7 taken 1 times.
✗ Branch 8 not taken.
✓ Branch 12 taken 1 times.
✗ Branch 13 not taken.
✓ Branch 15 taken 1 times.
✗ Branch 16 not taken.
✓ Branch 19 taken 1 times.
✗ Branch 20 not taken.
✓ Branch 22 taken 1 times.
✓ Branch 23 taken 1 times.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 40 not taken.
✗ Branch 41 not taken.
✗ Branch 45 not taken.
✗ Branch 46 not taken.
✗ Branch 50 not taken.
✗ Branch 51 not taken.
✗ Branch 53 not taken.
✗ Branch 54 not taken.
✗ Branch 57 not taken.
✗ Branch 58 not taken.
✗ Branch 60 not taken.
✗ Branch 61 not taken.
✗ Branch 67 not taken.
✗ Branch 68 not taken.
5 outputKinds_ {{"Output", device.numChannels(), double(0)}}
356 {
357 // device.input is the write target the generic (kind, index)
358 // API's "Output" channel resolves to (see setOutputAt()
359 // below), hence registered under that same name -- not under
360 // its old pdserv-only path "/Input" (the terminal's own
361 // "input" to its forcing/scaling logic), so setAlias("Output",
362 // ...) can find it. device.output (the effective, post-forcing
363 // value) has no matching ChannelKind -- it's a read-only
364 // diagnostic signal, not itself part of the generic API -- so
365 // it keeps its own pdserv-only name instead.
366
2/8
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
1 registerOutputChannel("Output", device.input);
367
2/8
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
1 registerOutputChannel("EffectiveOutput", device.output);
368
3/12
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 1 times.
✗ Branch 10 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
1 registerParameter("FullScale", "Output", device.fullScale);
369
3/12
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 1 times.
✗ Branch 10 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
1 registerParameter(
370 "EnableForcing", "Output", device.enableForcing,
371 AsBoolean {});
372
3/12
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 1 times.
✗ Branch 10 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
1 registerParameter("ForcedValue", "Output", device.forcedValue);
373 1 }
374
375 1 void updateInputs() override {}
376 1 void updateOutputs() override { device.update(); }
377
378 2 const std::vector<ChannelKind> &inputKinds() const override
379 {
380 2 return inputKinds_;
381 }
382 12 const std::vector<ChannelKind> &outputKinds() const override
383 {
384 12 return outputKinds_;
385 }
386
387 1 void setScale(unsigned int channel, double scale) override
388 {
389 1 device.setFullScale(channel, scale);
390 1 }
391
392 /** Ex4xxx has no offset concept -- a legitimate no-op. */
393 1 void setOffset(unsigned int, double) override {}
394
395 protected:
396 ✗ ChannelValue getInputAt(size_t) const override
397 {
398 ✗ throw UnknownChannel("Ex4xxx has no input channels.");
399 }
400 4 void setOutputAt(size_t i, const ChannelValue &value) override
401 {
402 7 device.setOutput(
403 4 static_cast<unsigned int>(i), std::get<double>(value));
404 3 }
405
406 private:
407 Ex4xxx<mode> device;
408 std::vector<ChannelKind> inputKinds_;
409 std::vector<ChannelKind> outputKinds_;
410 };
411
412 /****************************************************************************/
413
414 namespace {
415
416 84 DeviceFactoryFn makeEx4xxxFactory(unsigned int typeIdx)
417 {
418 2 return [typeIdx](const SlaveContext &ctx) -> unique_ptr<SubDevice> {
419
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (ctx.mode == Mode::Control) {
420 ✗ return make_unique<Ex4xxxAdapter<Mode::Control>>(
421 ✗ ctx, static_cast<Ex4xxx<Mode::Control>::Type>(typeIdx));
422 }
423 else {
424
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
2 return make_unique<Ex4xxxAdapter<Mode::Simulation>>(
425 ctx,
426 3 static_cast<Ex4xxx<Mode::Simulation>::Type>(typeIdx));
427 }
428 84 };
429 }
430
431 struct Ex4xxxRegistrations
432 {
433 12 Ex4xxxRegistrations()
434 {
435
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 12 times.
96 for (unsigned int i = 0; i < Ex4xxx<Mode::Control>::NumTypes; i++) {
436 84 const auto &t = ex4xxxType[i];
437
4/8
✓ Branch 3 taken 84 times.
✗ Branch 4 not taken.
✓ Branch 7 taken 84 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 84 times.
✗ Branch 11 not taken.
✓ Branch 13 taken 84 times.
✗ Branch 14 not taken.
420 Registry::instance().add(
438 168 {t.name, __FILE__, 0x00000002, t.product_code,
439
1/2
✓ Branch 1 taken 84 times.
✗ Branch 2 not taken.
168 AnyRevision, wrapFactory(makeEx4xxxFactory(i))});
440 }
441 12 }
442 12 } ex4xxxRegistrations;
443
444 } // namespace
445
446 36 } // namespace EcApp
447
448 /****************************************************************************/
449