roboapi 0.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. roboapi/__init__.py +2 -0
  2. roboapi/calibration/__init__.py +6 -0
  3. roboapi/calibration/profile.py +139 -0
  4. roboapi/calibration/session.py +350 -0
  5. roboapi/cli.py +215 -0
  6. roboapi/config/__init__.py +22 -0
  7. roboapi/config/loader.py +119 -0
  8. roboapi/config/models.py +191 -0
  9. roboapi/embodiment.py +251 -0
  10. roboapi/errors.py +37 -0
  11. roboapi/geometry.py +337 -0
  12. roboapi/hardware/__init__.py +5 -0
  13. roboapi/hardware/base.py +113 -0
  14. roboapi/hardware/camera.py +167 -0
  15. roboapi/hardware/franka/__init__.py +5 -0
  16. roboapi/hardware/franka/arm.py +476 -0
  17. roboapi/hardware/franka/desk.py +555 -0
  18. roboapi/hardware/franka/feedback.py +196 -0
  19. roboapi/hardware/franka/guiding.py +104 -0
  20. roboapi/hardware/franka/ik.py +543 -0
  21. roboapi/hardware/franka/limits.py +110 -0
  22. roboapi/hardware/franka/settings.py +116 -0
  23. roboapi/hardware/robotiq.py +350 -0
  24. roboapi/hardware/ur/__init__.py +16 -0
  25. roboapi/hardware/ur/build.py +92 -0
  26. roboapi/hardware/ur/compliant.py +128 -0
  27. roboapi/hardware/ur/dashboard.py +103 -0
  28. roboapi/hardware/ur/feedback.py +159 -0
  29. roboapi/hardware/ur/ipc.py +206 -0
  30. roboapi/hardware/ur/native/CMakeLists.txt +37 -0
  31. roboapi/hardware/ur/native/dependencies.lock.json +13 -0
  32. roboapi/hardware/ur/native/include/ur_cpp/cartesian_speedl.hpp +170 -0
  33. roboapi/hardware/ur/native/include/ur_cpp/control_loop.hpp +126 -0
  34. roboapi/hardware/ur/native/include/ur_cpp/geom.hpp +72 -0
  35. roboapi/hardware/ur/native/include/ur_cpp/motion_primitives.hpp +350 -0
  36. roboapi/hardware/ur/native/include/ur_cpp/robot_io.hpp +26 -0
  37. roboapi/hardware/ur/native/include/ur_cpp/streaming_pose.hpp +369 -0
  38. roboapi/hardware/ur/native/src/control_loop.cpp +512 -0
  39. roboapi/hardware/ur/native/src/robot_io_urcl.cpp +143 -0
  40. roboapi/hardware/ur/native/tests/cartesian_speedl_unit.cpp +113 -0
  41. roboapi/hardware/ur/native/tests/control_modes_unit.cpp +137 -0
  42. roboapi/hardware/ur/native/tests/streaming_pose_unit.cpp +348 -0
  43. roboapi/hardware/ur/native/tests/velocity_governor_unit.cpp +65 -0
  44. roboapi/hardware/ur/native/worker.cpp +147 -0
  45. roboapi/hardware/ur/position.py +253 -0
  46. roboapi/hardware/ur/settings.py +95 -0
  47. roboapi/images.py +16 -0
  48. roboapi/initialize.py +137 -0
  49. roboapi/mcp.py +497 -0
  50. roboapi/motion.py +125 -0
  51. roboapi/naming.py +7 -0
  52. roboapi/operations.py +45 -0
  53. roboapi/policy/__init__.py +7 -0
  54. roboapi/policy/client.py +277 -0
  55. roboapi/policy/protocol.py +166 -0
  56. roboapi/policy/settings.py +79 -0
  57. roboapi/product-names.json +12 -0
  58. roboapi/records.py +149 -0
  59. roboapi/runtime.py +911 -0
  60. roboapi/semantics.py +6 -0
  61. roboapi/tasks.py +337 -0
  62. roboapi/web/__init__.py +5 -0
  63. roboapi/web/presets.py +44 -0
  64. roboapi/web/server.py +419 -0
  65. roboapi/web/static/assets/Bundler-LICENSE.txt +48 -0
  66. roboapi/web/static/assets/CalibrationScene-BO_spRoL.js +4220 -0
  67. roboapi/web/static/assets/Inter-OFL.txt +93 -0
  68. roboapi/web/static/assets/Inter-Variable-c8O0ljhh.ttf +0 -0
  69. roboapi/web/static/assets/Lucide-LICENSE.txt +43 -0
  70. roboapi/web/static/assets/Meshoptimizer-LICENSE.txt +23 -0
  71. roboapi/web/static/assets/MiSans-Regular-ES4DrbaF.woff2 +0 -0
  72. roboapi/web/static/assets/MiSans-Semibold-B34HIk9C.woff2 +0 -0
  73. roboapi/web/static/assets/PokeBrand-NOTICE.txt +20 -0
  74. roboapi/web/static/assets/React-LICENSE.txt +23 -0
  75. roboapi/web/static/assets/Three-LICENSE.txt +23 -0
  76. roboapi/web/static/assets/home-reference-CTpf8-oV.png +0 -0
  77. roboapi/web/static/assets/index-CGLhpUm0.js +28 -0
  78. roboapi/web/static/assets/index-QXqV-fDC.css +1 -0
  79. roboapi/web/static/assets/robotiq-ace-pro-2-EJuoOXTy.glb +0 -0
  80. roboapi/web/static/index.html +15 -0
  81. roboapi-0.4.2.dist-info/METADATA +205 -0
  82. roboapi-0.4.2.dist-info/RECORD +94 -0
  83. roboapi-0.4.2.dist-info/WHEEL +5 -0
  84. roboapi-0.4.2.dist-info/entry_points.txt +2 -0
  85. roboapi-0.4.2.dist-info/licenses/LICENSE +202 -0
  86. roboapi-0.4.2.dist-info/licenses/NOTICE +16 -0
  87. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Bundler-LICENSE.txt +48 -0
  88. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Inter-OFL.txt +93 -0
  89. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Lucide-LICENSE.txt +43 -0
  90. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Meshoptimizer-LICENSE.txt +23 -0
  91. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/PokeBrand-NOTICE.txt +20 -0
  92. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/React-LICENSE.txt +23 -0
  93. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Three-LICENSE.txt +23 -0
  94. roboapi-0.4.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,512 @@
1
+ #include "ur_cpp/control_loop.hpp"
2
+
3
+ #include <algorithm>
4
+ #include <chrono>
5
+ #include <cmath>
6
+ #include <cstdio>
7
+ #include <cstring>
8
+ #include <exception>
9
+ #include <memory>
10
+ #include <pthread.h>
11
+ #include <sched.h>
12
+ #include <thread>
13
+ #include <unistd.h>
14
+
15
+ #include "ur_cpp/cartesian_speedl.hpp"
16
+ #include "ur_cpp/geom.hpp"
17
+ #include "ur_cpp/robot_io.hpp"
18
+
19
+ using steady = std::chrono::steady_clock;
20
+
21
+ static uint64_t nowNs() {
22
+ return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
23
+ std::chrono::steady_clock::now().time_since_epoch())
24
+ .count());
25
+ }
26
+
27
+ enum Mode : uint8_t {
28
+ MODE_IDLE = 0,
29
+ MODE_TRACK_POSE = 1,
30
+ MODE_SWITCHING = 4,
31
+ MODE_TRACK_POSITION = 5,
32
+ };
33
+
34
+ static const char *modeName(uint8_t m) {
35
+ switch (m) {
36
+ case MODE_IDLE:
37
+ return "IDLE";
38
+ case MODE_TRACK_POSE:
39
+ return "TRACK_POSE";
40
+ case MODE_TRACK_POSITION:
41
+ return "TRACK_POSITION";
42
+ case MODE_SWITCHING:
43
+ return "SWITCHING";
44
+ default:
45
+ return "?";
46
+ }
47
+ }
48
+
49
+ struct RollingDt {
50
+ std::array<double, 512> buf_{};
51
+ size_t n_ = 0, head_ = 0;
52
+ double max_ms_ = 0;
53
+ void push(double ms) {
54
+ buf_[head_] = ms;
55
+ head_ = (head_ + 1) % buf_.size();
56
+ n_ = std::min(n_ + 1, buf_.size());
57
+ max_ms_ = std::max(max_ms_, ms);
58
+ }
59
+ void fill(Snapshot &s) const {
60
+ if (n_ < 2)
61
+ return;
62
+ double mean = 0;
63
+ for (size_t i = 0; i < n_; ++i)
64
+ mean += buf_[i];
65
+ mean /= static_cast<double>(n_);
66
+ double var = 0;
67
+ for (size_t i = 0; i < n_; ++i)
68
+ var += (buf_[i] - mean) * (buf_[i] - mean);
69
+ std::array<double, 512> tmp = buf_;
70
+ std::sort(tmp.begin(), tmp.begin() + static_cast<long>(n_));
71
+ s.dt_mean_ms_ = mean;
72
+ s.dt_std_ms_ = std::sqrt(var / static_cast<double>(n_));
73
+ s.dt_p99_ms_ = tmp[static_cast<size_t>(0.99 * (n_ - 1))];
74
+ s.dt_max_ms_ = max_ms_;
75
+ if (mean > 0)
76
+ s.host_hz_ = 1000.0 / mean;
77
+ }
78
+ void fillRtdeHz(Snapshot &s) const {
79
+ if (n_ < 2)
80
+ return;
81
+ double mean = 0;
82
+ for (size_t i = 0; i < n_; ++i)
83
+ mean += buf_[i];
84
+ mean /= static_cast<double>(n_);
85
+ if (mean > 0)
86
+ s.rtde_hz_ = 1000.0 / mean;
87
+ }
88
+ };
89
+
90
+ bool validateArgs(const Args &a, std::string &error) {
91
+ return CartesianSpeedlLaw(a).validate(error);
92
+ }
93
+
94
+ static void tryRealtimePriority(int prio) {
95
+ if (prio <= 0)
96
+ return;
97
+ sched_param sp{};
98
+ sp.sched_priority = prio;
99
+ if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp) == 0)
100
+ std::printf("[rt] SCHED_FIFO priority %d acquired\n", prio);
101
+ else
102
+ std::printf("[rt] SCHED_FIFO unavailable (%s) — default scheduler\n", std::strerror(errno));
103
+ }
104
+
105
+ void ControlLoop::watchdogMain() {
106
+ constexpr double stall_s = 2.0;
107
+ constexpr uint64_t limit_ns = static_cast<uint64_t>(stall_s * 1e9);
108
+ while (!watchdog_quit_) {
109
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
110
+ if (watchdog_quit_)
111
+ return;
112
+ const uint64_t last = last_tick_ns_.load();
113
+ if (last != 0 && nowNs() - last > limit_ns) {
114
+ std::fprintf(stderr, "[halt] loop stalled > %.1f s — hard-exit\n", stall_s);
115
+ std::fflush(nullptr);
116
+ ::_exit(3);
117
+ }
118
+ }
119
+ }
120
+
121
+ // One RTDE period: intent → guards → write → snapshot.
122
+ struct LoopCycle {
123
+ ControlLoop &rt_;
124
+ RobotIO &io_;
125
+ const Args &args_;
126
+
127
+ vec6 tcp_pose_{}, q_{}, wrench_{};
128
+ Mode mode_ = MODE_IDLE;
129
+ uint8_t logged_mode_ = 0xFF;
130
+ vec6 ref_pose_{};
131
+ CartesianSpeedlLaw control_;
132
+ double switch_started_ = 0, settled_since_ = 0;
133
+ RollingDt dts_;
134
+ RollingDt robot_dts_;
135
+ LoopFault fault_ = LoopFault::None;
136
+ steady::time_point last_ = steady::now();
137
+ uint64_t ticks_ = 0;
138
+ bool ok_ = true;
139
+ double last_robot_time_ = 0;
140
+
141
+ LoopCycle(ControlLoop &runtime, RobotIO &robot)
142
+ : rt_(runtime), io_(robot), args_(runtime.args_), control_(runtime.args_) {}
143
+
144
+ bool latched() const { return fault_ != LoopFault::None; }
145
+
146
+ void trip(LoopFault fault) {
147
+ if (!latched())
148
+ fault_ = fault;
149
+ }
150
+
151
+ bool failReady(const std::string &msg) {
152
+ std::lock_guard<std::mutex> lock(rt_.snap_mu_);
153
+ rt_.snap_.loop_ok_ = false;
154
+ rt_.snap_.last_error_ = msg;
155
+ return false;
156
+ }
157
+
158
+ // First robot sample: seed refs/clocks so tick 1 holds here, not at origin.
159
+ bool initializeFromRobot() {
160
+ if (!io_.tick(tcp_pose_, q_, wrench_))
161
+ return failReady("no robot data");
162
+ ref_pose_ = tcp_pose_;
163
+ control_.reset(tcp_pose_, vec6{});
164
+ last_robot_time_ = io_.robot_time_;
165
+ last_ = steady::now();
166
+ std::printf("[loop] robot at %.0f Hz — holding [%.4f %.4f %.4f]\n",
167
+ args_.freq_, tcp_pose_[0], tcp_pose_[1], tcp_pose_[2]);
168
+ std::fflush(stdout);
169
+ return true;
170
+ }
171
+
172
+ bool prepareMode(Mode requested) {
173
+ if (mode_ == requested)
174
+ return true;
175
+ if (mode_ != MODE_SWITCHING) {
176
+ dropToIdle();
177
+ mode_ = MODE_SWITCHING;
178
+ switch_started_ = io_.robot_time_;
179
+ settled_since_ = 0;
180
+ // Send keepalive first: URScript joins/brakes the old motion thread.
181
+ return false;
182
+ }
183
+ if (io_.robot_time_ - switch_started_ > 2.0) {
184
+ trip(LoopFault::ModeSwitchTimeout);
185
+ return false;
186
+ }
187
+ const auto still = [](double speed) { return std::abs(speed) <= 0.005; };
188
+ if (!std::all_of(io_.qd_.begin(), io_.qd_.end(), still) ||
189
+ !std::all_of(io_.tcp_speed_.begin(), io_.tcp_speed_.end(), still)) {
190
+ settled_since_ = 0;
191
+ return false;
192
+ }
193
+ if (settled_since_ == 0)
194
+ settled_since_ = io_.robot_time_;
195
+ if (io_.robot_time_ - settled_since_ < 0.05)
196
+ return false;
197
+ // Seed from post-stop feedback, never from the old source's snapshot.
198
+ control_.reset(tcp_pose_, io_.tcp_speed_);
199
+ mode_ = requested;
200
+ return true;
201
+ }
202
+
203
+ void applyMode(const ControlLoop::Intent &intent) {
204
+ using Kind = ControlLoop::Kind;
205
+ switch (intent.kind_) {
206
+ case Kind::TrackPose:
207
+ if (prepareMode(MODE_TRACK_POSE))
208
+ ref_pose_ = intent.pose_;
209
+ break;
210
+ case Kind::TrackPosition:
211
+ if (prepareMode(MODE_TRACK_POSITION))
212
+ ref_pose_ = intent.pose_;
213
+ break;
214
+ case Kind::Idle:
215
+ dropToIdle();
216
+ break;
217
+ }
218
+ }
219
+
220
+ void applyIntent() {
221
+ using Kind = ControlLoop::Kind;
222
+ const auto intent = rt_.takeIntent();
223
+ if ((intent.kind_ == Kind::TrackPose || intent.kind_ == Kind::TrackPosition) && !finite6(intent.pose_))
224
+ trip(LoopFault::VirtualStateInvalid);
225
+
226
+ if (intent.zero_ft_) {
227
+ if (io_.zeroFT())
228
+ control_.resetAdmittance();
229
+ else
230
+ std::fprintf(stderr, "[warning] zero_ftsensor refused; continuing without zeroing\n");
231
+ }
232
+ if (intent.stop_motion_)
233
+ dropToIdle();
234
+ else if (!latched())
235
+ applyMode(intent);
236
+ }
237
+
238
+ void dropToIdle() { mode_ = MODE_IDLE; }
239
+
240
+ void applyGuards() {
241
+ if (!finite6(tcp_pose_) || !finite6(q_) || !finite6(wrench_) || !finite6(io_.tcp_speed_) ||
242
+ !finite6(io_.qd_))
243
+ trip(LoopFault::VirtualStateInvalid);
244
+ if (latched())
245
+ dropToIdle();
246
+ }
247
+
248
+ void logMode() {
249
+ if (mode_ == logged_mode_)
250
+ return;
251
+ std::printf("[loop] mode %s -> %s\n", logged_mode_ == 0xFF ? "INIT" : modeName(logged_mode_),
252
+ modeName(mode_));
253
+ std::fflush(stdout);
254
+ logged_mode_ = mode_;
255
+ }
256
+
257
+ bool writeCommand(double dt) {
258
+ bool written = false;
259
+ switch (mode_) {
260
+ case MODE_TRACK_POSE:
261
+ case MODE_TRACK_POSITION: {
262
+ CartesianCommand command;
263
+ if (!control_.step({ref_pose_, tcp_pose_, wrench_, dt}, command, mode_ == MODE_TRACK_POSE)) {
264
+ trip(command.fault_);
265
+ return failCycle();
266
+ }
267
+ written = io_.writeTwist(command.twist_);
268
+ break;
269
+ }
270
+ case MODE_IDLE:
271
+ case MODE_SWITCHING:
272
+ written = io_.writeKeepalive();
273
+ break;
274
+ }
275
+ if (written)
276
+ return true;
277
+ trip(LoopFault::RobotIoFailed);
278
+ std::fprintf(stderr, "[halt] robot write failed (program stopped / protective stop?)\n");
279
+ return failCycle();
280
+ }
281
+
282
+ void publishSnap() {
283
+ Snapshot s;
284
+ s.q_ = q_;
285
+ s.qd_ = io_.qd_;
286
+ s.tcp_ = tcp_pose_;
287
+ s.tcp_speed_ = io_.tcp_speed_;
288
+ s.wrench_ = wrench_;
289
+ s.mode_ = mode_;
290
+ s.fault_ = fault_;
291
+ s.safety_mode_ = io_.safety_mode_;
292
+ s.ticks_ = ticks_;
293
+ dts_.fill(s);
294
+ robot_dts_.fillRtdeHz(s);
295
+ s.program_running_ = io_.program_running_.load();
296
+ s.loop_alive_ = true;
297
+ s.loop_ok_ = ok_;
298
+ std::lock_guard<std::mutex> lock(rt_.snap_mu_);
299
+ rt_.snap_ = s;
300
+ }
301
+
302
+ bool failCycle() {
303
+ ok_ = false;
304
+ return false;
305
+ }
306
+
307
+ bool readRobot() {
308
+ if (io_.tick(tcp_pose_, q_, wrench_)) {
309
+ rt_.last_tick_ns_ = nowNs();
310
+ return true;
311
+ }
312
+ const auto read_fault = (io_.safety_mode_ != 1 && io_.safety_mode_ != 2)
313
+ ? LoopFault::RobotSafetyStop
314
+ : LoopFault::RobotIoFailed;
315
+ trip(read_fault);
316
+ std::fprintf(stderr, "[halt] %s safety_mode=%d\n",
317
+ read_fault == LoopFault::RobotSafetyStop ? "robot protective/safety stop"
318
+ : "lost robot stream",
319
+ io_.safety_mode_);
320
+ return failCycle();
321
+ }
322
+
323
+ // Pace off the robot clock. A host hitch can make wall_dt jump while robot_dt
324
+ // stays 2 ms — that is not a dropped sample.
325
+ bool pace(double &dt) {
326
+ const auto now = steady::now();
327
+ const double wall_dt = std::chrono::duration<double>(now - last_).count();
328
+ last_ = now;
329
+ dt = 1.0 / args_.freq_;
330
+ const double robot_dt = io_.robot_time_ - last_robot_time_;
331
+ last_robot_time_ = io_.robot_time_;
332
+ if (ticks_ > 0) {
333
+ dts_.push(wall_dt * 1e3);
334
+ robot_dts_.push(robot_dt * 1e3);
335
+ }
336
+ ++ticks_;
337
+ if (!std::isfinite(robot_dt) || (ticks_ > 1 && std::abs(robot_dt - dt) > dt * .1)) {
338
+ trip(LoopFault::SampleDiscontinuity);
339
+ std::fprintf(stderr, "[halt] discontinuous robot samples dt=%.6f wall=%.6f\n", robot_dt,
340
+ wall_dt);
341
+ return failCycle();
342
+ }
343
+ if (wall_dt > .010)
344
+ std::fprintf(stderr, "[warn] host tick hitch wall=%.6f s (robot dt=%.6f)\n", wall_dt,
345
+ robot_dt);
346
+ return true;
347
+ }
348
+
349
+ bool step() {
350
+ if (!readRobot())
351
+ return false;
352
+ double dt = 0;
353
+ if (!pace(dt))
354
+ return false;
355
+ applyIntent();
356
+ if (rt_.stop_)
357
+ return false;
358
+ applyGuards();
359
+ logMode();
360
+ if (!writeCommand(dt))
361
+ return false;
362
+ publishSnap();
363
+ return true;
364
+ }
365
+
366
+ void finish() {
367
+ std::lock_guard<std::mutex> lock(rt_.snap_mu_);
368
+ rt_.snap_.loop_alive_ = false;
369
+ rt_.snap_.loop_ok_ = ok_;
370
+ rt_.snap_.fault_ = fault_;
371
+ if (!ok_ && rt_.snap_.last_error_.empty())
372
+ rt_.snap_.last_error_ = fault_ != LoopFault::None ? loopFaultName(fault_) : "loop_exit";
373
+ }
374
+ };
375
+
376
+ ControlLoop::ControlLoop(Args args) : args_(std::move(args)) {}
377
+
378
+ ControlLoop::~ControlLoop() {
379
+ requestStop();
380
+ join();
381
+ }
382
+
383
+ bool ControlLoop::start() {
384
+ if (thread_.joinable())
385
+ return true;
386
+ stop_ = false;
387
+ watchdog_quit_ = false;
388
+ thread_ = std::thread([this] { threadMain(); });
389
+ return true;
390
+ }
391
+
392
+ void ControlLoop::requestStop() { stop_ = true; }
393
+
394
+ void ControlLoop::join() {
395
+ if (thread_.joinable())
396
+ thread_.join();
397
+ }
398
+
399
+ void ControlLoop::setPoseRef(const vec6 &tcp, bool admittance) {
400
+ std::lock_guard<std::mutex> lock(intent_mu_);
401
+ intent_.kind_ = admittance ? Kind::TrackPose : Kind::TrackPosition;
402
+ intent_.pose_ = tcp;
403
+ }
404
+
405
+ void ControlLoop::zeroFt() {
406
+ std::lock_guard<std::mutex> lock(intent_mu_);
407
+ intent_.zero_ft_ = true;
408
+ }
409
+
410
+ void ControlLoop::stopMotion() {
411
+ std::lock_guard<std::mutex> lock(intent_mu_);
412
+ intent_.kind_ = Kind::Idle;
413
+ intent_.stop_motion_ = true;
414
+ }
415
+
416
+ ControlLoop::Intent ControlLoop::takeIntent() {
417
+ std::lock_guard<std::mutex> lock(intent_mu_);
418
+ Intent out = intent_;
419
+ // A command racing with stop must not erase it or resume on the next tick.
420
+ if (out.stop_motion_)
421
+ intent_.kind_ = Kind::Idle;
422
+ intent_.zero_ft_ = false;
423
+ intent_.stop_motion_ = false;
424
+ return out;
425
+ }
426
+
427
+ Snapshot ControlLoop::snapshot() const {
428
+ std::lock_guard<std::mutex> lock(snap_mu_);
429
+ return snap_;
430
+ }
431
+
432
+ bool ControlLoop::waitReady(double timeout_s) {
433
+ const auto deadline = steady::now() + std::chrono::duration<double>(timeout_s);
434
+ while (steady::now() < deadline) {
435
+ const auto s = snapshot();
436
+ if (s.loop_alive_)
437
+ return true;
438
+ if (!s.loop_ok_ && !s.last_error_.empty())
439
+ return false;
440
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
441
+ }
442
+ return false;
443
+ }
444
+
445
+ void ControlLoop::threadMain() {
446
+ std::setvbuf(stdout, nullptr, _IOLBF, 0);
447
+ // Keep these outside the try block: unwinding a joinable watchdog terminates
448
+ // the process, and the robot session must survive until stopControl is tried.
449
+ std::unique_ptr<RobotIO> io;
450
+ std::thread watchdog;
451
+ const auto reportException = [this](const char *phase, const char *detail) {
452
+ std::fprintf(stderr, "[halt] %s: %s\n", phase, detail);
453
+ std::lock_guard<std::mutex> lock(snap_mu_);
454
+ snap_.loop_alive_ = false;
455
+ snap_.loop_ok_ = false;
456
+ if (snap_.fault_ == LoopFault::None)
457
+ snap_.fault_ = LoopFault::RobotIoFailed;
458
+ if (snap_.last_error_.empty())
459
+ snap_.last_error_ = std::string(phase) + ": " + detail;
460
+ };
461
+ try {
462
+ const auto run = [&] {
463
+ std::string err;
464
+ io = openRobotIo(args_, stop_, &err);
465
+ if (!io) {
466
+ std::lock_guard<std::mutex> lock(snap_mu_);
467
+ snap_.loop_ok_ = false;
468
+ snap_.last_error_ = err;
469
+ return;
470
+ }
471
+
472
+ tryRealtimePriority(args_.rt_prio_);
473
+ LoopCycle cycle(*this, *io);
474
+ if (!cycle.initializeFromRobot())
475
+ return;
476
+
477
+ last_tick_ns_ = nowNs();
478
+ watchdog = std::thread(&ControlLoop::watchdogMain, this);
479
+ while (!stop_ && cycle.step()) {
480
+ }
481
+ watchdog_quit_ = true;
482
+ watchdog.join();
483
+ cycle.finish();
484
+
485
+ Snapshot fin{};
486
+ cycle.dts_.fill(fin);
487
+ cycle.robot_dts_.fillRtdeHz(fin);
488
+ std::printf("[exit] %llu ticks, rtde %.1f Hz host %.1f Hz, dt mean %.3f ms std %.3f ms "
489
+ "p99 %.2f ms max %.2f ms\n",
490
+ static_cast<unsigned long long>(cycle.ticks_), fin.rtde_hz_, fin.host_hz_,
491
+ fin.dt_mean_ms_, fin.dt_std_ms_, fin.dt_p99_ms_, fin.dt_max_ms_);
492
+ };
493
+ run();
494
+ } catch (const std::exception &exc) {
495
+ reportException("control_loop_exception", exc.what());
496
+ } catch (...) {
497
+ reportException("control_loop_exception", "unknown exception");
498
+ }
499
+
500
+ watchdog_quit_ = true;
501
+ if (watchdog.joinable())
502
+ watchdog.join();
503
+ if (io) {
504
+ try {
505
+ io->stopControl();
506
+ } catch (const std::exception &exc) {
507
+ reportException("stop_control_exception", exc.what());
508
+ } catch (...) {
509
+ reportException("stop_control_exception", "unknown exception");
510
+ }
511
+ }
512
+ }
@@ -0,0 +1,143 @@
1
+ #include "ur_cpp/robot_io.hpp"
2
+
3
+ #include <algorithm>
4
+ #include <chrono>
5
+ #include <cstdio>
6
+ #include <thread>
7
+ #include <vector>
8
+
9
+ #include <ur_client_library/log.h>
10
+ #include <ur_client_library/primary/robot_message/error_code_message.h>
11
+ #include <ur_client_library/rtde/rtde_client.h>
12
+ #include <ur_client_library/types.h>
13
+ #include <ur_client_library/ur/datatypes.h>
14
+ #include <ur_client_library/ur/ur_driver.h>
15
+
16
+ namespace {
17
+
18
+ urcl::vector6d_t toUrcl(const vec6 &in) {
19
+ urcl::vector6d_t v{};
20
+ std::copy(in.begin(), in.end(), v.begin());
21
+ return v;
22
+ }
23
+
24
+ void fromUrcl(const urcl::vector6d_t &in, vec6 &out) {
25
+ std::copy(in.begin(), in.end(), out.begin());
26
+ }
27
+
28
+ } // namespace
29
+
30
+ struct UrclIO : RobotIO {
31
+ std::atomic<bool> *stop_ = nullptr;
32
+ std::unique_ptr<urcl::UrDriver> driver_;
33
+ std::unique_ptr<urcl::rtde_interface::DataPackage> pkg_;
34
+ std::chrono::steady_clock::time_point last_error_log_{};
35
+
36
+ explicit UrclIO(std::atomic<bool> &s) : stop_(&s) {}
37
+
38
+ bool init(const Args &args) {
39
+ urcl::setLogLevel(urcl::LogLevel::INFO);
40
+ const std::vector<std::string> out_recipe = {
41
+ "timestamp", "actual_q", "actual_TCP_pose", "actual_TCP_force",
42
+ "safety_mode", "actual_qd", "actual_TCP_speed"};
43
+ // No RTDE inputs are written. Leave their ownership available to other clients.
44
+ const std::vector<std::string> in_recipe{};
45
+ urcl::UrDriverConfiguration cfg;
46
+ cfg.robot_ip = args.robot_ip_;
47
+ cfg.script_file = args.script_file_;
48
+ cfg.output_recipe = out_recipe;
49
+ cfg.input_recipe = in_recipe;
50
+ cfg.headless_mode = true;
51
+ cfg.handle_program_state = [this](bool running) {
52
+ program_running_ = running;
53
+ std::printf("[robot] external-control program %s\n", running ? "RUNNING" : "STOPPED");
54
+ std::fflush(stdout);
55
+ };
56
+ driver_ = std::make_unique<urcl::UrDriver>(cfg);
57
+ driver_->resetRTDEClient(out_recipe, in_recipe, args.freq_);
58
+ for (int i = 0; i < 100 && !program_running_ && !(stop_ && stop_->load()); ++i) {
59
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
60
+ if (i == 50)
61
+ driver_->sendRobotProgram();
62
+ }
63
+ if (!program_running_) {
64
+ std::fprintf(
65
+ stderr,
66
+ "external-control program did not start (remote control? another RTDE client?)\n");
67
+ return false;
68
+ }
69
+ driver_->startRTDECommunication(false);
70
+ // URCL reallocates a DataPackage on every blocking read if this pointer is
71
+ // empty. Keep one recipe-sized buffer for the life of the session.
72
+ pkg_ = std::make_unique<urcl::rtde_interface::DataPackage>(out_recipe);
73
+ return true;
74
+ }
75
+
76
+ void drainPrimaryErrors() {
77
+ // Drain the primary error queue even when logging is rate-limited;
78
+ // otherwise repeated protective-stop messages accumulate without bound.
79
+ const auto codes = driver_->getErrorCodes();
80
+ if (codes.empty())
81
+ return;
82
+ using clock = std::chrono::steady_clock;
83
+ const auto now = clock::now();
84
+ if (now - last_error_log_ < std::chrono::seconds(1))
85
+ return;
86
+ last_error_log_ = now;
87
+ const auto &c = codes.back();
88
+ if (c.report_level == urcl::ReportLevel::DEBUG ||
89
+ c.report_level == urcl::ReportLevel::DEVL_DEBUG)
90
+ return;
91
+ std::fprintf(stderr, "[robot] %s code=%d %s (%zu queued)\n",
92
+ c.to_string.empty() ? c.text.c_str() : c.to_string.c_str(),
93
+ static_cast<int>(c.message_code), c.text.c_str(), codes.size());
94
+ }
95
+
96
+ bool tick(vec6 &tcp_pose, vec6 &q, vec6 &wrench) override {
97
+ drainPrimaryErrors();
98
+ if (!driver_->getDataPackageBlocking(pkg_))
99
+ return false;
100
+ urcl::vector6d_t v{};
101
+ if (!pkg_->getData("actual_TCP_pose", v))
102
+ return false;
103
+ fromUrcl(v, tcp_pose);
104
+ if (!pkg_->getData("actual_q", v))
105
+ return false;
106
+ fromUrcl(v, q);
107
+ if (!pkg_->getData("actual_TCP_force", v))
108
+ return false;
109
+ fromUrcl(v, wrench);
110
+ if (!pkg_->getData("timestamp", robot_time_) || !pkg_->getData("safety_mode", safety_mode_))
111
+ return false;
112
+ if (!pkg_->getData("actual_TCP_speed", v))
113
+ return false;
114
+ fromUrcl(v, tcp_speed_);
115
+ if (!pkg_->getData("actual_qd", v))
116
+ return false;
117
+ fromUrcl(v, qd_);
118
+ if (safety_mode_ != 1 && safety_mode_ != 2)
119
+ return false;
120
+ return true;
121
+ }
122
+
123
+ bool writeTwist(const vec6 &tw) override {
124
+ return driver_->writeJointCommand(toUrcl(tw), urcl::comm::ControlMode::MODE_SPEEDL,
125
+ urcl::RobotReceiveTimeout::millisec(100));
126
+ }
127
+ bool writeKeepalive() override {
128
+ return driver_->writeKeepalive(urcl::RobotReceiveTimeout::millisec(1000));
129
+ }
130
+ bool zeroFT() override { return driver_->zeroFTSensor(); }
131
+ void stopControl() override { driver_->stopControl(); }
132
+ };
133
+
134
+ std::unique_ptr<RobotIO> openLiveRobotIo(const Args &args, std::atomic<bool> &stop,
135
+ std::string *error) {
136
+ auto urio = std::make_unique<UrclIO>(stop);
137
+ if (!urio->init(args)) {
138
+ if (error)
139
+ *error = "external_control_start_failed";
140
+ return nullptr;
141
+ }
142
+ return urio;
143
+ }