YARP CiA-402 EtherCAT Device 0.6.0
YARP device plugin for EtherCAT CiA-402 drives
Loading...
Searching...
No Matches
CiA402MotionControl.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
9
10// YARP
11#include <yarp/os/LogStream.h>
12#include <yarp/os/Value.h>
13
14// STD
15#include <algorithm>
16#include <chrono>
17#include <cmath>
18#include <cstring>
19#include <memory>
20#include <mutex>
21#include <string>
22#include <string_view>
23#include <vector>
24
25namespace yarp::dev
26{
27
28/**
29 * Private implementation holding configuration, runtime state, and helpers.
30 * Concurrency:
31 * - variables.mutex guards feedback snapshots returned to YARP APIs
32 * - controlModeState.mutex guards target/active control modes
33 * - setPoints.mutex guards user setpoints and their first‑cycle latches
34 * Units:
35 * - Positions: degrees (joint and motor)
36 * - Velocities: degrees/second
37 * - Torques: joint Nm (converted to motor Nm and then to per‑thousand for PDOs)
38 */
40{
41 enum class PosSrc
42 {
43 S6064, // Use CiA 402 Position Actual Value (0x6064)
44 Enc1, // Use encoder 1 position (0x2111:02 in PDO)
45 Enc2 // Use encoder 2 position (0x2113:02 in PDO)
46 };
47 // Which object supplies velocity feedback for a given consumer (joint/motor)
48 enum class VelSrc
49 {
50 S606C, // Use CiA 402 Velocity Actual Value (0x606C) [rpm]
51 Enc1, // Use encoder 1 velocity (0x2111:03) [rpm]
52 Enc2 // Use encoder 2 velocity (0x2113:03) [rpm]
53 };
54 // Where each encoder is physically mounted relative to the gearbox
55 enum class Mount
56 {
57 None, // Not present / unknown
58 Motor, // Pre‑gearbox (motor shaft)
59 Joint // Post‑gearbox (load/joint shaft)
60 };
61 // Which sensor the drive uses internally for the closed loop
62 enum class SensorSrc
63 {
64 Unknown, // Not reported via SDOs (fallback heuristics apply)
65 Enc1, // Internal loop uses encoder 1
66 Enc2 // Internal loop uses encoder 2
67 };
68
69 // Human-readable name for log messages
70 static constexpr std::string_view kClassName = "CiA402MotionControl";
71
72 // EtherCat manager
74
75 // mutex to protect the run and the stop
77
78 // Constants (unit conversions used throughout)
79 static constexpr double MICROSECONDS_TO_SECONDS = 1e-6; // µs → s
80 // Device timestamp wraps every ~42.949672 s (spec: (2^32 - 1)/100 µs)
81 // Use +1 to account for the fractional remainder and avoid drift negative steps.
82 // 42,949,673 µs
83 static constexpr uint64_t TIMESTAMP_WRAP_PERIOD_US = (((uint64_t)1 << 32) - 1) / 100 + 1;
84 // Heuristic for wrap detection: consider a wrap only if the value is jumps backward by more
85 // than half of its range. Small backward steps can happen due to clock adjustments/resets and
86 // must not be treated as wraps.
87 static constexpr uint32_t TIMESTAMP_WRAP_HALF_RANGE = TIMESTAMP_WRAP_PERIOD_US / 2;
88 // Parameters that come from the .xml file (see open())
89 //--------------------------------------------------------------------------
90 size_t numAxes{0}; // how many joints we expose to YARP
91 int firstSlave{1}; // bus index of the first drive we care about
92 std::string expectedName{}; // sanity-check each slave.name if not empty
93
94 //--------------------------------------------------------------------------
95 // Runtime bookkeeping
96 //--------------------------------------------------------------------------
97 std::vector<double> gearRatio; // motor-revs : load-revs
99 std::vector<double> torqueConstants; // [Nm/A] from 0x2003:01
103 std::vector<uint32_t> enc1Res, enc2Res; // from 0x2110, 0x2112
105 std::vector<Mount> enc1Mount, enc2Mount; // from config (and SDO sanity)
106 std::vector<PosSrc> posSrcJoint, posSrcMotor; // from config (0x2012:08 semantics)
107 std::vector<VelSrc> velSrcJoint, velSrcMotor; // from config (0x2011:04 semantics)
108 std::vector<SensorSrc> posLoopSrc; // drive internal pos loop source (0x2012:09)
109 std::vector<SensorSrc> velLoopSrc; // drive internal vel loop source (0x2011:05)
110 std::vector<double> velToDegS; // multiply device value to get deg/s
111 std::vector<double> degSToVel; // multiply deg/s to get device value
112 std::vector<bool> invertedMotionSenseDirection; // if true the torque, current, velocity and
113 // position have inverted sign
114 struct Limits
115 {
118 std::vector<bool> usePositionLimitsFromConfig; // if false, read 0x607D from SDO and cache
119 // here
120 std::mutex mutex; // protects *all* the above vectors
122
124 {
125 mutable std::mutex mutex; // protect the variables in this structure from concurrent access
126 std::vector<double> motorEncoders; // for getMotorEncoders()
127 std::vector<double> motorVelocities; // for getMotorVelocities()
128 std::vector<double> motorAccelerations; // for getMotorAccelerations()
129 std::vector<double> jointPositions; // for getJointPositions()
130 std::vector<double> jointVelocities; // for getJointVelocities()
131 std::vector<double> jointAccelerations; // for getJointAccelerations()
132 std::vector<double> jointTorques; // for getJointTorques()
133 std::vector<double> motorCurrents; // for getCurrents()
134 std::vector<double> feedbackTime; // feedback time in seconds. Computed from
135 // drive’s own internal timestep that starts when the
136 // drive boots (or resets)
140 std::vector<bool> targetReached; // cached Statusword bit 10
141 std::vector<double> driveTemperatures; // cached drive temperature if available
142
144 {
145 this->motorEncoders.resize(numAxes);
146 this->motorVelocities.resize(numAxes);
147 this->motorAccelerations.resize(numAxes);
148 this->feedbackTime.resize(numAxes);
149 this->jointPositions.resize(numAxes);
150 this->jointVelocities.resize(numAxes);
151 this->jointAccelerations.resize(numAxes);
152 this->jointNames.resize(numAxes);
153 this->jointTorques.resize(numAxes);
154 this->motorCurrents.resize(numAxes);
155 this->STO.resize(numAxes);
156 this->SBC.resize(numAxes);
157 this->targetReached.resize(numAxes);
158 this->driveTemperatures.resize(numAxes);
159 }
160 };
161
163
165 {
166 std::vector<int> target; // what the user asked for
167 std::vector<int> active; // what the drive is really doing
168 std::vector<int> cstFlavor; // custom flavor for each axis (torque/current)
169 std::vector<int> prevCstFlavor; // previous flavor, for change detection
170 mutable std::mutex mutex; // protects *both* vectors
171
173 {
174 target.assign(n, VOCAB_CM_IDLE);
175 active = target;
176 cstFlavor.assign(n, VOCAB_CM_UNKNOWN);
178 }
179 };
181
183 {
184 std::mutex mutex; // protects the following vectors
186 std::vector<double> jointVelocities; // for velocityMove() or joint velocity commands
188 std::vector<bool> hasTorqueSP; // user provided a torque since entry?
189 std::vector<bool> hasVelSP; // user provided a velocity since entry?
190 std::vector<bool> hasCurrentSP; // user provided a current since entry?
191
192 std::vector<double> ppJointTargetsDeg; // last requested PP target [deg] joint-side
193 std::vector<int32_t> ppTargetCounts; // computed PP target for 0x607A (loop-shaft)
194 std::vector<bool> ppHasPosSP; // a new PP command available this cycle?
195 std::vector<bool> ppIsRelative; // this PP set-point is relative
196 std::vector<bool> ppPulseHi; // drive CW bit4 high this cycle?
197 std::vector<bool> ppPulseCoolDown; // bring bit4 low next
198
201
203 {
204 jointTorques.resize(n);
205 jointVelocities.resize(n);
206 motorCurrents.resize(n);
207 hasTorqueSP.assign(n, false);
208 hasVelSP.assign(n, false);
209 hasCurrentSP.assign(n, false);
210
211 ppJointTargetsDeg.resize(n);
212 ppTargetCounts.resize(n);
213 ppHasPosSP.assign(n, false);
214 ppIsRelative.assign(n, false);
215 ppPulseHi.assign(n, false);
216 ppPulseCoolDown.assign(n, false);
217
220 }
221
222 void reset()
223 {
224 std::lock_guard<std::mutex> lock(this->mutex);
225 std::fill(this->jointTorques.begin(), this->jointTorques.end(), 0.0);
226 std::fill(this->jointVelocities.begin(), this->jointVelocities.end(), 0.0);
227 std::fill(this->motorCurrents.begin(), this->motorCurrents.end(), 0.0);
228 std::fill(this->hasTorqueSP.begin(), this->hasTorqueSP.end(), false);
229 std::fill(this->hasCurrentSP.begin(), this->hasCurrentSP.end(), false);
230 std::fill(this->hasVelSP.begin(), this->hasVelSP.end(), false);
231 std::fill(this->ppJointTargetsDeg.begin(), this->ppJointTargetsDeg.end(), 0.0);
232 std::fill(this->ppTargetCounts.begin(), this->ppTargetCounts.end(), 0);
233 std::fill(this->ppHasPosSP.begin(), this->ppHasPosSP.end(), false);
234 std::fill(this->ppIsRelative.begin(), this->ppIsRelative.end(), false);
235 std::fill(this->ppPulseHi.begin(), this->ppPulseHi.end(), false);
236 std::fill(this->ppPulseCoolDown.begin(), this->ppPulseCoolDown.end(), false);
237
238 std::fill(this->positionDirectJointTargetsDeg.begin(),
239 this->positionDirectJointTargetsDeg.end(),
240 0.0);
241 std::fill(this->positionDirectTargetCounts.begin(),
242 this->positionDirectTargetCounts.end(),
243 0);
244 }
245
246 void reset(int axis)
247 {
248 std::lock_guard<std::mutex> lock(this->mutex);
249 this->jointTorques[axis] = 0.0;
250 this->jointVelocities[axis] = 0.0;
251 this->motorCurrents[axis] = 0.0;
252 this->hasTorqueSP[axis] = false;
253 this->hasCurrentSP[axis] = false;
254 this->ppJointTargetsDeg[axis] = 0.0;
255 this->ppTargetCounts[axis] = 0;
256 this->hasVelSP[axis] = false;
257 this->ppHasPosSP[axis] = false;
258 this->ppIsRelative[axis] = false;
259 this->ppPulseHi[axis] = false;
260 this->ppPulseCoolDown[axis] = false;
261 this->positionDirectJointTargetsDeg[axis] = 0.0;
262 this->positionDirectTargetCounts[axis] = 0;
263 }
264 };
265
267
268 /* --------------- CiA-402 power-state machine ------------------------ */
270
271 // First-cycle latches and seed
273 std::vector<bool> posLatched; // true if we have a valid target
276
277 // Timestamp unwrap (per-axis) for 32-bit drive timestamps
278 std::vector<uint32_t> tsLastRaw; // last raw 32-bit timestamp read from drive
279 std::vector<uint64_t> tsWraps; // number of wraps (increments on large backward jumps)
280
289
290 bool opRequested{false}; // true after the first run() call
291
292 // ------------- runtime profiling (run() duration and 5s average) -------------
293 std::chrono::steady_clock::time_point avgWindowStart{std::chrono::steady_clock::now()};
294 double runTimeAccumSec{0.0};
296
297 /** Dummy interaction mode for all the joints */
298 yarp::dev::InteractionModeEnum dummyInteractionMode{
299 yarp::dev::InteractionModeEnum::VOCAB_IM_STIFF};
300
301 Impl() = default;
302 ~Impl() = default;
303
304 // Convert joint degrees to loop-shaft counts for 0x607A, using the configured
305 // position-loop source (Enc1/Enc2) and mount (Motor/Joint).
306 int32_t jointDegToTargetCounts(size_t j, double jointDeg) const
307 {
308 // choose source and resolution
309 uint32_t res = 0;
310 Mount m = Mount::None;
311 switch (posLoopSrc[j])
312 {
313 case SensorSrc::Enc1:
314 res = enc1Res[j];
315 m = enc1Mount[j];
316 break;
317 case SensorSrc::Enc2:
318 res = enc2Res[j];
319 m = enc2Mount[j];
320 break;
321 default:
322 if (enc1Mount[j] != Mount::None)
323 {
324 res = enc1Res[j];
325 m = enc1Mount[j];
326 } else
327 {
328 res = enc2Res[j];
329 m = enc2Mount[j];
330 }
331 break;
332 }
333 double shaftDeg = jointDeg;
334 if (m == Mount::Motor)
335 shaftDeg = jointDeg * gearRatio[j];
336 else if (m == Mount::Joint)
337 shaftDeg = jointDeg; // pass-through
338 const double cnt = (res ? (shaftDeg / 360.0) * static_cast<double>(res) : 0.0);
339 long long q = llround(cnt);
344 return static_cast<int32_t>(q);
345 }
346
347 // Convert loop-shaft counts (0x607D/0x607A domain) back to joint degrees,
348 // applying encoder mount, gear ratio, and motion-sense inversion.
349 double targetCountsToJointDeg(size_t j, int32_t counts) const
350 {
351 // Apply motion-sense inversion: counts stored in the drive follow the loop shaft.
352 // If our joint coordinate is inverted, flip the sign to get a consistent mapping.
353 const bool inv = invertedMotionSenseDirection[j];
354 const int32_t cAdj = inv ? -counts : counts;
355
356 // choose source and resolution/mount just like jointDegToTargetCounts
357 uint32_t res = 0;
358 Mount m = Mount::None;
359 switch (posLoopSrc[j])
360 {
361 case SensorSrc::Enc1:
362 res = enc1Res[j];
363 m = enc1Mount[j];
364 break;
365 case SensorSrc::Enc2:
366 res = enc2Res[j];
367 m = enc2Mount[j];
368 break;
369 default:
370 if (enc1Mount[j] != Mount::None)
371 {
372 res = enc1Res[j];
373 m = enc1Mount[j];
374 } else
375 {
376 res = enc2Res[j];
377 m = enc2Mount[j];
378 }
379 break;
380 }
381
382 // counts -> shaft degrees
383 const double shaftDeg = res ? (double(cAdj) / double(res)) * 360.0 : 0.0;
384 // shaft -> joint degrees depending on mount
385 if (m == Mount::Motor)
386 {
387 return gearRatioInv[j] * shaftDeg; // joint = motor/gearRatio
388 }
389 // Joint-mounted or unknown → pass-through
390 return shaftDeg;
391 }
392
393 bool setPositionCountsLimits(int axis, int32_t minCounts, int32_t maxCounts)
394 {
395 constexpr auto logPrefix = "[setPositionCountsLimits]";
396 auto errorCode = this->ethercatManager.writeSDO<int32_t>(this->firstSlave + axis,
397 0x607D,
398 0x01,
399 minCounts);
401 {
402 yCError(CIA402,
403 "%s: setLimits: SDO write 0x607D:01 (min) failed for axis %d",
404 Impl::kClassName.data(),
405 axis);
406 return false;
407 }
408
409 errorCode = this->ethercatManager.writeSDO<int32_t>(this->firstSlave + axis,
410 0x607D,
411 0x02,
412 maxCounts);
414 {
415 yCError(CIA402,
416 "%s: setLimits: SDO write 0x607D:02 (max) failed for axis %d",
417 Impl::kClassName.data(),
418 axis);
419 return false;
420 }
421
422 return true;
423 }
424
425 //--------------------------------------------------------------------------
426 // One-shot SDO reads – here we only fetch encoder resolutions
427 //--------------------------------------------------------------------------
429 {
430 constexpr auto logPrefix = "[readEncoderResolutions]";
431
432 enc1Res.resize(numAxes);
433 enc2Res.resize(numAxes);
434 enc1ResInv.resize(numAxes);
435 enc2ResInv.resize(numAxes);
436
437 for (size_t j = 0; j < numAxes; ++j)
438 {
439 const int s = firstSlave + int(j);
440
441 uint32_t r1 = 0;
442 auto e1 = ethercatManager.readSDO<uint32_t>(s, 0x2110, 0x03, r1);
444
445 uint32_t r2 = 0;
446 auto e2 = ethercatManager.readSDO<uint32_t>(s, 0x2112, 0x03, r2);
448 }
449 for (size_t j = 0; j < numAxes; ++j)
450 {
451 enc1ResInv[j] = enc1Res[j] ? 1.0 / double(enc1Res[j]) : 0.0;
452 enc2ResInv[j] = enc2Res[j] ? 1.0 / double(enc2Res[j]) : 0.0;
453 }
454
455 // Print encoder resolution information for all axes
456 yCInfo(CIA402, "%s successfully read encoder resolutions from SDO", logPrefix);
457 for (size_t j = 0; j < numAxes; ++j)
458 {
459 yCDebug(CIA402,
460 "%s j=%zu enc1_resolution=%u (inv=%.9f), enc2_resolution=%u (inv=%.9f)",
461 logPrefix,
462 j,
463 enc1Res[j],
464 enc1ResInv[j],
465 enc2Res[j],
466 enc2ResInv[j]);
467 }
468
469 return true;
470 }
471
473 {
474 // Known Synapticon encodings (prefix scales; middle bytes encode rev, last timebase)
475 // RPM family
476 if (v == 0x00B44700u)
477 return {6.0, 1.0 / 6.0, "1 RPM"}; // 1 rpm -> 6 deg/s
478 if (v == 0xFFB44700u)
479 return {0.6, 1.0 / 0.6, "0.1 RPM"};
480 if (v == 0xFEB44700u)
481 return {0.06, 1.0 / 0.06, "0.01 RPM"};
482 if (v == 0xFDB44700u)
483 return {0.006, 1.0 / 0.006, "0.001 RPM"};
484 // RPS family
485 if (v == 0x00B40300u)
486 return {360.0, 1.0 / 360.0, "1 RPS"};
487 if (v == 0xFFB40300u)
488 return {36.0, 1.0 / 36.0, "0.1 RPS"};
489 if (v == 0xFEB40300u)
490 return {3.6, 1.0 / 3.6, "0.01 RPS"};
491 if (v == 0xFDB40300u)
492 return {0.36, 1.0 / 0.36, "0.001 RPS"};
493
494 // Fallback (spec says default is 1 RPM). Warn & assume 1 RPM.
495 return {6.0, 1.0 / 6.0, "UNKNOWN (assume 1 RPM)"};
496 }
497
499 {
500 constexpr auto logPrefix = "[readSiVelocityUnits]";
501
502 this->velToDegS.resize(numAxes);
503 this->degSToVel.resize(numAxes);
504
505 for (size_t j = 0; j < numAxes; ++j)
506 {
507 const int s = firstSlave + int(j);
508 // default 1 RPM See
509 // https://doc.synapticon.com/circulo/sw5.1/objects_html/6xxx/60a9.html?Highlight=0x60a9
510 uint32_t raw = 0x00B44700u;
511 auto e = ethercatManager.readSDO<uint32_t>(s, 0x60A9, 0x00, raw);
512
513 auto [toDegS, toDev, name] = this->decode60A9(raw);
514 velToDegS[j] = toDegS;
515 degSToVel[j] = toDev;
516
518 yCDebug(CIA402,
519 "%s j=%zu 0x60A9=0x%08X velocity_unit=%s (1_unit=%.6f_deg/s)",
520 logPrefix,
521 j,
522 raw,
523 name,
524 toDegS);
525 else
526 yWarning("%s j=%zu failed to read 0x60A9, assuming 1_RPM (1_unit=6_deg/s)",
527 logPrefix,
528 j);
529 }
530
531 yCInfo(CIA402, "%s successfully read velocity conversion units from SDO", logPrefix);
532 return true;
533 }
534
535 // Convert loop-shaft counts to joint degrees, using the configured
536 double loopCountsToJointDeg(std::size_t j, double counts) const
537 {
538 // Pick loop sensor & mount (we already filled posLoopSrc[] in open())
539 uint32_t res = 0;
540 Mount mount = Mount::None;
541 switch (posLoopSrc[j])
542 {
543 case SensorSrc::Enc1:
544 res = enc1Res[j];
545 mount = enc1Mount[j];
546 break;
547 case SensorSrc::Enc2:
548 res = enc2Res[j];
549 mount = enc2Mount[j];
550 break;
551 default: // fallback: prefer enc1 if available
552 if (enc1Mount[j] != Mount::None && enc1Res[j] != 0)
553 {
554 res = enc1Res[j];
555 mount = enc1Mount[j];
556 } else
557 {
558 res = enc2Res[j];
559 mount = enc2Mount[j];
560 }
561 break;
562 }
563 if (res == 0 || mount == Mount::None)
564 return 0.0; // no info
565
566 const double shaftDeg = (counts / double(res)) * 360.0; // degrees on the measured shaft
567 // Convert to JOINT side depending on mount
568
569 yCDebug(CIA402,
570 "loopCountsToJointDeg j=%zu counts=%.3f res=%u mount=%d -> shaftDeg=%.9f",
571 j,
572 counts,
573 res,
574 static_cast<int>(mount),
575 shaftDeg);
576
577 if (mount == Mount::Motor)
578 return shaftDeg * gearRatioInv[j]; // motor → joint
579 /* mount == Mount::Joint */ return shaftDeg; // already joint
580 }
581
582 bool setPositionWindowDeg(int j, double winDeg, double winTime_ms)
583 {
584 if (j < 0 || j >= static_cast<int>(this->numAxes))
585 {
586 yCError(CIA402,
587 "%s: setPositionWindowDeg: invalid joint index",
588 Impl::kClassName.data());
589 return false;
590 }
591 const int s = this->firstSlave + j;
592
593 uint32_t rawWin = 0;
594 if (std::isinf(winDeg))
595 {
596 // Per doc: monitoring OFF → target reached bit stays 0.
597 rawWin = 0xFFFFFFFFu;
598 } else
599 {
600 rawWin = static_cast<uint32_t>(
601 std::round(std::abs(this->jointDegToTargetCounts(std::size_t(j), winDeg))));
602 }
603 uint32_t rawTime = static_cast<uint32_t>(std::round(std::max(0.0, winTime_ms)));
604
605 auto e1 = this->ethercatManager.writeSDO<uint32_t>(s, 0x6067, 0x00, rawWin);
607 {
608 yCError(CIA402,
609 "%s: setPositionWindowDeg: SDO 0x6067 write failed on joint %d",
610 Impl::kClassName.data(),
611 j);
612 return false;
613 }
614 auto e2 = this->ethercatManager.writeSDO<uint32_t>(s, 0x6068, 0x00, rawTime);
616 {
617 yCError(CIA402,
618 "%s: setPositionWindowDeg: SDO 0x6068 write failed on joint %d",
619 Impl::kClassName.data(),
620 j);
621 return false;
622 }
623 return true;
624 }
625
626 bool getPositionControlStrategy(uint16_t& strategyValue)
627 {
628 constexpr auto logPrefix = "[getPositionControlStrategy]";
629
630 // Read from first axis only (all should be the same)
631 const int slaveIdx = firstSlave;
632 const auto err
633 = ethercatManager.readSDO<uint16_t>(slaveIdx, 0x2002, 0x00, strategyValue);
635 {
636 yCError(CIA402,
637 "%s failed to read 0x2002:00",
638 logPrefix);
639 return false;
640 }
641
642 yCInfo(CIA402,
643 "%s read position strategy %u from first axis",
644 logPrefix,
645 static_cast<unsigned>(strategyValue));
646 return true;
647 }
648
649 bool setPositionControlStrategy(uint16_t strategyValue)
650 {
651 constexpr auto logPrefix = "[setPositionControlStrategy]";
652
653 for (size_t j = 0; j < numAxes; ++j)
654 {
655 const int slaveIdx = firstSlave + static_cast<int>(j);
656 const auto err
657 = ethercatManager.writeSDO<uint16_t>(slaveIdx, 0x2002, 0x00, strategyValue);
659 {
660 yCError(CIA402,
661 "%s j=%zu failed to set 0x2002:00 (value=%u)",
662 logPrefix,
663 j,
664 static_cast<unsigned>(strategyValue));
665 return false;
666 }
667 }
668
669 yCInfo(CIA402,
670 "%s configured %zu axes with position strategy %u",
671 logPrefix,
672 numAxes,
673 static_cast<unsigned>(strategyValue));
674 return true;
675 }
676
677
679 std::vector<double>& kdNmSecPerDeg)
680 {
681 constexpr auto logPrefix = "[readSimplePidGains]";
682
683 kpNmPerDeg.resize(numAxes);
684 kdNmSecPerDeg.resize(numAxes);
685
686 for (size_t j = 0; j < numAxes; ++j)
687 {
688 const int slaveIdx = firstSlave + static_cast<int>(j);
689 float32 kpValue = 0.0f;
690 float32 kdValue = 0.0f;
691
692 const auto errKp = ethercatManager.readSDO<float32>(slaveIdx, 0x2012, 0x01, kpValue);
693 const auto errKd = ethercatManager.readSDO<float32>(slaveIdx, 0x2012, 0x03, kdValue);
694
697 {
698 yCError(CIA402,
699 "%s j=%zu failed to read gains (errs=%d,%d)",
700 logPrefix,
701 j,
702 static_cast<int>(errKp),
703 static_cast<int>(errKd));
704 return false;
705 }
706
707 const double degPerCount = loopCountsToJointDeg(j, 1.0);
708 const double kpMotor_mNm_per_deg = static_cast<double>(kpValue) / degPerCount;
709 const double kdMotor_mNmS_per_deg = static_cast<double>(kdValue) / degPerCount;
710
711 kpNmPerDeg[j] = (kpMotor_mNm_per_deg * gearRatio[j]) / 1000.0;
712 kdNmSecPerDeg[j] = (kdMotor_mNmS_per_deg * gearRatio[j]) / 1000.0;
713
714 yCDebug(CIA402,
715 "%s j=%zu read gains: kp_device=%.3f[mNm/inc], kd_device=%.3f[mNm*s/inc] "
716 "=> kp_motor=%.6f[mNm/deg], kd_motor=%.6f[mNm*s/deg] => "
717 "kp_joint=%.6f[Nm/deg], kd_joint=%.6f[Nm*s/deg]",
718 logPrefix,
719 j,
720 kpValue,
721 kdValue,
722 kpMotor_mNm_per_deg,
723 kdMotor_mNmS_per_deg,
724 kpNmPerDeg[j],
725 kdNmSecPerDeg[j]);
726 }
727 yCInfo(CIA402, "%s read simple PID gains for %zu axes", logPrefix, numAxes);
728 return true;
729 }
730
732 const std::vector<double>& kdNmSecPerDeg)
733 {
734 constexpr auto logPrefix = "[configureSimplePidGains]";
735
736 if (kpNmPerDeg.size() != numAxes || kdNmSecPerDeg.size() != numAxes)
737 {
738 yCError(CIA402,
739 "%s mismatched gain vector sizes (kp=%zu kd=%zu expected=%zu)",
740 logPrefix,
741 kpNmPerDeg.size(),
742 kdNmSecPerDeg.size(),
743 numAxes);
744 return false;
745 }
746
747 for (size_t j = 0; j < numAxes; ++j)
748 {
749 if (gearRatio[j] == 0.0)
750 {
751 yCError(CIA402, "%s j=%zu invalid gear ratio (0)", logPrefix, j);
752 return false;
753 }
754
755 const double degPerCount = loopCountsToJointDeg(j, 1.0);
756 if (degPerCount == 0.0)
757 {
758 yCError(CIA402,
759 "%s j=%zu cannot derive counts/deg (loop source missing)",
760 logPrefix,
761 j);
762 return false;
763 }
764 const double kpMotor_mNm_per_deg = (kpNmPerDeg[j] / gearRatio[j]) * 1000.0;
765 const double kdMotor_mNmS_per_deg = (kdNmSecPerDeg[j] / gearRatio[j]) * 1000.0;
766
767 const double kpDevice = kpMotor_mNm_per_deg * degPerCount;
768 const double kdDevice = kdMotor_mNmS_per_deg * degPerCount;
769
770 const float32 kpValue = static_cast<float32>(kpDevice);
771 const float32 kdValue = static_cast<float32>(kdDevice);
772 constexpr float32 kiValue = 0.0f;
773
774
775 yCDebug(CIA402,
776 "%s j=%zu computing gains: kp_joint=%.6f[Nm/deg], kd_joint=%.6f[Nm*s/deg], "
777 "gearRatio=%.6f, degPerCount=%.9f => kp_motor=%.6f[mNm/deg], "
778 "kd_motor=%.6f[mNm*s/deg] => kp_device=%.3f[mNm/inc], kd_device=%.3f[mNm*s/inc]",
779 logPrefix,
780 j,
781 kpNmPerDeg[j],
782 kdNmSecPerDeg[j],
783 gearRatio[j],
784 degPerCount,
785 kpMotor_mNm_per_deg,
786 kdMotor_mNmS_per_deg,
787 kpDevice,
788 kdDevice);
789
790 const int slaveIdx = firstSlave + static_cast<int>(j);
791 const auto errKp = ethercatManager.writeSDO<float32>(slaveIdx, 0x2012, 0x01, kpValue);
792 const auto errKi = ethercatManager.writeSDO<float32>(slaveIdx, 0x2012, 0x02, kiValue);
793 const auto errKd = ethercatManager.writeSDO<float32>(slaveIdx, 0x2012, 0x03, kdValue);
794
798 {
799 yCError(CIA402,
800 "%s j=%zu failed to program gains (errs=%d,%d,%d). Gains "
801 "Kp=%.3f[mNm/inc]=%.3f[Nm/deg], Kd=%.3f[mNm*s/inc]=%.3f[Nm*s/deg]",
802 logPrefix,
803 j,
804 static_cast<int>(errKp),
805 static_cast<int>(errKi),
806 static_cast<int>(errKd),
807 kpValue,
808 kpNmPerDeg[j],
809 kdValue,
810 kdNmSecPerDeg[j]);
811 return false;
812 }
813
814 yCDebug(CIA402,
815 "%s j=%zu Kp=%.3f[mNm/inc] Kd=%.3f[mNm*s/inc]",
816 logPrefix,
817 j,
818 kpValue,
819 kdValue);
820 }
821
822 yCInfo(CIA402, "%s programmed simple PID gains for %zu axes", logPrefix, numAxes);
823 return true;
824 }
825
827 {
828 constexpr auto logPrefix = "[readMotorConstants]";
829
830 torqueConstants.resize(numAxes);
831 maxCurrentsA.resize(numAxes);
832
833 for (size_t j = 0; j < numAxes; ++j)
834 {
835 const int s = firstSlave + int(j);
836 int32_t tcMicroNmA = 0;
837 if (ethercatManager.readSDO<int32_t>(s, 0x2003, 0x02, tcMicroNmA)
839 || tcMicroNmA == 0)
840 {
841 yWarning("%s j=%zu cannot read torque constant (0x2003:02) or zero, assuming 1.0 "
842 "Nm/A",
843 logPrefix,
844 j);
845 torqueConstants[j] = 1.0;
846 } else
847 {
848 // convert from µNm/A to Nm/A
849 torqueConstants[j] = double(tcMicroNmA) * 1e-6;
850 }
851
852 // first of all we need to read the motor rated current
853 // 0x6075:00 Motor Rated Current (UNSIGNED32)
854 uint32_t ratedCurrentMilliA = 0;
855 double ratedCurrentA = 0.0;
856 if (ethercatManager.readSDO<uint32_t>(s, 0x6075, 0x00, ratedCurrentMilliA)
858 || ratedCurrentMilliA == 0)
859 {
860 yWarning("%s j=%zu cannot read motor rated current (0x6075:00) or zero, assuming "
861 "1.0 A",
862 logPrefix,
863 j);
864 ratedCurrentA = 1.0;
865 } else
866 {
867 // convert from mA to A
868 ratedCurrentA = double(ratedCurrentMilliA) * 1e-3;
869 }
870
871 // Now we can read the max current 0x6073:0 16 bit unsigned
872 uint16_t maxPermille = 0;
873 if (ethercatManager.readSDO<uint16_t>(s, 0x6073, 0x00, maxPermille)
875 {
876 yWarning("%s j=%zu cannot read max current (0x6073:00), assuming 1000 per_mille",
877 logPrefix,
878 j);
879 maxPermille = 1000;
880 }
881
882 yCDebug(CIA402,
883 "%s j=%zu max_current=%u_permille (%.3fA), torque_constant=%.6f_Nm/A",
884 logPrefix,
885 j,
886 maxPermille,
887 (double(maxPermille) / 1000.0) * ratedCurrentA,
888 this->torqueConstants[j]);
889
890 maxCurrentsA[j] = (double(maxPermille) / 1000.0) * ratedCurrentA;
891 }
892
893 yCInfo(CIA402, "%s successfully read motor constants from SDO", logPrefix);
894 return true;
895 }
896
897 /**
898 * Read the gear-ratio (motor-revs : load-revs) for every axis and cache both
899 * the ratio and its inverse.
900 *
901 * 0x6091:01 numerator (UNSIGNED32, default 1)
902 * 0x6091:02 denominator (UNSIGNED32, default 1)
903 *
904 * If the object is missing or invalid the code silently falls back to 1:1.
905 */
907 {
908 constexpr auto logPrefix = "[readGearRatios]";
909
910 gearRatio.resize(numAxes);
911 gearRatioInv.resize(numAxes);
912
913 for (size_t j = 0; j < numAxes; ++j)
914 {
915 const int slaveIdx = firstSlave + static_cast<int>(j);
916
917 uint32_t num = 1U; // default numerator
918 uint32_t den = 1U; // default denominator
919
920 // ---- numerator ------------------------------------------------------
921 if (ethercatManager.readSDO<uint32_t>(slaveIdx, 0x6091, 0x01, num)
923 {
924 yWarning("%s j=%zu gear_ratio_numerator not available (0x6091:01), assuming 1",
925 logPrefix,
926 j);
927 num = 1U;
928 }
929
930 // ---- denominator ----------------------------------------------------
931 if (ethercatManager.readSDO<uint32_t>(slaveIdx, 0x6091, 0x02, den)
933 || den == 0U)
934 {
935 yWarning("%s j=%zu gear_ratio_denominator not available/zero (0x6091:02), assuming "
936 "1",
937 logPrefix,
938 j);
939 den = 1U;
940 }
941
942 yCDebug(CIA402,
943 "%s j=%zu gear_ratio=%u:%u (ratio=%.6f)",
944 logPrefix,
945 j,
946 num,
947 den,
948 static_cast<double>(num) / static_cast<double>(den));
949
950 // ---- cache value ----------------------------------------------------
951 gearRatio[j] = static_cast<double>(num) / static_cast<double>(den);
952 }
953
954 // Pre‑compute the inverse for fast use elsewhere -------------------------
955 for (size_t j = 0; j < numAxes; ++j)
956 {
957 gearRatioInv[j] = (gearRatio[j] != 0.0) ? (1.0 / gearRatio[j]) : 0.0;
958 }
959
960 yCInfo(CIA402, "%s successfully read gear ratios from SDO", logPrefix);
961 return true;
962 }
963
965 {
966 constexpr auto logPrefix = "[readTorqueValues]";
967
970
971 for (size_t j = 0; j < numAxes; ++j)
972 {
973 const int s = firstSlave + int(j);
974 uint32_t rated_mNm = 0;
975 if (ethercatManager.readSDO<uint32_t>(s, 0x6076, 0x00, rated_mNm)
977 {
978 yCError(CIA402, "%s j=%zu cannot read rated torque (0x6076:00)", logPrefix, j);
979 return false;
980 }
981 ratedMotorTorqueNm[j] = double(rated_mNm) / 1000.0; // motor Nm
982 }
983 for (size_t j = 0; j < numAxes; ++j)
984 {
985 const int s = firstSlave + int(j);
986 uint16_t maxPerm = 0;
987 if (ethercatManager.readSDO<uint16_t>(s, 0x6072, 0x00, maxPerm)
989 {
990 yCError(CIA402, "%s j=%zu cannot read max torque (0x6072:00)", logPrefix, j);
991 return false;
992 }
993 maxMotorTorqueNm[j] = (double(maxPerm) / 1000.0) * ratedMotorTorqueNm[j];
994 yCDebug(CIA402,
995 "%s j=%zu motor_rated_torque=%.3fNm max_torque=%.3fNm",
996 logPrefix,
997 j,
1000 }
1001
1002 yCInfo(CIA402, "%s successfully read torque values from SDO", logPrefix);
1003 return true;
1004 }
1005
1007 {
1008 constexpr auto logPrefix = "[configureMaxTorqueFromJointNm]";
1009
1010 if (maxJointTorqueNm.size() != numAxes)
1011 {
1012 yCError(CIA402,
1013 "%s invalid vector size (%zu), expected %zu",
1014 logPrefix,
1015 maxJointTorqueNm.size(),
1016 numAxes);
1017 return false;
1018 }
1019
1020 for (size_t j = 0; j < numAxes; ++j)
1021 {
1022 const double jointNm = maxJointTorqueNm[j];
1023 if (jointNm <= 0.0)
1024 {
1025 yCError(CIA402,
1026 "%s j=%zu invalid max joint torque %.6f Nm (must be > 0)",
1027 logPrefix,
1028 j,
1029 jointNm);
1030 return false;
1031 }
1032
1033 if (gearRatio[j] <= 0.0)
1034 {
1035 yCError(CIA402,
1036 "%s j=%zu invalid gear ratio %.6f",
1037 logPrefix,
1038 j,
1039 gearRatio[j]);
1040 return false;
1041 }
1042
1043 if (ratedMotorTorqueNm[j] <= 0.0)
1044 {
1045 yCError(CIA402,
1046 "%s j=%zu invalid rated motor torque %.6f Nm",
1047 logPrefix,
1048 j,
1050 return false;
1051 }
1052
1053 constexpr uint16_t maxTorqueAdmissibleValue = 32767; // per SDO definition (0x6072 is uint16_t)
1054 const double motorNm = jointNm / gearRatio[j];
1055 const double perThousand = (motorNm / ratedMotorTorqueNm[j]) * 1000.0;
1056 const double clamped = std::clamp(perThousand, 0.0, static_cast<double>(maxTorqueAdmissibleValue));
1057 const uint16_t maxPerm = static_cast<uint16_t>(std::llround(clamped));
1058 const int s = firstSlave + static_cast<int>(j);
1059
1060 const auto err = ethercatManager.writeSDO<uint16_t>(s, 0x6072, 0x00, maxPerm);
1062 {
1063 yCError(CIA402,
1064 "%s j=%zu failed to write 0x6072:00",
1065 logPrefix,
1066 j);
1067 return false;
1068 }
1069
1070 maxMotorTorqueNm[j] = (static_cast<double>(maxPerm) / 1000.0) * ratedMotorTorqueNm[j];
1071
1072 yCInfo(CIA402,
1073 "%s j=%zu configured 0x6072 to %u permille (joint=%.6f Nm, motor=%.6f Nm)",
1074 logPrefix,
1075 j,
1076 static_cast<unsigned>(maxPerm),
1077 jointNm,
1078 maxMotorTorqueNm[j]);
1079 }
1080
1081 return true;
1082 }
1083
1084 void setSDORefSpeed(int j, double spDegS)
1085 {
1086 // ---- map JOINT deg/s -> LOOP SHAFT deg/s (based on pos loop source + mount) ----
1087 double shaft_deg_s = spDegS; // default assume joint shaft
1088 switch (this->posLoopSrc[j])
1089 {
1091 if (this->enc1Mount[j] == Impl::Mount::Motor)
1092 shaft_deg_s = spDegS * this->gearRatio[j];
1093 else if (this->enc1Mount[j] == Impl::Mount::Joint)
1094 shaft_deg_s = spDegS;
1095 break;
1097 if (this->enc2Mount[j] == Impl::Mount::Motor)
1098 shaft_deg_s = spDegS * this->gearRatio[j];
1099 else if (this->enc2Mount[j] == Impl::Mount::Joint)
1100 shaft_deg_s = spDegS;
1101 break;
1103 default:
1104 // Fallback: if we know which encoder is motor-mounted, assume that one; otherwise leave
1105 // as joint.
1106 if (this->enc1Mount[j] == Impl::Mount::Motor
1107 || this->enc2Mount[j] == Impl::Mount::Motor)
1108 shaft_deg_s = spDegS * this->gearRatio[j];
1109 break;
1110 }
1111
1112 // Convert deg/s on the loop shaft -> device velocity units using 0x60A9
1113 const int s = this->firstSlave + j;
1114 const int32_t vel = static_cast<int32_t>(std::llround(shaft_deg_s * this->degSToVel[j]));
1115 (void)this->ethercatManager.writeSDO<int32_t>(s, 0x6081, 0x00, vel);
1116 }
1117
1119 {
1120 std::lock_guard<std::mutex> lock(this->setPoints.mutex);
1121
1122 /**
1123 * Push user setpoints into RxPDOs according to the active control mode.
1124 * - Torque (CST): joint Nm → motor Nm → per‑thousand of rated motor torque (0x6071)
1125 * - Velocity (CSV): joint deg/s → loop‑shaft deg/s (mount aware) → rpm (0x60FF)
1126 * First‑cycle latches (velLatched/trqLatched) zero the command once when entering.
1127 */
1128 for (size_t j = 0; j < this->numAxes; ++j)
1129 {
1130 const int s = firstSlave + int(j);
1131 auto rx = this->ethercatManager.getRxPDO(s);
1132 const int opMode = this->controlModeState.active[j];
1133
1134 // clean rx targets
1135 rx->TargetPosition = 0;
1136 rx->TargetTorque = 0;
1137 rx->TargetVelocity = 0;
1138
1139 // ---------- TORQUE (CST) ----------
1140 if (opMode == VOCAB_CM_TORQUE)
1141 {
1142 if (!this->trqLatched[j])
1143 {
1144 rx->TargetTorque = 0;
1145 this->trqLatched[j] = true;
1146 } else
1147 {
1148 // YARP gives joint torque [Nm] → convert to MOTOR torque before 0x6071
1149 const double jointNm = setPoints.hasTorqueSP[j] ? setPoints.jointTorques[j]
1150 : 0.0;
1151 const double motorNm = (gearRatio[j] != 0.0) ? (jointNm / gearRatio[j]) : 0.0;
1152
1153 // 0x6071 is per-thousand of rated MOTOR torque (0x6076 in Nm)
1154 const int16_t tq_thousand = static_cast<int16_t>(std::llround(
1155 (ratedMotorTorqueNm[j] != 0.0 ? motorNm / ratedMotorTorqueNm[j] : 0.0)
1156 * 1000.0));
1157 rx->TargetTorque = this->invertedMotionSenseDirection[j] ? -tq_thousand
1158 : tq_thousand;
1159 }
1160 }
1161
1162 // ---------- VELOCITY (CSV) ----------
1163 if (opMode == VOCAB_CM_VELOCITY)
1164 {
1165 if (!velLatched[j])
1166 {
1167 rx->TargetVelocity = 0;
1168 velLatched[j] = true;
1169 } else
1170 {
1171 // YARP gives JOINT-side velocity [deg/s]
1172 const double joint_deg_s = setPoints.hasVelSP[j] ? setPoints.jointVelocities[j]
1173 : 0.0;
1174
1175 // Command must be on the SHAFT used by the velocity loop:
1176 // - if loop sensor is motor-mounted → convert joint→motor (× gearRatio)
1177 // - if loop sensor is joint-mounted → pass through
1178 double shaft_deg_s = joint_deg_s; // default: joint shaft
1179
1180 // The desired velocity setpoint depends on which encoder is used for
1181 // the position loop and how it is mounted.
1182 switch (posLoopSrc[j])
1183 {
1185 if (enc1Mount[j] == Impl::Mount::Motor)
1186 shaft_deg_s = joint_deg_s * gearRatio[j];
1187 else if (enc1Mount[j] == Impl::Mount::Joint)
1188 shaft_deg_s = joint_deg_s;
1189 // Impl::Mount::None → leave default (best-effort)
1190 break;
1191
1193 if (enc2Mount[j] == Impl::Mount::Motor)
1194 shaft_deg_s = joint_deg_s * gearRatio[j];
1195 else if (enc2Mount[j] == Impl::Mount::Joint)
1196 shaft_deg_s = joint_deg_s;
1197 break;
1198
1200 default:
1201 // Heuristic fallback: if we *know* the configured velocity feedback
1202 // is motor-mounted, assume motor shaft; otherwise joint shaft.
1203 // (Leave as joint if you don't keep velSrc vectors here.)
1204 break;
1205 }
1206
1207 // Convert deg/s → native velocity on the selected shaft for 0x60FF
1208 const double vel = shaft_deg_s * this->degSToVel[j];
1209 rx->TargetVelocity = static_cast<int32_t>(std::llround(vel))
1210 * (this->invertedMotionSenseDirection[j] ? -1 : 1);
1211 }
1212 }
1213
1214 // ---------- POSITION (PP) ----------
1215 if (opMode == VOCAB_CM_POSITION)
1216 {
1217
1218 if (this->ppState.ppHaltRequested[j])
1219 rx->Controlword |= (1u << 8);
1220 else
1221 rx->Controlword &= ~(1u << 8);
1222 // latch on first cycle in PP → do nothing until a user set-point arrives
1223 if (!posLatched[j])
1224 {
1225 // keep bit 5 asserted in PP (single-set-point method)
1226 rx->Controlword |= (1u << 5); // Change set immediately
1227 rx->Controlword &= ~(1u << 4); // make sure New set-point is low first
1228 posLatched[j] = true;
1229
1230 // compute seed from current measured joint angle
1231 const double currentJointDeg = this->variables.jointPositions[j];
1232 const int32_t seedStoreCounts
1233 = this->jointDegToTargetCounts(j, currentJointDeg);
1234
1235 // If the user already queued a PP target before the first cycle in PP, do
1236 // not overwrite it with the seed; otherwise seed the cached target with the
1237 // current position so the drive starts from a consistent reference.
1238 if (!setPoints.ppHasPosSP[j])
1239 {
1240 setPoints.ppTargetCounts[j] = seedStoreCounts;
1241 setPoints.ppJointTargetsDeg[j] = currentJointDeg;
1242 setPoints.ppIsRelative[j] = false;
1243 }
1244
1245 // Populate 0x607A with the cached target (seeded or user provided). The
1246 // rising edge on bit4 will be generated in the next cycle.
1247 const int32_t driveTargetCounts = this->invertedMotionSenseDirection[j]
1248 ? -setPoints.ppTargetCounts[j]
1249 : setPoints.ppTargetCounts[j];
1250 rx->TargetPosition = driveTargetCounts;
1251
1252 // If no user set-point was pending, schedule a one-shot bit4 pulse to align
1253 // the drive target to the current position.
1254 if (!setPoints.ppHasPosSP[j] && !setPoints.ppPulseHi[j])
1255 {
1256 setPoints.ppPulseHi[j] = true; // sync drive target to current position
1257 }
1258 } else
1259 {
1260 // (A) Always assert bit 5 in PP
1261 rx->Controlword |= (1u << 5);
1262
1263 // (B) If the previous cycle drove bit4 high, bring it low now (one-shot pulse)
1264 if (setPoints.ppPulseCoolDown[j])
1265 {
1266 rx->Controlword &= ~(1u << 4);
1267 setPoints.ppPulseCoolDown[j] = false;
1268 }
1269
1270 // (C) New set-point pending? write 0x607A and raise bit4
1271 int32_t targetPositionCounts = 0;
1272 if (setPoints.ppHasPosSP[j] || setPoints.ppPulseHi[j])
1273 {
1274 // Absolute/Relative selection (CW bit 6)
1275 if (setPoints.ppIsRelative[j])
1276 rx->Controlword |= (1u << 6);
1277 else
1278 rx->Controlword &= ~(1u << 6);
1279
1280 // Target position (0x607A)
1281 targetPositionCounts = this->invertedMotionSenseDirection[j]
1282 ? -setPoints.ppTargetCounts[j]
1283 : setPoints.ppTargetCounts[j];
1284
1285 // New set-point pulse (rising edge)
1286 rx->Controlword |= (1u << 4);
1287
1288 // consume the request and arm cooldown to drop bit4 next cycle
1289 setPoints.ppHasPosSP[j] = false;
1290 setPoints.ppPulseHi[j] = false;
1291 setPoints.ppPulseCoolDown[j] = true;
1292 } else
1293 {
1294 auto tx = this->ethercatManager.getTxView(s);
1295 targetPositionCounts = this->invertedMotionSenseDirection[j]
1296 ? -setPoints.ppTargetCounts[j]
1297 : setPoints.ppTargetCounts[j];
1299 {
1300 targetPositionCounts = tx.get<int32_t>(CiA402::TxField::Position6064,
1301 targetPositionCounts);
1302 }
1303
1304 setPoints.ppTargetCounts[j] = this->invertedMotionSenseDirection[j]
1305 ? -targetPositionCounts
1306 : targetPositionCounts;
1307 setPoints.ppJointTargetsDeg[j]
1308 = this->targetCountsToJointDeg(j, targetPositionCounts);
1309 setPoints.ppIsRelative[j] = false;
1310 }
1311
1312 rx->TargetPosition = targetPositionCounts;
1313 }
1314 }
1315
1316 // ---------- POSITION DIRECT (CSP) ----------
1317 if (opMode == VOCAB_CM_POSITION_DIRECT)
1318 {
1319 if (!posLatched[j])
1320 {
1321 const double currentJointDeg = this->variables.jointPositions[j];
1322 const int32_t seedCounts = this->jointDegToTargetCounts(j, currentJointDeg);
1323 setPoints.positionDirectJointTargetsDeg[j] = currentJointDeg;
1324 setPoints.positionDirectTargetCounts[j] = seedCounts;
1325 posLatched[j] = true;
1326 }
1327
1328 const int32_t driveCounts = this->invertedMotionSenseDirection[j]
1329 ? -setPoints.positionDirectTargetCounts[j]
1330 : setPoints.positionDirectTargetCounts[j];
1331 rx->TargetPosition = driveCounts;
1332 }
1333
1334 if (opMode == VOCAB_CM_CURRENT)
1335 {
1336 if (!this->currLatched[j])
1337 {
1338 // the current control is actually a torque control
1339 rx->TargetTorque = 0;
1340 this->currLatched[j] = true;
1341 } else
1342 {
1343 // YARP gives Currents [A] → convert to MOTOR torque before 0x6071
1344 const double currentA = setPoints.hasCurrentSP[j] ? setPoints.motorCurrents[j]
1345 : 0.0;
1346
1347 // convert the current in torque using the torque constant
1348 const double torqueNm = currentA * this->torqueConstants[j];
1349
1350 // 0x6071 is per-thousand of rated MOTOR torque (0x6076 in Nm)
1351 const int16_t tq_thousand = static_cast<int16_t>(std::llround(
1352 (ratedMotorTorqueNm[j] != 0.0 ? torqueNm / ratedMotorTorqueNm[j] : 0.0)
1353 * 1000.0));
1354 rx->TargetTorque = this->invertedMotionSenseDirection[j] ? -tq_thousand
1355 : tq_thousand;
1356 }
1357 }
1358 }
1359 return true;
1360 }
1361
1363 {
1364 std::lock_guard<std::mutex> lock(this->variables.mutex);
1365
1366 // =====================================================================
1367 // SHAFT TRANSFORMATION HELPERS
1368 // =====================================================================
1369 // These lambdas handle the complex coordinate transformations between
1370 // motor shaft and joint/load shaft based on encoder mounting and gear ratios
1371
1372 // Position transformation: converts degrees on a specific shaft to the requested side
1373 auto shaftFromMount_pos = [&](double deg, Mount m, size_t j, bool asMotor) -> double {
1374 // deg = input position in degrees on the physical shaft of mount 'm'
1375 // asMotor = true if we want result on motor shaft, false for joint shaft
1376
1377 if (m == Impl::Mount::Motor)
1378 {
1379 // Input is on motor shaft
1380 return asMotor ? deg // Motor->Motor: no change
1381 : deg * gearRatioInv[j]; // Motor->Joint: divide by gear ratio
1382 }
1383 if (m == Impl::Mount::Joint)
1384 {
1385 // Input is on joint/load shaft
1386 return asMotor ? deg * gearRatio[j] // Joint->Motor: multiply by gear ratio
1387 : deg; // Joint->Joint: no change
1388 }
1389 return 0.0; // Impl::Mount::None or invalid
1390 };
1391
1392 // Velocity transformation: same logic as position but for velocities
1393 auto shaftFromMount_vel = [&](double degs, Mount m, size_t j, bool asMotor) -> double {
1394 if (m == Impl::Mount::Motor)
1395 {
1396 return asMotor ? degs : degs * gearRatioInv[j];
1397 }
1398 if (m == Impl::Mount::Joint)
1399 {
1400 return asMotor ? degs * gearRatio[j] : degs;
1401 }
1402 return 0.0;
1403 };
1404
1405 for (size_t j = 0; j < this->numAxes; ++j)
1406 {
1407 const int s = this->firstSlave + int(j);
1408 auto tx = this->ethercatManager.getTxView(s);
1409
1410 // =====================================================================
1411 // Status word bits
1412 // =====================================================================
1413 const uint16_t sw = tx.get<uint16_t>(CiA402::TxField::Statusword);
1414 this->variables.targetReached[j] = (sw & (1u << 10)) != 0;
1415
1416 // =====================================================================
1417 // RAW DATA EXTRACTION FROM PDOs
1418 // =====================================================================
1419 // Read raw encoder counts and velocities from the EtherCAT PDOs
1420 // These are the fundamental data sources before any interpretation
1421
1422 // Position data (in encoder counts)
1423 const int32_t p6064 = tx.get<int32_t>(CiA402::TxField::Position6064, 0); // CiA402
1424 // standard
1425 // position
1426 const int32_t pE1 = tx.has(CiA402::TxField::Enc1Pos2111_02) // Encoder 1 position (if
1427 // mapped)
1428 ? tx.get<int32_t>(CiA402::TxField::Enc1Pos2111_02)
1429 : 0;
1430 const int32_t pE2 = tx.has(CiA402::TxField::Enc2Pos2113_02) // Encoder 2 position (if
1431 // mapped)
1432 ? tx.get<int32_t>(CiA402::TxField::Enc2Pos2113_02)
1433 : 0;
1434
1435 // Velocity data (in RPM for CiA402, encoder-specific units for others)
1436 const int32_t v606C = tx.get<int32_t>(CiA402::TxField::Velocity606C, 0); // CiA402
1437 // standard
1438 // velocity
1439 // (RPM)
1440 const int32_t vE1 = tx.has(CiA402::TxField::Enc1Vel2111_03) // Encoder 1 velocity (if
1441 // mapped)
1442 ? tx.get<int32_t>(CiA402::TxField::Enc1Vel2111_03)
1443 : 0;
1444 const int32_t vE2 = tx.has(CiA402::TxField::Enc2Vel2113_03) // Encoder 2 velocity (if
1445 // mapped)
1446 ? tx.get<int32_t>(CiA402::TxField::Enc2Vel2113_03)
1447 : 0;
1448
1449 // =====================================================================
1450 // SOURCE INTERPRETATION HELPERS
1451 // =====================================================================
1452 // These lambdas convert raw data to degrees on the encoder's own shaft,
1453 // taking into account encoder resolution and the mounting location
1454
1455 // Convert position source to degrees on its own physical shaft
1456 auto posDegOnOwnShaft = [&](PosSrc s) -> std::pair<double, Mount> {
1457 switch (s)
1458 {
1459 case Impl::PosSrc::Enc1:
1460 // Direct encoder 1 readout: counts -> degrees using enc1 resolution
1461 return {double(pE1) * enc1ResInv[j] * 360.0, enc1Mount[j]};
1462
1463 case Impl::PosSrc::Enc2:
1464 // Direct encoder 2 readout: counts -> degrees using enc2 resolution
1465 return {double(pE2) * enc2ResInv[j] * 360.0, enc2Mount[j]};
1466
1468 default: {
1469 // CiA402 standard object - interpretation depends on drive's loop source
1470 // The drive tells us which encoder it uses internally via posLoopSrc
1471 if (posLoopSrc[j] == Impl::SensorSrc::Enc1 && enc1ResInv[j] != 0.0)
1472 return {double(p6064) * enc1ResInv[j] * 360.0, enc1Mount[j]};
1473 if (posLoopSrc[j] == Impl::SensorSrc::Enc2 && enc2ResInv[j] != 0.0)
1474 return {double(p6064) * enc2ResInv[j] * 360.0, enc2Mount[j]};
1475
1476 // Fallback: if loop source unknown, prefer enc1 if available
1477 if (enc1Mount[j] != Impl::Mount::None && enc1ResInv[j] != 0.0)
1478 return {double(p6064) * enc1ResInv[j] * 360.0, enc1Mount[j]};
1479 if (enc2Mount[j] != Impl::Mount::None && enc2ResInv[j] != 0.0)
1480 return {double(p6064) * enc2ResInv[j] * 360.0, enc2Mount[j]};
1481 return {0.0, Impl::Mount::None};
1482 }
1483 }
1484 };
1485 // Convert velocity source to deg/s on its own physical shaft
1486 auto velDegSOnOwnShaft = [&](VelSrc s) -> std::pair<double, Mount> {
1487 const double k = this->velToDegS[j]; // 1 device unit -> k deg/s
1488 switch (s)
1489 {
1490 case Impl::VelSrc::Enc1:
1491 // Direct encoder 1 velocity (already in RPM from PDO)
1492 return {double(vE1) * k, enc1Mount[j]};
1493
1494 case Impl::VelSrc::Enc2:
1495 // Direct encoder 2 velocity (already in RPM from PDO)
1496 return {double(vE2) * k, enc2Mount[j]};
1497
1499 default: {
1500 // CiA402 standard velocity (0x606C)
1501 // Per Synapticon docs, 0x606C is reported in the POSITION reference frame
1502 // ("driving shaft"). That is, if a Position encoder is configured, 606C is
1503 // scaled to that shaft (typically the joint/load). Only when no position
1504 // encoder exists, it falls back to the velocity/torque encoder shaft (often
1505 // motor).
1506 // Therefore, select the mount based on the POSITION loop source (0x2012:09),
1507 // not the velocity loop source.
1508
1509 // Primary: use reported position loop source
1511 return {double(v606C) * k, enc1Mount[j]};
1513 return {double(v606C) * k, enc2Mount[j]};
1514
1515 // Fallback heuristics when 0x2012:09 is Unknown:
1516 // - If we have a joint-side encoder on enc2 (typical dual-encoder setup),
1517 // assume 606C is joint-side.
1518 if (enc2Mount[j] == Impl::Mount::Joint)
1519 return {double(v606C) * k, enc2Mount[j]};
1520 // - Else prefer enc1 if present; otherwise enc2; else unknown
1521 if (enc1Mount[j] != Impl::Mount::None)
1522 return {double(v606C) * k, enc1Mount[j]};
1523 if (enc2Mount[j] != Impl::Mount::None)
1524 return {double(v606C) * k, enc2Mount[j]};
1525 return {0.0, Impl::Mount::None};
1526 }
1527 }
1528 };
1529
1530 // =====================================================================
1531 // POSITION FEEDBACK PROCESSING
1532 // =====================================================================
1533 // Apply the configured source selection and coordinate transformations
1534 {
1535 // Get raw position data from configured sources (on their own shafts)
1536 auto [degJ_src, mountJ] = posDegOnOwnShaft(posSrcJoint[j]); // Joint position source
1537 auto [degM_src, mountM] = posDegOnOwnShaft(posSrcMotor[j]); // Motor position source
1538
1539 // Transform to the requested coordinate systems
1540 const double jointDeg = shaftFromMount_pos(degJ_src, mountJ, j, /*asMotor*/ false);
1541 const double motorDeg = shaftFromMount_pos(degM_src, mountM, j, /*asMotor*/ true);
1542
1543 // Store in output variables
1544 this->variables.jointPositions[j]
1545 = this->invertedMotionSenseDirection[j] ? -jointDeg : jointDeg;
1546 this->variables.motorEncoders[j] //
1547 = this->invertedMotionSenseDirection[j] ? -motorDeg : motorDeg;
1548 }
1549
1550 // =====================================================================
1551 // VELOCITY FEEDBACK PROCESSING
1552 // =====================================================================
1553 // Same logic as position but for velocities
1554 {
1555 // Get raw velocity data from configured sources (on their own shafts)
1556 auto [degsJ_src, mountJ] = velDegSOnOwnShaft(velSrcJoint[j]); // Joint velocity
1557 // source
1558 auto [degsM_src, mountM] = velDegSOnOwnShaft(velSrcMotor[j]); // Motor velocity
1559 // source
1560
1561 // Transform to the requested coordinate systems
1562 const double jointDegS
1563 = shaftFromMount_vel(degsJ_src, mountJ, j, /*asMotor*/ false);
1564 const double motorDegS = shaftFromMount_vel(degsM_src, mountM, j, /*asMotor*/ true);
1565
1566 // Store in output variables
1567 this->variables.jointVelocities[j]
1568 = this->invertedMotionSenseDirection[j] ? -jointDegS : jointDegS;
1569 this->variables.motorVelocities[j]
1570 = this->invertedMotionSenseDirection[j] ? -motorDegS : motorDegS;
1571 }
1572
1573 // =====================================================================
1574 // OTHER FEEDBACK PROCESSING
1575 // =====================================================================
1576
1577 // Accelerations not provided by the drives (would require differentiation)
1578 this->variables.jointAccelerations[j] = 0.0;
1579 this->variables.motorAccelerations[j] = 0.0;
1580
1581 // --------- Torque feedback (motor → joint conversion) ----------
1582 // CiA402 torque feedback (0x6077) is always motor-side, per-thousand of rated torque
1583 const double tq_per_thousand
1584 = double(tx.get<int16_t>(CiA402::TxField::Torque6077, 0)) / 1000.0;
1585 const double motorNm = tq_per_thousand * this->ratedMotorTorqueNm[j];
1586 // Convert motor torque to joint torque using gear ratio
1587 const double signedMotorNm = this->invertedMotionSenseDirection[j] ? -motorNm : motorNm;
1588 this->variables.jointTorques[j] = signedMotorNm * this->gearRatio[j];
1589 this->variables.motorCurrents[j] = signedMotorNm / this->torqueConstants[j];
1590
1591 // --------- Safety signals (if mapped into PDOs) ----------
1592 // These provide real-time status of safety functions
1593 this->variables.STO[j] = tx.has(CiA402::TxField::STO_6621_01)
1594 ? tx.get<uint8_t>(CiA402::TxField::STO_6621_01)
1595 : 0; // Safe Torque Off status
1596 this->variables.SBC[j] = tx.has(CiA402::TxField::SBC_6621_02)
1597 ? tx.get<uint8_t>(CiA402::TxField::SBC_6621_02)
1598 : 0; // Safe Brake Control status
1599
1600 // --------- Timestamp (if available) ----------
1601 // Provides drive-side timing information for synchronization
1603 {
1604 const uint32_t raw = tx.get<uint32_t>(CiA402::TxField::Timestamp20F0, 0);
1605 // Unwrap 32-bit microsecond counter with threshold to avoid false wraps
1606 // due to small, non-monotonic clock adjustments.
1607 // Consider a wrap only if the backward jump is larger than half the range.
1608 if (raw < this->tsLastRaw[j])
1609 {
1610 const uint32_t back = this->tsLastRaw[j] - raw;
1611 if (back > TIMESTAMP_WRAP_HALF_RANGE)
1612 {
1613 this->tsWraps[j] += 1u;
1614 }
1615 // else: small backward step → no wraps increment
1616 }
1617 this->tsLastRaw[j] = raw;
1618
1619 const uint64_t us_ext
1620 = this->tsWraps[j] * TIMESTAMP_WRAP_PERIOD_US + static_cast<uint64_t>(raw);
1621 this->variables.feedbackTime[j]
1622 = static_cast<double>(us_ext) * MICROSECONDS_TO_SECONDS;
1623 } else
1624 {
1625 this->variables.feedbackTime[j] = 0.0;
1626 }
1627
1628 // the temperature is given in mC we need to convert Celsius
1629 this->variables.driveTemperatures[j]
1631 ? double(tx.get<int32_t>(CiA402::TxField::TemperatureDrive, 0)) * 1e-3
1632 : 0.0;
1633 }
1634
1635 return true;
1636 }
1637
1638 //--------------------------------------------------------------------------
1639 // YARP ➜ CiA-402 (Op-mode sent in RxPDO::OpMode)
1640 //--------------------------------------------------------------------------
1641 static int yarpToCiaOp(int cm)
1642 {
1643 using namespace yarp::dev;
1644 switch (cm)
1645 {
1646 case VOCAB_CM_IDLE:
1647 case VOCAB_CM_FORCE_IDLE:
1648 return 0; // “No mode” → disables the power stage
1649 case VOCAB_CM_POSITION:
1650 return 1; // Profile-Position (PP)
1651 case VOCAB_CM_VELOCITY:
1652 return 9; // Cyclic-Synch-Velocity (CSV)
1653 case VOCAB_CM_TORQUE:
1654 case VOCAB_CM_CURRENT:
1655 return 10; // Cyclic-Synch-Torque (CST)
1656 case VOCAB_CM_POSITION_DIRECT:
1657 return 8; // Cyclic-Synch-Position (CSP)
1658 default:
1659 return -1; // not supported
1660 }
1661 }
1662
1663 //--------------------------------------------------------------------------
1664 // CiA-402 ➜ YARP (decode for diagnostics)
1665 //--------------------------------------------------------------------------
1666 int ciaOpToYarpWithFlavor(int op, int cstFlavor)
1667 {
1668 if (op == 10)
1669 {
1670 return (cstFlavor == VOCAB_CM_CURRENT) ? VOCAB_CM_CURRENT : VOCAB_CM_TORQUE;
1671 }
1672 return this->ciaOpToYarp(op);
1673 }
1674
1675 static int ciaOpToYarp(int op)
1676 {
1677 using namespace yarp::dev;
1678 switch (op)
1679 {
1680 case 0:
1681 return VOCAB_CM_IDLE;
1682 case 1:
1683 return VOCAB_CM_POSITION;
1684 case 3:
1685 return VOCAB_CM_VELOCITY; // PP-vel still possible
1686 case 8:
1687 return VOCAB_CM_POSITION_DIRECT;
1688 case 9:
1689 return VOCAB_CM_VELOCITY;
1690 case 10:
1691 return VOCAB_CM_TORQUE;
1692 default:
1693 return VOCAB_CM_UNKNOWN;
1694 }
1695 }
1696
1697 static std::string_view yarpToString(int op)
1698 {
1699 using namespace yarp::dev;
1700 switch (op)
1701 {
1702 case VOCAB_CM_IDLE:
1703 return "VOCAB_CM_IDLE";
1704 case VOCAB_CM_FORCE_IDLE:
1705 return "VOCAB_CM_FORCE_IDLE";
1706 case VOCAB_CM_POSITION:
1707 return "VOCAB_CM_POSITION";
1708 case VOCAB_CM_VELOCITY:
1709 return "VOCAB_CM_VELOCITY";
1710 case VOCAB_CM_TORQUE:
1711 return "VOCAB_CM_TORQUE";
1712 case VOCAB_CM_CURRENT:
1713 return "VOCAB_CM_CURRENT";
1714 case VOCAB_CM_POSITION_DIRECT:
1715 return "VOCAB_CM_POSITION_DIRECT";
1716 default:
1717 return "UNKNOWN";
1718 }
1719 }
1720
1721 /**
1722 * Decode CiA 402 error code (0x603F:00) into a human‑readable string.
1723 * Includes explicit vendor codes seen in the field and broader family fallbacks.
1724 */
1726 {
1727 switch (code)
1728 {
1729 case 0x0000:
1730 return "No fault";
1731
1732 // === Your explicit mappings ===
1733 case 0x2220:
1734 return "Continuous over current (device internal)";
1735 case 0x2250:
1736 return "Short circuit (device internal)";
1737 case 0x2350:
1738 return "Load level fault (I2t, thermal state)";
1739 case 0x2351:
1740 return "Load level warning (I2t, thermal state)";
1741 case 0x3130:
1742 return "Phase failure";
1743 case 0x3131:
1744 return "Phase failure L1";
1745 case 0x3132:
1746 return "Phase failure L2";
1747 case 0x3133:
1748 return "Phase failure L3";
1749 case 0x3210:
1750 return "DC link over-voltage";
1751 case 0x3220:
1752 return "DC link under-voltage";
1753 case 0x3331:
1754 return "Field circuit interrupted";
1755 case 0x4210:
1756 return "Excess temperature device";
1757 case 0x4310:
1758 return "Excess temperature drive";
1759 case 0x5200:
1760 return "Control";
1761 case 0x5300:
1762 return "Operating unit";
1763 case 0x6010:
1764 return "Software reset (watchdog)";
1765 case 0x6320:
1766 return "Parameter error";
1767 case 0x7121:
1768 return "Motor blocked";
1769 case 0x7300:
1770 return "Sensor";
1771 case 0x7303:
1772 return "Resolver 1 fault";
1773 case 0x7304:
1774 return "Resolver 2 fault";
1775 case 0x7500:
1776 return "Communication";
1777 case 0x8612:
1778 return "Positioning controller (reference limit)";
1779 case 0xF002:
1780 return "Sub-synchronous run";
1781
1782 default:
1783 // === Family fallbacks (upper byte) ===
1784 switch (code & 0xFF00)
1785 {
1786 case 0x2200:
1787 return "Current/device-internal fault";
1788 case 0x2300:
1789 return "Motor/output circuit fault";
1790 case 0x3100:
1791 return "Phase/mains supply issue";
1792 case 0x3200:
1793 return "DC link voltage issue";
1794 case 0x3300:
1795 return "Field/armature circuit issue";
1796 case 0x4200:
1797 return "Excess temperature (device)";
1798 case 0x4300:
1799 return "Excess temperature (drive)";
1800 case 0x5200:
1801 return "Control device hardware / limits";
1802 case 0x5300:
1803 return "Operating unit / safety";
1804 case 0x6000:
1805 return "Software reset / watchdog";
1806 case 0x6300:
1807 return "Parameter/configuration error";
1808 case 0x7100:
1809 return "Motor blocked / mechanical issue";
1810 case 0x7300:
1811 return "Sensor/encoder fault";
1812 case 0x7500:
1813 return "Communication/internal";
1814 case 0x8600:
1815 return "Positioning controller (vendor specific)";
1816 case 0xF000:
1817 return "Timing/performance warning";
1818 default: {
1819 char buf[64];
1820 std::snprintf(buf, sizeof(buf), "Unknown 0x603F error: 0x%04X", code);
1821 return std::string(buf);
1822 }
1823 }
1824 }
1825 }
1826
1827}; // struct Impl
1828
1829// CiA402MotionControl — ctor / dtor
1830
1831CiA402MotionControl::CiA402MotionControl(double period, yarp::os::ShouldUseSystemClock useSysClock)
1832 : yarp::os::PeriodicThread(period, useSysClock, yarp::os::PeriodicThreadClock::Absolute)
1833 , m_impl(std::make_unique<Impl>())
1834{
1835}
1836
1838 : yarp::os::PeriodicThread(0.001 /*1 kHz*/,
1839 yarp::os::ShouldUseSystemClock::Yes,
1840 yarp::os::PeriodicThreadClock::Absolute)
1841 , m_impl(std::make_unique<Impl>())
1842{
1843}
1844
1846
1847// open() — bring the ring to OPERATIONAL and start the cyclic thread
1848
1849bool CiA402MotionControl::open(yarp::os::Searchable& cfg)
1850{
1851 constexpr auto logPrefix = "[open]";
1852
1853 // ---------------------------------------------------------------------
1854 // Parse driver parameters
1855 // ---------------------------------------------------------------------
1856 if (!cfg.check("ifname") || !cfg.find("ifname").isString())
1857 {
1858 yCError(CIA402, "%s: 'ifname' parameter is not a string", Impl::kClassName.data());
1859 return false;
1860 }
1861 if (!cfg.check("num_axes") || !cfg.find("num_axes").isInt32())
1862 {
1863 yCError(CIA402, "%s: 'num_axes' parameter is not an integer", Impl::kClassName.data());
1864 return false;
1865 }
1866
1867 if (!cfg.check("period") || !cfg.find("period").isFloat64())
1868 {
1869 yCError(CIA402, "%s: 'period' parameter is not a float64", Impl::kClassName.data());
1870 return false;
1871 }
1872 const double period = cfg.find("period").asFloat64();
1873 if (period <= 0.0)
1874 {
1875 yCError(CIA402, "%s: 'period' parameter must be positive", Impl::kClassName.data());
1876 return false;
1877 }
1878 this->setPeriod(period);
1879 yCDebug(CIA402, "%s: using period = %.6f s", logPrefix, period);
1880
1881 m_impl->numAxes = static_cast<size_t>(cfg.find("num_axes").asInt32());
1882 m_impl->firstSlave = cfg.check("first_slave", yarp::os::Value(1)).asInt32();
1883 if (cfg.check("expected_slave_name"))
1884 m_impl->expectedName = cfg.find("expected_slave_name").asString();
1885
1886 // =========================================================================
1887 // ENCODER CONFIGURATION PARSING
1888 // =========================================================================
1889 // This system supports dual encoders per axis with flexible source selection:
1890 // - Enc1: Typically motor-mounted (high resolution, motor shaft feedback)
1891 // - Enc2: Typically joint-mounted (load-side feedback after gearbox)
1892 // - 6064/606C: Standard CiA402 objects that follow the configured loop sources
1893 //
1894 // Each feedback type (position/velocity for joint/motor) can independently
1895 // choose its source, allowing configurations like:
1896 // - Joint position from load encoder, motor position from motor encoder
1897 // - Joint velocity from load encoder, motor velocity from 606C
1898
1899 // Parse helper functions for configuration strings
1900 auto parsePos = [&](const std::string& s) -> Impl::PosSrc {
1901 if (s == "enc1")
1902 return Impl::PosSrc::Enc1; // Use encoder 1 directly
1903 if (s == "enc2")
1904 return Impl::PosSrc::Enc2; // Use encoder 2 directly
1905 return Impl::PosSrc::S6064; // Use CiA402 standard object (follows loop source)
1906 };
1907 auto parseVel = [&](const std::string& s) -> Impl::VelSrc {
1908 if (s == "enc1")
1909 return Impl::VelSrc::Enc1; // Use encoder 1 velocity directly
1910 if (s == "enc2")
1911 return Impl::VelSrc::Enc2; // Use encoder 2 velocity directly
1912 return Impl::VelSrc::S606C; // Use CiA402 standard object (follows loop source)
1913 };
1914 auto parseMount = [&](const std::string& s) -> Impl::Mount {
1915 if (s == "motor")
1916 return Impl::Mount::Motor; // Encoder measures motor shaft (pre-gearbox)
1917 if (s == "joint")
1918 return Impl::Mount::Joint; // Encoder measures joint/load shaft (post-gearbox)
1919 if (s == "none")
1920 return Impl::Mount::None; // Encoder not present/not used
1921 yWarning("%s: invalid mount '%s' (allowed: none|motor|joint) → 'none'",
1922 Impl::kClassName.data(),
1923 s.c_str());
1924 return Impl::Mount::None;
1925 };
1926
1927 auto extractListOfStringFromSearchable = [this](const yarp::os::Searchable& cfg,
1928 const char* key,
1929 const std::vector<std::string>& acceptedKeys,
1930 std::vector<std::string>& result) -> bool {
1931 result.clear();
1932
1933 if (!cfg.check(key))
1934 {
1935 yCError(CIA402, "%s: missing key '%s'", Impl::kClassName.data(), key);
1936 return false;
1937 }
1938
1939 const yarp::os::Value& v = cfg.find(key);
1940 if (!v.isList())
1941 {
1942 yCError(CIA402, "%s: key '%s' is not a list", Impl::kClassName.data(), key);
1943 return false;
1944 }
1945
1946 const yarp::os::Bottle* lst = v.asList();
1947 if (!lst)
1948 {
1949 yCError(CIA402,
1950 "%s: internal error: list for key '%s' is null",
1951 Impl::kClassName.data(),
1952 key);
1953 return false;
1954 }
1955
1956 const size_t expected = static_cast<size_t>(m_impl->numAxes);
1957 const size_t actual = static_cast<size_t>(lst->size());
1958 if (actual != expected)
1959 {
1960 yCError(CIA402,
1961 "%s: list for key '%s' has incorrect size (%zu), expected (%zu)",
1962 Impl::kClassName.data(),
1963 key,
1964 actual,
1965 expected);
1966 return false;
1967 }
1968
1969 result.reserve(expected);
1970 for (int i = 0; i < lst->size(); ++i)
1971 {
1972 const yarp::os::Value& elem = lst->get(i);
1973
1974 if (!elem.isString())
1975 {
1976 yCError(CIA402,
1977 "%s: element %d in list for key '%s' is not a string",
1978 Impl::kClassName.data(),
1979 i,
1980 key);
1981 result.clear();
1982 return false;
1983 }
1984
1985 const std::string val = elem.asString();
1986
1987 // if acceptedKeys is non-empty, validate the value
1988 if (!acceptedKeys.empty())
1989 {
1990 if (std::find(acceptedKeys.begin(), acceptedKeys.end(), val) == acceptedKeys.end())
1991 {
1992 yCError(CIA402,
1993 "%s: invalid value '%s' in list for key '%s'",
1994 Impl::kClassName.data(),
1995 val.c_str(),
1996 key);
1997 result.clear();
1998 return false;
1999 }
2000 }
2001 result.push_back(val);
2002 }
2003
2004 return true;
2005 };
2006
2007 auto extractListOfDoubleFromSearchable = [this](const yarp::os::Searchable& cfg,
2008 const char* key,
2009 std::vector<double>& result) -> bool {
2010 result.clear();
2011
2012 if (!cfg.check(key))
2013 {
2014 yCError(CIA402, "%s: missing key '%s'", Impl::kClassName.data(), key);
2015 return false;
2016 }
2017
2018 const yarp::os::Value& v = cfg.find(key);
2019 if (!v.isList())
2020 {
2021 yCError(CIA402, "%s: key '%s' is not a list", Impl::kClassName.data(), key);
2022 return false;
2023 }
2024
2025 const yarp::os::Bottle* lst = v.asList();
2026 if (!lst)
2027 {
2028 yCError(CIA402,
2029 "%s: internal error: list for key '%s' is null",
2030 Impl::kClassName.data(),
2031 key);
2032 return false;
2033 }
2034
2035 const size_t expected = static_cast<size_t>(m_impl->numAxes);
2036 const size_t actual = static_cast<size_t>(lst->size());
2037 if (actual != expected)
2038 {
2039 yCError(CIA402,
2040 "%s: list for key '%s' has incorrect size (%zu), expected (%zu)",
2041 Impl::kClassName.data(),
2042 key,
2043 actual,
2044 expected);
2045 return false;
2046 }
2047
2048 result.reserve(expected);
2049 for (int i = 0; i < lst->size(); ++i)
2050 {
2051 const yarp::os::Value& elem = lst->get(i);
2052
2053 if (!elem.isFloat64() && !elem.isInt32() && !elem.isInt64() && !elem.isFloat32())
2054 {
2055 yCError(CIA402,
2056 "%s: element %d in list for key '%s' is not a number",
2057 Impl::kClassName.data(),
2058 i,
2059 key);
2060 result.clear();
2061 return false;
2062 }
2063
2064 result.push_back(elem.asFloat64());
2065 }
2066
2067 return true;
2068 };
2069
2070 auto extractListOfBoolFromSearchable = [this](const yarp::os::Searchable& cfg,
2071 const char* key,
2072 std::vector<bool>& result) -> bool {
2073 result.clear();
2074
2075 if (!cfg.check(key))
2076 {
2077 yCError(CIA402, "%s: missing key '%s'", Impl::kClassName.data(), key);
2078 return false;
2079 }
2080
2081 const yarp::os::Value& v = cfg.find(key);
2082 if (!v.isList())
2083 {
2084 yCError(CIA402, "%s: key '%s' is not a list", Impl::kClassName.data(), key);
2085 return false;
2086 }
2087
2088 const yarp::os::Bottle* lst = v.asList();
2089 if (!lst)
2090 {
2091 yCError(CIA402,
2092 "%s: internal error: list for key '%s' is null",
2093 Impl::kClassName.data(),
2094 key);
2095 return false;
2096 }
2097
2098 const size_t expected = static_cast<size_t>(m_impl->numAxes);
2099 const size_t actual = static_cast<size_t>(lst->size());
2100 if (actual != expected)
2101 {
2102 yCError(CIA402,
2103 "%s: list for key '%s' has incorrect size (%zu), expected (%zu)",
2104 Impl::kClassName.data(),
2105 key,
2106 actual,
2107 expected);
2108 return false;
2109 }
2110
2111 result.reserve(expected);
2112 for (int i = 0; i < lst->size(); ++i)
2113 {
2114 const yarp::os::Value& elem = lst->get(i);
2115
2116 if (!elem.isBool())
2117 {
2118 yCError(CIA402,
2119 "%s: element %d in list for key '%s' is not a boolean",
2120 Impl::kClassName.data(),
2121 i,
2122 key);
2123 result.clear();
2124 return false;
2125 }
2126
2127 result.push_back(elem.asBool());
2128 }
2129
2130 return true;
2131 };
2132
2133 // Encoder mounting configuration (where each encoder is physically located)
2134 // enc1_mount must be list of "motor" or "joint"
2135 // enc2_mount must be list of "motor", "joint" or "none"
2136 std::vector<std::string> enc1MStr; // encoder 1 mount per axis
2137 std::vector<std::string> enc2MStr; // encoder 2 mount per axis
2138
2139 if (!extractListOfStringFromSearchable(cfg, "enc1_mount", {"motor", "joint"}, enc1MStr))
2140 return false;
2141 if (!extractListOfStringFromSearchable(cfg, "enc2_mount", {"motor", "joint", "none"}, enc2MStr))
2142 return false;
2143
2144 std::vector<std::string> posSrcJointStr;
2145 std::vector<std::string> posSrcMotorStr;
2146 std::vector<std::string> velSrcJointStr;
2147 std::vector<std::string> velSrcMotorStr;
2148 if (!extractListOfStringFromSearchable(cfg,
2149 "position_feedback_joint",
2150 {"6064", "enc1", "enc2"},
2151 posSrcJointStr))
2152 return false;
2153 if (!extractListOfStringFromSearchable(cfg,
2154 "position_feedback_motor",
2155 {"6064", "enc1", "enc2"},
2156 posSrcMotorStr))
2157 return false;
2158 if (!extractListOfStringFromSearchable(cfg,
2159 "velocity_feedback_joint",
2160 {"606C", "enc1", "enc2"},
2161 velSrcJointStr))
2162 return false;
2163 if (!extractListOfStringFromSearchable(cfg,
2164 "velocity_feedback_motor",
2165 {"606C", "enc1", "enc2"},
2166 velSrcMotorStr))
2167 return false;
2168
2169 std::vector<double> positionWindowDeg; // position window for targetReached
2170 std::vector<double> timingWindowMs; // timing window for targetReached
2171 std::vector<double> maxJointTorqueNmFromConfig; // optional max torque on joint side [Nm]
2172 std::vector<double> simplePidKpNmPerDeg; // optional simple PID gains
2173 std::vector<double> simplePidKdNmSecPerDeg;
2174
2175 constexpr uint16_t kPositionStrategySimplePid = 1u;
2176 constexpr uint16_t kPositionStrategyCascadedPid = 2u;
2177 const bool useSimplePidMode
2178 = cfg.check("use_simple_pid_mode") ? cfg.find("use_simple_pid_mode").asBool() : false;
2179 const uint16_t desiredPositionStrategy
2180 = useSimplePidMode ? kPositionStrategySimplePid : kPositionStrategyCascadedPid;
2181
2182 if (!extractListOfDoubleFromSearchable(cfg, "position_window_deg", positionWindowDeg))
2183 return false;
2184 if (!extractListOfDoubleFromSearchable(cfg, "timing_window_ms", timingWindowMs))
2185 return false;
2186
2187 const bool hasMaxJointTorqueCfg = cfg.check("max_torque_joint_nm");
2188 if (hasMaxJointTorqueCfg
2189 && !extractListOfDoubleFromSearchable(cfg,
2190 "max_torque_joint_nm",
2191 maxJointTorqueNmFromConfig))
2192 {
2193 return false;
2194 }
2195
2196 const bool hasSimplePidKpCfg = cfg.check("simple_pid_kp_nm_per_deg");
2197 const bool hasSimplePidKdCfg = cfg.check("simple_pid_kd_nm_s_per_deg");
2198 const bool programSimplePidGains = hasSimplePidKpCfg || hasSimplePidKdCfg;
2199 if (hasSimplePidKpCfg != hasSimplePidKdCfg)
2200 {
2201 yCError(CIA402,
2202 "%s configuration must provide both simple_pid_kp_nm_per_deg and "
2203 "simple_pid_kd_nm_s_per_deg",
2204 logPrefix);
2205 return false;
2206 }
2207 if (programSimplePidGains)
2208 {
2209 if (!extractListOfDoubleFromSearchable(cfg, "simple_pid_kp_nm_per_deg", simplePidKpNmPerDeg))
2210 return false;
2211 if (!extractListOfDoubleFromSearchable(cfg,
2212 "simple_pid_kd_nm_s_per_deg",
2213 simplePidKdNmSecPerDeg))
2214 return false;
2215 }
2216
2217 for (size_t j = 0; j < m_impl->numAxes; ++j)
2218 {
2219 m_impl->enc1Mount.push_back(parseMount(enc1MStr[j]));
2220 m_impl->enc2Mount.push_back(parseMount(enc2MStr[j]));
2221 m_impl->posSrcJoint.push_back(parsePos(posSrcJointStr[j]));
2222 m_impl->posSrcMotor.push_back(parsePos(posSrcMotorStr[j]));
2223 m_impl->velSrcJoint.push_back(parseVel(velSrcJointStr[j]));
2224 m_impl->velSrcMotor.push_back(parseVel(velSrcMotorStr[j]));
2225 }
2226
2227 const std::string ifname = cfg.find("ifname").asString();
2228 // Optional runtime knobs
2229 const int pdoTimeoutUs = cfg.check("pdo_timeout_us") ? cfg.find("pdo_timeout_us").asInt32()
2230 : -1;
2231 const bool enableDc = cfg.check("enable_dc") ? cfg.find("enable_dc").asBool() : true;
2232 const int dcShiftNs = cfg.check("dc_shift_ns") ? cfg.find("dc_shift_ns").asInt32() : 0;
2233 yCInfo(CIA402, "%s opening EtherCAT manager on interface %s", logPrefix, ifname.c_str());
2234
2235 if (!extractListOfBoolFromSearchable(cfg,
2236 "inverted_motion_sense_direction",
2237 m_impl->invertedMotionSenseDirection))
2238 return false;
2239
2240 auto vecBoolToString = [](const std::vector<bool>& v) -> std::string {
2241 std::string s;
2242 for (size_t i = 0; i < v.size(); ++i)
2243 {
2244 s += (v[i] ? "true" : "false");
2245 if (i + 1 < v.size())
2246 s += ", ";
2247 }
2248 return s;
2249 };
2250
2251 yCDebug(CIA402,
2252 "%s: inverted_motion_sense_direction = [%s]",
2253 logPrefix,
2254 vecBoolToString(m_impl->invertedMotionSenseDirection).c_str());
2255
2256 // ---------------------------------------------------------------------
2257 // Initialize the EtherCAT manager (ring in SAFE‑OP)
2258 // ---------------------------------------------------------------------
2259
2260 const auto ecErr = m_impl->ethercatManager.init(ifname);
2262 {
2263 yCError(CIA402,
2264 "%s EtherCAT initialization failed with error code %d",
2265 logPrefix,
2266 static_cast<int>(ecErr));
2267 return false;
2268 }
2269
2270 if (pdoTimeoutUs > 0)
2271 {
2272 m_impl->ethercatManager.setPdoTimeoutUs(pdoTimeoutUs);
2273 yCInfo(CIA402,
2274 "%s PDO receive timeout set to %d us",
2275 logPrefix,
2276 m_impl->ethercatManager.getPdoTimeoutUs());
2277 } else
2278 {
2279 yCInfo(CIA402,
2280 "%s using default PDO receive timeout of %d us",
2281 logPrefix,
2282 m_impl->ethercatManager.getPdoTimeoutUs());
2283 }
2284
2285 // We'll enable DC later, after going OP, using the configured period
2286 const uint32_t cycleNs = static_cast<uint32_t>(std::llround(this->getPeriod() * 1e9));
2287
2288 // =========================================================================
2289 // DRIVE LOOP SOURCE CONFIGURATION READING
2290 // =========================================================================
2291 // Read which encoders the drive uses internally for position/velocity loops
2292 // This affects how 6064/606C values should be interpreted
2293
2294 // Loop-source readers (local lambdas)
2295 auto readPosLoopSrc = [&](size_t j) -> Impl::SensorSrc {
2296 const int s = m_impl->firstSlave + int(j);
2297 uint8_t src = 0;
2298 auto e = m_impl->ethercatManager.readSDO<uint8_t>(s, 0x2012, 0x09, src);
2300 {
2301 yCError(CIA402,
2302 "%s: failed to read position loop source (joint %zu)",
2303 Impl::kClassName.data(),
2304 j);
2306 }
2307 return (src == 1) ? Impl::SensorSrc::Enc1
2308 : (src == 2) ? Impl::SensorSrc::Enc2
2310 };
2311 auto readVelLoopSrc = [&](size_t j) -> Impl::SensorSrc {
2312 const int s = m_impl->firstSlave + int(j);
2313 uint8_t src = 0;
2314 auto e = m_impl->ethercatManager.readSDO<uint8_t>(s, 0x2011, 0x05, src);
2317 return (src == 1) ? Impl::SensorSrc::Enc1
2318 : (src == 2) ? Impl::SensorSrc::Enc2
2320 };
2321
2322 // --------- PDO Mapping Availability Checks ----------
2323 // These check if encoder data was successfully mapped into the PDOs
2324 // during the EtherCAT initialization phase
2325 auto hasEnc1Pos = [&](size_t j) {
2326 const int s = m_impl->firstSlave + int(j);
2327 return m_impl->ethercatManager.getTxView(s).has(CiA402::TxField::Enc1Pos2111_02);
2328 };
2329 auto hasEnc1Vel = [&](size_t j) {
2330 const int s = m_impl->firstSlave + int(j);
2331 return m_impl->ethercatManager.getTxView(s).has(CiA402::TxField::Enc1Vel2111_03);
2332 };
2333 auto hasEnc2Pos = [&](size_t j) {
2334 const int s = m_impl->firstSlave + int(j);
2335 return m_impl->ethercatManager.getTxView(s).has(CiA402::TxField::Enc2Pos2113_02);
2336 };
2337 auto hasEnc2Vel = [&](size_t j) {
2338 const int s = m_impl->firstSlave + int(j);
2339 return m_impl->ethercatManager.getTxView(s).has(CiA402::TxField::Enc2Vel2113_03);
2340 };
2341
2342 yCInfo(CIA402,
2343 "%s using %zu axes, EtherCAT slaves %d to %d",
2344 logPrefix,
2345 m_impl->numAxes,
2346 m_impl->firstSlave,
2347 m_impl->firstSlave + int(m_impl->numAxes) - 1);
2348
2349 // --------- Read drive-internal loop sources ----------
2350 // These determine how CiA402 standard objects (6064/606C) should be interpreted
2351 m_impl->posLoopSrc.resize(m_impl->numAxes, Impl::SensorSrc::Unknown);
2352 m_impl->velLoopSrc.resize(m_impl->numAxes, Impl::SensorSrc::Unknown);
2353 for (size_t j = 0; j < m_impl->numAxes; ++j)
2354 {
2355 m_impl->posLoopSrc[j] = readPosLoopSrc(j);
2356 m_impl->velLoopSrc[j] = readVelLoopSrc(j);
2357 }
2358
2359 // =========================================================================
2360 // CONFIGURATION VALIDATION
2361 // =========================================================================
2362 // Strict validation: requested encoders must be both mapped in PDOs AND
2363 // have valid mount points. This prevents runtime errors from misconfiguration.
2364 for (size_t j = 0; j < m_impl->numAxes; ++j)
2365 {
2366 auto bad = [&](const char* what) {
2367 yCError(CIA402, "%s j=%zu invalid configuration: %s", logPrefix, j, what);
2368 return true;
2369 };
2370
2371 // --------- Position feedback validation ----------
2372 if (m_impl->posSrcJoint[j] == Impl::PosSrc::Enc1
2373 && (m_impl->enc1Mount[j] == Impl::Mount::None || !hasEnc1Pos(j)))
2374 {
2375 if (bad("pos_joint=enc1 but enc1 not mounted/mapped"))
2376 return false;
2377 }
2378 if (m_impl->posSrcJoint[j] == Impl::PosSrc::Enc2
2379 && (m_impl->enc2Mount[j] == Impl::Mount::None || !hasEnc2Pos(j)))
2380 {
2381 if (bad("pos_joint=enc2 but enc2 not mounted/mapped"))
2382 return false;
2383 }
2384
2385 if (m_impl->posSrcMotor[j] == Impl::PosSrc::Enc1
2386 && (m_impl->enc1Mount[j] == Impl::Mount::None || !hasEnc1Pos(j)))
2387 {
2388 if (bad("pos_motor=enc1 but enc1 not mounted/mapped"))
2389 return false;
2390 }
2391 if (m_impl->posSrcMotor[j] == Impl::PosSrc::Enc2
2392 && (m_impl->enc2Mount[j] == Impl::Mount::None || !hasEnc2Pos(j)))
2393 {
2394 if (bad("pos_motor=enc2 but enc2 not mounted/mapped"))
2395 return false;
2396 }
2397
2398 // --------- Velocity feedback validation ----------
2399 if (m_impl->velSrcJoint[j] == Impl::VelSrc::Enc1
2400 && (m_impl->enc1Mount[j] == Impl::Mount::None || !hasEnc1Vel(j)))
2401 {
2402 if (bad("vel_joint=enc1 but enc1 not mounted/mapped"))
2403 return false;
2404 }
2405 if (m_impl->velSrcJoint[j] == Impl::VelSrc::Enc2
2406 && (m_impl->enc2Mount[j] == Impl::Mount::None || !hasEnc2Vel(j)))
2407 {
2408 if (bad("vel_joint=enc2 but enc2 not mounted/mapped"))
2409 return false;
2410 }
2411
2412 if (m_impl->velSrcMotor[j] == Impl::VelSrc::Enc1
2413 && (m_impl->enc1Mount[j] == Impl::Mount::None || !hasEnc1Vel(j)))
2414 {
2415 if (bad("vel_motor=enc1 but enc1 not mounted/mapped"))
2416 return false;
2417 }
2418 if (m_impl->velSrcMotor[j] == Impl::VelSrc::Enc2
2419 && (m_impl->enc2Mount[j] == Impl::Mount::None || !hasEnc2Vel(j)))
2420 {
2421 if (bad("vel_motor=enc2 but enc2 not mounted/mapped"))
2422 return false;
2423 }
2424 }
2425
2426 // ---------------------------------------------------------------------
2427 // Read static SDO parameters (encoders, velocity units, gear ratios, motor limits)
2428 // ---------------------------------------------------------------------
2429 if (!m_impl->readEncoderResolutions())
2430 {
2431 yCError(CIA402, "%s failed to read encoder resolutions from SDO", logPrefix);
2432 return false;
2433 }
2434 if (!m_impl->readSiVelocityUnits())
2435 {
2436 yCError(CIA402, "%s failed to read velocity conversions from SDO", logPrefix);
2437 return false;
2438 }
2439 if (!m_impl->readGearRatios())
2440 {
2441 yCError(CIA402, "%s failed to read gear ratios from SDO", logPrefix);
2442 return false;
2443 }
2444 if (!m_impl->readMotorConstants())
2445 {
2446 yCError(CIA402, "%s failed to read motor constants from SDO", logPrefix);
2447 return false;
2448 }
2449 if (!m_impl->readTorqueValues())
2450 {
2451 yCError(CIA402, "%s failed to read torque values from SDO", logPrefix);
2452 return false;
2453 }
2454
2455 if (hasMaxJointTorqueCfg)
2456 {
2457 if (!m_impl->configureMaxTorqueFromJointNm(maxJointTorqueNmFromConfig))
2458 {
2459 yCError(CIA402,
2460 "%s failed to configure max torque (0x6072) from max_torque_joint_nm",
2461 logPrefix);
2462 return false;
2463 }
2464 } else
2465 {
2466 yCDebug(CIA402,
2467 "%s max_torque_joint_nm not provided: using drive 0x6072 values",
2468 logPrefix);
2469 }
2470
2471 // get the current control strategy
2472 uint16_t currentStrategy = 0;
2473 if (!m_impl->getPositionControlStrategy(currentStrategy))
2474 {
2475 yCError(CIA402, "%s failed to get current control strategy", logPrefix);
2476 return false;
2477 }
2478 yCInfo(CIA402,
2479 "%s current control strategy is %u",
2480 logPrefix,
2481 static_cast<unsigned>(currentStrategy));
2482
2483
2484 yCInfo(CIA402,
2485 "%s requested position strategy %u (%s)",
2486 logPrefix,
2487 static_cast<unsigned>(desiredPositionStrategy),
2488 useSimplePidMode ? "simple PID" : "cascaded PID");
2489
2490 if (!m_impl->setPositionControlStrategy(desiredPositionStrategy))
2491 {
2492 yCError(CIA402, "%s failed to configure position control strategy", logPrefix);
2493 return false;
2494 }
2495
2496 // get again the control strategy to verify
2497 if (!m_impl->getPositionControlStrategy(currentStrategy))
2498 {
2499 yCError(CIA402, "%s failed to get current control strategy", logPrefix);
2500 return false;
2501 }
2502 yCInfo(CIA402,
2503 "%s current control strategy after configuration is %u",
2504 logPrefix,
2505 static_cast<unsigned>(currentStrategy));
2506
2507 const bool enableSimplePidGainsIo = (desiredPositionStrategy == kPositionStrategySimplePid);
2508 if (!enableSimplePidGainsIo && programSimplePidGains)
2509 {
2510 yCWarning(CIA402,
2511 "%s simple PID gains provided but position strategy is not simple PID; skipping "
2512 "0x2012 gains programming",
2513 logPrefix);
2514 }
2515
2516 if (enableSimplePidGainsIo && programSimplePidGains)
2517 {
2518 if (!m_impl->configureSimplePidGains(simplePidKpNmPerDeg, simplePidKdNmSecPerDeg))
2519 {
2520 yCError(CIA402, "%s failed to program simple PID gains", logPrefix);
2521 return false;
2522 }
2523 } else if (enableSimplePidGainsIo)
2524 {
2525 yCDebug(CIA402,
2526 "%s skipping simple PID gains programming (config keys not provided)",
2527 logPrefix);
2528 }
2529
2530 std::vector<double> readKp;
2531 std::vector<double> readKd;
2532 bool simplePidGainsAvailable = false;
2533 if (enableSimplePidGainsIo)
2534 {
2535 // read the simple PID gains to verify
2536 if (!m_impl->readSimplePidGains(readKp, readKd))
2537 {
2538 yCError(CIA402, "%s failed to read back simple PID gains", logPrefix);
2539 return false;
2540 }
2541 simplePidGainsAvailable = true;
2542
2543 for (size_t j = 0; j < m_impl->numAxes; ++j)
2544 {
2545 yCDebug(CIA402,
2546 "%s j=%zu simple PID gains: Kp=%.6f Nm/deg, Kd=%.6f Nm·s/deg",
2547 logPrefix,
2548 j,
2549 readKp[j],
2550 readKd[j]);
2551 }
2552 }
2553
2554 // ---------------------------------------------------------------------
2555 // Set the position limits (SDO 607D)
2556 // ---------------------------------------------------------------------
2557 if (!extractListOfDoubleFromSearchable(cfg,
2558 "pos_limit_min_deg",
2559 m_impl->limits.minPositionLimitDeg))
2560 {
2561 yCError(CIA402, "%s failed to parse pos_limit_min_deg", logPrefix);
2562 return false;
2563 }
2564 if (!extractListOfDoubleFromSearchable(cfg,
2565 "pos_limit_max_deg",
2566 m_impl->limits.maxPositionLimitDeg))
2567 {
2568 yCError(CIA402, "%s failed to parse pos_limit_max_deg", logPrefix);
2569 return false;
2570 }
2571 if (!extractListOfBoolFromSearchable(cfg,
2572 "use_position_limits_from_config",
2573 m_impl->limits.usePositionLimitsFromConfig))
2574 {
2575 yCError(CIA402, "%s failed to parse use_position_limits_from_config", logPrefix);
2576 return false;
2577 }
2578
2579 if (m_impl->limits.minPositionLimitDeg.size() != m_impl->numAxes
2580 || m_impl->limits.maxPositionLimitDeg.size() != m_impl->numAxes
2581 || m_impl->limits.usePositionLimitsFromConfig.size() != m_impl->numAxes)
2582 {
2583 yCError(CIA402,
2584 "%s position limit lists must have exactly %zu elements",
2585 logPrefix,
2586 m_impl->numAxes);
2587 return false;
2588 }
2589
2590 for (size_t j = 0; j < m_impl->numAxes; ++j)
2591 {
2592 const bool inv = m_impl->invertedMotionSenseDirection[j];
2593
2594 if (m_impl->limits.usePositionLimitsFromConfig[j])
2595 {
2596 // Convert joint degrees to loop-shaft counts and write to SDO 0x607D
2597 const int32_t lowerCounts
2598 = m_impl->jointDegToTargetCounts(j, m_impl->limits.minPositionLimitDeg[j]);
2599 const int32_t upperCounts
2600 = m_impl->jointDegToTargetCounts(j, m_impl->limits.maxPositionLimitDeg[j]);
2601
2602 int32_t minCounts = 0;
2603 int32_t maxCounts = 0;
2604 if (!inv)
2605 {
2606 minCounts = lowerCounts;
2607 maxCounts = upperCounts;
2608 } else
2609 {
2610 // Decreasing mapping f(x) = -counts(x) → [L, U] maps to [f(U), f(L)]
2611 minCounts = -upperCounts;
2612 maxCounts = -lowerCounts;
2613 }
2614
2615 if (!m_impl->setPositionCountsLimits(j, minCounts, maxCounts))
2616 {
2617 yCError(CIA402, "%s j=%zu failed to set position limits", logPrefix, j);
2618 return false;
2619 }
2620 } else
2621 {
2622 // Read 0x607D:min/max from SDO, convert to degrees, and cache into limits vectors.
2623 const int s = m_impl->firstSlave + static_cast<int>(j);
2624 int32_t minCounts = 0;
2625 int32_t maxCounts = 0;
2626 auto e1 = m_impl->ethercatManager.readSDO<int32_t>(s, 0x607D, 0x01, minCounts);
2627 auto e2 = m_impl->ethercatManager.readSDO<int32_t>(s, 0x607D, 0x02, maxCounts);
2630 {
2631 yCError(CIA402, "%s j=%zu failed to read position limits from SDO", logPrefix, j);
2632 return false;
2633 }
2634
2635 // Convert counts back to joint-space degrees.
2636 double minDeg = 0.0;
2637 double maxDeg = 0.0;
2638 if (!inv)
2639 {
2640 minDeg = m_impl->targetCountsToJointDeg(j, minCounts);
2641 maxDeg = m_impl->targetCountsToJointDeg(j, maxCounts);
2642 } else
2643 {
2644 // When inverted, SDO holds [-U, -L]; swap to recover [L, U] in degrees.
2645 minDeg = m_impl->targetCountsToJointDeg(j, maxCounts);
2646 maxDeg = m_impl->targetCountsToJointDeg(j, minCounts);
2647 }
2648
2649 m_impl->limits.minPositionLimitDeg[j] = minDeg;
2650 m_impl->limits.maxPositionLimitDeg[j] = maxDeg;
2651 }
2652 }
2653
2654 // ---------------------------------------------------------------------
2655 // Idle all drives (switch‑on disabled, no mode selected)
2656 // ---------------------------------------------------------------------
2657 for (size_t j = 0; j < m_impl->numAxes; ++j)
2658 {
2659 const int slave = m_impl->firstSlave + static_cast<int>(j);
2660 auto* rx = m_impl->ethercatManager.getRxPDO(slave);
2661 if (!rx)
2662 {
2663 yCError(CIA402, "%s invalid slave index %d for axis %zu", logPrefix, slave, j);
2664 return false;
2665 }
2666 rx->Controlword = 0x0000;
2667 rx->OpMode = 0;
2668 }
2669
2670 // Send one frame so the outputs take effect
2671 if (m_impl->ethercatManager.sendReceive() != ::CiA402::EthercatManager::Error::NoError)
2672 {
2673 yCError(CIA402, "%s initial EtherCAT send/receive after SDO reading failed", logPrefix);
2674 return false;
2675 }
2676
2677 // ---------------------------------------------------------------------
2678 // Configure position windows (targetReached thresholds)
2679 // ---------------------------------------------------------------------
2680 for (int j = 0; j < m_impl->numAxes; ++j)
2681 {
2682 if (!m_impl->setPositionWindowDeg(j, positionWindowDeg[j], timingWindowMs[j]))
2683 {
2684 yCError(CIA402, "%s j=%d failed to set position window", logPrefix, j);
2685 return false;
2686 }
2687 }
2688
2689 // ---------------------------------------------------------------------
2690 // Allocate runtime containers and CiA‑402 state machines
2691 // ---------------------------------------------------------------------
2692
2693 m_impl->variables.resizeContainers(m_impl->numAxes);
2694 m_impl->setPoints.resize(m_impl->numAxes);
2695 m_impl->setPoints.reset();
2696 m_impl->controlModeState.resize(m_impl->numAxes);
2697 m_impl->sm.resize(m_impl->numAxes);
2698 m_impl->velLatched.assign(m_impl->numAxes, false);
2699 m_impl->trqLatched.assign(m_impl->numAxes, false);
2700 m_impl->currLatched.assign(m_impl->numAxes, false);
2701 m_impl->posLatched.assign(m_impl->numAxes, false);
2702 m_impl->torqueSeedNm.assign(m_impl->numAxes, 0.0);
2703 m_impl->tsLastRaw.assign(m_impl->numAxes, 0u);
2704 m_impl->tsWraps.assign(m_impl->numAxes, 0u);
2705 m_impl->ppState.ppRefSpeedDegS.assign(m_impl->numAxes, 0.0);
2706 m_impl->ppState.ppRefAccelerationDegSS.assign(m_impl->numAxes, 0.0);
2707 m_impl->ppState.ppHaltRequested.assign(m_impl->numAxes, false);
2708
2709 for (size_t j = 0; j < m_impl->numAxes; ++j)
2710 {
2711 m_impl->sm[j] = std::make_unique<CiA402::StateMachine>();
2712 m_impl->sm[j]->reset();
2713 }
2714
2715 // get the axes names (if available)
2716 if (!extractListOfStringFromSearchable(cfg, "axes_names", {}, m_impl->variables.jointNames))
2717 return false;
2718
2719 constexpr double initialPositionVelocityDegs = 10;
2720 for (size_t j = 0; j < m_impl->numAxes; ++j)
2721 {
2722 // Cache for getRefSpeed()
2723 {
2724 std::lock_guard<std::mutex> lock(m_impl->ppState.mutex);
2725 m_impl->ppState.ppRefSpeedDegS[j] = initialPositionVelocityDegs;
2726 }
2727 m_impl->setSDORefSpeed(j, initialPositionVelocityDegs);
2728 }
2729
2730 yCInfo(CIA402, "%s opened %zu axes, initialization complete", logPrefix, m_impl->numAxes);
2731
2732 // ---------------------------------------------------------------------
2733 // Transition to OPERATIONAL and enable DC
2734 // ---------------------------------------------------------------------
2735 {
2736 const auto opErr = m_impl->ethercatManager.goOperational();
2738 {
2739 yCError(CIA402, "%s failed to enter OPERATIONAL state", logPrefix);
2740 return false;
2741 }
2742
2743 if (enableDc)
2744 {
2745 const auto dcErr
2746 = m_impl->ethercatManager.enableDCSync0(cycleNs, /*shift_ns=*/dcShiftNs);
2748 {
2749 yCError(CIA402, "%s failed to enable DC SYNC0", logPrefix);
2750 return false;
2751 }
2752 } else
2753 {
2754 yWarning("%s DC synchronization disabled by configuration", logPrefix);
2755 }
2756 }
2757
2758 // ---------------------------------------------------------------------
2759 // Startup safety check: verify joints are within limits
2760 // ---------------------------------------------------------------------
2761 {
2762 // Refresh PDO inputs once so feedback reflects the current position
2763 if (m_impl->ethercatManager.sendReceive() != ::CiA402::EthercatManager::Error::NoError)
2764 {
2765 yCError(CIA402, "%s initial send/receive in OPERATIONAL failed", logPrefix);
2766 return false;
2767 }
2768
2769 (void)m_impl->readFeedback();
2770
2771 bool outOfLimits = false;
2772 for (size_t j = 0; j < m_impl->numAxes; ++j)
2773 {
2774 const double posDeg = m_impl->variables.jointPositions[j];
2775 const double minDeg = m_impl->limits.minPositionLimitDeg[j];
2776 const double maxDeg = m_impl->limits.maxPositionLimitDeg[j];
2777
2778 const bool belowMin = (posDeg < minDeg);
2779 const bool aboveMax = (posDeg > maxDeg);
2780
2781 if (belowMin || aboveMax)
2782 {
2783 const char* axisLabel = nullptr;
2784 if (j < m_impl->variables.jointNames.size()
2785 && !m_impl->variables.jointNames[j].empty())
2786 {
2787 axisLabel = m_impl->variables.jointNames[j].c_str();
2788 }
2789
2790 yCError(CIA402,
2791 "%s joint %zu%s%s%s out of limits: pos=%.6f deg, min=%.6f, max=%.6f. "
2792 "Move the joint within limits before starting.",
2793 logPrefix,
2794 j,
2795 axisLabel ? " (" : "",
2796 axisLabel ? axisLabel : "",
2797 axisLabel ? ")" : "",
2798 posDeg,
2799 minDeg,
2800 maxDeg);
2801 outOfLimits = true;
2802 }
2803 }
2804
2805 if (outOfLimits)
2806 {
2807 // Disable power stage before aborting
2808 for (size_t j = 0; j < m_impl->numAxes; ++j)
2809 {
2810 const int slave = m_impl->firstSlave + static_cast<int>(j);
2811 auto* rx = m_impl->ethercatManager.getRxPDO(slave);
2812 if (rx)
2813 {
2814 rx->Controlword = 0x0000;
2815 rx->OpMode = 0;
2816 }
2817 }
2818
2819 (void)m_impl->ethercatManager.sendReceive();
2820 return false;
2821 }
2822 }
2823
2824 // ---------------------------------------------------------------------
2825 // Launch the device thread
2826 // ---------------------------------------------------------------------
2827 if (!this->start())
2828 {
2829 yCError(CIA402, "%s failed to start device thread", logPrefix);
2830 return false;
2831 }
2832
2833 yCInfo(CIA402,
2834 "%s device thread started (position strategy=%u: %s)",
2835 logPrefix,
2836 static_cast<unsigned>(desiredPositionStrategy),
2837 useSimplePidMode ? "simple PID" : "cascaded PID");
2838
2839 if (simplePidGainsAvailable)
2840 {
2841 yCInfo(CIA402, "%s simple PID gains:", logPrefix);
2842 for (size_t j = 0; j < m_impl->numAxes; ++j)
2843 {
2844 const char* axisLabel = nullptr;
2845 if (j < m_impl->variables.jointNames.size() && !m_impl->variables.jointNames[j].empty())
2846 {
2847 axisLabel = m_impl->variables.jointNames[j].c_str();
2848 }
2849
2850 const char* open = axisLabel ? " (" : "";
2851 const char* label = axisLabel ? axisLabel : "";
2852 const char* close = axisLabel ? ")" : "";
2853
2854 yCInfo(CIA402,
2855 "%s j=%zu%s%s%s Kp=%.6f Nm/deg, Kd=%.6f Nm\xC2\xB7s/deg",
2856 logPrefix,
2857 j,
2858 open,
2859 label,
2860 close,
2861 readKp[j],
2862 readKd[j]);
2863 }
2864 }
2865
2866 return true;
2867}
2868
2869// close() — stop the thread & release the NIC
2871{
2872 std::lock_guard<std::mutex> closeGuard(m_impl->CiA402MotionControlMutex);
2873 this->suspend();
2874 this->stop(); // PeriodicThread → graceful stop
2875 yCInfo(CIA402, "%s: EtheCAT master closed.", Impl::kClassName.data());
2876 return true;
2877}
2878
2879// run() — gets called every period (real-time control loop)
2880/**
2881 * Control loop phases:
2882 * 1) Apply user‑requested control modes (CiA‑402 power state machine)
2883 * 2) Push user setpoints to PDOs (units and shaft conversions handled)
2884 * 3) Cyclic exchange over EtherCAT (outputs sent / inputs read)
2885 * 4) Read back feedback (encoders, torque, safety, timestamp)
2886 * 5) Diagnostics and bookkeeping (active mode tracking, latching, inhibit)
2887 */
2889{
2890 constexpr auto logPrefix = "[run]";
2891 const auto tStart = std::chrono::steady_clock::now();
2892
2893 // lock the mutex
2894 std::lock_guard<std::mutex> runGuard(m_impl->CiA402MotionControlMutex);
2895
2896 /* ------------------------------------------------------------------
2897 * 1. APPLY USER-REQUESTED CONTROL MODES (CiA-402 power machine)
2898 * ----------------------------------------------------------------*/
2899 {
2900 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
2901
2902 for (size_t j = 0; j < m_impl->numAxes; ++j)
2903 {
2904 const int slaveIdx = m_impl->firstSlave + static_cast<int>(j);
2905 ::CiA402::RxPDO* rx = m_impl->ethercatManager.getRxPDO(slaveIdx);
2906 auto tx = m_impl->ethercatManager.getTxView(slaveIdx);
2907
2908 const int tgt = m_impl->controlModeState.target[j];
2909
2910 // If in FAULT the only way to escape from the fault is to do FORCE IDLE
2911 if (m_impl->controlModeState.active[j] == VOCAB_CM_HW_FAULT
2912 && tgt == VOCAB_CM_FORCE_IDLE)
2913 {
2914 const auto cmd = m_impl->sm[j]->faultReset(); // CW=0x0080
2915 rx->Controlword = cmd.controlword;
2916 rx->OpMode = 0; // neutral
2917 // Clear user setpoints/latches immediately
2918 m_impl->setPoints.reset((int)j);
2919 m_impl->velLatched[j] = m_impl->trqLatched[j] = m_impl->currLatched[j] = false;
2920 continue; // skip normal update-path this cycle
2921 }
2922
2923 /* ------------ normal control-mode path --------------------- */
2924 const int8_t opReq = Impl::yarpToCiaOp(tgt); // −1 = unsupported
2925 if (opReq < 0)
2926 { // ignore unknown modes
2927 continue;
2928 }
2929
2930 const bool hwInhibit = (tx.has(CiA402::TxField::STO_6621_01)
2931 && tx.get<uint8_t>(CiA402::TxField::STO_6621_01))
2933 && tx.get<uint8_t>(CiA402::TxField::SBC_6621_02));
2934 const uint16_t sw = tx.get<uint16_t>(CiA402::TxField::Statusword, 0);
2935 const int8_t od = tx.get<int8_t>(CiA402::TxField::OpModeDisplay, 0);
2936 CiA402::StateMachine::Command cmd = m_impl->sm[j]->update(sw, od, opReq, hwInhibit);
2937
2938 rx->Controlword = cmd.controlword;
2939 if (cmd.writeOpMode)
2940 {
2941 rx->OpMode = cmd.opMode;
2942 }
2943 }
2944 } /* mutex unlocked – PDOs are now ready to send */
2945
2946 /* ------------------------------------------------------------------
2947 * 2. SET USER-REQUESTED SETPOINTS (torque, position, velocity)
2948 * ----------------------------------------------------------------*/
2949 m_impl->setSetPoints();
2950
2951 /* ------------------------------------------------------------------
2952 * 3. CYCLIC EXCHANGE (send outputs / read inputs)
2953 * ----------------------------------------------------------------*/
2954 if (m_impl->ethercatManager.sendReceive() != ::CiA402::EthercatManager::Error::NoError)
2955 {
2956 yCError(CIA402,
2957 "%s sendReceive() failed, expected_wkc=%d got_wkc=%d",
2958 logPrefix,
2959 m_impl->ethercatManager.getExpectedWorkingCounter(),
2960 m_impl->ethercatManager.getWorkingCounter());
2961 m_impl->ethercatManager.dumpDiagnostics();
2962 }
2963
2964 /* ------------------------------------------------------------------
2965 * 4. COPY ENCODERS & OTHER FEEDBACK
2966 * ----------------------------------------------------------------*/
2967 m_impl->readFeedback();
2968
2969 /* ------------------------------------------------------------------
2970 * 5. DIAGNOSTICS (fill active[] according to feedback)
2971 * ----------------------------------------------------------------*/
2972 {
2973 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
2974
2975 for (size_t j = 0; j < m_impl->numAxes; ++j)
2976 {
2977 const int slaveIdx = m_impl->firstSlave + static_cast<int>(j);
2978 auto tx = m_impl->ethercatManager.getTxView(slaveIdx);
2979 bool flavourChanged = false;
2980
2981 const bool hwInhibit = (tx.has(CiA402::TxField::STO_6621_01)
2982 && tx.get<uint8_t>(CiA402::TxField::STO_6621_01))
2984 && tx.get<uint8_t>(CiA402::TxField::SBC_6621_02));
2985 const bool enabled = CiA402::StateMachine::isOperationEnabled(
2986 tx.get<uint16_t>(CiA402::TxField::Statusword, 0));
2987 const bool inFault
2990 tx.get<uint16_t>(CiA402::TxField::Statusword, 0));
2991
2992 int newActive = VOCAB_CM_IDLE;
2993 if (inFault)
2994 {
2995 newActive = VOCAB_CM_HW_FAULT;
2996 } else if (!hwInhibit && enabled)
2997 {
2998 const int activeOp = m_impl->sm[j]->getActiveOpMode();
2999 // If the drive is in CST (10), reflect the user-facing label (CURRENT or TORQUE)
3000 // that was last requested for this axis. Otherwise use the generic mapping.
3001 if (activeOp == 10)
3002 {
3003 // Guarded by the same mutex we already hold
3004 int flavor = m_impl->controlModeState.cstFlavor[j];
3005 // Safety net: constrain to CURRENT/TORQUE only
3006 if (flavor != VOCAB_CM_CURRENT && flavor != VOCAB_CM_TORQUE)
3007 {
3008 flavor = VOCAB_CM_TORQUE;
3009 }
3010 newActive = flavor;
3011
3012 // detect if the CST flavor changed since last time
3013 flavourChanged
3014 = (flavor != m_impl->controlModeState.prevCstFlavor[j])
3015 || (m_impl->controlModeState.prevCstFlavor[j] == VOCAB_CM_UNKNOWN);
3016 } else
3017 {
3018 newActive = Impl::ciaOpToYarp(activeOp);
3019 }
3020 }
3021
3022 // If IDLE or FAULT or inhibited → clear SPs and latches; force target to IDLE on HW
3023 // inhibit
3024 if (newActive == VOCAB_CM_IDLE || newActive == VOCAB_CM_HW_FAULT
3025 || newActive == VOCAB_CM_FORCE_IDLE)
3026 {
3027 m_impl->setPoints.reset(j);
3028 m_impl->velLatched[j] = m_impl->trqLatched[j] = m_impl->posLatched[j]
3029 = m_impl->currLatched[j] = false;
3030 if (hwInhibit)
3031 {
3032 m_impl->controlModeState.target[j] = VOCAB_CM_IDLE;
3033 }
3034 }
3035
3036 // Detect mode entry to (re)arm first-cycle latches
3037 if (m_impl->controlModeState.active[j] != newActive || flavourChanged)
3038 {
3039 // entering a control mode: arm latches and clear "has SP" flags
3040 m_impl->velLatched[j] = m_impl->trqLatched[j] = m_impl->posLatched[j]
3041 = m_impl->currLatched[j] = false;
3042 m_impl->setPoints.reset(j);
3043 }
3044
3045 m_impl->controlModeState.active[j] = newActive;
3046 m_impl->controlModeState.prevCstFlavor[j] = m_impl->controlModeState.cstFlavor[j];
3047 }
3048 }
3049
3050 // ---- profiling: compute per-call duration and log 30s average ----
3051 constexpr double printingIntervalSec = 30.0;
3052 const auto tEnd = std::chrono::steady_clock::now();
3053 const double dtSec
3055 m_impl->runTimeAccumSec += dtSec;
3056 m_impl->runTimeSamples += 1;
3057 const auto windowSec
3058 = std::chrono::duration_cast<std::chrono::duration<double>>(tEnd - m_impl->avgWindowStart)
3059 .count();
3060 if (windowSec >= printingIntervalSec)
3061 {
3062 const double avgMs
3063 = (m_impl->runTimeSamples > 0)
3064 ? (m_impl->runTimeAccumSec / static_cast<double>(m_impl->runTimeSamples)) * 1000.0
3065 : 0.0;
3066 yCInfoThrottle(CIA402,
3067 printingIntervalSec,
3068 "%s %.1fs window: avg run() time = %.3f ms (frequency=%.1f Hz) over %zu "
3069 "cycles (window=%.1fs) - Expected period=%.3f ms (%.1f Hz)",
3070 logPrefix,
3071 printingIntervalSec,
3072 avgMs,
3073 (m_impl->runTimeSamples > 0) ? (1000.0 / avgMs) : 0.0,
3074 m_impl->runTimeSamples,
3075 windowSec,
3076 this->getPeriod() * 1000.0,
3077 1.0 / this->getPeriod());
3078 m_impl->runTimeAccumSec = 0.0;
3079 m_impl->runTimeSamples = 0;
3080 m_impl->avgWindowStart = tEnd;
3081 }
3082}
3083
3084// -----------------------------------------------
3085// ----------------- IMotorEncoders --------------
3086// -----------------------------------------------
3087
3089{
3090 if (num == nullptr)
3091 {
3092 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3093 return false;
3094 }
3095 *num = static_cast<int>(m_impl->numAxes);
3096 return true;
3097}
3098
3100{
3101 yCError(CIA402, "%s: resetMotorEncoder() not implemented", Impl::kClassName.data());
3102 return false;
3103}
3104
3106{
3107 yCError(CIA402, "%s: resetMotorEncoders() not implemented", Impl::kClassName.data());
3108 return false;
3109}
3110
3112{
3113 yCError(CIA402,
3114 "%s: setMotorEncoderCountsPerRevolution() not implemented",
3115 Impl::kClassName.data());
3116 return false;
3117}
3118
3119bool CiA402MotionControl::setMotorEncoder(int m, const double v)
3120{
3121 yCError(CIA402, "%s: setMotorEncoder() not implemented", Impl::kClassName.data());
3122 return false;
3123}
3124
3126{
3127 yCError(CIA402, "%s: setMotorEncoders() not implemented", Impl::kClassName.data());
3128 return false;
3129}
3130
3132{
3133 if (cpr == nullptr)
3134 {
3135 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3136 return false;
3137 }
3138 if (m >= static_cast<int>(m_impl->numAxes))
3139 {
3140 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), m);
3141 return false;
3142 }
3143
3144 {
3145 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3146
3147 // check which encoder is mounted on the motor side
3148 if (m_impl->enc1Mount[m] == Impl::Mount::Motor)
3149 *cpr = static_cast<double>(m_impl->enc1Res[m]);
3150 else if (m_impl->enc2Mount[m] == Impl::Mount::Motor)
3151 *cpr = static_cast<double>(m_impl->enc2Res[m]);
3152 else
3153 return false; // no encoder on motor side
3154 }
3155 return true;
3156}
3157
3159{
3160 if (v == nullptr)
3161 {
3162 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3163 return false;
3164 }
3165 if (m >= static_cast<int>(m_impl->numAxes))
3166 {
3167 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), m);
3168 return false;
3169 }
3170 {
3171 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3172 *v = m_impl->variables.motorEncoders[m];
3173 }
3174 return true;
3175}
3176
3178{
3179 if (encs == nullptr)
3180 {
3181 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3182 return false;
3183 }
3184 {
3185 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3186 std::memcpy(encs, m_impl->variables.motorEncoders.data(), m_impl->numAxes * sizeof(double));
3187 }
3188
3189 return true;
3190}
3191
3192bool CiA402MotionControl::getMotorEncodersTimed(double* encs, double* time)
3193{
3194 if (encs == nullptr || time == nullptr)
3195 {
3196 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3197 return false;
3198 }
3199 {
3200 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3201 std::memcpy(encs, m_impl->variables.motorEncoders.data(), m_impl->numAxes * sizeof(double));
3202 std::memcpy(time, m_impl->variables.feedbackTime.data(), m_impl->numAxes * sizeof(double));
3203 }
3204 return true;
3205}
3206
3207bool CiA402MotionControl::getMotorEncoderTimed(int m, double* encs, double* time)
3208{
3209 if (encs == nullptr || time == nullptr)
3210 {
3211 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3212 return false;
3213 }
3214 if (m >= static_cast<int>(m_impl->numAxes))
3215 {
3216 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), m);
3217 return false;
3218 }
3219 {
3220 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3221 *encs = m_impl->variables.motorEncoders[m];
3222 *time = m_impl->variables.feedbackTime[m];
3223 }
3224 return true;
3225}
3226
3228{
3229 if (sp == nullptr)
3230 {
3231 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3232 return false;
3233 }
3234 if (m >= static_cast<int>(m_impl->numAxes))
3235 {
3236 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), m);
3237 return false;
3238 }
3239 {
3240 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3241 *sp = m_impl->variables.motorVelocities[m];
3242 }
3243 return true;
3244}
3245
3247{
3248 if (spds == nullptr)
3249 {
3250 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3251 return false;
3252 }
3253 {
3254 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3255 std::memcpy(spds,
3256 m_impl->variables.motorVelocities.data(),
3257 m_impl->numAxes * sizeof(double));
3258 }
3259 return true;
3260}
3261
3263{
3264 if (acc == nullptr)
3265 {
3266 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3267 return false;
3268 }
3269 if (m >= static_cast<int>(m_impl->numAxes))
3270 {
3271 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), m);
3272 return false;
3273 }
3274 {
3275 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3276 *acc = m_impl->variables.motorAccelerations[m];
3277 }
3278 return true;
3279}
3280
3282{
3283 if (accs == nullptr)
3284 {
3285 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3286 return false;
3287 }
3288 {
3289 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3290 std::memcpy(accs,
3291 m_impl->variables.motorAccelerations.data(),
3292 m_impl->numAxes * sizeof(double));
3293 }
3294 return true;
3295}
3296
3297// -----------------------------------------------
3298// ----------------- IEncoders ------------------
3299// -----------------------------------------------
3300bool CiA402MotionControl::getEncodersTimed(double* encs, double* time)
3301{
3302 if (encs == nullptr || time == nullptr)
3303 {
3304 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3305 return false;
3306 }
3307 {
3308 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3309 std::memcpy(encs,
3310 m_impl->variables.jointPositions.data(),
3311 m_impl->numAxes * sizeof(double));
3312 std::memcpy(time, m_impl->variables.feedbackTime.data(), m_impl->numAxes * sizeof(double));
3313 }
3314 return true;
3315}
3316
3317bool CiA402MotionControl::getEncoderTimed(int j, double* encs, double* time)
3318{
3319 if (encs == nullptr || time == nullptr)
3320 {
3321 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3322 return false;
3323 }
3324 if (j >= static_cast<int>(m_impl->numAxes))
3325 {
3326 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3327 return false;
3328 }
3329 {
3330 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3331 *encs = m_impl->variables.jointPositions[j];
3332 *time = m_impl->variables.feedbackTime[j];
3333 }
3334 return true;
3335}
3336
3338{
3339 if (ax == nullptr)
3340 {
3341 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3342 return false;
3343 }
3344 *ax = static_cast<int>(m_impl->numAxes);
3345 return true;
3346}
3347
3349{
3350 yCError(CIA402, "%s: resetEncoder() not implemented", Impl::kClassName.data());
3351 return false;
3352}
3353
3355{
3356 yCError(CIA402, "%s: resetEncoders() not implemented", Impl::kClassName.data());
3357 return false;
3358}
3359
3360bool CiA402MotionControl::setEncoder(int j, const double val)
3361{
3362 yCError(CIA402, "%s: setEncoder() not implemented", Impl::kClassName.data());
3363 return false;
3364}
3365
3366bool CiA402MotionControl::setEncoders(const double* vals)
3367{
3368 yCError(CIA402, "%s: setEncoders() not implemented", Impl::kClassName.data());
3369 return false;
3370}
3371
3373{
3374 if (v == nullptr)
3375 {
3376 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3377 return false;
3378 }
3379 if (j >= static_cast<int>(m_impl->numAxes))
3380 {
3381 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3382 return false;
3383 }
3384 {
3385 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3386 *v = m_impl->variables.jointPositions[j];
3387 }
3388 return true;
3389}
3390
3392{
3393 if (encs == nullptr)
3394 {
3395 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3396 return false;
3397 }
3398 {
3399 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3400 std::memcpy(encs,
3401 m_impl->variables.jointPositions.data(),
3402 m_impl->numAxes * sizeof(double));
3403 }
3404 return true;
3405}
3406
3408{
3409 if (sp == nullptr)
3410 {
3411 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3412 return false;
3413 }
3414 if (j >= static_cast<int>(m_impl->numAxes))
3415 {
3416 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3417 return false;
3418 }
3419 {
3420 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3421 *sp = m_impl->variables.jointVelocities[j];
3422 }
3423 return true;
3424}
3425
3427{
3428 if (spds == nullptr)
3429 {
3430 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3431 return false;
3432 }
3433 {
3434 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3435 std::memcpy(spds,
3436 m_impl->variables.jointVelocities.data(),
3437 m_impl->numAxes * sizeof(double));
3438 }
3439 return true;
3440}
3441
3443{
3444 if (spds == nullptr)
3445 {
3446 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3447 return false;
3448 }
3449 if (j >= static_cast<int>(m_impl->numAxes))
3450 {
3451 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3452 return false;
3453 }
3454 {
3455 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3456 *spds = m_impl->variables.jointAccelerations[j];
3457 }
3458 return true;
3459}
3460
3462{
3463 if (accs == nullptr)
3464 {
3465 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3466 return false;
3467 }
3468 {
3469 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3470 std::memcpy(accs,
3471 m_impl->variables.jointAccelerations.data(),
3472 m_impl->numAxes * sizeof(double));
3473 }
3474 return true;
3475}
3476
3477/// -----------------------------------------------
3478/// ----------------- IAxisInfo ------------------
3479/// -----------------------------------------------
3480
3482{
3483 if (j >= static_cast<int>(m_impl->numAxes))
3484 {
3485 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3486 return false;
3487 }
3488
3489 // We assume that the name is already filled in the open() method and does not change
3490 // during the lifetime of the object. For this reason we do not need to lock the mutex
3491 // here.
3492 name = m_impl->variables.jointNames[j];
3493 return true;
3494}
3495
3496bool CiA402MotionControl::getJointType(int axis, yarp::dev::JointTypeEnum& type)
3497{
3498 if (axis >= static_cast<int>(m_impl->numAxes))
3499 {
3500 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), axis);
3501 return false;
3502 }
3503 type = yarp::dev::JointTypeEnum::VOCAB_JOINTTYPE_REVOLUTE; // TODO: add support for linear
3504 // joints
3505 return true;
3506}
3507
3508// -------------------------- IControlMode ------------------------------------
3510{
3511 if (mode == nullptr)
3512 {
3513 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3514 return false;
3515 }
3516 if (j >= static_cast<int>(m_impl->numAxes))
3517 {
3518 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3519 return false;
3520 }
3521
3522 {
3523 std::lock_guard<std::mutex> lock(m_impl->controlModeState.mutex);
3524 *mode = m_impl->controlModeState.active[j];
3525 }
3526 return true;
3527}
3528
3530{
3531 if (modes == nullptr)
3532 {
3533 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3534 return false;
3535 }
3536
3537 {
3538 std::lock_guard<std::mutex> l(m_impl->controlModeState.mutex);
3539 std::memcpy(modes, m_impl->controlModeState.active.data(), m_impl->numAxes * sizeof(int));
3540 }
3541 return true;
3542}
3543
3544bool CiA402MotionControl::getControlModes(const int n, const int* joints, int* modes)
3545{
3546 if (modes == nullptr || joints == nullptr)
3547 {
3548 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3549 return false;
3550 }
3551 if (n <= 0)
3552 {
3553 yCError(CIA402, "%s: invalid number of joints %d", Impl::kClassName.data(), n);
3554 return false;
3555 }
3556
3557 {
3558 std::lock_guard<std::mutex> l(m_impl->controlModeState.mutex);
3559 for (int k = 0; k < n; ++k)
3560 {
3561 if (joints[k] >= static_cast<int>(m_impl->numAxes))
3562 {
3563 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), joints[k]);
3564 return false;
3565 }
3566 modes[k] = m_impl->controlModeState.active[joints[k]];
3567 }
3568 }
3569 return true;
3570}
3571
3572bool CiA402MotionControl::setControlMode(const int j, const int mode)
3573{
3574 if (j >= static_cast<int>(m_impl->numAxes))
3575 {
3576 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3577 return false;
3578 }
3579 if (Impl::yarpToCiaOp(mode) < 0)
3580 {
3581 yCError(CIA402, "%s: control mode %d not supported", Impl::kClassName.data(), mode);
3582 return false;
3583 }
3584
3585 std::lock_guard<std::mutex> l(m_impl->controlModeState.mutex);
3586 m_impl->controlModeState.target[j] = mode;
3587 if (mode == VOCAB_CM_CURRENT || mode == VOCAB_CM_TORQUE)
3588 {
3589 m_impl->controlModeState.cstFlavor[j] = mode;
3590 }
3591
3592 return true;
3593}
3594
3595bool CiA402MotionControl::setControlModes(const int n, const int* joints, int* modes)
3596{
3597 if (modes == nullptr || joints == nullptr)
3598 {
3599 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3600 return false;
3601 }
3602 if (n <= 0)
3603 {
3604 yCError(CIA402, "%s: invalid number of joints %d", Impl::kClassName.data(), n);
3605 return false;
3606 }
3607 std::lock_guard<std::mutex> l(m_impl->controlModeState.mutex);
3608 for (int k = 0; k < n; ++k)
3609 {
3610 if (joints[k] >= static_cast<int>(m_impl->numAxes))
3611 {
3612 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), joints[k]);
3613 return false;
3614 }
3615 m_impl->controlModeState.target[joints[k]] = modes[k];
3616
3617 if (modes[k] == VOCAB_CM_CURRENT || modes[k] == VOCAB_CM_TORQUE)
3618 {
3619 m_impl->controlModeState.cstFlavor[joints[k]] = modes[k];
3620 }
3621 }
3622 return true;
3623}
3624
3626{
3627 if (modes == nullptr)
3628 {
3629 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3630 return false;
3631 }
3632 std::lock_guard<std::mutex> l(m_impl->controlModeState.mutex);
3633 std::memcpy(m_impl->controlModeState.target.data(), modes, m_impl->numAxes * sizeof(int));
3634
3635 for (size_t j = 0; j < m_impl->numAxes; ++j)
3636 {
3637 if (m_impl->controlModeState.target[j] == VOCAB_CM_CURRENT
3638 || m_impl->controlModeState.target[j] == VOCAB_CM_TORQUE)
3639 {
3640 m_impl->controlModeState.cstFlavor[j] = m_impl->controlModeState.target[j];
3641 }
3642 }
3643
3644 return true;
3645}
3646
3647//// -------------------------- ITorqueControl ------------------------------------
3649{
3650 if (t == nullptr)
3651 {
3652 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3653 return false;
3654 }
3655 if (j >= static_cast<int>(m_impl->numAxes))
3656 {
3657 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3658 return false;
3659 }
3660 {
3661 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3662 *t = m_impl->variables.jointTorques[j];
3663 }
3664 return true;
3665}
3666
3668{
3669 if (t == nullptr)
3670 {
3671 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3672 return false;
3673 }
3674 {
3675 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
3676
3677 std::memcpy(t, m_impl->variables.jointTorques.data(), m_impl->numAxes * sizeof(double));
3678 }
3679 return true;
3680}
3681
3683{
3684 if (t == nullptr)
3685 {
3686 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3687 return false;
3688 }
3689 if (j >= static_cast<int>(m_impl->numAxes))
3690 {
3691 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3692 return false;
3693 }
3694 {
3695 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
3696 *t = m_impl->setPoints.jointTorques[j];
3697 }
3698 return true;
3699}
3700
3702{
3703 if (t == nullptr)
3704 {
3705 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3706 return false;
3707 }
3708 {
3709 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
3710 std::memcpy(t, m_impl->setPoints.jointTorques.data(), m_impl->numAxes * sizeof(double));
3711 }
3712 return true;
3713}
3714
3716{
3717 if (j >= static_cast<int>(m_impl->numAxes))
3718 {
3719 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3720 return false;
3721 }
3722
3723 // (a/b) Only accept if TORQUE is ACTIVE; otherwise reject (not considered)
3724 {
3725 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
3726 if (m_impl->controlModeState.active[j] != VOCAB_CM_TORQUE)
3727 {
3728 yCError(CIA402,
3729 "%s: setRefTorque rejected: TORQUE mode is not active for the joint %d",
3730 Impl::kClassName.data(),
3731 j);
3732 return false;
3733 }
3734 }
3735
3736 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
3737 m_impl->setPoints.jointTorques[j] = t;
3738 m_impl->setPoints.hasTorqueSP[j] = true; // (b)
3739 return true;
3740}
3741
3743{
3744 if (t == nullptr)
3745 {
3746 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3747 return false;
3748 }
3749 {
3750 // check that all the joints are in TORQUE mode use std function without for loop
3751 std::lock_guard<std::mutex> lock(m_impl->controlModeState.mutex);
3752 for (size_t j = 0; j < m_impl->numAxes; ++j)
3753 {
3754 if (m_impl->controlModeState.active[j] != VOCAB_CM_TORQUE)
3755 {
3756 yCError(CIA402,
3757 "%s: setRefTorques rejected: TORQUE mode is not active for the joint "
3758 "%zu",
3759 Impl::kClassName.data(),
3760 j);
3761 return false; // reject
3762 }
3763 }
3764 }
3765
3766 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
3767 std::memcpy(m_impl->setPoints.jointTorques.data(), t, m_impl->numAxes * sizeof(double));
3768 std::fill(m_impl->setPoints.hasTorqueSP.begin(), m_impl->setPoints.hasTorqueSP.end(), true);
3769 return true;
3770}
3771
3772bool CiA402MotionControl::setRefTorques(const int n_joint, const int* joints, const double* t)
3773{
3774 if (t == nullptr || joints == nullptr)
3775 {
3776 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3777 return false;
3778 }
3779 if (n_joint <= 0)
3780 {
3781 yCError(CIA402, "%s: invalid number of joints %d", Impl::kClassName.data(), n_joint);
3782 return false;
3783 }
3784
3785 // check that all the joints are in TORQUE mode
3786 {
3787 std::lock_guard<std::mutex> lock(m_impl->controlModeState.mutex);
3788 for (int k = 0; k < n_joint; ++k)
3789 {
3790 if (joints[k] >= static_cast<int>(m_impl->numAxes))
3791 {
3792 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), joints[k]);
3793 return false;
3794 }
3795 if (m_impl->controlModeState.active[joints[k]] != VOCAB_CM_TORQUE)
3796 {
3797 yCError(CIA402,
3798 "%s: setRefTorques rejected: TORQUE mode is not active for the joint %d",
3799 Impl::kClassName.data(),
3800 joints[k]);
3801 return false; // reject
3802 }
3803 }
3804 }
3805
3806 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
3807 for (int k = 0; k < n_joint; ++k)
3808 {
3809 m_impl->setPoints.jointTorques[joints[k]] = t[k];
3810 m_impl->setPoints.hasTorqueSP[joints[k]] = true;
3811 }
3812 return true;
3813}
3814
3815bool CiA402MotionControl::getTorqueRange(int j, double* min, double* max)
3816{
3817 if (min == nullptr || max == nullptr)
3818 {
3819 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3820 return false;
3821 }
3822 if (j >= static_cast<int>(m_impl->numAxes))
3823 {
3824 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3825 return false;
3826 }
3827
3828 // multiply by the transmission ratio to convert motor torque to joint torque
3829 *min = -m_impl->maxMotorTorqueNm[j] * m_impl->gearRatio[j];
3830 *max = m_impl->maxMotorTorqueNm[j] * m_impl->gearRatio[j];
3831
3832 return true;
3833}
3834
3835bool CiA402MotionControl::getTorqueRanges(double* min, double* max)
3836{
3837 if (min == nullptr || max == nullptr)
3838 {
3839 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3840 return false;
3841 }
3842
3843 for (size_t j = 0; j < m_impl->numAxes; ++j)
3844 {
3845 min[j] = -m_impl->maxMotorTorqueNm[j] * m_impl->gearRatio[j];
3846 max[j] = m_impl->maxMotorTorqueNm[j] * m_impl->gearRatio[j];
3847 }
3848 return true;
3849}
3850
3852{
3853 if (j >= static_cast<int>(m_impl->numAxes))
3854 {
3855 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
3856 return false;
3857 }
3858
3859 // (a/b) Only accept if VELOCITY is ACTIVE; otherwise reject
3860 {
3861 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
3862 if (m_impl->controlModeState.active[j] != VOCAB_CM_VELOCITY)
3863 {
3864 yCError(CIA402,
3865 "%s: velocityMove rejected: VELOCITY mode is not active for the joint %d",
3866 Impl::kClassName.data(),
3867 j);
3868
3869 // this will return true to indicate the rejection was handled
3870 return true;
3871 }
3872 }
3873
3874 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
3875 m_impl->setPoints.jointVelocities[j] = spd;
3876 m_impl->setPoints.hasVelSP[j] = true; // (b)
3877 return true;
3878}
3879
3881{
3882 if (spds == nullptr)
3883 {
3884 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3885 return false;
3886 }
3887
3888 // check that all the joints are in VELOCITY mode
3889 {
3890 std::lock_guard<std::mutex> lock(m_impl->controlModeState.mutex);
3891 for (size_t j = 0; j < m_impl->numAxes; ++j)
3892 {
3893 if (m_impl->controlModeState.active[j] != VOCAB_CM_VELOCITY)
3894 {
3895 yCError(CIA402,
3896 "%s: velocityMove rejected: VELOCITY mode is not active for the joint "
3897 "%zu",
3898 Impl::kClassName.data(),
3899 j);
3900 return false; // reject
3901 }
3902 }
3903 }
3904
3905 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
3906 std::memcpy(m_impl->setPoints.jointVelocities.data(), spds, m_impl->numAxes * sizeof(double));
3907 std::fill(m_impl->setPoints.hasVelSP.begin(), m_impl->setPoints.hasVelSP.end(), true);
3908 return true;
3909}
3910
3911bool CiA402MotionControl::getRefVelocity(const int joint, double* vel)
3912{
3913 if (vel == nullptr)
3914 {
3915 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3916 return false;
3917 }
3918 if (joint >= static_cast<int>(m_impl->numAxes))
3919 {
3920 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), joint);
3921 return false;
3922 }
3923 {
3924 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
3925 *vel = m_impl->setPoints.jointVelocities[joint];
3926 }
3927 return true;
3928}
3929
3931{
3932 if (spds == nullptr)
3933 {
3934 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3935 return false;
3936 }
3937 {
3938 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
3939 std::memcpy(spds,
3940 m_impl->setPoints.jointVelocities.data(),
3941 m_impl->numAxes * sizeof(double));
3942 }
3943 return true;
3944}
3945
3946bool CiA402MotionControl::getRefVelocities(const int n_joint, const int* joints, double* vels)
3947{
3948 if (vels == nullptr || joints == nullptr)
3949 {
3950 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
3951 return false;
3952 }
3953 if (n_joint <= 0)
3954 {
3955 yCError(CIA402, "%s: invalid number of joints %d", Impl::kClassName.data(), n_joint);
3956 return false;
3957 }
3958
3959 {
3960 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
3961 for (int k = 0; k < n_joint; ++k)
3962 {
3963 if (joints[k] >= static_cast<int>(m_impl->numAxes))
3964 {
3965 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), joints[k]);
3966 return false;
3967 }
3968 vels[k] = m_impl->setPoints.jointVelocities[joints[k]];
3969 }
3970 }
3971 return true;
3972}
3973
3974// Acceleration is expressed in YARP joint units [deg/s^2].
3975// For PP we SDO-write 0x6083/0x6084/0x6085 in rpm/s (deg/s^2 ÷ 6).
3976// For CSV we just cache the value (no standard accel SDO in CSV).
3977
3979{
3980 if (j < 0 || j >= static_cast<int>(m_impl->numAxes))
3981 {
3982 yCError(CIA402, "%s: setRefAcceleration: joint %d out of range", Impl::kClassName.data(), j);
3983 return false;
3984 }
3985
3986 // store magnitude
3987 if (accDegS2 < 0.0)
3988 {
3989 accDegS2 = -accDegS2;
3990 }
3991
3992 // saturate to maximum allowed 10 deg/s^2
3993 constexpr double maxAcc = 10.0;
3994 if (accDegS2 > maxAcc)
3995 {
3996 yCWarning(CIA402,
3997 "%s: setRefAcceleration: joint %d: acceleration %.2f deg/s^2 too high, "
3998 "saturating to %.2f deg/s^2",
3999 Impl::kClassName.data(),
4000 j,
4001 accDegS2,
4002 maxAcc);
4003 accDegS2 = maxAcc;
4004 }
4005
4006 // Only touch SDOs if PP is ACTIVE
4007 int controlMode = -1;
4008 {
4009 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4010 controlMode = m_impl->controlModeState.active[j];
4011 }
4012
4013 if (controlMode != VOCAB_CM_POSITION)
4014 {
4015 // do nothing if not in PP
4016 return true;
4017 }
4018
4019 const int s = m_impl->firstSlave + j;
4020 const int32_t acc = static_cast<int32_t>(std::llround(accDegS2 * m_impl->degSToVel[j]));
4021 {
4022 std::lock_guard<std::mutex> lock(m_impl->ppState.mutex);
4023 m_impl->ppState.ppRefAccelerationDegSS[j] = accDegS2;
4024 }
4025
4026 // Profile acceleration / deceleration / quick-stop deceleration
4027 (void)m_impl->ethercatManager.writeSDO<int32_t>(s, 0x6083, 0x00, acc);
4028 (void)m_impl->ethercatManager.writeSDO<int32_t>(s, 0x6084, 0x00, acc);
4029 (void)m_impl->ethercatManager.writeSDO<int32_t>(s, 0x6085, 0x00, acc);
4030
4031 return true;
4032}
4033
4034bool CiA402MotionControl::setRefAccelerations(const double* accsDegS2)
4035{
4036 if (!accsDegS2)
4037 {
4038 yCError(CIA402, "%s: setRefAccelerations: null pointer", Impl::kClassName.data());
4039 return false;
4040 }
4041 bool ok = true;
4042 for (size_t j = 0; j < m_impl->numAxes; ++j)
4043 {
4044 ok = setRefAcceleration(static_cast<int>(j), accsDegS2[j]) && ok;
4045 }
4046 return ok;
4047}
4048
4050{
4051 if (acc == nullptr)
4052 {
4053 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
4054 return false;
4055 }
4056 if (j >= static_cast<int>(m_impl->numAxes))
4057 {
4058 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), j);
4059 return false;
4060 }
4061
4062 int controlMode = -1;
4063 {
4064 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4065 controlMode = m_impl->controlModeState.active[j];
4066 }
4067
4068 if (controlMode == VOCAB_CM_POSITION)
4069 {
4070 // if in PP return the cached value
4071 std::lock_guard<std::mutex> lock(m_impl->ppState.mutex);
4072 *acc = m_impl->ppState.ppRefAccelerationDegSS[j];
4073 return true;
4074 }
4075
4076 *acc = 0.0;
4077 return true;
4078}
4079
4081{
4082 if (accs == nullptr)
4083 {
4084 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
4085 return false;
4086 }
4087
4088 for (size_t j = 0; j < m_impl->numAxes; ++j)
4089 {
4090 int controlMode = -1;
4091 {
4092 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4093 controlMode = m_impl->controlModeState.active[j];
4094 }
4095
4096 if (controlMode == VOCAB_CM_POSITION)
4097 {
4098 // if in PP return the cached value
4099 std::lock_guard<std::mutex> lock(m_impl->ppState.mutex);
4100 accs[j] = m_impl->ppState.ppRefAccelerationDegSS[j];
4101 } else
4102 {
4103 accs[j] = 0.0;
4104 }
4105 }
4106 return true;
4107}
4108
4109// stop() semantics:
4110// • PP → set CW bit 8 (HALT), decelerating with 0x6084.
4111// • CSV/others → zero TargetVelocity (current behavior kept).
4112
4114{
4115 if (j < 0 || j >= static_cast<int>(m_impl->numAxes))
4116 {
4117 yCError(CIA402, "%s: stop: joint %d out of range", Impl::kClassName.data(), j);
4118 return false;
4119 }
4120
4121 int controlMode = -1;
4122 {
4123 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4124 controlMode = m_impl->controlModeState.active[j];
4125 }
4126
4127 if (controlMode == VOCAB_CM_POSITION)
4128 {
4129 std::lock_guard<std::mutex> lock(m_impl->ppState.mutex);
4130 m_impl->ppState.ppHaltRequested[j] = true; // consumed in setSetPoints()
4131 return true;
4132 }
4133
4134 // CSV/others → zero velocity set-point
4135 {
4136 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4137 m_impl->setPoints.jointVelocities[j] = 0.0;
4138 m_impl->setPoints.hasVelSP[j] = true;
4139 }
4140 return true;
4141}
4142
4144{
4145 // PP axes → HALT; non-PP axes → velocity 0
4146 {
4147 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4148 for (size_t j = 0; j < m_impl->numAxes; ++j)
4149 {
4150 if (m_impl->controlModeState.active[j] == VOCAB_CM_POSITION)
4151 {
4152 std::lock_guard<std::mutex> lock(m_impl->ppState.mutex);
4153 m_impl->ppState.ppHaltRequested[j] = true;
4154 }
4155 }
4156 }
4157 {
4158 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4159 std::fill(m_impl->setPoints.jointVelocities.begin(),
4160 m_impl->setPoints.jointVelocities.end(),
4161 0.0);
4162 std::fill(m_impl->setPoints.hasVelSP.begin(), m_impl->setPoints.hasVelSP.end(), true);
4163 }
4164 return true;
4165}
4166
4167bool CiA402MotionControl::velocityMove(const int n_joint, const int* joints, const double* spds)
4168{
4169 if (spds == nullptr || joints == nullptr)
4170 {
4171 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
4172 return false;
4173 }
4174 if (n_joint <= 0)
4175 {
4176 yCError(CIA402, "%s: invalid number of joints %d", Impl::kClassName.data(), n_joint);
4177 return false;
4178 }
4179
4180 // check that all the joints are in VELOCITY mode
4181 {
4182 std::lock_guard<std::mutex> lock(m_impl->controlModeState.mutex);
4183 for (int k = 0; k < n_joint; ++k)
4184 {
4185 if (joints[k] >= static_cast<int>(m_impl->numAxes))
4186 {
4187 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), joints[k]);
4188 return false;
4189 }
4190 if (m_impl->controlModeState.active[joints[k]] != VOCAB_CM_VELOCITY)
4191 {
4192 yCError(CIA402,
4193 "%s: velocityMove rejected: VELOCITY mode is not active for the joint "
4194 "%d",
4195 Impl::kClassName.data(),
4196 joints[k]);
4197 return false; // reject
4198 }
4199 }
4200 }
4201
4202 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4203 for (int k = 0; k < n_joint; ++k)
4204 {
4205 m_impl->setPoints.jointVelocities[joints[k]] = spds[k];
4206 m_impl->setPoints.hasVelSP[joints[k]] = true;
4207 }
4208
4209 return true;
4210}
4211
4213 const int* joints,
4214 const double* accs)
4215{
4216 if (accs == nullptr || joints == nullptr)
4217 {
4218 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
4219 return false;
4220 }
4221 if (n_joint <= 0)
4222 {
4223 yCError(CIA402, "%s: invalid number of joints %d", Impl::kClassName.data(), n_joint);
4224 return false;
4225 }
4226
4227 // no operation
4228 return true;
4229}
4230
4231bool CiA402MotionControl::getRefAccelerations(const int n_joint, const int* joints, double* accs)
4232{
4233 if (accs == nullptr || joints == nullptr)
4234 {
4235 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
4236 return false;
4237 }
4238 std::memset(accs, 0, n_joint * sizeof(double)); // CiA-402 does not support acceleration
4239 // setpoints, so we return 0.0 for all axes
4240 return true;
4241}
4242
4243bool CiA402MotionControl::stop(const int n_joint, const int* joints)
4244{
4245 if (joints == nullptr)
4246 {
4247 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
4248 return false;
4249 }
4250 if (n_joint <= 0)
4251 {
4252 yCError(CIA402, "%s: invalid number of joints %d", Impl::kClassName.data(), n_joint);
4253 return false;
4254 }
4255
4256 {
4257 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4258 for (int k = 0; k < n_joint; ++k)
4259 {
4260 if (joints[k] >= static_cast<int>(m_impl->numAxes))
4261 {
4262 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), joints[k]);
4263 return false;
4264 }
4265 m_impl->setPoints.jointVelocities[joints[k]] = 0.0;
4266 m_impl->setPoints.hasVelSP[joints[k]] = true;
4267 }
4268 }
4269 return true;
4270}
4271
4273{
4274 if (j < 0 || j >= static_cast<int>(m_impl->numAxes))
4275 {
4276 yCError(CIA402, "%s: getLastJointFault: joint %d out of range", Impl::kClassName.data(), j);
4277 return false;
4278 }
4279
4280 const int slave = m_impl->firstSlave + j;
4281
4282 // --- 0x603F:00 Error code (UINT16) ---
4283 uint16_t code = 0;
4284 auto err = m_impl->ethercatManager.readSDO<uint16_t>(slave, 0x603F, 0x00, code);
4286 {
4287 yCError(CIA402,
4288 "%s: getLastJointFault: SDO read 0x603F:00 failed (joint %d)",
4289 Impl::kClassName.data(),
4290 j);
4291 return false;
4292 }
4293
4294 fault = static_cast<int>(code);
4295 message = m_impl->describe603F(code);
4296
4297 // --- 0x203F:01 Error report (STRING(8)) ---
4298 // Use a fixed char[8] so we can call the templated readSDO<T> unmodified.
4299 char report[8] = {0}; // zero-init so partial reads are safely NUL-terminated
4300 auto err2 = m_impl->ethercatManager.readSDO(slave, 0x203F, 0x01, report);
4302 {
4303 // Trim at first NUL (STRING(8) is not guaranteed to be fully used)
4304 std::size_t len = 0;
4305 while (len < sizeof(report) && report[len] != '\0')
4306 ++len;
4307
4308 if (len > 0)
4309 {
4310 // If it's clean printable ASCII, append as text; otherwise show hex bytes.
4311 bool printable = true;
4312 for (std::size_t i = 0; i < len; ++i)
4313 {
4314 const unsigned char c = static_cast<unsigned char>(report[i]);
4315 if (c < 0x20 || c > 0x7E)
4316 {
4317 printable = false;
4318 break;
4319 }
4320 }
4321
4322 if (printable)
4323 {
4324 message += " — report: ";
4325 message.append(report, len);
4326 } else
4327 {
4328 char hex[3 * 8 + 1] = {0};
4329 int pos = 0;
4330 for (std::size_t i = 0; i < len && pos <= static_cast<int>(sizeof(hex)) - 3; ++i)
4331 pos += std::snprintf(hex + pos,
4332 sizeof(hex) - pos,
4333 "%02X%s",
4334 static_cast<unsigned char>(report[i]),
4335 (i + 1 < len) ? " " : "");
4336 message += " — report: [";
4337 message += hex;
4338 message += "]";
4339 }
4340 }
4341 }
4342 return true;
4343}
4344
4345bool CiA402MotionControl::positionMove(int j, double refDeg)
4346{
4347 if (j < 0 || j >= static_cast<int>(m_impl->numAxes))
4348 {
4349 yCError(CIA402, "%s: positionMove: joint %d out of range", Impl::kClassName.data(), j);
4350 return false;
4351 }
4352 // accept only if PP is ACTIVE (same policy as your CSV path)
4353 int controlMode = -1;
4354 {
4355 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4356 controlMode = m_impl->controlModeState.active[j];
4357 }
4358
4359 if (controlMode != VOCAB_CM_POSITION)
4360 {
4361 yCError(CIA402,
4362 "%s: positionMove rejected: POSITION mode not active for joint %d",
4363 Impl::kClassName.data(),
4364 j);
4365 return false;
4366 }
4367
4368 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4369 m_impl->setPoints.ppJointTargetsDeg[j] = refDeg;
4370 m_impl->setPoints.ppTargetCounts[j] = m_impl->jointDegToTargetCounts(size_t(j), refDeg);
4371 m_impl->setPoints.ppIsRelative[j] = false;
4372 m_impl->setPoints.ppHasPosSP[j] = true;
4373 m_impl->setPoints.ppPulseHi[j] = true; // schedule rising edge on CW bit4
4374 return true;
4375}
4376
4377bool CiA402MotionControl::positionMove(const double* refsDeg)
4378{
4379 if (!refsDeg)
4380 {
4381 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
4382 return false;
4383 }
4384
4385 // all axes must be in PP
4386 {
4387 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4388 for (size_t j = 0; j < m_impl->numAxes; ++j)
4389 if (m_impl->controlModeState.active[j] != VOCAB_CM_POSITION)
4390 {
4391 yCError(CIA402,
4392 "%s: positionMove rejected: POSITION mode not active on joint %zu",
4393 Impl::kClassName.data(),
4394 j);
4395 return false;
4396 }
4397 }
4398
4399 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4400 for (size_t j = 0; j < m_impl->numAxes; ++j)
4401 {
4402 m_impl->setPoints.ppJointTargetsDeg[j] = refsDeg[j];
4403 m_impl->setPoints.ppTargetCounts[j] = m_impl->jointDegToTargetCounts(j, refsDeg[j]);
4404 m_impl->setPoints.ppIsRelative[j] = false;
4405 m_impl->setPoints.ppHasPosSP[j] = true;
4406 m_impl->setPoints.ppPulseHi[j] = true;
4407 }
4408 return true;
4409}
4410
4411bool CiA402MotionControl::positionMove(const int n, const int* joints, const double* refsDeg)
4412{
4413 if (!joints || !refsDeg || n <= 0)
4414 {
4415 yCError(CIA402, "%s: invalid args", Impl::kClassName.data());
4416 return false;
4417 }
4418 {
4419 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4420 for (int k = 0; k < n; ++k)
4421 {
4422 if (joints[k] < 0 || joints[k] >= static_cast<int>(m_impl->numAxes))
4423 return false;
4424 if (m_impl->controlModeState.active[joints[k]] != VOCAB_CM_POSITION)
4425 {
4426 yCError(CIA402,
4427 "%s: positionMove rejected: POSITION mode not active on joint %d",
4428 Impl::kClassName.data(),
4429 joints[k]);
4430 return false;
4431 }
4432 }
4433 }
4434 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4435 for (int k = 0; k < n; ++k)
4436 {
4437 const int j = joints[k];
4438 m_impl->setPoints.ppJointTargetsDeg[j] = refsDeg[k];
4439 m_impl->setPoints.ppTargetCounts[j] = m_impl->jointDegToTargetCounts(size_t(j), refsDeg[k]);
4440 m_impl->setPoints.ppIsRelative[j] = false;
4441 m_impl->setPoints.ppHasPosSP[j] = true;
4442 m_impl->setPoints.ppPulseHi[j] = true;
4443 }
4444 return true;
4445}
4446
4447bool CiA402MotionControl::relativeMove(int j, double deltaDeg)
4448{
4449 if (j < 0 || j >= static_cast<int>(m_impl->numAxes))
4450 return false;
4451 {
4452 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4453 if (m_impl->controlModeState.active[j] != VOCAB_CM_POSITION)
4454 {
4455 yCError(CIA402,
4456 "%s: relativeMove rejected: POSITION mode not active for joint %d",
4457 Impl::kClassName.data(),
4458 j);
4459 return true;
4460 }
4461 }
4462 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4463 m_impl->setPoints.ppJointTargetsDeg[j] += deltaDeg; // cache last target in deg
4464 m_impl->setPoints.ppTargetCounts[j] = m_impl->jointDegToTargetCounts(size_t(j), deltaDeg);
4465 m_impl->setPoints.ppIsRelative[j] = true;
4466 m_impl->setPoints.ppHasPosSP[j] = true;
4467 m_impl->setPoints.ppPulseHi[j] = true;
4468 return true;
4469}
4470
4471bool CiA402MotionControl::relativeMove(const double* deltasDeg)
4472{
4473 if (!deltasDeg)
4474 return false;
4475 {
4476 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4477 for (size_t j = 0; j < m_impl->numAxes; ++j)
4478 if (m_impl->controlModeState.active[j] != VOCAB_CM_POSITION)
4479 return false;
4480 }
4481 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4482 for (size_t j = 0; j < m_impl->numAxes; ++j)
4483 {
4484 m_impl->setPoints.ppJointTargetsDeg[j] += deltasDeg[j];
4485 m_impl->setPoints.ppTargetCounts[j] = m_impl->jointDegToTargetCounts(j, deltasDeg[j]);
4486 m_impl->setPoints.ppIsRelative[j] = true;
4487 m_impl->setPoints.ppHasPosSP[j] = true;
4488 m_impl->setPoints.ppPulseHi[j] = true;
4489 }
4490 return true;
4491}
4492
4493bool CiA402MotionControl::relativeMove(const int n, const int* joints, const double* deltasDeg)
4494{
4495 if (!joints || !deltasDeg || n <= 0)
4496 return false;
4497 {
4498 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4499 for (int k = 0; k < n; ++k)
4500 if (m_impl->controlModeState.active[joints[k]] != VOCAB_CM_POSITION)
4501 return false;
4502 }
4503 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4504 for (int k = 0; k < n; ++k)
4505 {
4506 const int j = joints[k];
4507 m_impl->setPoints.ppJointTargetsDeg[j] += deltasDeg[k];
4508 m_impl->setPoints.ppTargetCounts[j]
4509 = m_impl->jointDegToTargetCounts(size_t(j), deltasDeg[k]);
4510 m_impl->setPoints.ppIsRelative[j] = true;
4511 m_impl->setPoints.ppHasPosSP[j] = true;
4512 m_impl->setPoints.ppPulseHi[j] = true;
4513 }
4514 return true;
4515}
4516
4518{
4519 if (flag == nullptr)
4520 {
4521 yCError(CIA402, "%s: checkMotionDone: null pointer", Impl::kClassName.data());
4522 return false;
4523 }
4524
4525 if (j < 0 || j >= static_cast<int>(m_impl->numAxes))
4526 {
4527 yCError(CIA402, "%s: checkMotionDone: joint %d out of range", Impl::kClassName.data(), j);
4528 return false;
4529 }
4530
4531 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
4532 *flag = m_impl->variables.targetReached[j];
4533 return true;
4534}
4535
4537{
4538 if (flag == nullptr)
4539 {
4540 yCError(CIA402, "%s: checkMotionDone: null pointer", Impl::kClassName.data());
4541 return false;
4542 }
4543
4544 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
4545 for (size_t j = 0; j < m_impl->numAxes; ++j)
4546 {
4547 flag[j] = m_impl->variables.targetReached[j];
4548 }
4549
4550 return true;
4551}
4552
4553bool CiA402MotionControl::checkMotionDone(const int n, const int* joints, bool* flag)
4554{
4555 if (!joints || !flag || n <= 0)
4556 return false;
4557 bool all = true;
4558 for (int k = 0; k < n; ++k)
4559 {
4560 bool f = true;
4561 checkMotionDone(joints[k], &f);
4562 all = all && f;
4563 }
4564 *flag = all;
4565 return true;
4566}
4567
4568bool CiA402MotionControl::setRefSpeed(int j, double spDegS)
4569{
4570 if (j < 0 || j >= static_cast<int>(m_impl->numAxes))
4571 {
4572 yCError(CIA402, "%s: setRefSpeed: joint %d out of range", Impl::kClassName.data(), j);
4573 return false;
4574 }
4575
4576 // Only write SDO if PP is active (current behavior)
4577 int cm = -1;
4578 {
4579 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4580 cm = m_impl->controlModeState.active[j];
4581 }
4582 if (cm != VOCAB_CM_POSITION)
4583 {
4584 yCError(CIA402,
4585 "%s: setRefSpeed: POSITION mode not active for joint %d, not writing SDO",
4586 Impl::kClassName.data(),
4587 j);
4588 return true;
4589 }
4590
4591 // Cache for getRefSpeed()
4592 {
4593 std::lock_guard<std::mutex> lock(m_impl->ppState.mutex);
4594 m_impl->ppState.ppRefSpeedDegS[j] = spDegS;
4595 }
4596
4597 m_impl->setSDORefSpeed(j, spDegS);
4598
4599 return true;
4600}
4601
4602bool CiA402MotionControl::setRefSpeeds(const double* spDegS)
4603{
4604 if (spDegS == nullptr)
4605 {
4606 yCError(CIA402, "%s: setRefSpeeds: null pointer", Impl::kClassName.data());
4607 return false;
4608 }
4609 for (size_t j = 0; j < m_impl->numAxes; ++j)
4610 {
4611 this->setRefSpeed(static_cast<int>(j), spDegS[j]);
4612 }
4613 return true;
4614}
4615
4616bool CiA402MotionControl::setRefSpeeds(const int n, const int* joints, const double* spDegS)
4617{
4618 if (!joints || !spDegS || n <= 0)
4619 {
4620 yCError(CIA402, "%s: setRefSpeeds: invalid args", Impl::kClassName.data());
4621 return false;
4622 }
4623
4624 for (int k = 0; k < n; ++k)
4625 {
4626 this->setRefSpeed(joints[k], spDegS[k]);
4627 }
4628 return true;
4629}
4630
4631bool CiA402MotionControl::getRefSpeed(int j, double* ref)
4632{
4633 if (!ref || j < 0 || j >= static_cast<int>(m_impl->numAxes))
4634 {
4635 yCError(CIA402, "%s: getRefSpeed: invalid args", Impl::kClassName.data());
4636 return false;
4637 }
4638
4639 std::lock_guard<std::mutex> lock(m_impl->ppState.mutex);
4640 *ref = m_impl->ppState.ppRefSpeedDegS[j];
4641 return true;
4642}
4644{
4645 if (!spds)
4646 {
4647 yCError(CIA402, "%s: getRefSpeeds: null pointer", Impl::kClassName.data());
4648 return false;
4649 }
4650 std::lock_guard<std::mutex> lock(m_impl->ppState.mutex);
4651 std::memcpy(spds, m_impl->ppState.ppRefSpeedDegS.data(), m_impl->numAxes * sizeof(double));
4652 return true;
4653}
4654
4655bool CiA402MotionControl::getRefSpeeds(const int n, const int* joints, double* spds)
4656{
4657 if (!joints || !spds || n <= 0)
4658 {
4659 yCError(CIA402, "%s: getRefSpeeds: invalid args", Impl::kClassName.data());
4660 return false;
4661 }
4662 std::lock_guard<std::mutex> lock(m_impl->ppState.mutex);
4663 for (int k = 0; k < n; ++k)
4664 {
4665 spds[k] = m_impl->ppState.ppRefSpeedDegS[joints[k]];
4666 }
4667 return true;
4668}
4669
4670bool CiA402MotionControl::getTargetPosition(const int j, double* ref)
4671{
4672 if (!ref || j < 0 || j >= static_cast<int>(m_impl->numAxes))
4673 {
4674 yCError(CIA402, "%s: getTargetPosition: invalid args", Impl::kClassName.data());
4675 return false;
4676 }
4677
4678 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4679 *ref = m_impl->setPoints.ppJointTargetsDeg[j];
4680 return true;
4681}
4682
4684{
4685 if (refs == nullptr)
4686 {
4687 yCError(CIA402, "%s: getTargetPositions: null pointer", Impl::kClassName.data());
4688 return false;
4689 }
4690
4691 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4692 std::memcpy(refs, m_impl->setPoints.ppJointTargetsDeg.data(), m_impl->numAxes * sizeof(double));
4693 return true;
4694}
4695
4696bool CiA402MotionControl::getTargetPositions(const int n, const int* joints, double* refs)
4697{
4698 if (!joints || !refs || n <= 0)
4699 {
4700 yCError(CIA402, "%s: getTargetPositions: invalid args", Impl::kClassName.data());
4701 return false;
4702 }
4703 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4704 for (int k = 0; k < n; ++k)
4705 {
4706 refs[k] = m_impl->setPoints.ppJointTargetsDeg[joints[k]];
4707 }
4708 return true;
4709}
4710
4711// ---------------- IPositionDirect --------------
4712
4713bool CiA402MotionControl::setPosition(int j, double refDeg)
4714{
4715 if (j < 0 || j >= static_cast<int>(m_impl->numAxes))
4716 {
4717 yCError(CIA402, "%s: setPosition: joint %d out of range", Impl::kClassName.data(), j);
4718 return false;
4719 }
4720
4721 {
4722 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
4723 if (m_impl->controlModeState.active[j] != VOCAB_CM_POSITION_DIRECT)
4724 {
4725 yCError(CIA402,
4726 "%s: setPosition rejected: POSITION_DIRECT mode is not active for joint %d",
4727 Impl::kClassName.data(),
4728 j);
4729 return false;
4730 }
4731 }
4732
4733 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4734 m_impl->setPoints.positionDirectJointTargetsDeg[j] = refDeg;
4735 m_impl->setPoints.positionDirectTargetCounts[j]
4736 = m_impl->jointDegToTargetCounts(static_cast<size_t>(j), refDeg);
4737 return true;
4738}
4739
4741{
4742 if (refs == nullptr)
4743 {
4744 yCError(CIA402, "%s: setPositions: null pointer", Impl::kClassName.data());
4745 return false;
4746 }
4747
4748 {
4749 std::lock_guard<std::mutex> lock(m_impl->controlModeState.mutex);
4750 for (size_t j = 0; j < m_impl->numAxes; ++j)
4751 {
4752 if (m_impl->controlModeState.active[j] != VOCAB_CM_POSITION_DIRECT)
4753 {
4754 yCError(CIA402,
4755 "%s: setPositions rejected: POSITION_DIRECT mode is not active for joint "
4756 "%zu",
4757 Impl::kClassName.data(),
4758 j);
4759 return false;
4760 }
4761 }
4762 }
4763
4764 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4765 for (size_t j = 0; j < m_impl->numAxes; ++j)
4766 {
4767 m_impl->setPoints.positionDirectJointTargetsDeg[j] = refs[j];
4768 m_impl->setPoints.positionDirectTargetCounts[j]
4769 = m_impl->jointDegToTargetCounts(j, refs[j]);
4770 }
4771 return true;
4772}
4773
4774bool CiA402MotionControl::setPositions(const int n_joint, const int* joints, const double* refs)
4775{
4776 if (!joints || !refs)
4777 {
4778 yCError(CIA402, "%s: setPositions: null pointer", Impl::kClassName.data());
4779 return false;
4780 }
4781 if (n_joint <= 0)
4782 {
4783 yCError(CIA402,
4784 "%s: setPositions: invalid number of joints %d",
4785 Impl::kClassName.data(),
4786 n_joint);
4787 return false;
4788 }
4789
4790 {
4791 std::lock_guard<std::mutex> lock(m_impl->controlModeState.mutex);
4792 for (int k = 0; k < n_joint; ++k)
4793 {
4794 if (joints[k] < 0 || joints[k] >= static_cast<int>(m_impl->numAxes))
4795 {
4796 yCError(CIA402,
4797 "%s: setPositions: joint %d out of range",
4798 Impl::kClassName.data(),
4799 joints[k]);
4800 return false;
4801 }
4802 if (m_impl->controlModeState.active[joints[k]] != VOCAB_CM_POSITION_DIRECT)
4803 {
4804 yCError(CIA402,
4805 "%s: setPositions rejected: POSITION_DIRECT mode is not active for joint "
4806 "%d",
4807 Impl::kClassName.data(),
4808 joints[k]);
4809 return false;
4810 }
4811 }
4812 }
4813
4814 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4815 for (int k = 0; k < n_joint; ++k)
4816 {
4817 const int j = joints[k];
4818 m_impl->setPoints.positionDirectJointTargetsDeg[j] = refs[k];
4819 m_impl->setPoints.positionDirectTargetCounts[j]
4820 = m_impl->jointDegToTargetCounts(static_cast<size_t>(j), refs[k]);
4821 }
4822 return true;
4823}
4824
4825bool CiA402MotionControl::getRefPosition(const int joint, double* ref)
4826{
4827 if (!ref)
4828 {
4829 yCError(CIA402, "%s: getRefPosition: null pointer", Impl::kClassName.data());
4830 return false;
4831 }
4832 if (joint < 0 || joint >= static_cast<int>(m_impl->numAxes))
4833 {
4834 yCError(CIA402, "%s: getRefPosition: joint %d out of range", Impl::kClassName.data(), joint);
4835 return false;
4836 }
4837
4838 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4839 *ref = m_impl->setPoints.positionDirectJointTargetsDeg[joint];
4840 return true;
4841}
4842
4844{
4845 if (!refs)
4846 {
4847 yCError(CIA402, "%s: getRefPositions: null pointer", Impl::kClassName.data());
4848 return false;
4849 }
4850
4851 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4852 std::memcpy(refs,
4853 m_impl->setPoints.positionDirectJointTargetsDeg.data(),
4854 m_impl->numAxes * sizeof(double));
4855 return true;
4856}
4857
4858bool CiA402MotionControl::getRefPositions(const int n_joint, const int* joints, double* refs)
4859{
4860 if (!joints || !refs)
4861 {
4862 yCError(CIA402, "%s: getRefPositions: null pointer", Impl::kClassName.data());
4863 return false;
4864 }
4865 if (n_joint <= 0)
4866 {
4867 yCError(CIA402,
4868 "%s: getRefPositions: invalid number of joints %d",
4869 Impl::kClassName.data(),
4870 n_joint);
4871 return false;
4872 }
4873
4874 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
4875 for (int k = 0; k < n_joint; ++k)
4876 {
4877 if (joints[k] < 0 || joints[k] >= static_cast<int>(m_impl->numAxes))
4878 {
4879 yCError(CIA402,
4880 "%s: getRefPositions: joint %d out of range",
4881 Impl::kClassName.data(),
4882 joints[k]);
4883 return false;
4884 }
4885 refs[k] = m_impl->setPoints.positionDirectJointTargetsDeg[joints[k]];
4886 }
4887 return true;
4888}
4889
4891{
4892 if (!num)
4893 {
4894 yCError(CIA402, "%s: getNumberOfMotors: null pointer", Impl::kClassName.data());
4895 return false;
4896 }
4897 *num = m_impl->numAxes;
4898 return true;
4899}
4900
4902{
4903 if (val == nullptr)
4904 {
4905 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
4906 return false;
4907 }
4908 if (m < 0 || m >= static_cast<int>(m_impl->numAxes))
4909 {
4910 yCError(CIA402, "%s: motor %d out of range", Impl::kClassName.data(), m);
4911 return false;
4912 }
4913 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
4914 *val = m_impl->variables.driveTemperatures[m];
4915 return true;
4916}
4917
4919{
4920 if (vals == nullptr)
4921 {
4922 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
4923 return false;
4924 }
4925 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
4926 std::memcpy(vals, m_impl->variables.driveTemperatures.data(), m_impl->numAxes * sizeof(double));
4927 return true;
4928}
4929
4931{
4932 // The get temperature limit function is not implemented
4933 yCError(CIA402,
4934 "%s: The getTemperatureLimit function is not implemented",
4935 Impl::kClassName.data());
4936 return false;
4937}
4938
4939bool CiA402MotionControl::setTemperatureLimit(int m, const double temp)
4940{
4941 // The set temperature limit function is not implemented
4942 yCError(CIA402,
4943 "%s: The setTemperatureLimit function is not implemented",
4944 Impl::kClassName.data());
4945 return false;
4946}
4947
4949{
4950 if (val == nullptr)
4951 {
4952 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
4953 return false;
4954 }
4955 if (m < 0 || m >= static_cast<int>(m_impl->numAxes))
4956 {
4957 yCError(CIA402, "%s: motor %d out of range", Impl::kClassName.data(), m);
4958 return false;
4959 }
4960 *val = m_impl->gearRatio[m];
4961 return true;
4962}
4963
4964bool CiA402MotionControl::setGearboxRatio(int m, const double val)
4965{
4966 // The setGearboxRatio function is not implemented
4967 yCError(CIA402, "%s: The setGearboxRatio function is not implemented", Impl::kClassName.data());
4968 return false;
4969}
4970
4971bool CiA402MotionControl::getCurrent(int m, double* curr)
4972{
4973 if (curr == nullptr)
4974 {
4975 yCError(CIA402, "%s: getCurrent: null pointer", Impl::kClassName.data());
4976 return false;
4977 }
4978 if (m < 0 || m >= static_cast<int>(m_impl->numAxes))
4979 {
4980 yCError(CIA402, "%s: getCurrent: motor %d out of range", Impl::kClassName.data(), m);
4981 return false;
4982 }
4983 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
4984 *curr = m_impl->variables.motorCurrents[m];
4985 return true;
4986};
4987
4989{
4990 if (currs == nullptr)
4991 {
4992 yCError(CIA402, "%s: getCurrents: null pointer", Impl::kClassName.data());
4993 return false;
4994 }
4995 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
4996 std::memcpy(currs, m_impl->variables.motorCurrents.data(), m_impl->numAxes * sizeof(double));
4997 return true;
4998}
4999
5000bool CiA402MotionControl::getCurrentRange(int m, double* min, double* max)
5001{
5002 if (min == nullptr || max == nullptr)
5003 {
5004 yCError(CIA402, "%s: getCurrentRange: null pointer", Impl::kClassName.data());
5005 return false;
5006 }
5007 if (m < 0 || m >= static_cast<int>(m_impl->numAxes))
5008 {
5009 yCError(CIA402, "%s: getCurrentRange: motor %d out of range", Impl::kClassName.data(), m);
5010 return false;
5011 }
5012 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
5013 *min = -m_impl->maxCurrentsA[m];
5014 *max = m_impl->maxCurrentsA[m];
5015 return true;
5016}
5017
5018bool CiA402MotionControl::getCurrentRanges(double* min, double* max)
5019{
5020 if (min == nullptr || max == nullptr)
5021 {
5022 yCError(CIA402, "%s: getCurrentRanges: null pointer", Impl::kClassName.data());
5023 return false;
5024 }
5025 std::lock_guard<std::mutex> lock(m_impl->variables.mutex);
5026 for (size_t m = 0; m < m_impl->numAxes; ++m)
5027 {
5028 min[m] = -m_impl->maxCurrentsA[m];
5029 max[m] = m_impl->maxCurrentsA[m];
5030 }
5031 return true;
5032}
5033
5035{
5036 if (currs == nullptr)
5037 {
5038 yCError(CIA402, "%s: setRefCurrents: null pointer", Impl::kClassName.data());
5039 return false;
5040 }
5041
5042 {
5043 // check that all the joints are in CURRENT mode
5044 std::lock_guard<std::mutex> lock(m_impl->controlModeState.mutex);
5045 for (size_t j = 0; j < m_impl->numAxes; ++j)
5046 {
5047 if (m_impl->controlModeState.active[j] != VOCAB_CM_CURRENT)
5048 {
5049 yCError(CIA402,
5050 "%s: setRefCurrents rejected: CURRENT mode is not active for the joint "
5051 "%zu",
5052 Impl::kClassName.data(),
5053 j);
5054 return false; // reject
5055 }
5056 }
5057 }
5058
5059 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
5060 std::memcpy(m_impl->setPoints.motorCurrents.data(), currs, m_impl->numAxes * sizeof(double));
5061 std::fill(m_impl->setPoints.hasCurrentSP.begin(), m_impl->setPoints.hasCurrentSP.end(), true);
5062 return true;
5063}
5064
5066{
5067 if (m < 0 || m >= static_cast<int>(m_impl->numAxes))
5068 {
5069 yCError(CIA402, "%s: setRefCurrent: motor %d out of range", Impl::kClassName.data(), m);
5070 return false;
5071 }
5072
5073 // (a/b) Only accept if CURRENT is ACTIVE; otherwise reject (not considered)
5074 {
5075 std::lock_guard<std::mutex> g(m_impl->controlModeState.mutex);
5076 if (m_impl->controlModeState.active[m] != VOCAB_CM_CURRENT)
5077 {
5078 yCError(CIA402,
5079 "%s: setRefCurrent rejected: CURRENT mode is not active for the joint %d",
5080 Impl::kClassName.data(),
5081 m);
5082 return false;
5083 }
5084 }
5085
5086 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
5087 m_impl->setPoints.motorCurrents[m] = curr;
5088 m_impl->setPoints.hasCurrentSP[m] = true; // (b)
5089
5090 return true;
5091}
5092
5093bool CiA402MotionControl::setRefCurrents(const int n_motor, const int* motors, const double* currs)
5094{
5095 if (currs == nullptr || motors == nullptr)
5096 {
5097 yCError(CIA402, "%s: setRefCurrents: null pointer", Impl::kClassName.data());
5098 return false;
5099 }
5100 if (n_motor <= 0)
5101 {
5102 yCError(CIA402,
5103 "%s: setRefCurrents: invalid number of motors %d",
5104 Impl::kClassName.data(),
5105 n_motor);
5106 return false;
5107 }
5108 for (int k = 0; k < n_motor; ++k)
5109 {
5110 if (motors[k] < 0 || motors[k] >= static_cast<int>(m_impl->numAxes))
5111 {
5112 yCError(CIA402,
5113 "%s: setRefCurrents: motor %d out of range",
5114 Impl::kClassName.data(),
5115 motors[k]);
5116 return false;
5117 }
5118 }
5119
5120 // check that all the joints are in CURRENT mode
5121 {
5122 std::lock_guard<std::mutex> lock(m_impl->controlModeState.mutex);
5123 for (int k = 0; k < n_motor; ++k)
5124 {
5125 if (m_impl->controlModeState.active[motors[k]] != VOCAB_CM_CURRENT)
5126 {
5127 yCError(CIA402,
5128 "%s: setRefCurrents rejected: CURRENT mode is not active for the joint %d",
5129 Impl::kClassName.data(),
5130 motors[k]);
5131 return false; // reject
5132 }
5133 }
5134 }
5135
5136 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
5137 for (int k = 0; k < n_motor; ++k)
5138 {
5139 m_impl->setPoints.motorCurrents[motors[k]] = currs[k];
5140 m_impl->setPoints.hasCurrentSP[motors[k]] = true;
5141 }
5142 return true;
5143}
5144
5146{
5147 if (currs == nullptr)
5148 {
5149 yCError(CIA402, "%s: getRefCurrents: null pointer", Impl::kClassName.data());
5150 return false;
5151 }
5152
5153 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
5154 std::memcpy(currs, m_impl->setPoints.motorCurrents.data(), m_impl->numAxes * sizeof(double));
5155 return true;
5156}
5157
5158bool CiA402MotionControl::getRefCurrent(int m, double* curr)
5159{
5160 if (curr == nullptr)
5161 {
5162 yCError(CIA402, "%s: getRefCurrent: null pointer", Impl::kClassName.data());
5163 return false;
5164 }
5165 if (m < 0 || m >= static_cast<int>(m_impl->numAxes))
5166 {
5167 yCError(CIA402, "%s: getRefCurrent: motor %d out of range", Impl::kClassName.data(), m);
5168 return false;
5169 }
5170
5171 std::lock_guard<std::mutex> lock(m_impl->setPoints.mutex);
5172 *curr = m_impl->setPoints.motorCurrents[m];
5173 return true;
5174}
5175
5176bool CiA402MotionControl::setLimits(int axis, double min, double max)
5177{
5178 if (axis < 0 || axis >= static_cast<int>(m_impl->numAxes))
5179 {
5180 yCError(CIA402, "%s: setLimits: axis %d out of range", Impl::kClassName.data(), axis);
5181 return false;
5182 }
5183 // If both bounds are provided (non-negative), enforce min < max.
5184 // When either bound is negative, we treat that side as disabled like in open(),
5185 // and skip the ordering check.
5186 if (!(min < 0.0 || max < 0.0) && (min >= max))
5187 {
5188 yCError(CIA402,
5189 "%s: setLimits: invalid limits [min=%g, max=%g]",
5190 Impl::kClassName.data(),
5191 min,
5192 max);
5193 return false;
5194 }
5195
5196 {
5197 std::lock_guard<std::mutex> lock(m_impl->limits.mutex);
5198 m_impl->limits.maxPositionLimitDeg[axis] = max;
5199 m_impl->limits.minPositionLimitDeg[axis] = min;
5200 }
5201
5202 // set the software limits in the drive (SDO 0x607D)
5203 // convert the limits from deg to counts, honoring inversion and disabled bounds
5204 const bool inv = m_impl->invertedMotionSenseDirection[axis];
5205 const bool lowerLimitDisabled = (min < 0.0);
5206 const bool upperLimitDisabled = (max < 0.0);
5207
5208 const auto lowerLimitCounts
5209 = lowerLimitDisabled ? 0 : m_impl->jointDegToTargetCounts(size_t(axis), min);
5210 const auto upperLimitCounts
5211 = upperLimitDisabled ? 0 : m_impl->jointDegToTargetCounts(size_t(axis), max);
5212
5213 int32_t minCounts = 0;
5214 int32_t maxCounts = 0;
5215
5216 if (!inv)
5217 {
5218 minCounts = lowerLimitDisabled ? std::numeric_limits<int32_t>::min() : lowerLimitCounts;
5219 maxCounts = upperLimitDisabled ? std::numeric_limits<int32_t>::max() : upperLimitCounts;
5220 } else
5221 {
5222 // Inverted: f(x) = -counts(x), so [min,max] -> [f(max), f(min)]
5223 minCounts = upperLimitDisabled ? std::numeric_limits<int32_t>::min() : -upperLimitCounts;
5224 maxCounts = lowerLimitDisabled ? std::numeric_limits<int32_t>::max() : -lowerLimitCounts;
5225 }
5226
5227 return m_impl->setPositionCountsLimits(axis, minCounts, maxCounts);
5228}
5229
5230bool CiA402MotionControl::getLimits(int axis, double* min, double* max)
5231{
5232 if (min == nullptr || max == nullptr)
5233 {
5234 yCError(CIA402, "%s: getLimits: null pointer", Impl::kClassName.data());
5235 return false;
5236 }
5237 if (axis < 0 || axis >= static_cast<int>(m_impl->numAxes))
5238 {
5239 yCError(CIA402, "%s: getLimits: axis %d out of range", Impl::kClassName.data(), axis);
5240 return false;
5241 }
5242
5243 std::lock_guard<std::mutex> lock(m_impl->limits.mutex);
5244 *min = m_impl->limits.minPositionLimitDeg[axis];
5245 *max = m_impl->limits.maxPositionLimitDeg[axis];
5246 return true;
5247}
5248
5249bool CiA402MotionControl::setVelLimits(int axis, double min, double max)
5250{
5251 // not implemented yet
5252 constexpr auto logPrefix = "[setVelLimits] ";
5253 yCError(CIA402, "%s: The setVelLimits function is not implemented", logPrefix);
5254 return false;
5255}
5256
5257bool CiA402MotionControl::getVelLimits(int axis, double* min, double* max)
5258{
5259 // not implemented yet
5260 constexpr auto logPrefix = "[getVelLimits] ";
5261 yCError(CIA402, "%s: The getVelLimits function is not implemented", logPrefix);
5262 return false;
5263}
5264
5265bool CiA402MotionControl::getInteractionMode(int axis, yarp::dev::InteractionModeEnum* mode)
5266{
5267 if (!mode)
5268 {
5269 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
5270 return false;
5271 }
5272
5273 *mode = m_impl->dummyInteractionMode;
5274 return true;
5275}
5276
5278 int* joints,
5279 yarp::dev::InteractionModeEnum* modes)
5280{
5281 if (!joints || !modes || n_joints <= 0)
5282 {
5283 yCError(CIA402, "%s: invalid args", Impl::kClassName.data());
5284 return false;
5285 }
5286
5287 for (int k = 0; k < n_joints; ++k)
5288 {
5289 if (joints[k] < 0 || joints[k] >= static_cast<int>(m_impl->numAxes))
5290 {
5291 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), joints[k]);
5292 return false;
5293 }
5294 modes[k] = m_impl->dummyInteractionMode;
5295 }
5296 return true;
5297}
5298
5299bool CiA402MotionControl::getInteractionModes(yarp::dev::InteractionModeEnum* modes)
5300{
5301 if (modes == nullptr)
5302 {
5303 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
5304 return false;
5305 }
5306
5307 for (size_t j = 0; j < m_impl->numAxes; ++j)
5308 {
5309 modes[j] = m_impl->dummyInteractionMode;
5310 }
5311 return true;
5312}
5313
5314bool CiA402MotionControl::setInteractionMode(int axis, yarp::dev::InteractionModeEnum mode)
5315{
5316 if (axis < 0 || axis >= static_cast<int>(m_impl->numAxes))
5317 {
5318 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), axis);
5319 return false;
5320 }
5321
5322 // The interaction mode is not implemented in this driver.
5323 yCError(CIA402,
5324 "%s: The setInteractionMode function is not implemented",
5325 Impl::kClassName.data());
5326 return false;
5327}
5328
5330 int* joints,
5331 yarp::dev::InteractionModeEnum* modes)
5332{
5333 if (!joints || !modes || n_joints <= 0)
5334 {
5335 yCError(CIA402, "%s: invalid args", Impl::kClassName.data());
5336 return false;
5337 }
5338
5339 for (int k = 0; k < n_joints; ++k)
5340 {
5341 if (joints[k] < 0 || joints[k] >= static_cast<int>(m_impl->numAxes))
5342 {
5343 yCError(CIA402, "%s: joint %d out of range", Impl::kClassName.data(), joints[k]);
5344 return false;
5345 }
5346 }
5347
5348 // The interaction mode is not implemented in this driver.
5349 yCError(CIA402,
5350 "%s: The setInteractionModes function is not implemented",
5351 Impl::kClassName.data());
5352 return false;
5353}
5354
5355bool CiA402MotionControl::setInteractionModes(yarp::dev::InteractionModeEnum* modes)
5356{
5357 if (modes == nullptr)
5358 {
5359 yCError(CIA402, "%s: null pointer", Impl::kClassName.data());
5360 return false;
5361 }
5362
5363 yCError(CIA402,
5364 "%s: The setInteractionModes function is not implemented",
5365 Impl::kClassName.data());
5366 return false;
5367}
5368
5369} // namespace yarp::dev
T append(T... args)
T begin(T... args)
T c_str(T... args)
Minimal EtherCAT master built on SOEM for CiA-402 devices.
TxView getTxView(int slaveIndex) const noexcept
Obtain a typed, bounds-checked view over the TxPDO (slave→master).
const RxPDO * getRxPDO(int slaveIndex) const noexcept
Access the RxPDO (master→slave) buffer for a given slave.
@ NoError
Operation completed successfully.
Error writeSDO(int slaveIndex, uint16_t idx, uint8_t subIdx, const T &value) noexcept
Write an SDO value to a slave (blocking call).
static bool isOperationEnabled(uint16_t sw)
static bool isFault(uint16_t sw)
static bool isFaultReactionActive(uint16_t sw)
T get(TxField f, T fallback={}) const
Read a field from the TxPDO image with a typed accessor.
bool setGearboxRatio(int m, const double val) override
Sets the gearbox ratio of a specific motor.
bool setRefAcceleration(int j, double acc) override
(Unused in CSV mode) Sets the reference acceleration for a specific joint.
bool getTargetPosition(int j, double *ref) override
Gets the target position for a specific joint.
bool getCurrentRange(int m, double *min, double *max) override
Gets the allowable current range for a specific motor.
bool resetMotorEncoders() override
Resets all motor encoders to zero.
bool setRefCurrent(int m, double curr) override
Sets the reference current for a specific motor.
bool setEncoder(int j, double val) override
Sets the value of a specific encoder.
bool getVelLimits(int axis, double *min, double *max) override
Gets the velocity limits for a specific axis.
void run() override
Runs the periodic thread.
bool setMotorEncoder(int m, const double val) override
Sets the value of a specific motor encoder.
bool getRefVelocity(const int joint, double *vel) override
Gets the reference velocity for a specific joint.
bool setMotorEncoders(const double *vals) override
Sets the values of all motor encoders.
bool getNumberOfMotors(int *num) override
Gets the number of motors controlled by the device.
bool getEncoderAcceleration(int j, double *spds) override
Gets the acceleration of a specific encoder.
bool getEncoderSpeed(int j, double *sp) override
Gets the speed of a specific encoder.
bool getEncoders(double *encs) override
Gets the values of all encoders.
bool getEncoderSpeeds(double *spds) override
Gets the speeds of all encoders.
bool setVelLimits(int axis, double min, double max) override
Sets the velocity limits for a specific axis.
bool positionMove(int j, double ref) override
Commands an absolute position move for a specific joint using Profile Position Mode (PP).
bool getMotorEncoder(int m, double *v) override
Gets the value of a specific motor encoder.
bool getEncoderTimed(int j, double *encs, double *time) override
Gets the value and timestamp of a specific encoder.
bool getAxisName(int axis, std::string &name) override
Gets the name of a specific axis.
bool setPosition(int j, double ref) override
Streams a single joint reference in Cyclic Synchronous Position mode.
bool relativeMove(int j, double delta) override
Commands a relative position move for a specific joint.
bool getLastJointFault(int j, int &fault, std::string &message) override
Gets the last joint fault for a specific joint.
bool getJointType(int axis, yarp::dev::JointTypeEnum &type) override
Gets the type of a specific axis.
bool getRefPosition(const int joint, double *ref) override
Returns the most recent position-direct command for a joint.
bool getMotorEncoderCountsPerRevolution(int m, double *cpr) override
Gets the counts per revolution for a specific motor encoder.
~CiA402MotionControl() override
Destructor.
bool resetEncoders() override
Resets all encoders to zero.
bool getTargetPositions(double *refs) override
Gets the target positions for all joints.
bool setRefCurrents(const double *currs) override
Sets the reference currents for all motors.
bool getTorques(double *t) override
Gets the measured torques for all joints.
bool getRefCurrent(int m, double *curr) override
Gets the last commanded reference current for a specific motor.
bool getRefCurrents(double *currs) override
Gets the last commanded reference currents for all motors.
bool stop() override
Stops motion for all joints.
bool setRefTorques(const double *t) override
Sets the reference torques for all joints.
bool getControlMode(int j, int *mode) override
Gets the control mode of a specific joint.
bool getEncoderAccelerations(double *accs) override
Gets the accelerations of all encoders.
bool getRefSpeed(int j, double *ref) override
Gets the current reference speed (profile velocity) for a specific joint.
bool setLimits(int axis, double min, double max) override
Sets the position limits for a specific axis.
bool setInteractionModes(int n_joints, int *joints, yarp::dev::InteractionModeEnum *modes) override
Sets the interaction modes of a subset of joints.
bool setTemperatureLimit(int m, const double temp) override
Sets the temperature limit of a specific motor.
bool getTorque(int j, double *t) override
Gets the measured torque for a specific joint.
bool getRefPositions(double *refs) override
Returns the position-direct references for all joints.
bool getLimits(int axis, double *min, double *max) override
Gets the position limits for a specific axis.
bool velocityMove(int j, double sp) override
Commands a velocity move for a specific joint.
bool setMotorEncoderCountsPerRevolution(int m, const double cpr) override
Sets the counts per revolution for a specific motor encoder.
bool getRefVelocities(double *vels) override
Gets the reference velocities for all joints.
bool getEncodersTimed(double *encs, double *time) override
Gets the values and timestamps of all encoders.
bool getTorqueRanges(double *min, double *max) override
Gets the torque ranges for all joints.
bool getControlModes(int *modes) override
Gets the control modes of all joints.
bool resetEncoder(int j) override
Resets the specified encoder to zero.
bool setControlModes(const int n, const int *joints, int *modes) override
Sets the control modes of a subset of joints.
bool getMotorEncoderSpeed(int m, double *sp) override
Gets the speed of a specific motor encoder.
bool getMotorEncoders(double *encs) override
Gets the values of all motor encoders.
bool setPositions(const int n_joint, const int *joints, const double *refs) override
Streams references for a subset of joints.
bool setRefSpeed(int j, double sp) override
Sets the reference speed (profile velocity) for a specific joint.
bool getMotorEncoderAcceleration(int m, double *acc) override
Gets the acceleration of a specific motor encoder.
bool getEncoder(int j, double *v) override
Gets the value of a specific encoder.
bool getMotorEncoderTimed(int m, double *encs, double *time) override
Gets the value and timestamp of a specific motor encoder.
bool getCurrentRanges(double *min, double *max) override
Gets the allowable current ranges for all motors.
bool setControlMode(const int j, const int mode) override
Sets the control mode of a specific joint.
bool getTemperatureLimit(int m, double *temp) override
Gets the temperature limit of a specific motor.
bool getInteractionMode(int axis, yarp::dev::InteractionModeEnum *mode) override
Gets the interaction mode of a specific axis.
bool getRefAcceleration(int j, double *acc) override
(Unused in CSV mode) Gets the reference acceleration for a specific joint.
bool getCurrents(double *currs) override
Gets the measured currents of all motors.
bool checkMotionDone(int j, bool *flag) override
Checks if a specific joint has completed its motion.
bool getGearboxRatio(int m, double *val) override
Gets the gearbox ratio of a specific motor.
bool open(yarp::os::Searchable &config) override
Opens the device driver.
bool getTemperature(int m, double *val) override
Gets the temperature of a specific motor.
bool setRefSpeeds(const double *spds) override
Sets the reference speeds (profile velocities) for all joints.
bool getInteractionModes(int n_joints, int *joints, yarp::dev::InteractionModeEnum *modes) override
Gets the interaction modes of a subset of joints.
bool setRefAccelerations(const double *accs) override
(Unused in CSV mode) Sets the reference accelerations for all joints.
bool getMotorEncodersTimed(double *encs, double *time) override
Gets the values and timestamps of all motor encoders.
bool getMotorEncoderAccelerations(double *accs) override
Gets the accelerations of all motor encoders.
bool getRefTorque(int j, double *t) override
Gets the reference torque for a specific joint.
bool setEncoders(const double *vals) override
Sets the values of all encoders.
bool close() override
Closes the device driver.
bool getNumberOfMotorEncoders(int *num) override
Gets the number of motor encoders.
bool getCurrent(int m, double *curr) override
Gets the measured current of a specific motor.
bool getAxes(int *ax) override
Gets the number of axes (encoders).
bool setRefTorque(int j, double t) override
Sets the reference torque for a specific joint.
bool getTemperatures(double *vals) override
Gets the temperatures of all motors.
bool getMotorEncoderSpeeds(double *spds) override
Gets the speeds of all motor encoders.
bool resetMotorEncoder(int m) override
Resets the specified motor encoder to zero.
bool getRefTorques(double *t) override
Gets the reference torques for all joints.
bool setInteractionMode(int axis, yarp::dev::InteractionModeEnum mode) override
Sets the interaction mode of a specific axis.
bool getTorqueRange(int j, double *min, double *max) override
Gets the torque range for a specific joint.
bool getRefAccelerations(double *accs) override
(Unused in CSV mode) Gets the reference accelerations for all joints.
bool getRefSpeeds(double *spds) override
Gets the current reference speeds (profile velocities) for all joints.
T duration_cast(T... args)
T fill(T... args)
T find(T... args)
T snprintf(T... args)
T isinf(T... args)
T max(T... args)
T memcpy(T... args)
T memset(T... args)
T min(T... args)
@ 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)
@ 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)
STL namespace.
T push_back(T... args)
T resize(T... args)
T round(T... args)
T size(T... args)
Process Data Object written from master to slave (RxPDO).
int8_t OpMode
0x6060: Modes of operation
uint16_t Controlword
0x6040: Controlword
static constexpr std::string_view kClassName
bool setPositionControlStrategy(uint16_t strategyValue)
static std::string_view yarpToString(int op)
yarp::dev::InteractionModeEnum dummyInteractionMode
bool setPositionWindowDeg(int j, double winDeg, double winTime_ms)
static constexpr uint64_t TIMESTAMP_WRAP_PERIOD_US
static std::tuple< double, double, const char * > decode60A9(uint32_t v)
static constexpr uint32_t TIMESTAMP_WRAP_HALF_RANGE
int32_t jointDegToTargetCounts(size_t j, double jointDeg) const
std::chrono::steady_clock::time_point avgWindowStart
double loopCountsToJointDeg(std::size_t j, double counts) const
std::vector< std::unique_ptr< CiA402::StateMachine > > sm
bool readSimplePidGains(std::vector< double > &kpNmPerDeg, std::vector< double > &kdNmSecPerDeg)
struct yarp::dev::CiA402MotionControl::Impl::Limits limits
bool configureSimplePidGains(const std::vector< double > &kpNmPerDeg, const std::vector< double > &kdNmSecPerDeg)
bool configureMaxTorqueFromJointNm(const std::vector< double > &maxJointTorqueNm)
bool setPositionCountsLimits(int axis, int32_t minCounts, int32_t maxCounts)
static constexpr double MICROSECONDS_TO_SECONDS
bool getPositionControlStrategy(uint16_t &strategyValue)
int ciaOpToYarpWithFlavor(int op, int cstFlavor)
double targetCountsToJointDeg(size_t j, int32_t counts) const