YARP CiA-402 EtherCAT Device 0.6.0
YARP device plugin for EtherCAT CiA-402 drives
Loading...
Searching...
No Matches
CheckEncoderCalibration.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Generative Bionics
2// SPDX-License-Identifier: BSD-3-Clause
3
4#include <chrono>
5#include <ctime>
6#include <fstream>
7#include <iomanip>
8#include <sstream>
9#include <string>
10#include <vector>
11
12#include <toml++/toml.h>
13
16#include <CiA402/LogComponent.h>
17#include <CiA402/TimeUtils.h>
18
20#include <yarp/os/LogStream.h>
21#include <yarp/os/ResourceFinder.h>
22
23using namespace CiA402;
24
25// ---- Helper: format a double with fixed precision ----
26static std::string fmtDouble(double v, int prec = 6)
27{
29 o << std::fixed << std::setprecision(prec) << v;
30 return o.str();
31}
32
33// ---- Helper: format an integer (with explicit sign for deltas) ----
34static std::string fmtInt64(int64_t v)
35{
36 return std::to_string(v);
37}
38
39static std::string fmtDeltaInt64(int64_t v)
40{
41 if (v > 0)
42 return "+" + std::to_string(v);
43 return std::to_string(v); // negative sign is implicit
44}
45
46static std::string fmtDeltaDouble(double v, int prec = 6)
47{
49 if (v > 0.0)
50 o << "+";
51 o << std::fixed << std::setprecision(prec) << v;
52 return o.str();
53}
54
55// ---- Data structures for a single encoder channel ----
57{
58 int64_t rawPosition = 0;
59 double rawPositionDegrees = 0.0;
60 int64_t adjustedPosition = 0;
63 double rawToDegreesFactor = 0.0;
64};
65
76
77// ---- Read one encoder channel from a TOML table ----
78static bool readEncoderFromToml(const toml::table& tbl,
79 const std::string& key,
81{
82 const auto* encTbl = tbl[key].as_table();
83 if (!encTbl)
84 return false;
85
86 auto warnMissing = [&](const char* field) {
87 yCWarning(CIA402,
88 "CheckEncoderCalibration: missing key '%s' in [%s], using default value",
89 field,
90 key.c_str());
91 };
92
93 if (auto v = (*encTbl)["raw_position"].value<int64_t>())
94 out.rawPosition = *v;
95 else
96 warnMissing("raw_position");
97 if (auto v = (*encTbl)["raw_position_degrees"].value<double>())
98 out.rawPositionDegrees = *v;
99 else
100 warnMissing("raw_position_degrees");
101 if (auto v = (*encTbl)["adjusted_position"].value<int64_t>())
102 out.adjustedPosition = *v;
103 else
104 warnMissing("adjusted_position");
105 if (auto v = (*encTbl)["adjusted_position_degrees"].value<double>())
107 else
108 warnMissing("adjusted_position_degrees");
109 if (auto v = (*encTbl)["counts_per_revolution"].value<int64_t>())
110 out.countsPerRevolution = *v;
111 else
112 warnMissing("counts_per_revolution");
113 if (auto v = (*encTbl)["raw_to_degrees_factor"].value<double>())
114 out.rawToDegreesFactor = *v;
115 else
116 warnMissing("raw_to_degrees_factor");
117
118 return true;
119}
120
121// ---- Read live encoder data from a slave via SDO ----
123 int slave,
124 uint16_t idxConfig,
125 uint16_t idxData,
127{
128 uint32_t resolution = 0;
129 uint32_t rawPos = 0;
130 int32_t adjPos = 0;
131
132 if (mgr.readSDO<uint32_t>(slave, idxConfig, 0x03, resolution)
134 {
135 yCWarning(CIA402, "s%02d: failed to read resolution from 0x%04X:03", slave, idxConfig);
136 }
137 if (mgr.readSDO<uint32_t>(slave, idxData, 0x01, rawPos) != EthercatManager::Error::NoError)
138 {
139 yCWarning(CIA402, "s%02d: failed to read raw position from 0x%04X:01", slave, idxData);
140 }
141 if (mgr.readSDO<int32_t>(slave, idxData, 0x02, adjPos) != EthercatManager::Error::NoError)
142 {
143 yCWarning(CIA402, "s%02d: failed to read adjusted position from 0x%04X:02", slave, idxData);
144 }
145
146 out.countsPerRevolution = static_cast<int64_t>(resolution);
147 out.rawPosition = static_cast<int64_t>(rawPos);
148 out.adjustedPosition = static_cast<int64_t>(adjPos);
149
150 const double resInv = resolution ? (1.0 / static_cast<double>(resolution)) : 0.0;
151 out.rawPositionDegrees = static_cast<double>(rawPos) * resInv * 360.0;
152 out.adjustedPositionDegrees = static_cast<double>(adjPos) * resInv * 360.0;
153 out.rawToDegreesFactor = resolution ? (360.0 / static_cast<double>(resolution)) : 0.0;
154
155 return true;
156}
157
158// ---- Write a single encoder comparison table in Markdown ----
160 const std::string& title,
161 const EncoderChannelData& ref,
162 const EncoderChannelData& live)
163{
164 os << "#### " << title << "\n\n";
165
166 os << "| Metric | Reference (TOML) | Current (Live) | Delta |\n";
167 os << "|:-------|-----------------:|---------------:|------:|\n";
168
169 // Raw position (counts)
170 const int64_t dRaw = live.rawPosition - ref.rawPosition;
171 os << "| Raw position (counts) | "
172 << fmtInt64(ref.rawPosition) << " | "
173 << fmtInt64(live.rawPosition) << " | "
174 << fmtDeltaInt64(dRaw) << " |\n";
175
176 // Raw position (degrees)
177 const double dRawDeg = live.rawPositionDegrees - ref.rawPositionDegrees;
178 os << "| Raw position (deg) | "
179 << fmtDouble(ref.rawPositionDegrees) << " | "
180 << fmtDouble(live.rawPositionDegrees) << " | "
181 << fmtDeltaDouble(dRawDeg) << " |\n";
182
183 // Adjusted position (counts)
184 const int64_t dAdj = live.adjustedPosition - ref.adjustedPosition;
185 os << "| Adjusted position (counts) | "
186 << fmtInt64(ref.adjustedPosition) << " | "
187 << fmtInt64(live.adjustedPosition) << " | "
188 << fmtDeltaInt64(dAdj) << " |\n";
189
190 // Adjusted position (degrees)
191 const double dAdjDeg = live.adjustedPositionDegrees - ref.adjustedPositionDegrees;
192 os << "| Adjusted position (deg) | "
193 << fmtDouble(ref.adjustedPositionDegrees) << " | "
194 << fmtDouble(live.adjustedPositionDegrees) << " | "
195 << fmtDeltaDouble(dAdjDeg) << " |\n";
196
197 // Counts per revolution
198 const int64_t dRes = live.countsPerRevolution - ref.countsPerRevolution;
199 os << "| Counts per revolution | "
200 << fmtInt64(ref.countsPerRevolution) << " | "
201 << fmtInt64(live.countsPerRevolution) << " | "
202 << fmtDeltaInt64(dRes) << " |\n";
203
204 // Raw-to-degrees factor
205 const double dFactor = live.rawToDegreesFactor - ref.rawToDegreesFactor;
206 os << "| Raw-to-degrees factor | "
207 << fmtDouble(ref.rawToDegreesFactor) << " | "
208 << fmtDouble(live.rawToDegreesFactor) << " | "
209 << fmtDeltaDouble(dFactor) << " |\n";
210
211 os << "\n";
212}
213
215{
216public:
217 bool run(const std::string& ifname, const std::string& tomlPath, const std::string& reportPath)
218 {
219 // ---- 1) Check that the TOML file exists and parse it ----
220 {
221 std::ifstream test(tomlPath);
222 if (!test.is_open())
223 {
224 yCError(CIA402,
225 "CheckEncoderCalibration: file not found: %s",
226 tomlPath.c_str());
227 return false;
228 }
229 }
230
231 toml::table refRoot;
232 try
233 {
234 refRoot = toml::parse_file(tomlPath);
235 } catch (const toml::parse_error& err)
236 {
237 yCError(CIA402,
238 "CheckEncoderCalibration: failed to parse %s: %s",
239 tomlPath.c_str(),
240 err.what());
241 return false;
242 }
243 yCInfo(CIA402, "CheckEncoderCalibration: loaded reference TOML from %s", tomlPath.c_str());
244
245 // ---- 2) Initialize EtherCAT master ----
246 yCInfo(CIA402, "CheckEncoderCalibration: initializing EtherCAT on %s", ifname.c_str());
247 const auto rc = mgr.init(ifname);
249 {
250 yCError(CIA402,
251 "CheckEncoderCalibration: init failed on %s (rc=%d)",
252 ifname.c_str(),
253 int(rc));
254 return false;
255 }
256
257 // ---- 3) Discover slaves ----
258 std::vector<int> slaves;
259 for (int s = 1;; ++s)
260 {
261 auto name = mgr.getName(s);
262 if (name.empty())
263 break;
264 yCInfo(CIA402, "CheckEncoderCalibration: found slave %d: %s", s, name.c_str());
265 slaves.push_back(s);
266 }
267 if (slaves.empty())
268 {
269 yCError(CIA402, "CheckEncoderCalibration: no slaves found");
270 return false;
271 }
272
273 // ---- 4) For each slave, read reference + live data ----
275 bool allOk = true;
276
277 for (int s : slaves)
278 {
279 const std::string slaveKey = "slave_" + std::to_string(s);
280 const auto* slaveTbl = refRoot[slaveKey].as_table();
281
282 if (!slaveTbl)
283 {
284 yCWarning(CIA402,
285 "CheckEncoderCalibration: no entry '%s' in TOML, skipping slave %d",
286 slaveKey.c_str(),
287 s);
288 continue;
289 }
290
291 SlaveReport rpt;
292 rpt.slaveIndex = s;
293
294 // Reference name from TOML
295 if (auto v = (*slaveTbl)["name"].value<std::string>())
296 rpt.name = *v;
297 else
298 rpt.name = mgr.getName(s);
299
300 // Read reference encoder data from TOML
301 if (!readEncoderFromToml(*slaveTbl, "encoder1", rpt.refEnc1))
302 {
303 yCWarning(CIA402,
304 "CheckEncoderCalibration: missing encoder1 in TOML for %s",
305 slaveKey.c_str());
306 }
307 if (!readEncoderFromToml(*slaveTbl, "encoder2", rpt.refEnc2))
308 {
309 yCWarning(CIA402,
310 "CheckEncoderCalibration: missing encoder2 in TOML for %s",
311 slaveKey.c_str());
312 }
313
314 // Read live encoder data from the drive
317
318 reports.push_back(rpt);
319 }
320
321 // ---- 5) Generate Markdown report ----
322 allOk = this->writeMarkdownReport(reports, reportPath, tomlPath);
323
324 return allOk;
325 }
326
327private:
328 bool writeMarkdownReport(const std::vector<SlaveReport>& reports,
329 const std::string& reportPath,
330 const std::string& tomlPath)
331 {
332 std::ofstream ofs(reportPath);
333 if (!ofs.is_open())
334 {
335 yCError(CIA402,
336 "CheckEncoderCalibration: cannot open %s for writing",
337 reportPath.c_str());
338 return false;
339 }
340
341 // ---- Header ----
342 const auto now = std::chrono::system_clock::now();
344 const std::tm tm = getLocalTime(t);
345 std::ostringstream tsStr;
346 tsStr << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
347
348 ofs << "# Encoder Calibration Check Report\n\n";
349
350 ofs << "| | |\n";
351 ofs << "|:--|:--|\n";
352 ofs << "| **Date** | " << tsStr.str() << " |\n";
353 ofs << "| **Reference TOML** | `" << tomlPath << "` |\n";
354 ofs << "| **Slaves checked** | " << reports.size() << " |\n";
355 ofs << "\n---\n\n";
356
357 // ---- Summary table ----
358 ofs << "## Summary\n\n";
359 ofs << "| Slave | Name | Enc1 Adj &Delta; (deg) | Enc2 Adj &Delta; (deg) |\n";
360 ofs << "|:-----:|:-----|----------------------:|-----------------------:|\n";
361
362 for (const auto& rpt : reports)
363 {
364 const double d1
365 = rpt.liveEnc1.adjustedPositionDegrees - rpt.refEnc1.adjustedPositionDegrees;
366 const double d2
367 = rpt.liveEnc2.adjustedPositionDegrees - rpt.refEnc2.adjustedPositionDegrees;
368 ofs << "| " << rpt.slaveIndex << " | " << rpt.name << " | " << fmtDeltaDouble(d1)
369 << " | " << fmtDeltaDouble(d2) << " |\n";
370 }
371 ofs << "\n---\n\n";
372
373 // ---- Per-slave detailed tables ----
374 for (const auto& rpt : reports)
375 {
376 ofs << "### Slave " << rpt.slaveIndex << " — " << rpt.name << "\n\n";
377
378 writeEncoderTable(ofs, "Encoder 1 (0x2111)", rpt.refEnc1, rpt.liveEnc1);
379 writeEncoderTable(ofs, "Encoder 2 (0x2113)", rpt.refEnc2, rpt.liveEnc2);
380
381 ofs << "---\n\n";
382 }
383
384 ofs << "*Report generated by `yarp-cia402-check-encoder-calibration`*\n";
385 ofs.close();
386 if (ofs.fail())
387 {
388 yCError(CIA402,
389 "CheckEncoderCalibration: error closing report file %s",
390 reportPath.c_str());
391 return false;
392 }
393
394 yCInfo(CIA402, "CheckEncoderCalibration: report written to %s", reportPath.c_str());
395 return true;
396 }
397
398 EthercatManager mgr;
399};
400
402{
403 m_impl = std::make_unique<Impl>();
404}
405
407
408bool CheckEncoderCalibration::run(yarp::os::ResourceFinder& rf)
409{
410 const std::string ifname
411 = rf.check("ifname") ? rf.find("ifname").asString() : std::string("eth0");
412
413 // The reference TOML is required
414 if (!rf.check("toml-input"))
415 {
416 yCError(CIA402,
417 "CheckEncoderCalibration: missing required parameter 'toml-input' "
418 "(path to the reference TOML file from store-home-position)");
419 return false;
420 }
421 const std::string tomlPath = rf.find("toml-input").asString();
422
423 // Report output path
424 std::string reportPath;
425 if (rf.check("report-output"))
426 {
427 reportPath = rf.find("report-output").asString();
428 } else
429 {
430 // Generate default filename with current date and time
431 const auto now = std::chrono::system_clock::now();
433 const std::tm tm = getLocalTime(t);
435 oss << "encoder_calibration_check_" << std::put_time(&tm, "%Y_%m_%d_%H_%M_%S") << ".md";
436 reportPath = oss.str();
437 }
438
439 yCInfo(CIA402,
440 "CheckEncoderCalibration: ifname=%s toml-input=%s report-output=%s",
441 ifname.c_str(),
442 tomlPath.c_str(),
443 reportPath.c_str());
444
445 return m_impl->run(ifname, tomlPath, reportPath);
446}
static bool readEncoderFromToml(const toml::table &tbl, const std::string &key, EncoderChannelData &out)
static void writeEncoderTable(std::ostream &os, const std::string &title, const EncoderChannelData &ref, const EncoderChannelData &live)
static std::string fmtDeltaInt64(int64_t v)
static std::string fmtInt64(int64_t v)
static std::string fmtDeltaDouble(double v, int prec=6)
static std::string fmtDouble(double v, int prec=6)
static bool readEncoderFromSlave(EthercatManager &mgr, int slave, uint16_t idxConfig, uint16_t idxData, EncoderChannelData &out)
T c_str(T... args)
bool run(const std::string &ifname, const std::string &tomlPath, const std::string &reportPath)
bool run(yarp::os::ResourceFinder &rf)
Run the full encoder check and produce a Markdown report.
Minimal EtherCAT master built on SOEM for CiA-402 devices.
Error readSDO(int slaveIndex, uint16_t idx, uint8_t subIdx, T &out) noexcept
Read an SDO value from a slave (blocking call).
@ NoError
Operation completed successfully.
T empty(T... args)
T find(T... args)
T fixed(T... args)
T is_open(T... args)
static constexpr uint16_t IDX_ENC2_CONFIG
:03 = resolution (counts/rev)
static constexpr uint16_t IDX_ENC1_DATA
:01 = raw position, :02 = adjusted position
static constexpr uint16_t IDX_ENC1_CONFIG
:03 = resolution (counts/rev)
std::tm getLocalTime(const std::time_t &t)
Definition TimeUtils.h:12
static constexpr uint16_t IDX_ENC2_DATA
:01 = raw position, :02 = adjusted position
T push_back(T... args)
T put_time(T... args)
T setprecision(T... args)
T size(T... args)
T str(T... args)
EncoderChannelData liveEnc2
EncoderChannelData refEnc2
EncoderChannelData refEnc1
EncoderChannelData liveEnc1
T to_string(T... args)