YARP CiA-402 EtherCAT Device 0.6.0
YARP device plugin for EtherCAT CiA-402 drives
Loading...
Searching...
No Matches
EthercatManager.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Fondazione Istituto Italiano di Tecnologia (IIT)
2// SPDX-License-Identifier: BSD-3-Clause
3
6
7// std
8#include <algorithm>
9#include <chrono>
10#include <limits>
11#include <thread>
12// yarp
13#include <yarp/os/LogStream.h>
14
15using namespace CiA402;
16
17/**
18 * @brief Helper function to create a PDO mapping entry.
19 *
20 * EtherCAT PDO mappings use a 32-bit value that encodes:
21 * - Bits 31-16: Object index (e.g., 0x6041 for statusword)
22 * - Bits 15-8: Object subindex (usually 0x00)
23 * - Bits 7-0: Size in bits (8, 16, 32, etc.)
24 *
25 * @param idx Object index (16 bits)
26 * @param sub Object subindex (8 bits)
27 * @param bits Size of the object in bits (8 bits)
28 * @return 32-bit mapping entry for use in PDO configuration
29 */
30constexpr uint32_t mapEntry(uint16_t idx, uint8_t sub, uint8_t bits)
31{
32 return (static_cast<uint32_t>(idx) << 16) | (static_cast<uint32_t>(sub) << 8) | bits;
33}
34
36
38{
39 m_runWatch = false;
40 if (m_watchThread.joinable())
41 {
42 m_watchThread.join();
43 }
44
45 // disable the synchronized clock (best-effort)
46 if (m_initialized)
47 {
48 (void)this->disableDCSync0();
49 }
50
51 // Close only if port was opened
52 if (m_portOpen)
53 {
54 ecx_close(&m_ctx);
55 }
56}
57
58EthercatManager::Error EthercatManager::configurePDOMapping(int s)
59{
60 // Helper lambdas to write SDO values of different sizes to slave 's'
61 // These wrap ec_SDOwrite with proper type casting and timeout handling
62 auto wr8 = [this, s](uint16_t idx, uint8_t sub, uint8_t v) {
63 return ecx_SDOwrite(&m_ctx, s, idx, sub, FALSE, sizeof(v), &v, EC_TIMEOUTRXM);
64 };
65 auto wr16 = [this, s](uint16_t idx, uint8_t sub, uint16_t v) {
66 return ecx_SDOwrite(&m_ctx, s, idx, sub, FALSE, sizeof(v), &v, EC_TIMEOUTRXM);
67 };
68 auto wr32 = [this, s](uint16_t idx, uint8_t sub, uint32_t v) {
69 return ecx_SDOwrite(&m_ctx, s, idx, sub, FALSE, sizeof(v), &v, EC_TIMEOUTRXM);
70 };
71
72 // --------- Clear existing PDO assignments ----------
73 // Before configuring new mappings, we must clear all existing ones
74 // 0x1C12: RxPDO assignment (master → slave data)
75 // 0x1C13: TxPDO assignment (slave → master data)
76 wr8(0x1C12, 0x00, 0); // Clear RxPDO SyncManager assignment
77 wr8(0x1C13, 0x00, 0); // Clear TxPDO SyncManager assignment
78
79 // Clear all RxPDO mapping objects (0x1600-0x160F)
80 for (uint16_t p = 0x1600; p <= 0x160F; ++p)
81 wr8(p, 0x00, 0);
82
83 // Clear all TxPDO mapping objects (0x1A00-0x1A0F)
84 for (uint16_t p = 0x1A00; p <= 0x1A0F; ++p)
85 wr8(p, 0x00, 0);
86
87 // --------- Configure RxPDO 0x1600 (master → slave, fixed mapping) ----------
88 // RxPDO contains the control data we send to the drive every cycle
89 // This mapping is fixed because all CiA402 drives support these standard objects
90 uint8_t rx_n = 0; // Entry counter for RxPDO 0x1600
91
92 // Map standard CiA402 control objects into RxPDO 0x1600:
93 wr32(0x1600, ++rx_n, mapEntry(0x6040, 0x00, 16)); // Controlword (16 bits)
94 wr32(0x1600, ++rx_n, mapEntry(0x6060, 0x00, 8)); // Modes of operation (8 bits)
95 wr32(0x1600, ++rx_n, mapEntry(0x6071, 0x00, 16)); // Target torque (16 bits)
96 wr32(0x1600, ++rx_n, mapEntry(0x607A, 0x00, 32)); // Target position (32 bits)
97 wr32(0x1600, ++rx_n, mapEntry(0x60FF, 0x00, 32)); // Target velocity (32 bits)
98
99 // Write the total number of entries to finalize the RxPDO mapping
100 wr8(0x1600, 0x00, rx_n);
101
102 // --------- Dynamic TxPDO builder with automatic rollover ----------
103 // TxPDO contains feedback data from slave to master (status, positions, etc.)
104 // Unlike RxPDO, TxPDO mapping is dynamic because:
105 // 1. Not all drives support all optional objects (vendor-specific encoders, safety signals)
106 // 2. PDOs have size limits (~8 entries), so we may need multiple TxPDOs
107 // 3. We want to gracefully handle mapping failures for optional fields
108
109 constexpr int MAX_ENTRIES_PER_PDO = 8; // Conservative limit, many drives restrict to 8 entries
110 // per PDO
111 uint32_t byteOff = 0; // Cumulative byte offset in the TxPDO process image
112 m_txMap[s - 1].clear(); // Clear any existing field mapping for this slave
113
114 // Current TxPDO being built (starts with 0x1A00, then 0x1A01, 0x1A02, etc.)
115 uint16_t curTxIdx = 0x1A00;
116 uint8_t curCount = 0; // Number of entries in current TxPDO
117 std::vector<uint16_t> txUsed; // Track which TxPDO indices we actually used
118 txUsed.reserve(4); // Expect to use at most a few TxPDOs
119
120 // Lambda to start building a new TxPDO (clear entry count, add to used list)
121 auto beginTx = [&]() {
122 curCount = 0;
123 if (wr8(curTxIdx, 0x00, 0) <= 0) // Clear the PDO entry count
124 return false;
125 txUsed.push_back(curTxIdx); // Remember we're using this PDO index
126 return true;
127 };
128
129 // Lambda to finalize current TxPDO (write final entry count)
130 auto finalizeTx = [&]() { return wr8(curTxIdx, 0x00, curCount) > 0; };
131
132 // Lambda to move to the next TxPDO (finalize current, increment index, begin new)
133 auto nextTx = [&]() {
134 if (!finalizeTx())
135 return false;
136 ++curTxIdx; // Move to next PDO index (0x1A01, 0x1A02, ...)
137 return beginTx();
138 };
139
140 // Lambda to ensure we have capacity in current TxPDO (roll over to next if full)
141 auto ensureCap = [&]() {
142 if (curCount >= MAX_ENTRIES_PER_PDO)
143 return nextTx();
144 return true;
145 };
146 // Lambda to attempt mapping a field into the current TxPDO
147 // This is the core mapping logic that:
148 // 1. Ensures we have space in the current PDO (or creates a new one)
149 // 2. Attempts to map the object/subindex into the PDO
150 // 3. If successful, records the field location in our mapping table
151 // 4. If failed, silently continues (graceful degradation for optional fields)
152 auto tryMap = [&](TxField id, uint16_t idx, uint8_t sub, uint8_t bits) {
153 if (!ensureCap()) // Make sure we have space, roll over to next PDO if needed
154 return false;
155
156 // Try to add this mapping entry to the current PDO
157 if (wr32(curTxIdx, ++curCount, mapEntry(idx, sub, bits)) <= 0)
158 {
159 // Mapping rejected by slave (object doesn't exist or not mappable)
160 // Rollback the subindex counter and continue gracefully
161 --curCount;
162 return false;
163 }
164
165 // Success! Record where this field is located in the process image
166 m_txMap[s - 1][id] = FieldInfo{id, byteOff, bits};
167 byteOff += bits / 8; // Advance byte offset for next field
168 return true;
169 };
170
171 // Start building the first TxPDO (0x1A00)
172 if (!beginTx())
173 return Error::ConfigFailed;
174
175 // --------- Map mandatory CiA-402 fields (highest priority) ----------
176 // These fields are essential for basic CiA402 operation and should be
177 // placed in the first TxPDO for optimal performance and compatibility
178 tryMap(TxField::Statusword, 0x6041, 0x00, 16); // Drive status bits
179 tryMap(TxField::OpModeDisplay, 0x6061, 0x00, 8); // Current operation mode
180 tryMap(TxField::Position6064, 0x6064, 0x00, 32); // Actual position
181 tryMap(TxField::Velocity606C, 0x606C, 0x00, 32); // Actual velocity
182 tryMap(TxField::Torque6077, 0x6077, 0x00, 16); // Actual torque
183 tryMap(TxField::PositionError6065, 0x6065, 0x00, 32); // Position error
184
185 // --------- Map optional/vendor-specific fields ----------
186 // These fields may not be supported by all drives, so we use tryMap
187 // which gracefully handles mapping failures. If a PDO becomes full,
188 // the ensureCap() logic will automatically roll over to the next PDO.
189
190 // Optional timestamp for synchronized operation
191 tryMap(TxField::Timestamp20F0, 0x20F0, 0x00, 32);
192
193 // Safety signals mapped into PDOs for real-time access
194 // NOTE: This deviates from strict CiA402 compliance but provides significant
195 // advantages in fault management and control transitions
196 tryMap(TxField::STO_6621_01, 0x6621, 0x01, 8); // Safe Torque Off status
197 tryMap(TxField::SBC_6621_02, 0x6621, 0x02, 8); // Safe Brake Control status
198
199 // Vendor-specific encoder feedbacks (dual encoder support)
200 tryMap(TxField::Enc1Pos2111_02, 0x2111, 0x02, 32); // Encoder 1 position
201 tryMap(TxField::Enc1Vel2111_03, 0x2111, 0x03, 32); // Encoder 1 velocity
202 tryMap(TxField::Enc2Pos2113_02, 0x2113, 0x02, 32); // Encoder 2 position
203 tryMap(TxField::Enc2Vel2113_03, 0x2113, 0x03, 32); // Encoder 2 velocity
204
205 // temperature
206 tryMap(TxField::TemperatureDrive, 0x2031, 0x01, 32); // Drive temperature
207
208 // Finalize the last TxPDO we were building
209 if (!finalizeTx())
210 return Error::ConfigFailed;
211
212 // --------- Assign PDOs to SyncManagers ----------
213 // SyncManagers are hardware units in the EtherCAT slave that handle
214 // the timing and synchronization of process data exchange
215
216 // RxPDO assignment: SyncManager 2 (SM2) handles outputs (master → slave)
217 // We assign our single RxPDO 0x1600 to SM2
218 wr16(0x1C12, 1, 0x1600); // Assign PDO 0x1600 to SM2, entry 1
219 wr8(0x1C12, 0x00, 1); // Set SM2 to have 1 PDO total
220
221 // TxPDO assignment: SyncManager 3 (SM3) handles inputs (slave → master)
222 // We assign all the TxPDOs we created (0x1A00, 0x1A01, etc.) to SM3
223 wr8(0x1C13, 0x00, 0); // Clear SM3 assignment first
224 for (size_t k = 0; k < txUsed.size(); ++k)
225 wr16(0x1C13, static_cast<uint8_t>(k + 1), txUsed[k]); // Assign each TxPDO
226 wr8(0x1C13, 0x00, static_cast<uint8_t>(txUsed.size())); // Set total count
227
228 // Log the mapping result for debugging
229 yCDebug(CIA402,"Slave %d '%s': configured %d RxPDOs, %d TxPDOs (%d bytes input)",
230 s,
231 m_ctx.slavelist[s].name,
232 1,
233 static_cast<int>(txUsed.size()),
234 static_cast<int>(byteOff));
235
236 return Error::NoError;
237}
238
240{
241 std::lock_guard<std::mutex> lk(m_ioMtx);
242
243 // Prevent double initialization
244 if (m_initialized)
245 {
247 }
248
249 // --------- Initialize SOEM EtherCAT stack ----------
250 // SOEM (Simple Open EtherCAT Master) is the underlying EtherCAT library
251 yCInfo(CIA402,"%s: EtherCAT: init on %s", m_kClassName.data(), ifname.c_str());
252 if (ecx_init(&m_ctx, ifname.c_str()) <= 0)
253 {
254 yCError(CIA402,"%s: cannot initialize on %s", m_kClassName.data(), ifname.c_str());
255 return Error::InitFailed;
256 }
257
258 m_portOpen = true;
259
260 // --------- Discover slaves and transition to PRE-OP ----------
261 // This scans the EtherCAT ring and identifies all connected slaves
262 // Slaves start in INIT state and are transitioned to PRE-OP for configuration
263 if (ecx_config_init(&m_ctx) <= 0)
264 {
265 ecx_close(&m_ctx);
266 m_portOpen = false;
268 }
269 yCInfo(CIA402,"%s: found %d slaves", m_kClassName.data(), (m_ctx.slavecount ? m_ctx.slavecount : 0));
270
271 // --------- Validate slave capabilities ----------
272 // Before we can configure PDOs via SDO, we need to ensure each slave:
273 // 1. Is in PRE-OP state (required for mailbox communication)
274 // 2. Supports CoE (CANopen over EtherCAT) mailbox protocol
275 // 3. Supports SDO (Service Data Objects) for configuration
276 for (int s = 1; s <= m_ctx.slavecount; ++s)
277 {
278 // Ensure slave reached PRE-OP state (required for mailbox access)
279 ecx_statecheck(&m_ctx, s, EC_STATE_PRE_OP, EC_TIMEOUTSTATE);
280
281 if (!(m_ctx.slavelist[s].mbx_proto & ECT_MBXPROT_COE))
282 {
283 yCError(CIA402,"%s: Slave %d '%s' has no CoE mailbox → cannot use SDO",
284 m_kClassName.data(),
285 s,
286 m_ctx.slavelist[s].name);
287 return Error::ConfigFailed;
288 }
289
290 // Check if slave supports SDO within CoE
291 if (!(m_ctx.slavelist[s].CoEdetails & ECT_COEDET_SDO))
292 {
293 yCError(CIA402,"%s: Slave %d '%s' has no SDO support",
294 m_kClassName.data(),
295 s,
296 m_ctx.slavelist[s].name);
297 return Error::ConfigFailed;
298 }
299 }
300
301 // --------- Initialize internal data structures ----------
302 // Prepare containers for per-slave PDO pointers and mappings
303 m_rxPtr.assign(m_ctx.slavecount, nullptr); // RxPDO pointers (master → slave)
304 m_txRaw.assign(m_ctx.slavecount, nullptr); // Raw TxPDO buffers (slave → master)
305 m_txMap.assign(m_ctx.slavecount, {}); // TxPDO field mappings
306
307 // --------- Configure PDO mappings for each slave ----------
308 // This is the most critical phase: we configure how process data is organized
309 // in the cyclic exchange. Must be done while slaves are in PRE-OP state.
310 for (int s = 1; s <= m_ctx.slavecount; ++s)
311 {
312 yCInfo(CIA402,"%s: configuring slave %d: %s", m_kClassName.data(), s, m_ctx.slavelist[s].name);
313
314 if (configurePDOMapping(s) != Error::NoError)
315 {
316 return Error::ConfigFailed;
317 }
318 }
319
320 // --------- Build process data image ----------
321 // SOEM creates a contiguous memory buffer containing all slave PDOs
322 // This "IO map" is used for efficient cyclic data exchange
323 if (ecx_config_map_group(&m_ctx, m_ioMap, 0) <= 0)
324 {
325 return Error::ConfigFailed;
326 }
327
328 // Configure distributed clocks for time synchronization (optional but recommended)
329 ecx_configdc(&m_ctx);
330
331 // =========================================================================
332 // SAFE-OP STATE TRANSITION
333 // =========================================================================
334 // SAFE-OP is an intermediate state where:
335 // - PDO configuration is locked and validated
336 // - Cyclic communication is enabled but outputs are disabled for safety
337 // - Allows one test cycle to validate the PDO mapping
338
339 constexpr std::size_t ALL = 0; // 0 == broadcast to all slaves
340
341 // Request SAFE-OP state for all slaves
342 m_ctx.slavelist[ALL].state = EC_STATE_SAFE_OP;
343 ecx_writestate(&m_ctx, ALL);
344 ecx_statecheck(&m_ctx, ALL, EC_STATE_SAFE_OP, EC_TIMEOUTSTATE * 4);
345
346 if (m_ctx.slavelist[ALL].state != EC_STATE_SAFE_OP)
347 {
348 yCError(CIA402,"%s: Ring failed to reach SAFE-OP (AL-status 0x%04x)",
349 m_kClassName.data(),
350 m_ctx.slavelist[ALL].ALstatuscode);
351 return Error::SlavesNotOp;
352 }
353
354 // --------- Validate PDO mapping with dummy cycle ----------
355 // Many drives validate their PDO mapping during the first data exchange
356 // This dummy send/receive ensures the mapping is accepted before going to OP
357 ecx_send_processdata(&m_ctx);
358 ecx_receive_processdata(&m_ctx, EC_TIMEOUTRET);
359
360 // At this point we intentionally stop at SAFE-OP. The caller will decide
361 // when to enter OP via goOperational(). We still set up pointers so SDO
362 // and PDO views work for configuration checks.
363
364 // Set up PDO access pointers (valid after config_map)
365 for (int s = 1; s <= m_ctx.slavecount; ++s)
366 {
367 m_rxPtr[s - 1] = reinterpret_cast<RxPDO*>(m_ctx.slavelist[s].outputs);
368 m_txRaw[s - 1] = reinterpret_cast<uint8_t*>(m_ctx.slavelist[s].inputs);
369 }
370
371 // Mark base initialization as complete; not operational yet.
372 m_initialized = true;
373 m_isOperational = false;
374
375 yCInfo(CIA402,"%s: EtherCAT: ring is SAFE-OP (waiting for goOperational)", m_kClassName.data());
376 return Error::NoError;
377}
378
380{
381 // Ensure we are initialized before attempting communication
382 if (!m_initialized)
383 {
385 }
386 constexpr int kImmediateRetries = 3;
387
388 // Perform cyclic process data exchange with thread safety. In SAFE-OP, inputs
389 // can still be exchanged but outputs are not applied; allow a best-effort
390 // exchange so that configuration code can probe PDOs.
391 std::lock_guard<std::mutex> lk(m_ioMtx);
392
393 auto sendReceiveOnce = [&]() {
394 ecx_send_processdata(&m_ctx);
395 m_lastWkc = ecx_receive_processdata(&m_ctx, m_pdoTimeoutUs);
396 };
397
398 for (int attempt = 0; attempt < kImmediateRetries; ++attempt)
399 {
400 sendReceiveOnce();
401
402 if (!m_isOperational)
403 {
404 // In SAFE-OP there is no strict WKC expectation; treat any receive as success
405 return (m_lastWkc >= 0) ? Error::NoError : Error::PdoExchangeFailed;
406 }
407
408 if (m_lastWkc >= m_expectedWkc)
409 {
410 m_consecutivePdoErrors = 0;
411 return Error::NoError;
412 }
413 }
414
415 // Too many consecutive failures → attempt state-based recovery.
416 if (m_consecutivePdoErrors < std::numeric_limits<int>::max())
417 {
418 ++m_consecutivePdoErrors;
419 }
420
421 // Escalate by requesting the monitor thread to intervene after repeated failures.
422 constexpr int kRecoveryThreshold = 2;
423 if (m_consecutivePdoErrors >= kRecoveryThreshold)
424 {
425 m_ctx.grouplist[0].docheckstate = 1;
426 yCWarning(CIA402,
427 "%s: sendReceive: WKC=%d expected=%d (consecutive=%d) → scheduling recovery",
428 m_kClassName.data(),
429 m_lastWkc,
430 m_expectedWkc,
431 m_consecutivePdoErrors);
432 }
433
435}
436
438{
439 if (!m_initialized)
441
442 constexpr std::size_t ALL = 0;
443
444 // Request OPERATIONAL state for all slaves
445 {
446 std::lock_guard<std::mutex> lk(m_ioMtx);
447 m_ctx.slavelist[ALL].state = EC_STATE_OPERATIONAL;
448 ecx_writestate(&m_ctx, ALL);
449 }
450
451 // Wait for OP with timeout
452 std::size_t attempts = 200;
453 const int pollTimeout = 50'000; // 50 ms
454 do
455 {
456 ecx_statecheck(&m_ctx, ALL, EC_STATE_OPERATIONAL, pollTimeout);
457 } while (attempts-- && m_ctx.slavelist[ALL].state != EC_STATE_OPERATIONAL);
458
459 if (m_ctx.slavelist[ALL].state != EC_STATE_OPERATIONAL)
460 {
461 yCError(CIA402,"%s: Ring failed to reach OP (AL-status 0x%04x)",
462 m_kClassName.data(),
463 m_ctx.slavelist[ALL].ALstatuscode);
464 return Error::SlavesNotOp;
465 }
466
467 // Compute expected WKC now that OP is active
468 m_expectedWkc = m_ctx.grouplist[0].outputsWKC * 2 + m_ctx.grouplist[0].inputsWKC;
469
470 // Start background monitor
471 m_runWatch = true;
472 m_watchThread = std::thread(&EthercatManager::errorMonitorLoop, this);
473 m_isOperational = true;
474 m_consecutivePdoErrors = 0;
475 yCInfo(CIA402,"%s: EtherCAT: ring is OPERATIONAL", m_kClassName.data());
476 return Error::NoError;
477}
478
480{
481 if (!m_initialized)
483
484 // Stop background monitor if running
485 m_runWatch = false;
486 if (m_watchThread.joinable())
487 m_watchThread.join();
488
489 // Best-effort: disable DC sync if it had been enabled
490 (void)this->disableDCSync0();
491
492 constexpr std::size_t ALL = 0;
493 {
494 std::lock_guard<std::mutex> lk(m_ioMtx);
495 m_ctx.slavelist[ALL].state = EC_STATE_PRE_OP;
496 ecx_writestate(&m_ctx, ALL);
497 }
498
499 // Wait for PRE-OP
500 std::size_t attempts = 100;
501 const int pollTimeout = 50'000; // 50 ms
502 do
503 {
504 ecx_statecheck(&m_ctx, ALL, EC_STATE_PRE_OP, pollTimeout);
505 } while (attempts-- && m_ctx.slavelist[ALL].state != EC_STATE_PRE_OP);
506
507 if (m_ctx.slavelist[ALL].state != EC_STATE_PRE_OP)
508 {
509 yCError(CIA402,"%s: Ring failed to reach PRE-OP (AL-status 0x%04x)",
510 m_kClassName.data(),
511 m_ctx.slavelist[ALL].ALstatuscode);
512 return Error::ConfigFailed;
513 }
514
515 m_isOperational = false;
516 m_consecutivePdoErrors = 0;
517 yCInfo(CIA402,"%s: EtherCAT: ring is PRE-OP", m_kClassName.data());
518 return Error::NoError;
519}
520
522{
523 std::lock_guard<std::mutex> lk(m_ioMtx);
524 yCError(CIA402,"%s: WKC=%d expected=%d, slavecount=%d",
525 m_kClassName.data(),
526 m_lastWkc,
527 m_expectedWkc,
528 m_ctx.slavecount);
529
530 // Read current state from slaves
531 ecx_readstate(&m_ctx);
532 for (int s = 1; s <= m_ctx.slavecount; ++s)
533 {
534 const ec_slavet& sl = m_ctx.slavelist[s];
535 // SM2/SM3 are typically outputs/inputs
536 int sm2 = (EC_MAXSM > 3) ? sl.SM[2].StartAddr : -1;
537 int sm3 = (EC_MAXSM > 3) ? sl.SM[3].StartAddr : -1;
538 yCError(CIA402,"%s: s%02d '%s' state=0x%02X AL=0x%04X SM2=0x%04X SM3=0x%04X",
539 m_kClassName.data(),
540 s,
541 sl.name,
542 sl.state,
543 sl.ALstatuscode,
544 sm2,
545 sm3);
546 }
547}
548
549const RxPDO* EthercatManager::getRxPDO(int slaveIdx) const noexcept
550{
551 return this->indexValid(slaveIdx) ? m_rxPtr[slaveIdx - 1] : nullptr;
552}
553
554RxPDO* EthercatManager::getRxPDO(int slaveIdx) noexcept
555{
556 return this->indexValid(slaveIdx) ? m_rxPtr[slaveIdx - 1] : nullptr;
557}
558
559TxView EthercatManager::getTxView(int slaveIdx) const noexcept
560{
561 return this->indexValid(slaveIdx) ? TxView(m_txRaw[slaveIdx - 1], &m_txMap[slaveIdx - 1])
562 : TxView(nullptr, nullptr);
563}
564
565void EthercatManager::errorMonitorLoop() noexcept
566{
567 using namespace std::chrono_literals;
568 constexpr auto baseSleep = 50ms;
569 constexpr auto maxSleep = 500ms;
570 auto sleepDuration = baseSleep;
571
572 while (m_runWatch.load())
573 {
574 bool shouldCheck = false;
575 {
576 std::lock_guard<std::mutex> lk(m_ioMtx);
577 shouldCheck = m_isOperational
578 && ((m_lastWkc < m_expectedWkc)
579 || m_ctx.grouplist[0].docheckstate != 0);
580 }
581
582 if (!shouldCheck)
583 {
585 continue;
586 }
587
588 bool actionsPerformed = false;
589 bool recoveredAny = false;
590
591 {
592 std::lock_guard<std::mutex> lk(m_ioMtx);
593
594 ecx_readstate(&m_ctx);
595
596 for (int s = 1; s <= m_ctx.slavecount; ++s)
597 {
598 ec_slavet& sl = m_ctx.slavelist[s];
599
600 if (sl.state == EC_STATE_OPERATIONAL)
601 {
602 sl.islost = 0;
603 continue;
604 }
605
606 actionsPerformed = true;
607
608 yCWarning(CIA402,
609 "%s: monitor: slave %d '%s' state=0x%02X AL=0x%04X islost=%d",
610 m_kClassName.data(),
611 s,
612 sl.name,
613 sl.state,
614 sl.ALstatuscode,
615 sl.islost);
616
617 if ((sl.state & EC_STATE_ERROR) != 0)
618 {
619 sl.state = EC_STATE_SAFE_OP + EC_STATE_ACK;
620 ecx_writestate(&m_ctx, s);
621 ecx_statecheck(&m_ctx, s, EC_STATE_SAFE_OP, EC_TIMEOUTSTATE);
622 }
623
624 if (sl.state == EC_STATE_SAFE_OP)
625 {
626 sl.state = EC_STATE_OPERATIONAL;
627 ecx_writestate(&m_ctx, s);
628 ecx_statecheck(&m_ctx, s, EC_STATE_OPERATIONAL, EC_TIMEOUTSTATE);
629 }
630
631 if (sl.state != EC_STATE_OPERATIONAL)
632 {
633 if (sl.state == EC_STATE_NONE)
634 {
635 sl.islost = 1;
636 }
637
638 int rc = 0;
639 if (sl.islost)
640 {
641 yCWarning(CIA402,
642 "%s: monitor: attempting recover on slave %d '%s'",
643 m_kClassName.data(),
644 s,
645 sl.name);
646 rc = ecx_recover_slave(&m_ctx, static_cast<uint16>(s), EC_TIMEOUTRET3);
647 }
648 else
649 {
650 yCWarning(CIA402,
651 "%s: monitor: attempting reconfig on slave %d '%s'",
652 m_kClassName.data(),
653 s,
654 sl.name);
655 rc = ecx_reconfig_slave(&m_ctx, static_cast<uint16>(s), EC_TIMEOUTRET3);
656 }
657
658 if (rc > 0)
659 {
660 sl.islost = 0;
661 sl.state = EC_STATE_OPERATIONAL;
662 ecx_writestate(&m_ctx, s);
663 ecx_statecheck(&m_ctx, s, EC_STATE_OPERATIONAL, EC_TIMEOUTSTATE);
664 }
665 else
666 {
667 sl.islost = 1;
668 }
669 }
670
671 if (sl.state == EC_STATE_OPERATIONAL)
672 {
673 recoveredAny = true;
674 yCInfo(CIA402,
675 "%s: monitor: slave %d '%s' restored to OP",
676 m_kClassName.data(),
677 s,
678 sl.name);
679 }
680 else
681 {
682 yCError(CIA402,
683 "%s: monitor: slave %d '%s' still not OP (state=0x%02X AL=0x%04X)",
684 m_kClassName.data(),
685 s,
686 sl.name,
687 sl.state,
688 sl.ALstatuscode);
689 }
690 }
691
692 if (actionsPerformed)
693 {
694 ecx_send_processdata(&m_ctx);
695 m_lastWkc = ecx_receive_processdata(&m_ctx, m_pdoTimeoutUs);
696 if (m_lastWkc >= m_expectedWkc)
697 {
698 recoveredAny = true;
699 }
700 }
701
702 if (m_ctx.grouplist[0].docheckstate)
703 {
704 m_ctx.grouplist[0].docheckstate = 0;
705 }
706 }
707
708 if (recoveredAny)
709 {
710 sleepDuration = baseSleep;
711 }
712 else if (actionsPerformed)
713 {
714 sleepDuration = std::min(sleepDuration * 2, maxSleep);
715 }
716 else
717 {
718 sleepDuration = baseSleep;
719 }
720
721 std::this_thread::sleep_for(sleepDuration);
722 }
723}
724
725std::string EthercatManager::getName(int slaveIndex) const noexcept
726{
727 if (!indexValid(slaveIndex))
728 {
729 return {};
730 }
731 return m_ctx.slavelist[slaveIndex].name;
732}
733
734bool EthercatManager::indexValid(int slaveIndex) const noexcept
735{
736 return slaveIndex >= 1 && slaveIndex <= m_ctx.slavecount;
737}
738
739EthercatManager::Error EthercatManager::enableDCSync0(uint32_t cycleNs, int32_t shiftNs) noexcept
740{
741 if (!m_initialized)
742 {
744 }
745
746 std::lock_guard<std::mutex> lk(m_ioMtx);
747
748 // Ensure DC is configured at bus level (you already call ecx_configdc in init)
749 // Now enable SYNC0 on each slave (SYNC1 disabled)
750 for (int s = 1; s <= m_ctx.slavecount; ++s)
751 {
752 // ecx_dcsync0(ctx, slave, activate, CyclTime, ShiftTime)
753 // Master-mode behavior is handled by the master; slaves just emit SYNC0
754 ecx_dcsync0(&m_ctx, s, true, cycleNs, shiftNs);
755 }
756 return Error::NoError;
757}
758
760{
761 if (!m_initialized)
762 {
764 }
765 std::lock_guard<std::mutex> lk(m_ioMtx);
766 for (int s = 1; s <= m_ctx.slavecount; ++s)
767 {
768 ecx_dcsync0(&m_ctx, s, false, 0, 0);
769 }
770 return Error::NoError;
771}
constexpr uint32_t mapEntry(uint16_t idx, uint8_t sub, uint8_t bits)
Helper function to create a PDO mapping entry.
TxView getTxView(int slaveIndex) const noexcept
Obtain a typed, bounds-checked view over the TxPDO (slave→master).
~EthercatManager()
Destructor, stops background monitoring and releases resources.
Error goPreOp() noexcept
Transition all slaves back to PRE-OP and stop monitoring.
EthercatManager()
Construct the manager (no I/O started).
Error enableDCSync0(uint32_t cycleNs, int32_t shiftNs=0) noexcept
Enable DC Sync0 for a slave.
Error init(const std::string &ifname) noexcept
Initialize the EtherCAT stack and configure slaves.
std::string getName(int slaveIndex) const noexcept
Get the name of a slave device.
Error sendReceive() noexcept
Perform one cyclic send/receive exchange over the bus.
Error goOperational() noexcept
Transition all slaves to OPERATIONAL and start monitoring.
void dumpDiagnostics() noexcept
Print quick diagnostics about bus/slave state and WKC. Call this when a PDO exchange fails to get imm...
const RxPDO * getRxPDO(int slaveIndex) const noexcept
Access the RxPDO (master→slave) buffer for a given slave.
Error disableDCSync0() noexcept
Disable DC Sync0 for a slave.
Error
Error codes returned by EthercatManager APIs.
@ PdoExchangeFailed
Failed to exchange PDOs or read SDO.
@ SlavesNotOp
Slaves did not reach OP state.
@ ConfigFailed
PDO/SDO configuration failed.
@ InitFailed
Bus/stack initialization failed.
@ NoError
Operation completed successfully.
@ NotInitialized
Operation requires a prior successful init().
@ AlreadyInitialized
init() called more than once.
@ NoSlavesFound
No EtherCAT slaves were discovered.
Lightweight view over a slave's TxPDO image.
T load(T... args)
T max(T... args)
T min(T... args)
TxField
Identifiers for fields read from a slave (TxPDO → master).
@ Velocity606C
0x606C: Velocity actual value
@ Enc2Pos2113_02
0x2113:02 Vendor enc2 position
@ SBC_6621_02
0x6621:02 Safe Brake Control status (exposed via PDO)
@ Enc2Vel2113_03
0x2113:03 Vendor enc2 velocity
@ OpModeDisplay
0x6061: Modes of operation display
@ Timestamp20F0
0x20F0: Timestamp (if supported)
@ PositionError6065
0x6065: Position demand value / error
@ Statusword
0x6041: Statusword
@ TemperatureDrive
0x2031:01 Temperature (if supported)
@ Position6064
0x6064: Position actual value
@ Torque6077
0x6077: Torque actual value
@ Enc1Pos2111_02
0x2111:02 Vendor enc1 position
@ Enc1Vel2111_03
0x2111:03 Vendor enc1 velocity
@ STO_6621_01
0x6621:01 Safe Torque Off status (exposed via PDO)
T push_back(T... args)
T reserve(T... args)
T size(T... args)
T sleep_for(T... args)
Process Data Object written from master to slave (RxPDO).