A.R.G.U.S 2.1.0
Adaptive Real-Time Guardian for Unsafe Situations
Loading...
Searching...
No Matches
AppController.cpp
Go to the documentation of this file.
1
6#include "AppController.hpp"
7
8#include "CameraCapture.hpp"
9#include "CppTimerStdFuncCallback.h"
12#include "RobotInterlock.hpp"
13#include "VisionProcessor.hpp"
14
15#include <algorithm>
16#include <array>
17#include <atomic>
18#include <cctype>
19#include <chrono>
20#include <csignal>
21#include <cmath>
22#include <condition_variable>
23#include <cstdint>
24#include <deque>
25#include <exception>
26#include <iomanip>
27#include <iostream>
28#include <fstream>
29#include <limits>
30#include <memory>
31#include <mutex>
32#include <optional>
33#include <sstream>
34#include <stdexcept>
35#include <string>
36#include <thread>
37#include <vector>
38
39namespace {
40
41constexpr const char* kLiveCameraWindowName = "ARGUS live camera";
42constexpr const char* kLiveStatusWindowName = "ARGUS live status";
43constexpr const char* kLiveMetricsWindowName = "ARGUS live metrics";
44constexpr const char* kDefaultI2cDevicePath = "/dev/i2c-1";
45constexpr std::uint8_t kDefaultPca9685Address = 0x40;
46constexpr float kDefaultPwmFrequencyHz = 50.0f;
47constexpr std::uint8_t kBaseServoChannel = 0;
48constexpr std::uint8_t kLowerServoChannel = 4;
49constexpr std::uint8_t kUpperServoChannel = 8;
50constexpr std::uint8_t kGripServoChannel = 12;
51constexpr MotionChannelMap kMotionChannelMap{
52 kBaseServoChannel,
53 kLowerServoChannel,
54 kUpperServoChannel,
55 kGripServoChannel};
56// 15 bad frames keeps freeze pipeline latency around ~450-900 ms
57// across typical 20-30 FPS capture rates.
58constexpr int kLiveFreezeBadFrameThreshold = 15;
59constexpr int kLiveRecoverGoodFrameThreshold = 3;
60constexpr std::chrono::milliseconds kSmokeStepDwell{3000};
61constexpr int kSmokeBaseMinOffset = -90;
62constexpr int kSmokeBaseMaxOffset = 90;
63constexpr int kSmokeLowerMinOffset = -90;
64constexpr int kSmokeLowerMaxOffset = 90;
65constexpr int kSmokeUpperMinOffset = -90;
66constexpr int kSmokeUpperMaxOffset = 90;
67constexpr int kSmokeGripMinOffset = -90;
68constexpr int kSmokeGripMaxOffset = 90;
69constexpr int kSmokePositiveStep = 90;
70constexpr int kSmokeNegativeStep = -90;
71volatile std::sig_atomic_t g_interactive_servo_stop_requested = 0;
72constexpr std::chrono::milliseconds kMotionHomeSettleDwell{2000};
73constexpr std::chrono::milliseconds kDemoStepDwell{1000};
74constexpr int kDemoBaseMinOffset = -45;
75constexpr int kDemoBaseMaxOffset = 45;
76constexpr int kDemoLowerMinOffset = -45;
77constexpr int kDemoLowerMaxOffset = 45;
78constexpr int kDemoUpperMinOffset = -45;
79constexpr int kDemoUpperMaxOffset = 45;
80constexpr int kDemoGripMinOffset = -90;
81constexpr int kDemoGripMaxOffset = 90;
82constexpr int kDemoBaseStep = 45;
83constexpr int kDemoLowerStep = 45;
84constexpr int kDemoUpperStep = 45;
85constexpr int kDemoGripStep = 45;
86constexpr std::chrono::milliseconds kCaptureRetryBackoff{50};
87constexpr std::chrono::milliseconds kDemoSlewStepInterval{100};
88constexpr int kDemoSlewStepDegrees = 1;
89constexpr int kLiveManualNudgeDegrees = 5;
90constexpr std::chrono::milliseconds kSurgeryRetractDwell{350};
91constexpr int kSurgeryForwardOffset = 22;
92constexpr int kSurgeryBackwardOffset = -22;
93constexpr int kSurgeryPassOneDepthOffset = -12;
94constexpr int kSurgeryPassTwoDepthOffset = -24;
95constexpr int kSurgeryPassThreeDepthOffset = -36;
96constexpr int kSurgeryGripHoldOffset = 90;
97constexpr std::uint32_t kLiveInterlockWatchdogMaxDelayMs = 1000;
98
99class MotionControllerHardwareAdapter final : public RobotHardware {
100public:
101 explicit MotionControllerHardwareAdapter(MotionController& motion_controller) noexcept
102 : motion_controller_(motion_controller) {}
103
104 bool freezeMotion() noexcept override {
105 motion_controller_.freeze();
106 if (motion_controller_.outputState() == MotionOutputState::FAULT) {
107 std::cerr << "[MOTION] freezeMotion() failed: "
108 << motion_controller_.lastErrorString() << std::endl;
109 return false;
110 }
111 return true;
112 }
113
114 bool enableMotion() noexcept override {
115 if (!motion_controller_.enable()) {
116 std::cerr << "[MOTION] enableMotion() failed: "
117 << motion_controller_.lastErrorString() << std::endl;
118 return false;
119 }
120 return true;
121 }
122
123private:
124 MotionController& motion_controller_;
125};
126
127void handleInteractiveServoSignal(int) {
128 g_interactive_servo_stop_requested = 1;
129}
130
131bool waitForCppTimerDelay(std::chrono::nanoseconds delay,
132 std::string& error_message) {
133 if (delay.count() <= 0) {
134 error_message.clear();
135 return true;
136 }
137
138 std::mutex mutex;
139 std::condition_variable condition;
140 bool fired = false;
141
142 CppTimerCallback timer;
143 timer.registerEventCallback([&]() {
144 std::lock_guard<std::mutex> lock(mutex);
145 fired = true;
146 condition.notify_one();
147 });
148
149 try {
150 timer.startns(static_cast<long>(delay.count()), ONESHOT);
151 } catch (const char* exception) {
152 error_message = exception;
153 return false;
154 } catch (...) {
155 error_message = "CppTimer start failed";
156 return false;
157 }
158
159 std::unique_lock<std::mutex> lock(mutex);
160 condition.wait(lock, [&]() { return fired; });
161 timer.stop();
162 error_message.clear();
163 return true;
164}
165
166double computeFocusScore(const cv::Mat& image) {
167 if (image.empty()) {
168 return 0.0;
169 }
170
171 cv::Mat gray;
172 if (image.channels() == 1) {
173 gray = image;
174 } else {
175 cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
176 }
177
178 cv::Mat laplacian;
179 cv::Laplacian(gray, laplacian, CV_64F);
180 cv::Scalar mean;
181 cv::Scalar stddev;
182 cv::meanStdDev(laplacian, mean, stddev);
183 return stddev[0] * stddev[0];
184}
185
186const char* focusQualityLabel(double focus_score) {
187 if (focus_score < 60.0) {
188 return "BLURRY";
189 }
190 if (focus_score < 180.0) {
191 return "SOFT";
192 }
193 return "SHARP";
194}
195
196std::string formatFocusScore(double focus_score) {
197 std::ostringstream oss;
198 oss << std::fixed << std::setprecision(1) << focus_score;
199 return oss.str();
200}
201
202int normaliseHue(int hue) {
203 int value = hue % 180;
204 if (value < 0) {
205 value += 180;
206 }
207 return value;
208}
209
210int hueBandMidpoint(int lower, int upper) {
211 const int l = normaliseHue(lower);
212 const int u = normaliseHue(upper);
213 if (l <= u) {
214 return (l + u) / 2;
215 }
216
217 const int wrapped_span = (u + 180) - l;
218 return normaliseHue(l + (wrapped_span / 2));
219}
220
221std::string describeHueBand(int lower, int upper) {
222 const int l = normaliseHue(lower);
223 const int u = normaliseHue(upper);
224 if (l <= u) {
225 return std::to_string(l) + "-" + std::to_string(u);
226 }
227 return std::to_string(l) + "-179 + 0-" + std::to_string(u);
228}
229
230std::string hueFamilyName(int hue_midpoint) {
231 const int h = normaliseHue(hue_midpoint);
232 if (h <= 8 || h >= 170) {
233 return "Red";
234 }
235 if (h <= 20) {
236 return "Orange";
237 }
238 if (h <= 34) {
239 return "Yellow";
240 }
241 if (h <= 85) {
242 return "Green";
243 }
244 if (h <= 100) {
245 return "Cyan";
246 }
247 if (h <= 130) {
248 return "Blue";
249 }
250 if (h <= 145) {
251 return "Purple";
252 }
253 return "Pink/Magenta";
254}
255
256std::string describeForbiddenColour(const VisionConfig& config) {
257 const int hue_mid = hueBandMidpoint(config.depthHueLower1, config.depthHueUpper1);
258 return hueFamilyName(hue_mid);
259}
260
261std::string describeForbiddenColourThresholds(const VisionConfig& config) {
262 return "H " + describeHueBand(config.depthHueLower1, config.depthHueUpper1) +
263 " | S " + std::to_string(config.depthSatMin) + "-" +
264 std::to_string(config.depthSatMax) + " | V " +
265 std::to_string(config.depthValMin) + "-" +
266 std::to_string(config.depthValMax) + " | px >= " +
267 std::to_string(config.depthPixelThreshold);
268}
269
270cv::Scalar forbiddenColourSwatchBgr(const VisionConfig& config) {
271 const int hue = hueBandMidpoint(config.depthHueLower1, config.depthHueUpper1);
272 const int sat =
273 std::clamp((config.depthSatMin + config.depthSatMax) / 2, 0, 255);
274 const int val =
275 std::clamp((config.depthValMin + config.depthValMax) / 2, 0, 255);
276
277 cv::Mat hsv(1, 1, CV_8UC3, cv::Scalar(hue, sat, val));
278 cv::Mat bgr;
279 cv::cvtColor(hsv, bgr, cv::COLOR_HSV2BGR);
280 const cv::Vec3b pixel = bgr.at<cv::Vec3b>(0, 0);
281 return cv::Scalar(pixel[0], pixel[1], pixel[2]);
282}
283
284struct RuntimeLatencyMetrics {
285 long long vision_us = 0;
286 std::optional<long long> unsafe_detect_ms;
287 std::optional<long long> freeze_pipeline_ms;
288 std::optional<long long> freeze_cmd_ms;
289 std::optional<long long> total_stop_ms;
290 std::optional<long long> ack_to_resume_ms;
291};
292
293enum class ControllerEventKind {
294 ButtonInput,
295 DemoStepReady,
296 LiveStepReady,
297 FrameAvailable,
298 FrameCaptureFailed,
299};
300
301enum class ControllerEventDisposition {
302 Consumed,
303 Deferred,
304 Abort,
305};
306
307struct ControllerEvent {
308 ControllerEventKind kind;
309 std::optional<PhysicalButtonEvent> button_event;
310 std::optional<FrameEvent> frame_event;
311};
312
313class ControllerEventQueue {
314public:
315 void pushButton(PhysicalButtonEvent button_event) {
316 std::lock_guard<std::mutex> lock(mutex_);
317 queue_.push_back(ControllerEvent{
318 ControllerEventKind::ButtonInput,
319 button_event,
320 std::nullopt,
321 });
322 condition_.notify_one();
323 }
324
325 void pushDemoStepReady() {
326 std::lock_guard<std::mutex> lock(mutex_);
327 if (demo_step_pending_) {
328 return;
329 }
330 demo_step_pending_ = true;
331 queue_.push_back(ControllerEvent{
332 ControllerEventKind::DemoStepReady,
333 std::nullopt,
334 std::nullopt,
335 });
336 condition_.notify_one();
337 }
338
339 void pushLiveStepReady() {
340 std::lock_guard<std::mutex> lock(mutex_);
341 if (live_step_pending_) {
342 return;
343 }
344 live_step_pending_ = true;
345 queue_.push_back(ControllerEvent{
346 ControllerEventKind::LiveStepReady,
347 std::nullopt,
348 std::nullopt,
349 });
350 condition_.notify_one();
351 }
352
353 void pushFrame(FrameEvent frame_event) {
354 std::lock_guard<std::mutex> lock(mutex_);
355 queue_.push_back(ControllerEvent{
356 ControllerEventKind::FrameAvailable,
357 std::nullopt,
358 std::move(frame_event),
359 });
360 condition_.notify_one();
361 }
362
363 void pushFrameCaptureFailed() {
364 std::lock_guard<std::mutex> lock(mutex_);
365 queue_.push_back(ControllerEvent{
366 ControllerEventKind::FrameCaptureFailed,
367 std::nullopt,
368 std::nullopt,
369 });
370 condition_.notify_one();
371 }
372
373 bool waitForEvents(std::chrono::milliseconds timeout) {
374 std::unique_lock<std::mutex> lock(mutex_);
375 if (!queue_.empty()) {
376 return true;
377 }
378
379 if (timeout.count() <= 0) {
380 return false;
381 }
382
383 return condition_.wait_for(lock, timeout, [&]() { return !queue_.empty(); });
384 }
385
386 template <typename Handler>
387 bool drain(Handler&& handler) {
388 std::deque<ControllerEvent> drained;
389 {
390 std::lock_guard<std::mutex> lock(mutex_);
391 drained.swap(queue_);
392 demo_step_pending_ = false;
393 live_step_pending_ = false;
394 }
395
396 std::vector<ControllerEvent> deferred;
397 deferred.reserve(drained.size());
398 for (const ControllerEvent& event : drained) {
399 const ControllerEventDisposition disposition = handler(event);
400 if (disposition == ControllerEventDisposition::Abort) {
401 return false;
402 }
403 if (disposition == ControllerEventDisposition::Deferred) {
404 deferred.push_back(event);
405 }
406 }
407
408 if (!deferred.empty()) {
409 std::lock_guard<std::mutex> lock(mutex_);
410 for (const ControllerEvent& event : deferred) {
411 switch (event.kind) {
412 case ControllerEventKind::ButtonInput:
413 queue_.push_back(event);
414 break;
415 case ControllerEventKind::DemoStepReady:
416 if (!demo_step_pending_) {
417 demo_step_pending_ = true;
418 queue_.push_back(event);
419 }
420 break;
421 case ControllerEventKind::LiveStepReady:
422 if (!live_step_pending_) {
423 live_step_pending_ = true;
424 queue_.push_back(event);
425 }
426 break;
427 case ControllerEventKind::FrameAvailable:
428 case ControllerEventKind::FrameCaptureFailed:
429 queue_.push_back(event);
430 break;
431 }
432 }
433 condition_.notify_one();
434 }
435
436 return true;
437 }
438
439private:
440 std::mutex mutex_;
441 std::condition_variable condition_;
442 std::deque<ControllerEvent> queue_;
443 bool demo_step_pending_ = false;
444 bool live_step_pending_ = false;
445};
446
447long long elapsedMilliseconds(std::chrono::steady_clock::time_point from,
448 std::chrono::steady_clock::time_point to) {
449 return std::chrono::duration_cast<std::chrono::milliseconds>(to - from)
450 .count();
451}
452
453std::string formatLatencyMilliseconds(const std::optional<long long>& value) {
454 return value.has_value() ? std::to_string(*value) : "N/A";
455}
456
457void logLiveLatencySample(const char* event_label,
458 const RuntimeLatencyMetrics& metrics) {
459 std::cout << "[LIVE_TEST] latency event=" << event_label
460 << " vision_us=" << metrics.vision_us
461 << " unsafe_detect_ms="
462 << formatLatencyMilliseconds(metrics.unsafe_detect_ms)
463 << " freeze_pipeline_ms="
464 << formatLatencyMilliseconds(metrics.freeze_pipeline_ms)
465 << " freeze_cmd_ms="
466 << formatLatencyMilliseconds(metrics.freeze_cmd_ms)
467 << " total_stop_ms="
468 << formatLatencyMilliseconds(metrics.total_stop_ms)
469 << " ack_to_resume_ms="
470 << formatLatencyMilliseconds(metrics.ack_to_resume_ms)
471 << std::endl;
472}
473
474int uiPlainFontFace() {
475#ifdef CV_VERSION_MAJOR
476 return cv::FONT_HERSHEY_PLAIN;
477#else
478 return cv::FONT_HERSHEY_SIMPLEX;
479#endif
480}
481
482int frameWidth(const cv::Mat& frame) {
483#ifdef CV_VERSION_MAJOR
484 return frame.cols;
485#else
486 (void)frame;
487 return 640;
488#endif
489}
490
491int frameHeight(const cv::Mat& frame) {
492#ifdef CV_VERSION_MAJOR
493 return frame.rows;
494#else
495 (void)frame;
496 return 480;
497#endif
498}
499
500cv::Mat makePlaceholderFrame(int width, int height) {
501#ifdef CV_VERSION_MAJOR
502 return cv::Mat::zeros(height, width, CV_8UC3);
503#else
504 (void)width;
505 (void)height;
506 return cv::Mat();
507#endif
508}
509
510void drawRectangle(cv::Mat& frame,
511 cv::Point top_left,
512 cv::Point bottom_right,
513 const cv::Scalar& color,
514 int thickness) {
515#ifdef CV_VERSION_MAJOR
516 cv::rectangle(frame, top_left, bottom_right, color, thickness);
517#else
518 (void)frame;
519 (void)top_left;
520 (void)bottom_right;
521 (void)color;
522 (void)thickness;
523#endif
524}
525
526void drawLine(cv::Mat& frame,
527 cv::Point from,
528 cv::Point to,
529 const cv::Scalar& color,
530 int thickness) {
531#ifdef CV_VERSION_MAJOR
532 cv::line(frame, from, to, color, thickness);
533#else
534 (void)frame;
535 (void)from;
536 (void)to;
537 (void)color;
538 (void)thickness;
539#endif
540}
541
542struct SupervisoryUiRow {
543 std::string label;
544 std::string value;
545 cv::Scalar value_color;
546};
547
548struct SupervisoryUiModel {
549 std::string mode_title;
550 std::string state_label;
551 std::string state_description;
552 cv::Scalar state_color;
553 std::string motion_label;
554 cv::Scalar motion_color;
555 std::string operator_prompt;
556 std::string next_action;
557 std::string freeze_reason;
558 std::string footer_info;
559 RuntimeLatencyMetrics latency;
560 std::vector<double> vision_latency_history_us;
561 std::vector<double> unsafe_detect_history_ms;
562 std::vector<double> freeze_pipeline_history_ms;
563 std::vector<double> freeze_cmd_history_ms;
564 std::vector<double> total_stop_history_ms;
565 std::vector<double> ack_resume_history_ms;
566 std::vector<SupervisoryUiRow> status_rows;
567 std::string forbidden_colour_label;
568 std::string forbidden_colour_thresholds;
569 cv::Scalar forbidden_colour_swatch = cv::Scalar(150, 150, 150);
570 bool show_focus = false;
571 std::string focus_label;
572 cv::Scalar focus_color;
573 double focus_fraction = 0.0;
574 std::string camera_hud_text;
575 cv::Scalar camera_hud_color;
576 std::string camera_bottom_left;
577 std::string camera_bottom_right;
578 bool show_frozen_overlay = false;
579 std::string frozen_overlay_title;
580 std::string frozen_overlay_subtitle;
581 bool show_waiting_overlay = false;
582 std::string waiting_overlay_text;
583 bool emphasise_danger = false;
584};
585
586const char* safetyStateToUiString(SafetyState state) {
587 switch (state) {
589 return "Safe";
591 return "Tool not detected";
593 return "Outside allowed zone";
595 return "Excessive speed";
597 return "Invalid orientation";
599 return "Depth exceeded";
600 default:
601 return "Unknown";
602 }
603}
604
605const char* guardianStateToUiString(GuardianState state) {
606 switch (state) {
608 return "Safe (monitoring)";
610 return "Frozen (unsafe)";
612 return "Reset (pending)";
613 default:
614 return "Unknown";
615 }
616}
617
618const char* interlockStateToUiString(InterlockState state) {
619 switch (state) {
621 return "Safe";
623 return "Frozen";
625 return "Fault";
626 default:
627 return "Unknown";
628 }
629}
630
631const char* freezeReasonToUiString(FreezeReason reason) {
632 switch (reason) {
634 return "None";
636 return "Tool not detected";
638 return "Out of allowed zone";
640 return "Vision timeout";
642 return "Position error";
644 return "Depth exceeded";
646 return "Watchdog timeout";
648 default:
649 return "Unknown fault";
650 }
651}
652
653bool isFreezeReasonActiveForUi(const std::string& freeze_reason_text) {
654 return freeze_reason_text != "N/A" && freeze_reason_text != "None" &&
655 freeze_reason_text != "NONE";
656}
657
658cv::Scalar severityColor(const std::string& value) {
659 if (value.find("FAULT") != std::string::npos ||
660 value.find("FROZEN") != std::string::npos ||
661 value.find("UNSAFE") != std::string::npos ||
662 value.find("DETECTED") != std::string::npos ||
663 value.find("EXCEEDED") != std::string::npos ||
664 value.find("OUTSIDE") != std::string::npos) {
665 return cv::Scalar(60, 60, 200);
666 }
667 if (value.find("WAIT") != std::string::npos ||
668 value.find("PENDING") != std::string::npos ||
669 value.find("BLOCKED") != std::string::npos) {
670 return cv::Scalar(50, 180, 230);
671 }
672 if (value.find("SAFE") != std::string::npos ||
673 value.find("ALLOWED") != std::string::npos ||
674 value.find("RUNNING") != std::string::npos ||
675 value.find("READY") != std::string::npos) {
676 return cv::Scalar(60, 170, 80);
677 }
678 return cv::Scalar(25, 25, 25);
679}
680
681void drawPanel(cv::Mat& frame,
682 cv::Point top_left,
683 cv::Point bottom_right,
684 const cv::Scalar& fill,
685 const cv::Scalar& border) {
686 drawRectangle(frame, top_left, bottom_right, fill, -1);
687 drawRectangle(frame, top_left, bottom_right, border, 1);
688}
689
690const cv::Mat& argusLogoImage() {
691 static cv::Mat logo;
692 static bool loaded = false;
693 if (!loaded) {
694 loaded = true;
695 const std::array<const char*, 4> candidate_paths = {{
696 "gui/ENG5220_team-ARGUS_logo-icon_no-bg.png",
697 "./gui/ENG5220_team-ARGUS_logo-icon_no-bg.png",
698 "../gui/ENG5220_team-ARGUS_logo-icon_no-bg.png",
699 "../../gui/ENG5220_team-ARGUS_logo-icon_no-bg.png",
700 }};
701
702 for (const char* path : candidate_paths) {
703 cv::Mat candidate = cv::imread(path, cv::IMREAD_UNCHANGED);
704 if (!candidate.empty()) {
705 logo = std::move(candidate);
706 break;
707 }
708 }
709 }
710 return logo;
711}
712
713bool drawArgusLogo(cv::Mat& frame, const cv::Rect& slot) {
714 const cv::Mat& logo = argusLogoImage();
715 if (logo.empty() || slot.width <= 2 || slot.height <= 2) {
716 return false;
717 }
718
719 const int available_w = slot.width - 2;
720 const int available_h = slot.height - 2;
721 const double scale = std::min(
722 static_cast<double>(available_w) / static_cast<double>(logo.cols),
723 static_cast<double>(available_h) / static_cast<double>(logo.rows));
724 if (scale <= 0.0) {
725 return false;
726 }
727
728 const int draw_w = std::max(1, static_cast<int>(logo.cols * scale));
729 const int draw_h = std::max(1, static_cast<int>(logo.rows * scale));
730 cv::Mat resized_logo;
731 cv::resize(logo, resized_logo, cv::Size(draw_w, draw_h), 0, 0, cv::INTER_AREA);
732
733 const int draw_x = slot.x + (slot.width - draw_w) / 2;
734 const int draw_y = slot.y + (slot.height - draw_h) / 2;
735 if (draw_x < 0 || draw_y < 0 || draw_x + draw_w > frame.cols ||
736 draw_y + draw_h > frame.rows) {
737 return false;
738 }
739
740 cv::Mat dst_roi = frame(cv::Rect(draw_x, draw_y, draw_w, draw_h));
741 if (resized_logo.channels() == 4) {
742 cv::Mat src_bgr;
743 cv::cvtColor(resized_logo, src_bgr, cv::COLOR_BGRA2BGR);
744
745 std::vector<cv::Mat> channels;
746 cv::split(resized_logo, channels);
747 cv::Mat alpha_f;
748 channels[3].convertTo(alpha_f, CV_32FC1, 1.0 / 255.0);
749
750 cv::Mat src_f;
751 cv::Mat dst_f;
752 src_bgr.convertTo(src_f, CV_32FC3, 1.0 / 255.0);
753 dst_roi.convertTo(dst_f, CV_32FC3, 1.0 / 255.0);
754
755 cv::Mat alpha3;
756 std::vector<cv::Mat> alpha_channels(3, alpha_f);
757 cv::merge(alpha_channels, alpha3);
758 cv::Mat blended =
759 src_f.mul(alpha3) + dst_f.mul(cv::Scalar::all(1.0) - alpha3);
760 blended.convertTo(dst_roi, CV_8UC3, 255.0);
761 return true;
762 }
763
764 if (resized_logo.channels() == 3) {
765 resized_logo.copyTo(dst_roi);
766 return true;
767 }
768
769 return false;
770}
771
772void drawSparkline(cv::Mat& frame,
773 cv::Point top_left,
774 cv::Point bottom_right,
775 const std::vector<double>& samples,
776 const cv::Scalar& line_color) {
777 const cv::Scalar panel_fill(235, 235, 235);
778 const cv::Scalar panel_border(220, 220, 220);
779 drawPanel(frame, top_left, bottom_right, panel_fill, panel_border);
780
781 const int inner_x1 = top_left.x + 2;
782 const int inner_y1 = top_left.y + 2;
783 const int inner_x2 = bottom_right.x - 2;
784 const int inner_y2 = bottom_right.y - 2;
785 if (inner_x2 <= inner_x1 || inner_y2 <= inner_y1 || samples.empty()) {
786 return;
787 }
788
789 double min_value = samples.front();
790 double max_value = samples.front();
791 for (double sample : samples) {
792 min_value = std::min(min_value, sample);
793 max_value = std::max(max_value, sample);
794 }
795 if (max_value - min_value < 1e-6) {
796 max_value = min_value + 1.0;
797 }
798
799 const int usable_width = std::max(1, inner_x2 - inner_x1);
800 const std::size_t count = samples.size();
801 for (std::size_t i = 1; i < count; ++i) {
802 const int x_prev = inner_x1 +
803 static_cast<int>((usable_width * (i - 1)) /
804 std::max<std::size_t>(1, count - 1));
805 const int x_curr = inner_x1 +
806 static_cast<int>((usable_width * i) /
807 std::max<std::size_t>(1, count - 1));
808 const double prev_norm = (samples[i - 1] - min_value) / (max_value - min_value);
809 const double curr_norm = (samples[i] - min_value) / (max_value - min_value);
810 const int y_prev = inner_y2 -
811 static_cast<int>(prev_norm * (inner_y2 - inner_y1));
812 const int y_curr = inner_y2 -
813 static_cast<int>(curr_norm * (inner_y2 - inner_y1));
814 drawLine(frame,
815 cv::Point(x_prev, y_prev),
816 cv::Point(x_curr, y_curr),
817 line_color,
818 1);
819 }
820}
821
822void drawSupervisoryGui(cv::Mat& frame, const SupervisoryUiModel& model) {
823 if (frame.empty()) {
824 return;
825 }
826
827 const int width = frameWidth(frame);
828 const int height = frameHeight(frame);
829 const int header_height = std::max(34, height / 14);
830 const int state_bar_height = std::max(28, height / 16);
831 const int left_width = std::max(160, width / 4);
832 const int right_width = std::max(180, width / 4);
833 const int panel_top = header_height + state_bar_height + 8;
834 const int panel_bottom = height - 8;
835 const int left_x2 = left_width;
836 const int right_x1 = width - right_width;
837 const int camera_x1 = left_x2 + 8;
838 const int camera_x2 = right_x1 - 8;
839 const int camera_y1 = panel_top;
840 const int camera_y2 = panel_bottom;
841
842 const cv::Scalar panel_fill(245, 245, 245);
843 const cv::Scalar panel_border(220, 220, 220);
844 const cv::Scalar primary_text(25, 25, 25);
845 const cv::Scalar muted_text(120, 120, 120);
846 const cv::Scalar info_color(170, 140, 60);
847 const cv::Scalar white(255, 255, 255);
848
849 drawPanel(frame,
850 cv::Point(0, 0),
851 cv::Point(width - 1, header_height),
852 panel_fill,
853 panel_border);
854 drawPanel(frame,
855 cv::Point(0, header_height),
856 cv::Point(width - 1, header_height + state_bar_height),
857 panel_fill,
858 panel_border);
859 drawPanel(frame,
860 cv::Point(0, panel_top),
861 cv::Point(left_x2, panel_bottom),
862 panel_fill,
863 panel_border);
864 drawPanel(frame,
865 cv::Point(right_x1, panel_top),
866 cv::Point(width - 1, panel_bottom),
867 panel_fill,
868 panel_border);
869 drawRectangle(frame,
870 cv::Point(camera_x1, camera_y1),
871 cv::Point(camera_x2, camera_y2),
872 panel_border,
873 1);
874
875 drawPanel(frame,
876 cv::Point(12, 6),
877 cv::Point(40, 32),
878 panel_fill,
879 model.state_color);
880 if (!drawArgusLogo(frame, cv::Rect(12, 6, 29, 27))) {
881 cv::putText(frame,
882 "A",
883 cv::Point(22, 25),
884 cv::FONT_HERSHEY_SIMPLEX,
885 0.6,
886 primary_text,
887 2);
888 }
889
890 cv::putText(frame,
891 "ARGUS",
892 cv::Point(52, 21),
893 cv::FONT_HERSHEY_SIMPLEX,
894 0.55,
895 primary_text,
896 2);
897 cv::putText(frame,
898 "Safety Supervisor",
899 cv::Point(52, 34),
900 cv::FONT_HERSHEY_SIMPLEX,
901 0.35,
902 muted_text,
903 1);
904 cv::putText(frame,
905 "ONLINE",
906 cv::Point(width - 135, 20),
907 uiPlainFontFace(),
908 0.9,
909 cv::Scalar(60, 170, 80),
910 1);
911 cv::putText(frame,
912 model.mode_title,
913 cv::Point(width - 135, 34),
914 uiPlainFontFace(),
915 0.9,
916 primary_text,
917 1);
918
919 cv::putText(frame,
920 model.state_label,
921 cv::Point(16, header_height + 20),
922 uiPlainFontFace(),
923 1.0,
924 model.state_color,
925 1);
926 cv::putText(frame,
927 model.state_description,
928 cv::Point(84, header_height + 20),
929 cv::FONT_HERSHEY_SIMPLEX,
930 0.38,
931 muted_text,
932 1);
933 cv::putText(frame,
934 "Control: " + model.operator_prompt,
935 cv::Point(width - 240, header_height + 20),
936 cv::FONT_HERSHEY_SIMPLEX,
937 0.38,
938 model.state_color,
939 1);
940
941 const int card_margin = 12;
942 const int card_width = left_x2 - (2 * card_margin);
943 int card_y = panel_top + 10;
944 auto drawLeftCard = [&](int height_px,
945 const cv::Scalar& border_color,
946 const auto& painter) {
947 drawPanel(frame,
948 cv::Point(card_margin, card_y),
949 cv::Point(card_margin + card_width, card_y + height_px),
950 white,
951 border_color);
952 painter(card_margin, card_y);
953 card_y += height_px + 10;
954 };
955
956 drawLeftCard(82, model.state_color, [&](int x, int y0) {
957 cv::putText(frame,
958 "Safety state",
959 cv::Point(x + 12, y0 + 16),
960 cv::FONT_HERSHEY_SIMPLEX,
961 0.35,
962 muted_text,
963 1);
964 cv::putText(frame,
965 model.state_label,
966 cv::Point(x + 12, y0 + 48),
967 cv::FONT_HERSHEY_SIMPLEX,
968 0.85,
969 model.state_color,
970 2);
971 cv::putText(frame,
972 model.state_description,
973 cv::Point(x + 12, y0 + 66),
974 cv::FONT_HERSHEY_SIMPLEX,
975 0.34,
976 primary_text,
977 1);
978 });
979
980 drawLeftCard(62, model.motion_color, [&](int x, int y0) {
981 cv::putText(frame,
982 "Motion",
983 cv::Point(x + 12, y0 + 16),
984 cv::FONT_HERSHEY_SIMPLEX,
985 0.35,
986 muted_text,
987 1);
988 cv::putText(frame,
989 model.motion_label,
990 cv::Point(x + 12, y0 + 44),
991 uiPlainFontFace(),
992 1.0,
993 model.motion_color,
994 1);
995 });
996
997 drawLeftCard(62, panel_border, [&](int x, int y0) {
998 cv::putText(frame,
999 "Next step",
1000 cv::Point(x + 12, y0 + 16),
1001 cv::FONT_HERSHEY_SIMPLEX,
1002 0.35,
1003 muted_text,
1004 1);
1005 cv::putText(frame,
1006 model.next_action,
1007 cv::Point(x + 12, y0 + 44),
1008 cv::FONT_HERSHEY_SIMPLEX,
1009 0.42,
1010 primary_text,
1011 1);
1012 });
1013
1014 if (isFreezeReasonActiveForUi(model.freeze_reason)) {
1015 drawLeftCard(62, cv::Scalar(60, 60, 200), [&](int x, int y0) {
1016 cv::putText(frame,
1017 "Why stopped",
1018 cv::Point(x + 12, y0 + 16),
1019 cv::FONT_HERSHEY_SIMPLEX,
1020 0.35,
1021 cv::Scalar(60, 60, 200),
1022 1);
1023 cv::putText(frame,
1024 model.freeze_reason,
1025 cv::Point(x + 12, y0 + 44),
1026 uiPlainFontFace(),
1027 1.0,
1028 severityColor(model.freeze_reason),
1029 1);
1030 });
1031 }
1032
1033 cv::putText(frame,
1034 model.camera_hud_text,
1035 cv::Point(camera_x1 + 12, camera_y1 + 20),
1036 uiPlainFontFace(),
1037 0.95,
1038 model.camera_hud_color,
1039 1);
1040 cv::putText(frame,
1041 "FOCUS " + model.focus_label,
1042 cv::Point(camera_x2 - 145, camera_y1 + 20),
1043 uiPlainFontFace(),
1044 0.9,
1045 model.focus_color,
1046 1);
1047 cv::putText(frame,
1048 model.camera_bottom_left,
1049 cv::Point(camera_x1 + 12, camera_y2 - 10),
1050 uiPlainFontFace(),
1051 0.8,
1052 muted_text,
1053 1);
1054 cv::putText(frame,
1055 model.camera_bottom_right,
1056 cv::Point(camera_x2 - 90, camera_y2 - 10),
1057 uiPlainFontFace(),
1058 0.8,
1059 muted_text,
1060 1);
1061
1062 int rx = right_x1 + 12;
1063 int ry = panel_top + 18;
1064 auto drawRightSectionTitle = [&](const std::string& title) {
1065 cv::putText(frame,
1066 title,
1067 cv::Point(rx, ry),
1068 cv::FONT_HERSHEY_SIMPLEX,
1069 0.35,
1070 muted_text,
1071 1);
1072 ry += 12;
1073 drawLine(frame,
1074 cv::Point(right_x1 + 10, ry),
1075 cv::Point(width - 12, ry),
1076 panel_border,
1077 1);
1078 ry += 16;
1079 };
1080
1081 drawRightSectionTitle("Subsystems");
1082
1083 for (const auto& row : model.status_rows) {
1084 cv::putText(frame,
1085 row.label,
1086 cv::Point(rx, ry),
1087 cv::FONT_HERSHEY_SIMPLEX,
1088 0.42,
1089 muted_text,
1090 1);
1091 ry += 14;
1092 cv::putText(frame,
1093 row.value,
1094 cv::Point(rx, ry),
1095 uiPlainFontFace(),
1096 1.0,
1097 row.value_color,
1098 1);
1099 ry += 14;
1100 }
1101
1102 if (model.show_focus) {
1103 ry += 6;
1104 drawRightSectionTitle("FOCUS");
1105 drawPanel(frame,
1106 cv::Point(rx, ry),
1107 cv::Point(width - 24, ry + 16),
1108 cv::Scalar(235, 235, 235),
1109 panel_border);
1110 const int bar_width = std::max(
1111 0,
1112 static_cast<int>((width - right_x1 - 36) *
1113 std::clamp(model.focus_fraction, 0.0, 1.0)));
1114 if (bar_width > 0) {
1115 drawRectangle(frame,
1116 cv::Point(rx + 1, ry + 1),
1117 cv::Point(rx + bar_width, ry + 15),
1118 model.focus_color,
1119 -1);
1120 }
1121 ry += 32;
1122 cv::putText(frame,
1123 model.focus_label,
1124 cv::Point(rx, ry),
1125 uiPlainFontFace(),
1126 1.0,
1127 model.focus_color,
1128 1);
1129 ry += 18;
1130 }
1131
1132 ry += 6;
1133 drawRightSectionTitle("LATENCY");
1134 cv::putText(frame,
1135 "Time to process frame: " + std::to_string(model.latency.vision_us) + " us",
1136 cv::Point(rx, ry),
1137 uiPlainFontFace(),
1138 1.0,
1139 primary_text,
1140 1);
1141 ry += 16;
1142 cv::putText(frame,
1143 "Time to detect unsafe: " +
1144 formatLatencyMilliseconds(model.latency.unsafe_detect_ms),
1145 cv::Point(rx, ry),
1146 uiPlainFontFace(),
1147 1.0,
1148 info_color,
1149 1);
1150 ry += 16;
1151 cv::putText(frame,
1152 "Time to stop motion: " +
1153 formatLatencyMilliseconds(model.latency.total_stop_ms),
1154 cv::Point(rx, ry),
1155 uiPlainFontFace(),
1156 1.0,
1157 severityColor(formatLatencyMilliseconds(model.latency.total_stop_ms)),
1158 1);
1159 ry += 16;
1160 cv::putText(frame,
1161 "freeze_cmd_ms " +
1162 formatLatencyMilliseconds(model.latency.freeze_cmd_ms),
1163 cv::Point(rx, ry),
1164 uiPlainFontFace(),
1165 1.0,
1166 primary_text,
1167 1);
1168 ry += 16;
1169 cv::putText(frame,
1170 "ack_resume_ms " +
1171 formatLatencyMilliseconds(model.latency.ack_to_resume_ms),
1172 cv::Point(rx, ry),
1173 uiPlainFontFace(),
1174 1.0,
1175 primary_text,
1176 1);
1177
1178 if (model.show_frozen_overlay) {
1179 drawRectangle(frame,
1180 cv::Point(camera_x1 + 2, camera_y1 + 2),
1181 cv::Point(camera_x2 - 2, camera_y2 - 2),
1182 cv::Scalar(60, 60, 200),
1183 3);
1184 cv::putText(frame,
1185 model.frozen_overlay_title,
1186 cv::Point(camera_x1 + 80, camera_y1 + 110),
1187 cv::FONT_HERSHEY_SIMPLEX,
1188 1.1,
1189 cv::Scalar(60, 60, 200),
1190 3);
1191 cv::putText(frame,
1192 model.frozen_overlay_subtitle,
1193 cv::Point(camera_x1 + 70, camera_y1 + 140),
1194 cv::FONT_HERSHEY_SIMPLEX,
1195 0.45,
1196 cv::Scalar(60, 60, 200),
1197 1);
1198 } else if (model.show_waiting_overlay) {
1199 drawPanel(frame,
1200 cv::Point(camera_x1 + 80, camera_y2 - 60),
1201 cv::Point(camera_x2 - 80, camera_y2 - 28),
1202 white,
1203 cv::Scalar(50, 180, 230));
1204 cv::putText(frame,
1205 model.waiting_overlay_text,
1206 cv::Point(camera_x1 + 92, camera_y2 - 38),
1207 uiPlainFontFace(),
1208 0.95,
1209 cv::Scalar(50, 180, 230),
1210 1);
1211 }
1212
1213 cv::putText(frame,
1214 model.footer_info,
1215 cv::Point(16, height - 14),
1216 uiPlainFontFace(),
1217 0.9,
1218 muted_text,
1219 1);
1220
1221 const cv::Scalar border_color =
1222 model.emphasise_danger ? cv::Scalar(60, 60, 200) : model.state_color;
1223 const int border_thickness = model.emphasise_danger ? 5 : 3;
1224 drawRectangle(frame,
1225 cv::Point(2, 2),
1226 cv::Point(width - 3, height - 3),
1227 border_color,
1228 border_thickness);
1229}
1230
1231void drawCameraOverlay(cv::Mat& frame, const SupervisoryUiModel& model) {
1232 if (frame.empty()) {
1233 return;
1234 }
1235
1236 const int width = frameWidth(frame);
1237 const int height = frameHeight(frame);
1238 const cv::Scalar muted_text(120, 120, 120);
1239 const cv::Scalar white(255, 255, 255);
1240
1241 cv::putText(frame,
1242 model.camera_hud_text,
1243 cv::Point(14, 24),
1244 uiPlainFontFace(),
1245 0.95,
1246 model.camera_hud_color,
1247 1);
1248 if (model.show_focus) {
1249 const std::string focus_text = "FOCUS " + model.focus_label;
1250 const int focus_x = std::max(12, width - 290);
1251 cv::putText(frame,
1252 focus_text,
1253 cv::Point(focus_x, 24),
1254 uiPlainFontFace(),
1255 0.9,
1256 model.focus_color,
1257 1);
1258 }
1259
1260 cv::putText(frame,
1261 model.camera_bottom_left,
1262 cv::Point(12, height - 12),
1263 uiPlainFontFace(),
1264 0.8,
1265 muted_text,
1266 1);
1267 cv::putText(frame,
1268 model.camera_bottom_right,
1269 cv::Point(std::max(12, width - 90), height - 12),
1270 uiPlainFontFace(),
1271 0.8,
1272 muted_text,
1273 1);
1274
1275 if (model.show_frozen_overlay) {
1276 drawRectangle(frame,
1277 cv::Point(6, 6),
1278 cv::Point(width - 7, height - 7),
1279 cv::Scalar(60, 60, 200),
1280 3);
1281 const int title_y = std::max(80, height / 3);
1282 cv::putText(frame,
1283 model.frozen_overlay_title,
1284 cv::Point(24, title_y),
1285 cv::FONT_HERSHEY_SIMPLEX,
1286 1.1,
1287 cv::Scalar(60, 60, 200),
1288 3);
1289 cv::putText(frame,
1290 model.frozen_overlay_subtitle,
1291 cv::Point(24, title_y + 34),
1292 cv::FONT_HERSHEY_SIMPLEX,
1293 0.55,
1294 cv::Scalar(60, 60, 200),
1295 1);
1296 } else if (model.show_waiting_overlay) {
1297 const int box_w = std::max(320, width - 120);
1298 const int x1 = std::max(20, (width - box_w) / 2);
1299 const int y1 = std::max(40, height - 74);
1300 drawPanel(frame,
1301 cv::Point(x1, y1),
1302 cv::Point(x1 + box_w, y1 + 34),
1303 white,
1304 cv::Scalar(50, 180, 230));
1305 cv::putText(frame,
1306 model.waiting_overlay_text,
1307 cv::Point(x1 + 10, y1 + 22),
1308 uiPlainFontFace(),
1309 0.85,
1310 cv::Scalar(50, 180, 230),
1311 1);
1312 }
1313
1314 const cv::Scalar border_color =
1315 model.emphasise_danger ? cv::Scalar(60, 60, 200) : model.state_color;
1316 const int border_thickness = model.emphasise_danger ? 4 : 2;
1317 drawRectangle(frame,
1318 cv::Point(2, 2),
1319 cv::Point(width - 3, height - 3),
1320 border_color,
1321 border_thickness);
1322}
1323
1324void drawStatusDashboard(cv::Mat& frame, const SupervisoryUiModel& model) {
1325 if (frame.empty()) {
1326 return;
1327 }
1328
1329 const int width = frameWidth(frame);
1330 const int height = frameHeight(frame);
1331 const int header_height = 52;
1332 const int state_bar_height = 32;
1333 const int body_top = header_height + state_bar_height + 8;
1334 const cv::Scalar panel_fill(245, 245, 245);
1335 const cv::Scalar panel_border(220, 220, 220);
1336 const cv::Scalar primary_text(25, 25, 25);
1337 const cv::Scalar muted_text(120, 120, 120);
1338 const cv::Scalar white(255, 255, 255);
1339
1340 frame.setTo(panel_fill);
1341 drawPanel(frame,
1342 cv::Point(0, 0),
1343 cv::Point(width - 1, header_height),
1344 panel_fill,
1345 panel_border);
1346 drawPanel(frame,
1347 cv::Point(0, header_height),
1348 cv::Point(width - 1, header_height + state_bar_height),
1349 panel_fill,
1350 panel_border);
1351
1352 drawPanel(frame,
1353 cv::Point(10, 8),
1354 cv::Point(38, 34),
1355 panel_fill,
1356 model.state_color);
1357 if (!drawArgusLogo(frame, cv::Rect(10, 8, 29, 27))) {
1358 cv::putText(frame,
1359 "A",
1360 cv::Point(20, 27),
1361 cv::FONT_HERSHEY_SIMPLEX,
1362 0.6,
1363 primary_text,
1364 2);
1365 }
1366
1367 cv::putText(frame,
1368 "ARGUS",
1369 cv::Point(50, 23),
1370 cv::FONT_HERSHEY_SIMPLEX,
1371 0.55,
1372 primary_text,
1373 2);
1374 cv::putText(frame,
1375 "Safety supervisor",
1376 cv::Point(50, 38),
1377 cv::FONT_HERSHEY_SIMPLEX,
1378 0.35,
1379 muted_text,
1380 1);
1381 cv::putText(frame,
1382 model.mode_title,
1383 cv::Point(width - 124, 31),
1384 uiPlainFontFace(),
1385 0.9,
1386 primary_text,
1387 1);
1388
1389 cv::putText(frame,
1390 model.state_label,
1391 cv::Point(12, header_height + 22),
1392 uiPlainFontFace(),
1393 1.0,
1394 model.state_color,
1395 1);
1396 cv::putText(frame,
1397 model.state_description,
1398 cv::Point(92, header_height + 22),
1399 cv::FONT_HERSHEY_SIMPLEX,
1400 0.36,
1401 muted_text,
1402 1);
1403 cv::putText(frame,
1404 "Control: " + model.operator_prompt,
1405 cv::Point(12, header_height + 34),
1406 cv::FONT_HERSHEY_SIMPLEX,
1407 0.32,
1408 model.state_color,
1409 1);
1410
1411 const int card_margin = 12;
1412 const int card_width = width - (2 * card_margin);
1413 int card_y = body_top;
1414 auto drawCard = [&](int height_px,
1415 const cv::Scalar& border_color,
1416 const auto& painter) {
1417 drawPanel(frame,
1418 cv::Point(card_margin, card_y),
1419 cv::Point(card_margin + card_width, card_y + height_px),
1420 white,
1421 border_color);
1422 painter(card_margin, card_y);
1423 card_y += height_px + 8;
1424 };
1425
1426 drawCard(76, model.state_color, [&](int x, int y0) {
1427 cv::putText(frame,
1428 "Safety state",
1429 cv::Point(x + 10, y0 + 15),
1430 cv::FONT_HERSHEY_SIMPLEX,
1431 0.34,
1432 muted_text,
1433 1);
1434 cv::putText(frame,
1435 model.state_label,
1436 cv::Point(x + 10, y0 + 44),
1437 cv::FONT_HERSHEY_SIMPLEX,
1438 0.8,
1439 model.state_color,
1440 2);
1441 cv::putText(frame,
1442 model.state_description,
1443 cv::Point(x + 10, y0 + 62),
1444 cv::FONT_HERSHEY_SIMPLEX,
1445 0.34,
1446 primary_text,
1447 1);
1448 });
1449
1450 drawCard(56, model.motion_color, [&](int x, int y0) {
1451 cv::putText(frame,
1452 "Motion",
1453 cv::Point(x + 10, y0 + 15),
1454 cv::FONT_HERSHEY_SIMPLEX,
1455 0.34,
1456 muted_text,
1457 1);
1458 cv::putText(frame,
1459 model.motion_label,
1460 cv::Point(x + 10, y0 + 40),
1461 uiPlainFontFace(),
1462 1.0,
1463 model.motion_color,
1464 1);
1465 });
1466
1467 drawCard(56, panel_border, [&](int x, int y0) {
1468 cv::putText(frame,
1469 "Next step",
1470 cv::Point(x + 10, y0 + 15),
1471 cv::FONT_HERSHEY_SIMPLEX,
1472 0.34,
1473 muted_text,
1474 1);
1475 cv::putText(frame,
1476 model.next_action,
1477 cv::Point(x + 10, y0 + 40),
1478 cv::FONT_HERSHEY_SIMPLEX,
1479 0.4,
1480 primary_text,
1481 1);
1482 });
1483
1484 const bool freeze_reason_active = isFreezeReasonActiveForUi(model.freeze_reason);
1485 drawCard(56,
1486 freeze_reason_active ? cv::Scalar(60, 60, 200) : panel_border,
1487 [&](int x, int y0) {
1488 cv::putText(frame,
1489 "Why stopped",
1490 cv::Point(x + 10, y0 + 15),
1491 cv::FONT_HERSHEY_SIMPLEX,
1492 0.34,
1493 freeze_reason_active ? cv::Scalar(60, 60, 200) : muted_text,
1494 1);
1495 cv::putText(frame,
1496 model.freeze_reason,
1497 cv::Point(x + 10, y0 + 40),
1498 uiPlainFontFace(),
1499 1.0,
1500 freeze_reason_active ? severityColor(model.freeze_reason)
1501 : muted_text,
1502 1);
1503 });
1504
1505 drawCard(72, panel_border, [&](int x, int y0) {
1506 cv::putText(frame,
1507 "Forbidden colour",
1508 cv::Point(x + 10, y0 + 15),
1509 cv::FONT_HERSHEY_SIMPLEX,
1510 0.34,
1511 muted_text,
1512 1);
1513 drawPanel(frame,
1514 cv::Point(x + 10, y0 + 24),
1515 cv::Point(x + 44, y0 + 54),
1516 model.forbidden_colour_swatch,
1517 panel_border);
1518 cv::putText(frame,
1519 model.forbidden_colour_label,
1520 cv::Point(x + 54, y0 + 39),
1521 uiPlainFontFace(),
1522 0.95,
1523 primary_text,
1524 1);
1525 cv::putText(frame,
1526 model.forbidden_colour_thresholds,
1527 cv::Point(x + 10, y0 + 66),
1528 cv::FONT_HERSHEY_SIMPLEX,
1529 0.29,
1530 muted_text,
1531 1);
1532 });
1533
1534 int rx = card_margin + 2;
1535 int ry = card_y + 12;
1536 auto drawSectionTitle = [&](const std::string& title) {
1537 cv::putText(frame,
1538 title,
1539 cv::Point(rx, ry),
1540 cv::FONT_HERSHEY_SIMPLEX,
1541 0.34,
1542 muted_text,
1543 1);
1544 ry += 8;
1545 drawLine(frame,
1546 cv::Point(card_margin, ry),
1547 cv::Point(width - card_margin, ry),
1548 panel_border,
1549 1);
1550 ry += 14;
1551 };
1552
1553 drawSectionTitle("Subsystems");
1554 for (const auto& row : model.status_rows) {
1555 cv::putText(frame,
1556 row.label,
1557 cv::Point(rx, ry),
1558 cv::FONT_HERSHEY_SIMPLEX,
1559 0.4,
1560 muted_text,
1561 1);
1562 ry += 14;
1563 cv::putText(frame,
1564 row.value,
1565 cv::Point(rx, ry),
1566 uiPlainFontFace(),
1567 1.0,
1568 row.value_color,
1569 1);
1570 ry += 14;
1571 }
1572
1573 const cv::Scalar border_color =
1574 model.emphasise_danger ? cv::Scalar(60, 60, 200) : model.state_color;
1575 drawRectangle(frame,
1576 cv::Point(2, 2),
1577 cv::Point(width - 3, height - 3),
1578 border_color,
1579 model.emphasise_danger ? 4 : 2);
1580}
1581
1582void drawMetricsDashboard(cv::Mat& frame, const SupervisoryUiModel& model) {
1583 if (frame.empty()) {
1584 return;
1585 }
1586
1587 const int width = frameWidth(frame);
1588 const int header_height = 52;
1589 const int state_bar_height = 32;
1590 const int body_top = header_height + state_bar_height + 8;
1591 const cv::Scalar panel_fill(245, 245, 245);
1592 const cv::Scalar panel_border(220, 220, 220);
1593 const cv::Scalar primary_text(25, 25, 25);
1594 const cv::Scalar muted_text(120, 120, 120);
1595 const cv::Scalar moderate_color(50, 180, 230);
1596 const cv::Scalar white(255, 255, 255);
1597 const cv::Scalar good_color(60, 170, 80);
1598 const cv::Scalar slow_color(60, 60, 200);
1599
1600 frame.setTo(panel_fill);
1601 drawPanel(frame,
1602 cv::Point(0, 0),
1603 cv::Point(width - 1, header_height),
1604 panel_fill,
1605 panel_border);
1606 drawPanel(frame,
1607 cv::Point(0, header_height),
1608 cv::Point(width - 1, header_height + state_bar_height),
1609 panel_fill,
1610 panel_border);
1611
1612 drawPanel(frame,
1613 cv::Point(10, 8),
1614 cv::Point(38, 34),
1615 panel_fill,
1616 model.state_color);
1617 if (!drawArgusLogo(frame, cv::Rect(10, 8, 29, 27))) {
1618 cv::putText(frame,
1619 "A",
1620 cv::Point(20, 27),
1621 cv::FONT_HERSHEY_SIMPLEX,
1622 0.6,
1623 primary_text,
1624 2);
1625 }
1626
1627 cv::putText(frame,
1628 "ARGUS",
1629 cv::Point(50, 23),
1630 cv::FONT_HERSHEY_SIMPLEX,
1631 0.55,
1632 primary_text,
1633 2);
1634 cv::putText(frame,
1635 "Metrics",
1636 cv::Point(50, 38),
1637 cv::FONT_HERSHEY_SIMPLEX,
1638 0.35,
1639 muted_text,
1640 1);
1641 cv::putText(frame,
1642 model.mode_title,
1643 cv::Point(width - 124, 31),
1644 uiPlainFontFace(),
1645 0.9,
1646 primary_text,
1647 1);
1648
1649 cv::putText(frame,
1650 "Focus and safety timing",
1651 cv::Point(12, header_height + 22),
1652 uiPlainFontFace(),
1653 1.0,
1654 model.state_color,
1655 1);
1656 cv::putText(frame,
1657 "Live signal and event timing",
1658 cv::Point(12, header_height + 34),
1659 cv::FONT_HERSHEY_SIMPLEX,
1660 0.32,
1661 muted_text,
1662 1);
1663
1664 const int card_margin = 12;
1665 const int card_width = width - (2 * card_margin);
1666 int y = body_top;
1667
1668 drawPanel(frame,
1669 cv::Point(card_margin, y),
1670 cv::Point(card_margin + card_width, y + 62),
1671 white,
1672 panel_border);
1673 cv::putText(frame,
1674 "Focus",
1675 cv::Point(card_margin + 10, y + 15),
1676 cv::FONT_HERSHEY_SIMPLEX,
1677 0.34,
1678 muted_text,
1679 1);
1680 drawPanel(frame,
1681 cv::Point(card_margin + 10, y + 22),
1682 cv::Point(width - 22, y + 38),
1683 cv::Scalar(235, 235, 235),
1684 panel_border);
1685 const int bar_width = std::max(
1686 0,
1687 static_cast<int>((width - card_margin - 34) *
1688 std::clamp(model.focus_fraction, 0.0, 1.0)));
1689 if (model.show_focus && bar_width > 0) {
1690 drawRectangle(frame,
1691 cv::Point(card_margin + 11, y + 23),
1692 cv::Point(card_margin + 11 + bar_width, y + 37),
1693 model.focus_color,
1694 -1);
1695 }
1696 cv::putText(frame,
1697 model.show_focus ? model.focus_label : "N/A",
1698 cv::Point(card_margin + 10, y + 56),
1699 uiPlainFontFace(),
1700 0.95,
1701 model.show_focus ? model.focus_color : muted_text,
1702 1);
1703 y += 74;
1704
1705 cv::putText(frame,
1706 "Vision processing",
1707 cv::Point(card_margin + 2, y + 8),
1708 cv::FONT_HERSHEY_SIMPLEX,
1709 0.34,
1710 muted_text,
1711 1);
1712 y += 16;
1713 drawLine(frame,
1714 cv::Point(card_margin, y),
1715 cv::Point(width - card_margin, y),
1716 panel_border,
1717 1);
1718 y += 10;
1719 drawPanel(frame,
1720 cv::Point(card_margin, y),
1721 cv::Point(card_margin + card_width, y + 54),
1722 white,
1723 panel_border);
1724 cv::putText(frame,
1725 "Time to process frame: " + std::to_string(model.latency.vision_us) + " us",
1726 cv::Point(card_margin + 10, y + 16),
1727 uiPlainFontFace(),
1728 0.95,
1729 primary_text,
1730 1);
1731 drawSparkline(frame,
1732 cv::Point(card_margin + 10, y + 22),
1733 cv::Point(width - 20, y + 46),
1734 model.vision_latency_history_us,
1735 primary_text);
1736 y += 66;
1737
1738 cv::putText(frame,
1739 "Safety timing",
1740 cv::Point(card_margin + 2, y + 8),
1741 cv::FONT_HERSHEY_SIMPLEX,
1742 0.34,
1743 muted_text,
1744 1);
1745 y += 16;
1746 drawLine(frame,
1747 cv::Point(card_margin, y),
1748 cv::Point(width - card_margin, y),
1749 panel_border,
1750 1);
1751 y += 14;
1752
1753 auto drawLatencyBar = [&](const std::string& label,
1754 const std::optional<long long>& value_ms,
1755 long long good_threshold_ms) {
1756 const long long moderate_threshold_ms = good_threshold_ms * 2;
1757 const int card_height = 66;
1758 const int x1 = card_margin;
1759 const int x2 = card_margin + card_width;
1760
1761 drawPanel(frame,
1762 cv::Point(x1, y),
1763 cv::Point(x2, y + card_height),
1764 white,
1765 panel_border);
1766
1767 const std::string label_with_target =
1768 label + " (target: <= " + std::to_string(good_threshold_ms) + " ms)";
1769 cv::putText(frame,
1770 label_with_target,
1771 cv::Point(x1 + 10, y + 15),
1772 cv::FONT_HERSHEY_SIMPLEX,
1773 0.32,
1774 muted_text,
1775 1);
1776
1777 const int bar_x1 = x1 + 10;
1778 const int bar_x2 = x2 - 10;
1779 const int bar_y1 = y + 22;
1780 const int bar_y2 = y + 40;
1781 drawPanel(frame,
1782 cv::Point(bar_x1, bar_y1),
1783 cv::Point(bar_x2, bar_y2),
1784 cv::Scalar(235, 235, 235),
1785 panel_border);
1786
1787 cv::Scalar status_color = muted_text;
1788 std::string status_text = "No event";
1789 std::string value_text = "N/A";
1790 int fill_width = 0;
1791
1792 if (value_ms.has_value()) {
1793 const long long value = *value_ms;
1794 value_text = std::to_string(value) + " ms";
1795
1796 if (value <= good_threshold_ms) {
1797 status_text = "Good";
1798 status_color = good_color;
1799 } else if (value <= moderate_threshold_ms) {
1800 status_text = "Moderate";
1801 status_color = moderate_color;
1802 } else {
1803 status_text = "Slow";
1804 status_color = slow_color;
1805 }
1806
1807 const double fraction =
1808 std::clamp(static_cast<double>(value) /
1809 static_cast<double>(moderate_threshold_ms),
1810 0.0,
1811 1.0);
1812 fill_width = std::max(
1813 1,
1814 static_cast<int>((bar_x2 - bar_x1 - 2) * fraction));
1815 }
1816
1817 if (fill_width > 0) {
1818 drawRectangle(frame,
1819 cv::Point(bar_x1 + 1, bar_y1 + 1),
1820 cv::Point(bar_x1 + 1 + fill_width, bar_y2 - 1),
1821 status_color,
1822 -1);
1823 }
1824
1825 cv::putText(frame,
1826 value_text + " " + status_text,
1827 cv::Point(x1 + 10, y + 57),
1828 uiPlainFontFace(),
1829 0.95,
1830 status_color,
1831 1);
1832
1833 y += card_height + 8;
1834 };
1835
1836 drawLatencyBar("Time to detect unsafe", model.latency.unsafe_detect_ms, 30);
1837 drawLatencyBar("Time to issue freeze", model.latency.freeze_pipeline_ms, 900);
1838 drawLatencyBar("Time to stop motion", model.latency.total_stop_ms, 8000);
1839
1840 const cv::Scalar border_color =
1841 model.emphasise_danger ? cv::Scalar(60, 60, 200) : model.state_color;
1842 drawRectangle(frame,
1843 cv::Point(2, 2),
1844 cv::Point(width - 3, frameHeight(frame) - 3),
1845 border_color,
1846 model.emphasise_danger ? 4 : 2);
1847}
1848
1849const char* safetyStateToString(SafetyState state) {
1850 switch (state) {
1851 case SafetyState::SAFE:
1852 return "SAFE";
1854 return "TOOL_NOT_DETECTED";
1856 return "OUTSIDE_ALLOWED_ZONE";
1858 return "EXCESSIVE_SPEED";
1860 return "INVALID_ORIENTATION";
1862 return "DEPTH_EXCEEDED";
1863 default:
1864 return "UNKNOWN";
1865 }
1866}
1867
1868FreezeReason mapSafetyToFreezeReason(SafetyState state) {
1869 switch (state) {
1879 case SafetyState::SAFE:
1880 default:
1882 }
1883}
1884
1885const char* freezeReasonToString(FreezeReason reason) {
1886 switch (reason) {
1887 case FreezeReason::NONE:
1888 return "NONE";
1890 return "MARKER_LOST";
1892 return "MARKER_OUT_OF_ROI";
1894 return "VISION_TIMEOUT";
1896 return "POSITION_ERROR";
1898 return "DEPTH_EXCEEDED";
1900 return "WATCHDOG_TIMEOUT";
1902 default:
1903 return "UNKNOWN_FAULT";
1904 }
1905}
1906
1907const char* cameraBackendPreferenceToString(
1909 switch (preference) {
1911 return "auto";
1913 return "opencv";
1915 return "libcamera2opencv";
1916 }
1917 return "unknown";
1918}
1919
1920const char* interlockStateToString(InterlockState state) {
1921 switch (state) {
1923 return "SAFE";
1925 return "FROZEN";
1927 return "FAULT";
1928 default:
1929 return "UNKNOWN";
1930 }
1931}
1932
1933const char* motionControllerStateToString(MotionOutputState state) {
1934 switch (state) {
1936 return "UNINITIALISED";
1938 return "DISABLED";
1940 return "ENABLED";
1942 return "FAULT";
1943 default:
1944 return "UNKNOWN";
1945 }
1946}
1947
1948struct SmokeJointOffsets {
1949 int base{0};
1950 int lower{0};
1951 int upper{0};
1952 int grip{0};
1953};
1954
1955struct SmokeJointSpec {
1956 const char* logical_name;
1957 const char* mearm_name;
1958 const char* visual_check;
1959 std::uint8_t channel;
1960 int min_offset;
1961 int max_offset;
1962};
1963
1964struct SmokeJointRunPlan {
1965 std::array<std::size_t, MotionController::kServoCount> indices{};
1966 std::size_t count{0};
1967};
1968
1969struct JointPulseCalibration {
1970 std::uint16_t neg90_ticks;
1971 std::uint16_t zero_ticks;
1972 std::uint16_t pos90_ticks;
1973};
1974
1975struct JointCalibrationMarks {
1976 bool has_neg90{false};
1977 bool has_zero{false};
1978 bool has_pos90{false};
1979 std::uint16_t neg90_ticks{0};
1980 std::uint16_t zero_ticks{0};
1981 std::uint16_t pos90_ticks{0};
1982};
1983
1984constexpr std::array<SmokeJointSpec, MotionController::kServoCount> kSmokeJointSpecs = {{
1985 {"base", "MeArm BASE", "yaw left/right", kMotionChannelMap.base, kSmokeBaseMinOffset, kSmokeBaseMaxOffset},
1986 {"lower", "MeArm LEFT", "raise/lower", kMotionChannelMap.lower, kSmokeLowerMinOffset, kSmokeLowerMaxOffset},
1987 {"upper", "MeArm RIGHT", "bend/extend", kMotionChannelMap.upper, kSmokeUpperMinOffset, kSmokeUpperMaxOffset},
1988 {"grip", "MeArm CLAW", "open/close", kMotionChannelMap.gripper, kSmokeGripMinOffset, kSmokeGripMaxOffset},
1989}};
1990
1991constexpr std::array<JointPulseCalibration, MotionController::kServoCount>
1992 kJointPulseCalibration = {{
1993 {100, 300, 500},
1994 {100, 300, 500},
1995 {100, 290, 500},
1996 {100, 300, 500},
1997 }};
1998
1999constexpr SmokeJointOffsets kSmokeHomePose{0, 0, 0, 0};
2000constexpr SmokeJointOffsets kSurgeryRetractPose{
2001 0, 35, -30, kSurgeryGripHoldOffset};
2002
2003struct DemoPoseStep {
2004 const char* name;
2005 SmokeJointOffsets offsets;
2006};
2007
2008struct LiveRoutineDefinition {
2009 int number;
2010 const char* name;
2011 const DemoPoseStep* steps;
2012 std::size_t step_count;
2013 bool auto_progress;
2014};
2015
2016constexpr DemoPoseStep kDemoHomeStep{"Home", kSmokeHomePose};
2017constexpr DemoPoseStep kSurgeryRetractStep{"Retract (safe)", kSurgeryRetractPose};
2018
2019constexpr std::array<DemoPoseStep, 11> kLiveSurgeryCutSequence = {{
2020 {"Grip +90 (tool)", {0, 0, 0, kSurgeryGripHoldOffset}},
2021 {"Cut P1 forward", {0, 0, kSurgeryForwardOffset, kSurgeryGripHoldOffset}},
2022 {"Cut P1 down", {0, kSurgeryPassOneDepthOffset, kSurgeryForwardOffset, kSurgeryGripHoldOffset}},
2023 {"Cut P1 backward", {0, kSurgeryPassOneDepthOffset, kSurgeryBackwardOffset, kSurgeryGripHoldOffset}},
2024 {"Cut P2 forward", {0, 0, kSurgeryForwardOffset, kSurgeryGripHoldOffset}},
2025 {"Cut P2 down (deeper)", {0, kSurgeryPassTwoDepthOffset, kSurgeryForwardOffset, kSurgeryGripHoldOffset}},
2026 {"Cut P2 backward", {0, kSurgeryPassTwoDepthOffset, kSurgeryBackwardOffset, kSurgeryGripHoldOffset}},
2027 {"Cut P3 forward", {0, 0, kSurgeryForwardOffset, kSurgeryGripHoldOffset}},
2028 {"Cut P3 down (failure pass)", {0, kSurgeryPassThreeDepthOffset, kSurgeryForwardOffset, kSurgeryGripHoldOffset}},
2029 {"Cut P3 backward", {0, kSurgeryPassThreeDepthOffset, kSurgeryBackwardOffset, kSurgeryGripHoldOffset}},
2030 {"Home", {0, 0, 0, kSurgeryGripHoldOffset}},
2031}};
2032
2033constexpr std::array<DemoPoseStep, 5> kLiveBaseScanSequence = {{
2034 {"Home", {0, 0, 0, 0}},
2035 {"Base +45", {kDemoBaseStep, 0, 0, 0}},
2036 {"Home", {0, 0, 0, 0}},
2037 {"Base -45", {-kDemoBaseStep, 0, 0, 0}},
2038 {"Home", {0, 0, 0, 0}},
2039}};
2040
2041constexpr std::array<DemoPoseStep, 5> kLiveGripPulseSequence = {{
2042 {"Home", {0, 0, 0, 0}},
2043 {"Grip +45", {0, 0, 0, kDemoGripStep}},
2044 {"Home", {0, 0, 0, 0}},
2045 {"Grip -45", {0, 0, 0, -kDemoGripStep}},
2046 {"Home", {0, 0, 0, 0}},
2047}};
2048
2049LiveRoutineDefinition getLiveRoutineDefinition(std::size_t index) {
2050 switch (index) {
2051 case 1:
2052 return {1,
2053 "Surgery cut",
2054 kLiveSurgeryCutSequence.data(),
2055 kLiveSurgeryCutSequence.size(),
2056 true};
2057 case 2:
2058 return {2,
2059 "Base scan",
2060 kLiveBaseScanSequence.data(),
2061 kLiveBaseScanSequence.size(),
2062 true};
2063 case 3:
2064 return {3,
2065 "Grip pulse",
2066 kLiveGripPulseSequence.data(),
2067 kLiveGripPulseSequence.size(),
2068 true};
2069 case 0:
2070 default:
2071 return {0, "Manual", nullptr, 0, false};
2072 }
2073}
2074
2075bool liveRoutineIndexFromKey(int key, std::size_t& index) {
2076 switch (key) {
2077 case '0':
2078 index = 0;
2079 return true;
2080 case '1':
2081 index = 1;
2082 return true;
2083 case '2':
2084 index = 2;
2085 return true;
2086 case '3':
2087 index = 3;
2088 return true;
2089 default:
2090 return false;
2091 }
2092}
2093
2094std::string toLowerCopy(std::string text);
2095
2096const char* smokeJointSelectionToString(AppController::SmokeJoint joint) {
2097 switch (joint) {
2099 return "all";
2101 return "base";
2103 return "lower";
2105 return "upper";
2107 return "grip";
2108 default:
2109 return "unknown";
2110 }
2111}
2112
2113const char* smokeJointIndexToString(std::size_t index) {
2114 switch (index) {
2115 case 0:
2116 return "base";
2117 case 1:
2118 return "lower";
2119 case 2:
2120 return "upper";
2121 case 3:
2122 return "grip";
2123 default:
2124 return "unknown";
2125 }
2126}
2127
2128bool smokeJointIndexFromName(const std::string& name, std::size_t& index) {
2129 const std::string lower = toLowerCopy(name);
2130 if (lower == "base") {
2131 index = 0;
2132 return true;
2133 }
2134 if (lower == "lower") {
2135 index = 1;
2136 return true;
2137 }
2138 if (lower == "upper") {
2139 index = 2;
2140 return true;
2141 }
2142 if (lower == "grip" || lower == "gripper") {
2143 index = 3;
2144 return true;
2145 }
2146 return false;
2147}
2148
2149SmokeJointRunPlan makeSmokeJointRunPlan(AppController::SmokeJoint joint) {
2150 SmokeJointRunPlan plan{};
2151
2152 switch (joint) {
2154 plan.indices[0] = 0;
2155 plan.count = 1;
2156 break;
2158 plan.indices[0] = 1;
2159 plan.count = 1;
2160 break;
2162 plan.indices[0] = 2;
2163 plan.count = 1;
2164 break;
2166 plan.indices[0] = 3;
2167 plan.count = 1;
2168 break;
2170 default:
2171 plan.indices[0] = 0;
2172 plan.indices[1] = 1;
2173 plan.indices[2] = 2;
2174 plan.indices[3] = 3;
2175 plan.count = 4;
2176 break;
2177 }
2178
2179 return plan;
2180}
2181
2182int clampOffsetValue(int value, int min_value, int max_value, bool& clamped) {
2183 const int bounded = std::clamp(value, min_value, max_value);
2184 if (bounded != value) {
2185 clamped = true;
2186 }
2187 return bounded;
2188}
2189
2190SmokeJointOffsets clampOffsetsToWindow(const SmokeJointOffsets& requested,
2191 int base_min,
2192 int base_max,
2193 int lower_min,
2194 int lower_max,
2195 int upper_min,
2196 int upper_max,
2197 int grip_min,
2198 int grip_max,
2199 bool& clamped) {
2200 SmokeJointOffsets bounded = requested;
2201 bounded.base = clampOffsetValue(requested.base, base_min, base_max, clamped);
2202 bounded.lower = clampOffsetValue(requested.lower, lower_min, lower_max, clamped);
2203 bounded.upper = clampOffsetValue(requested.upper, upper_min, upper_max, clamped);
2204 bounded.grip = clampOffsetValue(requested.grip, grip_min, grip_max, clamped);
2205 return bounded;
2206}
2207
2208SmokeJointOffsets clampSmokeOffsets(const SmokeJointOffsets& requested,
2209 bool& clamped) {
2210 return clampOffsetsToWindow(requested,
2211 kSmokeBaseMinOffset,
2212 kSmokeBaseMaxOffset,
2213 kSmokeLowerMinOffset,
2214 kSmokeLowerMaxOffset,
2215 kSmokeUpperMinOffset,
2216 kSmokeUpperMaxOffset,
2217 kSmokeGripMinOffset,
2218 kSmokeGripMaxOffset,
2219 clamped);
2220}
2221
2222SmokeJointOffsets clampDemoOffsets(const SmokeJointOffsets& requested,
2223 bool& clamped) {
2224 return clampOffsetsToWindow(requested,
2225 kDemoBaseMinOffset,
2226 kDemoBaseMaxOffset,
2227 kDemoLowerMinOffset,
2228 kDemoLowerMaxOffset,
2229 kDemoUpperMinOffset,
2230 kDemoUpperMaxOffset,
2231 kDemoGripMinOffset,
2232 kDemoGripMaxOffset,
2233 clamped);
2234}
2235
2236std::uint16_t logicalAngleToPulseTicks(std::size_t joint_index,
2237 int angle_degrees,
2238 bool& clamped) {
2239 const JointPulseCalibration& calibration =
2240 kJointPulseCalibration.at(joint_index);
2241 const int zero_ticks = static_cast<int>(calibration.zero_ticks);
2242 const int delta_ticks = angle_degrees >= 0
2243 ? static_cast<int>(calibration.pos90_ticks) -
2244 zero_ticks
2245 : zero_ticks -
2246 static_cast<int>(calibration.neg90_ticks);
2247 const int raw = zero_ticks +
2248 static_cast<int>(std::lround(
2249 static_cast<double>(delta_ticks) *
2250 (static_cast<double>(angle_degrees) / 90.0)));
2251 const int bounded = std::clamp(
2252 raw, 0, static_cast<int>(MotionController::kMaxPulseTicks));
2253 if (bounded != raw) {
2254 clamped = true;
2255 }
2256 return static_cast<std::uint16_t>(bounded);
2257}
2258
2259std::uint16_t clampPulseTicks(int raw_ticks, bool& clamped) {
2260 const int bounded = std::clamp(raw_ticks,
2261 0,
2262 static_cast<int>(MotionController::kMaxPulseTicks));
2263 if (bounded != raw_ticks) {
2264 clamped = true;
2265 }
2266 return static_cast<std::uint16_t>(bounded);
2267}
2268
2269MeArmJointTargets makeSmokeTargets(const SmokeJointOffsets& requested,
2270 bool& clamped) {
2271 const SmokeJointOffsets bounded = clampSmokeOffsets(requested, clamped);
2272 return {
2273 logicalAngleToPulseTicks(0, bounded.base, clamped),
2274 logicalAngleToPulseTicks(1, bounded.lower, clamped),
2275 logicalAngleToPulseTicks(2, bounded.upper, clamped),
2276 logicalAngleToPulseTicks(3, bounded.grip, clamped)};
2277}
2278
2279MeArmJointTargets makeDemoTargets(const SmokeJointOffsets& requested,
2280 bool& clamped) {
2281 const SmokeJointOffsets bounded = clampDemoOffsets(requested, clamped);
2282 return {
2283 logicalAngleToPulseTicks(0, bounded.base, clamped),
2284 logicalAngleToPulseTicks(1, bounded.lower, clamped),
2285 logicalAngleToPulseTicks(2, bounded.upper, clamped),
2286 logicalAngleToPulseTicks(3, bounded.grip, clamped)};
2287}
2288
2289std::string formatOffsets(const SmokeJointOffsets& offsets) {
2290 std::ostringstream oss;
2291 oss << "{base=" << offsets.base
2292 << ", lower=" << offsets.lower
2293 << ", upper=" << offsets.upper
2294 << ", grip=" << offsets.grip
2295 << "}";
2296 return oss.str();
2297}
2298
2299std::string formatTargets(const MeArmJointTargets& targets) {
2300 std::ostringstream oss;
2301 oss << "{base=" << targets.base_ticks
2302 << ", lower=" << targets.lower_ticks
2303 << ", upper=" << targets.upper_ticks
2304 << ", gripper=" << targets.gripper_ticks
2305 << "}";
2306 return oss.str();
2307}
2308
2309std::uint16_t pulseForJoint(const MeArmJointTargets& targets, std::size_t index) {
2310 switch (index) {
2311 case 0:
2312 return targets.base_ticks;
2313 case 1:
2314 return targets.lower_ticks;
2315 case 2:
2316 return targets.upper_ticks;
2317 case 3:
2318 return targets.gripper_ticks;
2319 default:
2320 return 0;
2321 }
2322}
2323
2324void setPulseForJoint(MeArmJointTargets& targets,
2325 std::size_t index,
2326 std::uint16_t ticks) {
2327 switch (index) {
2328 case 0:
2329 targets.base_ticks = ticks;
2330 break;
2331 case 1:
2332 targets.lower_ticks = ticks;
2333 break;
2334 case 2:
2335 targets.upper_ticks = ticks;
2336 break;
2337 case 3:
2338 targets.gripper_ticks = ticks;
2339 break;
2340 default:
2341 break;
2342 }
2343}
2344
2345bool parseCalibrationPoint(const std::string& text,
2346 const char*& label,
2347 int& nominal_degrees) {
2348 const std::string lower = toLowerCopy(text);
2349 if (lower == "-90" || lower == "neg90" || lower == "minus90") {
2350 label = "-90";
2351 nominal_degrees = -90;
2352 return true;
2353 }
2354 if (lower == "0" || lower == "zero" || lower == "center" || lower == "centre") {
2355 label = "0";
2356 nominal_degrees = 0;
2357 return true;
2358 }
2359 if (lower == "+90" || lower == "90" || lower == "pos90" || lower == "plus90") {
2360 label = "+90";
2361 nominal_degrees = 90;
2362 return true;
2363 }
2364 return false;
2365}
2366
2367std::string formatCalibrationMarks(std::size_t joint_index,
2368 const JointCalibrationMarks& marks) {
2369 std::ostringstream oss;
2370 oss << smokeJointIndexToString(joint_index) << ": "
2371 << "-90=";
2372 if (marks.has_neg90) {
2373 oss << marks.neg90_ticks;
2374 } else {
2375 oss << "unset";
2376 }
2377 oss << " 0=";
2378 if (marks.has_zero) {
2379 oss << marks.zero_ticks;
2380 } else {
2381 oss << "unset";
2382 }
2383 oss << " +90=";
2384 if (marks.has_pos90) {
2385 oss << marks.pos90_ticks;
2386 } else {
2387 oss << "unset";
2388 }
2389 return oss.str();
2390}
2391
2392std::string buildCalibrationSummary(
2393 const std::array<JointCalibrationMarks, MotionController::kServoCount>& marks) {
2394 std::ostringstream oss;
2395 oss << "# ARGUS servo calibration summary\n"
2396 << "# This report is informational only; values are not applied automatically.\n"
2397 << "# Format: joint -90=<ticks> 0=<ticks> +90=<ticks>\n";
2398 for (std::size_t i = 0; i < marks.size(); ++i) {
2399 oss << formatCalibrationMarks(i, marks[i]) << "\n";
2400 }
2401 return oss.str();
2402}
2403
2404std::string toLowerCopy(std::string text) {
2405 std::transform(text.begin(),
2406 text.end(),
2407 text.begin(),
2408 [](unsigned char ch) {
2409 return static_cast<char>(std::tolower(ch));
2410 });
2411 return text;
2412}
2413
2414} // namespace
2415
2416AppController::AppController() noexcept = default;
2417
2418AppController::~AppController() noexcept = default;
2419
2420int AppController::runGuardianScenarioDemo() {
2421 GuardianStateMachine guardian(2, 3);
2422 guardian.setOnFreezeCallback([]() {
2423 std::cout << ">>> ROBOTIC ARM: Emergency stop activated! <<<" << std::endl;
2424 });
2425 guardian.setOnClearFreezeCallback([]() {
2426 std::cout << ">>> ROBOTIC ARM: Motion resumed, system operational <<<"
2427 << std::endl;
2428 });
2430 std::cout << ">>> STATE CHANGE NOTIFICATION: System transitioned <<<"
2431 << std::endl;
2432 });
2433
2434 std::cout << "\n========== GUARDIAN STATE MACHINE TEST ==========\n"
2435 << std::endl;
2436
2437 std::cout << "Scenario 1: Normal Operation" << std::endl;
2440 guardian.printStatus();
2441
2442 std::cout << "Scenario 2: Single Bad Frame" << std::endl;
2445 guardian.printStatus();
2446
2447 std::cout << "Scenario 3: freezeCount Consecutive Bad Frames (Freeze)"
2448 << std::endl;
2451 guardian.printStatus();
2452
2453 std::cout << "Scenario 4: Frames During Frozen State" << std::endl;
2456 guardian.printStatus();
2457
2458 std::cout << "Scenario 5: Operator Acknowledgment/Reset" << std::endl;
2459 guardian.operatorAcknowledge();
2460 guardian.printStatus();
2461
2462 std::cout << "Scenario 6: Bad Frame During Reset" << std::endl;
2465 guardian.printStatus();
2466
2467 std::cout << "Scenario 7: recoverCount Consecutive Good Frames (Clear Freeze)"
2468 << std::endl;
2472 guardian.printStatus();
2473
2474 std::cout << "========== TEST COMPLETE ==========\n" << std::endl;
2475 return 0;
2476}
2477
2479 PhysicalButtonModule button_module;
2480 std::cout << "[BUTTON_TEST] physical button test\n"
2481 << "[BUTTON_TEST] press Ctrl+C to stop\n"
2482 << "[BUTTON_TEST] module: "
2483 << (button_module.available() ? "configured" : "disabled");
2484 if (button_module.available()) {
2485 std::cout << " (" << button_module.statusString() << ")";
2486 } else {
2487 std::cout << " (" << button_module.lastErrorString() << ")";
2488 }
2489 std::cout << std::endl;
2490
2491 if (!button_module.available()) {
2492 return 1;
2493 }
2494
2495 while (true) {
2496 PhysicalButtonEvent event;
2497 if (button_module.waitForEvent(event, std::chrono::milliseconds(250))) {
2498 std::cout << "[BUTTON_TEST] event="
2499 << PhysicalButtonModule::eventToString(event) << std::endl;
2500 }
2501 }
2502}
2503
2505 std::cout
2506 << "[CAL] servo calibration console\n"
2507 << "[CAL] raw PCA9685 pulse control in ticks\n"
2508 << "[CAL] commands: base <ticks>, lower <ticks>, upper <ticks>, grip <ticks>\n"
2509 << "[CAL] nudges: base +5, base -5, grip +10, ...\n"
2510 << "[CAL] mark: mark <joint> <-90|0|+90>\n"
2511 << "[CAL] extras: home, status, summary, write [path], help\n"
2512 << "[CAL] quit: Ctrl+C or type 'exit'\n";
2513
2514 if (!motion_controller_.initialise(kDefaultI2cDevicePath,
2515 kDefaultPca9685Address,
2516 kDefaultPwmFrequencyHz,
2517 kMotionChannelMap)) {
2518 std::cerr << "[CAL] init failed: "
2519 << motion_controller_.lastErrorString() << std::endl;
2520 return 1;
2521 }
2522
2523 auto fail = [&](const std::string& message) {
2524 std::cerr << "[CAL] " << message << std::endl;
2525 motion_controller_.shutdown();
2526 return 1;
2527 };
2528
2529 bool home_clamped = false;
2530 MeArmJointTargets current_targets =
2531 makeSmokeTargets(kSmokeHomePose, home_clamped);
2532
2533 if (!motion_controller_.setTargets(current_targets)) {
2534 return fail(std::string("failed to stage home pulse set: ") +
2535 motion_controller_.lastErrorString());
2536 }
2537
2538 if (!motion_controller_.enable()) {
2539 return fail(std::string("failed to enable motion: ") +
2540 motion_controller_.lastErrorString());
2541 }
2542
2543 std::array<JointCalibrationMarks, MotionController::kServoCount> marks{};
2544 std::cout << "[CAL] home " << formatTargets(current_targets) << std::endl;
2545
2546 const auto previous_sigint = std::signal(SIGINT, handleInteractiveServoSignal);
2547 const auto previous_sigterm = std::signal(SIGTERM, handleInteractiveServoSignal);
2548 g_interactive_servo_stop_requested = 0;
2549
2550 auto restoreSignals = [&]() {
2551 std::signal(SIGINT, previous_sigint);
2552 std::signal(SIGTERM, previous_sigterm);
2553 };
2554
2555 auto applyTargets = [&](const MeArmJointTargets& requested,
2556 const std::string& label) -> bool {
2557 MeArmJointTargets bounded = requested;
2558 bool clamped = false;
2559 bounded.base_ticks = clampPulseTicks(static_cast<int>(requested.base_ticks), clamped);
2560 bounded.lower_ticks = clampPulseTicks(static_cast<int>(requested.lower_ticks), clamped);
2561 bounded.upper_ticks = clampPulseTicks(static_cast<int>(requested.upper_ticks), clamped);
2562 bounded.gripper_ticks = clampPulseTicks(static_cast<int>(requested.gripper_ticks), clamped);
2563
2564 if (!motion_controller_.setTargets(bounded)) {
2565 (void)fail(std::string("failed to set ") + label + ": " +
2566 motion_controller_.lastErrorString());
2567 return false;
2568 }
2569
2570 current_targets = bounded;
2571 std::cout << "[CAL] " << label << " " << formatTargets(current_targets);
2572 if (clamped) {
2573 std::cout << " [clamped]";
2574 }
2575 std::cout << std::endl;
2576 return true;
2577 };
2578
2579 auto writeSummaryToFile = [&](const std::string& path) -> bool {
2580 std::ofstream out(path);
2581 if (!out.is_open()) {
2582 std::cerr << "[CAL] write failed: " << path << std::endl;
2583 return false;
2584 }
2585 out << buildCalibrationSummary(marks);
2586 out.close();
2587 std::cout << "[CAL] wrote " << path << std::endl;
2588 return true;
2589 };
2590
2591 while (!g_interactive_servo_stop_requested) {
2592 std::cout << "cal> " << std::flush;
2593
2594 std::string line;
2595 if (!std::getline(std::cin, line)) {
2596 if (g_interactive_servo_stop_requested || std::cin.eof()) {
2597 break;
2598 }
2599 restoreSignals();
2600 return fail("stdin read failed");
2601 }
2602
2603 std::istringstream iss(line);
2604 std::string command;
2605 if (!(iss >> command)) {
2606 continue;
2607 }
2608
2609 command = toLowerCopy(command);
2610 if (command == "exit" || command == "quit") {
2611 break;
2612 }
2613
2614 if (command == "help") {
2615 std::cout
2616 << "[CAL] commands: <joint> <ticks>, <joint> +/-<delta>, mark <joint> <-90|0|+90>, home, status, summary, write [path], exit"
2617 << std::endl;
2618 continue;
2619 }
2620
2621 if (command == "home") {
2622 bool clamped = false;
2623 const MeArmJointTargets home_targets =
2624 makeSmokeTargets(kSmokeHomePose, clamped);
2625 if (!applyTargets(home_targets, "home")) {
2626 restoreSignals();
2627 return 1;
2628 }
2629 continue;
2630 }
2631
2632 if (command == "status") {
2633 std::cout << "[CAL] " << formatTargets(current_targets) << std::endl;
2634 for (std::size_t i = 0; i < marks.size(); ++i) {
2635 std::cout << "[CAL] " << formatCalibrationMarks(i, marks[i])
2636 << std::endl;
2637 }
2638 continue;
2639 }
2640
2641 if (command == "summary") {
2642 std::cout << buildCalibrationSummary(marks);
2643 continue;
2644 }
2645
2646 if (command == "write") {
2647 std::string path;
2648 if (!(iss >> path)) {
2649 path = "config/servo_calibration_latest.txt";
2650 }
2651 (void)writeSummaryToFile(path);
2652 continue;
2653 }
2654
2655 if (command == "mark") {
2656 std::string joint_name;
2657 std::string point_name;
2658 if (!(iss >> joint_name >> point_name)) {
2659 std::cout << "[CAL] expected: mark <joint> <-90|0|+90>" << std::endl;
2660 continue;
2661 }
2662
2663 std::size_t joint_index = 0;
2664 if (!smokeJointIndexFromName(joint_name, joint_index)) {
2665 std::cout << "[CAL] unknown joint: " << joint_name << std::endl;
2666 continue;
2667 }
2668
2669 const char* point_label = nullptr;
2670 int nominal_degrees = 0;
2671 if (!parseCalibrationPoint(point_name, point_label, nominal_degrees)) {
2672 std::cout << "[CAL] unknown calibration point: " << point_name
2673 << std::endl;
2674 continue;
2675 }
2676
2677 JointCalibrationMarks& joint_marks = marks[joint_index];
2678 const std::uint16_t current_ticks = pulseForJoint(current_targets, joint_index);
2679 if (nominal_degrees < 0) {
2680 joint_marks.has_neg90 = true;
2681 joint_marks.neg90_ticks = current_ticks;
2682 } else if (nominal_degrees > 0) {
2683 joint_marks.has_pos90 = true;
2684 joint_marks.pos90_ticks = current_ticks;
2685 } else {
2686 joint_marks.has_zero = true;
2687 joint_marks.zero_ticks = current_ticks;
2688 }
2689
2690 std::cout << "[CAL] marked " << smokeJointIndexToString(joint_index)
2691 << " " << point_label << "=" << current_ticks << std::endl;
2692 continue;
2693 }
2694
2695 std::size_t joint_index = 0;
2696 if (!smokeJointIndexFromName(command, joint_index)) {
2697 std::cout << "[CAL] unknown command or joint: " << command << std::endl;
2698 continue;
2699 }
2700
2701 std::string value_text;
2702 if (!(iss >> value_text)) {
2703 std::cout << "[CAL] expected: <joint> <ticks>" << std::endl;
2704 continue;
2705 }
2706
2707 int raw_value = 0;
2708 try {
2709 std::size_t parsed = 0;
2710 raw_value = std::stoi(value_text, &parsed);
2711 if (parsed != value_text.size()) {
2712 throw std::invalid_argument("trailing characters");
2713 }
2714 } catch (const std::exception&) {
2715 std::cout << "[CAL] invalid tick value: " << value_text << std::endl;
2716 continue;
2717 }
2718
2719 MeArmJointTargets requested = current_targets;
2720 const int requested_ticks =
2721 static_cast<int>(pulseForJoint(current_targets, joint_index)) + 0;
2722 bool value_clamped = false;
2723 if (!value_text.empty() && (value_text[0] == '+' || value_text[0] == '-')) {
2724 setPulseForJoint(requested,
2725 joint_index,
2726 clampPulseTicks(requested_ticks + raw_value,
2727 value_clamped));
2728 } else {
2729 setPulseForJoint(requested,
2730 joint_index,
2731 clampPulseTicks(raw_value, value_clamped));
2732 }
2733
2734 std::ostringstream label;
2735 label << smokeJointIndexToString(joint_index) << "="
2736 << pulseForJoint(requested, joint_index);
2737 if (!applyTargets(requested, label.str())) {
2738 restoreSignals();
2739 return 1;
2740 }
2741 }
2742
2743 restoreSignals();
2744 std::cout << buildCalibrationSummary(marks);
2745 motion_controller_.shutdown();
2746 std::cout << "[CAL] done" << std::endl;
2747 return 0;
2748}
2749
2751 std::cout
2752 << "[SERVO] interactive servo console\n"
2753 << "[SERVO] commands: base <deg>, lower <deg>, upper <deg>, grip <deg>\n"
2754 << "[SERVO] extras: home, status, help\n"
2755 << "[SERVO] range: -90..+90 logical degrees\n"
2756 << "[SERVO] quit: Ctrl+C or type 'exit'\n";
2757
2758 if (!motion_controller_.initialise(kDefaultI2cDevicePath,
2759 kDefaultPca9685Address,
2760 kDefaultPwmFrequencyHz,
2761 kMotionChannelMap)) {
2762 std::cerr << "[SERVO] init failed: "
2763 << motion_controller_.lastErrorString() << std::endl;
2764 return 1;
2765 }
2766
2767 auto fail = [&](const std::string& message) {
2768 std::cerr << "[SERVO] " << message << std::endl;
2769 motion_controller_.shutdown();
2770 return 1;
2771 };
2772
2773 SmokeJointOffsets current_offsets = kSmokeHomePose;
2774 bool initial_clamped = false;
2775 const MeArmJointTargets home_targets =
2776 makeSmokeTargets(current_offsets, initial_clamped);
2777
2778 if (!motion_controller_.setTargets(home_targets)) {
2779 return fail(std::string("failed to stage home pose: ") +
2780 motion_controller_.lastErrorString());
2781 }
2782
2783 if (!motion_controller_.enable()) {
2784 return fail(std::string("failed to enable motion: ") +
2785 motion_controller_.lastErrorString());
2786 }
2787
2788 std::cout << "[SERVO] home " << formatOffsets(current_offsets) << std::endl;
2789
2790 const auto previous_sigint = std::signal(SIGINT, handleInteractiveServoSignal);
2791 const auto previous_sigterm = std::signal(SIGTERM, handleInteractiveServoSignal);
2792 g_interactive_servo_stop_requested = 0;
2793
2794 auto restoreSignals = [&]() {
2795 std::signal(SIGINT, previous_sigint);
2796 std::signal(SIGTERM, previous_sigterm);
2797 };
2798
2799 auto applyOffsets = [&](const SmokeJointOffsets& requested,
2800 const std::string& label) -> bool {
2801 bool clamped = false;
2802 const SmokeJointOffsets bounded = clampSmokeOffsets(requested, clamped);
2803 const MeArmJointTargets targets = makeSmokeTargets(bounded, clamped);
2804
2805 if (!motion_controller_.setTargets(targets)) {
2806 (void)fail(std::string("failed to set ") + label + ": " +
2807 motion_controller_.lastErrorString());
2808 return false;
2809 }
2810
2811 current_offsets = bounded;
2812 std::cout << "[SERVO] " << label << " " << formatOffsets(current_offsets);
2813 if (clamped) {
2814 std::cout << " [clamped]";
2815 }
2816 std::cout << std::endl;
2817 return true;
2818 };
2819
2820 while (!g_interactive_servo_stop_requested) {
2821 std::cout << "servo> " << std::flush;
2822
2823 std::string line;
2824 if (!std::getline(std::cin, line)) {
2825 if (g_interactive_servo_stop_requested) {
2826 break;
2827 }
2828 if (std::cin.eof()) {
2829 break;
2830 }
2831 restoreSignals();
2832 return fail("stdin read failed");
2833 }
2834
2835 std::istringstream iss(line);
2836 std::string command;
2837 if (!(iss >> command)) {
2838 continue;
2839 }
2840
2841 command = toLowerCopy(command);
2842 if (command == "exit" || command == "quit") {
2843 break;
2844 }
2845
2846 if (command == "help") {
2847 std::cout
2848 << "[SERVO] commands: base <deg>, lower <deg>, upper <deg>, grip <deg>, home, status, exit"
2849 << std::endl;
2850 continue;
2851 }
2852
2853 if (command == "status") {
2854 std::cout << "[SERVO] " << formatOffsets(current_offsets)
2855 << std::endl;
2856 continue;
2857 }
2858
2859 if (command == "home") {
2860 if (!applyOffsets(kSmokeHomePose, "home")) {
2861 restoreSignals();
2862 return 1;
2863 }
2864 continue;
2865 }
2866
2867 int angle = 0;
2868 if (!(iss >> angle)) {
2869 std::cout << "[SERVO] expected: <joint> <angle>" << std::endl;
2870 continue;
2871 }
2872
2873 SmokeJointOffsets requested = current_offsets;
2874 if (command == "base") {
2875 requested.base = angle;
2876 } else if (command == "lower") {
2877 requested.lower = angle;
2878 } else if (command == "upper") {
2879 requested.upper = angle;
2880 } else if (command == "grip" || command == "gripper") {
2881 requested.grip = angle;
2882 } else {
2883 std::cout << "[SERVO] unknown joint: " << command << std::endl;
2884 continue;
2885 }
2886
2887 std::ostringstream label;
2888 label << command << "=" << angle;
2889 if (!applyOffsets(requested, label.str())) {
2890 restoreSignals();
2891 return 1;
2892 }
2893 }
2894
2895 restoreSignals();
2896 motion_controller_.shutdown();
2897 std::cout << "[SERVO] done" << std::endl;
2898 return 0;
2899}
2900
2902 std::cout << "[HOME] setting all joints to 0" << std::endl;
2903
2904 if (!motion_controller_.initialise(kDefaultI2cDevicePath,
2905 kDefaultPca9685Address,
2906 kDefaultPwmFrequencyHz,
2907 kMotionChannelMap)) {
2908 std::cerr << "[HOME] init failed: "
2909 << motion_controller_.lastErrorString() << std::endl;
2910 return 1;
2911 }
2912
2913 auto fail = [&](const std::string& message) {
2914 std::cerr << "[HOME] " << message << std::endl;
2915 motion_controller_.shutdown();
2916 return 1;
2917 };
2918
2919 bool clamped = false;
2920 const MeArmJointTargets home_targets =
2921 makeSmokeTargets(kSmokeHomePose, clamped);
2922
2923 if (!motion_controller_.setTargets(home_targets)) {
2924 return fail(std::string("failed to stage home pose: ") +
2925 motion_controller_.lastErrorString());
2926 }
2927
2928 if (!motion_controller_.enable()) {
2929 return fail(std::string("failed to enable motion: ") +
2930 motion_controller_.lastErrorString());
2931 }
2932
2933 std::cout << "[HOME] pose=HOME wait=2s";
2934 if (clamped) {
2935 std::cout << " [clamped]";
2936 }
2937 std::cout << std::endl;
2938
2939 std::string timer_error;
2940 if (!waitForCppTimerDelay(kMotionHomeSettleDwell, timer_error)) {
2941 return fail(std::string("home dwell timer failed: ") + timer_error);
2942 }
2943
2944 motion_controller_.shutdown();
2945 std::cout << "[HOME] done" << std::endl;
2946 return 0;
2947}
2948
2950 std::cout
2951 << "[SMOKE] joint=" << smokeJointSelectionToString(options.joint)
2952 << " 0 -> -90 -> +90 -> 0"
2953 << " wait=3s\n";
2954
2955 MotionChannelMap channel_map{};
2956 channel_map.base = kBaseServoChannel;
2957 channel_map.lower = kLowerServoChannel;
2958 channel_map.upper = kUpperServoChannel;
2959 channel_map.gripper = kGripServoChannel;
2960
2961 if (!motion_controller_.initialise(kDefaultI2cDevicePath,
2962 kDefaultPca9685Address,
2963 kDefaultPwmFrequencyHz,
2964 channel_map)) {
2965 std::cerr << "[SMOKE] init failed: "
2966 << motion_controller_.lastErrorString() << std::endl;
2967 return 1;
2968 }
2969
2970 auto fail = [&](const std::string& message) {
2971 std::cerr << "[SMOKE] " << message << std::endl;
2972 motion_controller_.shutdown();
2973 return 1;
2974 };
2975
2976 auto makeJointOffsets = [](std::size_t joint_index, int joint_offset) {
2977 SmokeJointOffsets offsets{};
2978 switch (joint_index) {
2979 case 0:
2980 offsets.base = joint_offset;
2981 break;
2982 case 1:
2983 offsets.lower = joint_offset;
2984 break;
2985 case 2:
2986 offsets.upper = joint_offset;
2987 break;
2988 case 3:
2989 offsets.grip = joint_offset;
2990 break;
2991 default:
2992 break;
2993 }
2994 return offsets;
2995 };
2996
2997 auto stagePose = [&](const std::string& label,
2998 const SmokeJointOffsets& requested) {
2999 bool clamped = false;
3000 const SmokeJointOffsets bounded = clampSmokeOffsets(requested, clamped);
3001 const MeArmJointTargets targets = makeSmokeTargets(bounded, clamped);
3002
3003 if (!motion_controller_.setTargets(targets)) {
3004 return fail(std::string("failed to stage pose ") + label + ": " +
3005 motion_controller_.lastErrorString());
3006 }
3007
3008 std::cout << "[SMOKE] " << label;
3009 if (clamped) {
3010 std::cout << " [clamped]";
3011 }
3012 std::cout << std::endl;
3013 std::cout << "[SMOKE] wait 3s" << std::endl;
3014
3015 std::string timer_error;
3016 if (!waitForCppTimerDelay(kSmokeStepDwell, timer_error)) {
3017 return fail(std::string("smoke dwell timer failed: ") + timer_error);
3018 }
3019 return 0;
3020 };
3021
3022 bool home_clamped = false;
3023 const MeArmJointTargets home_targets =
3024 makeSmokeTargets(kSmokeHomePose, home_clamped);
3025
3026 if (!motion_controller_.setTargets(home_targets)) {
3027 return fail(std::string("failed to stage home pose: ") +
3028 motion_controller_.lastErrorString());
3029 }
3030
3031 if (!motion_controller_.enable()) {
3032 return fail(std::string("failed to enable motion: ") +
3033 motion_controller_.lastErrorString());
3034 }
3035
3036 std::cout << "[SMOKE] all -> 0" << std::endl;
3037 std::cout << "[SMOKE] wait 3s" << std::endl;
3038 {
3039 std::string timer_error;
3040 if (!waitForCppTimerDelay(kSmokeStepDwell, timer_error)) {
3041 return fail(std::string("initial smoke dwell timer failed: ") +
3042 timer_error);
3043 }
3044 }
3045
3046 const SmokeJointRunPlan plan = makeSmokeJointRunPlan(options.joint);
3047 if (plan.count == 0) {
3048 return fail("no smoke-test joint selected");
3049 }
3050
3051 auto runJointSweep = [&](std::size_t index) {
3052 const SmokeJointSpec& spec = kSmokeJointSpecs[index];
3053 std::cout << "[SMOKE] " << spec.logical_name << std::endl;
3054
3055 const std::string home_label = std::string(spec.logical_name) + " -> 0";
3056 const std::string neg_label = std::string(spec.logical_name) + " -> -90";
3057 const std::string pos_label = std::string(spec.logical_name) + " -> +90";
3058
3059 if (stagePose(home_label, makeJointOffsets(index, 0)) != 0) {
3060 return 1;
3061 }
3062 if (stagePose(neg_label, makeJointOffsets(index, kSmokeNegativeStep)) != 0) {
3063 return 1;
3064 }
3065 if (stagePose(pos_label, makeJointOffsets(index, kSmokePositiveStep)) != 0) {
3066 return 1;
3067 }
3068 if (stagePose(home_label, makeJointOffsets(index, 0)) != 0) {
3069 return 1;
3070 }
3071
3072 return 0;
3073 };
3074
3075 for (std::size_t i = 0; i < plan.count; ++i) {
3076 if (runJointSweep(plan.indices[i]) != 0) {
3077 motion_controller_.shutdown();
3078 return 1;
3079 }
3080 }
3081
3082 motion_controller_.shutdown();
3083 std::cout << "[SMOKE] done" << std::endl;
3084 return 0;
3085}
3086
3088 constexpr int kValidationFrameTarget = 60;
3089 constexpr int kMaxConsecutiveFailures = 10;
3090
3091 std::cout << "[CAMERA_CHECK] camera=" << options.camera_index
3092 << " backend_request="
3093 << cameraBackendPreferenceToString(options.backend_preference)
3094 << " frames=" << kValidationFrameTarget << std::endl;
3095
3096 CameraCapture camera_capture(
3098 std::cout << "[CAMERA_CHECK] backend_active="
3099 << camera_capture.backendImplementation() << " ("
3100 << camera_capture.backendName() << ")" << std::endl;
3101
3102 FrameEvent frame_event;
3103 int frames_received = 0;
3104 int frame_failures = 0;
3105 int consecutive_failures = 0;
3106 int frame_width = 0;
3107 int frame_height = 0;
3108 std::optional<std::chrono::steady_clock::time_point> first_capture_timestamp;
3109 std::optional<std::chrono::steady_clock::time_point> last_capture_timestamp;
3110
3111 const auto validation_start = std::chrono::steady_clock::now();
3112 while (frames_received < kValidationFrameTarget &&
3113 consecutive_failures < kMaxConsecutiveFailures) {
3114 if (!camera_capture.waitForNextFrame(frame_event)) {
3115 ++frame_failures;
3116 ++consecutive_failures;
3117 std::cerr << "[CAMERA_CHECK] frame failure " << frame_failures
3118 << " (consecutive=" << consecutive_failures << ")"
3119 << std::endl;
3120 continue;
3121 }
3122
3123 ++frames_received;
3124 consecutive_failures = 0;
3125 frame_width = frameWidth(frame_event.image_data);
3126 frame_height = frameHeight(frame_event.image_data);
3127 if (!first_capture_timestamp.has_value()) {
3128 first_capture_timestamp = frame_event.capture_timestamp;
3129 std::cout << "[CAMERA_CHECK] first_frame_ms="
3130 << elapsedMilliseconds(validation_start,
3131 *first_capture_timestamp)
3132 << " size=" << frame_width << "x" << frame_height
3133 << std::endl;
3134 }
3135 last_capture_timestamp = frame_event.capture_timestamp;
3136 }
3137
3138 if (frames_received == 0) {
3139 std::cerr << "[CAMERA_CHECK] no frames received" << std::endl;
3140 return 1;
3141 }
3142
3143 const long long sample_window_ms =
3144 (first_capture_timestamp.has_value() && last_capture_timestamp.has_value())
3145 ? elapsedMilliseconds(*first_capture_timestamp,
3146 *last_capture_timestamp)
3147 : 0;
3148 const double approx_fps =
3149 (frames_received > 1 && sample_window_ms > 0)
3150 ? (1000.0 * static_cast<double>(frames_received - 1) /
3151 static_cast<double>(sample_window_ms))
3152 : 0.0;
3153
3154 std::ostringstream fps_stream;
3155 fps_stream << std::fixed << std::setprecision(1) << approx_fps;
3156
3157 std::cout << "[CAMERA_CHECK] summary: frames=" << frames_received
3158 << " failures=" << frame_failures
3159 << " sample_window_ms=" << sample_window_ms
3160 << " approx_fps=" << fps_stream.str()
3161 << " backend=" << camera_capture.backendImplementation() << " ("
3162 << camera_capture.backendName() << ")" << std::endl;
3163
3164 if (consecutive_failures >= kMaxConsecutiveFailures) {
3165 std::cerr << "[CAMERA_CHECK] failed after repeated capture errors"
3166 << std::endl;
3167 return 1;
3168 }
3169
3170 return 0;
3171}
3172
3174 std::cout
3175 << "[LIVE_TEST] Starting live marker safety test mode\n"
3176 << "[LIVE_TEST] Camera index: " << options.camera_index << "\n"
3177 << "[LIVE_TEST] Expected marker ID: " << options.expected_marker_id << "\n"
3178 << "[LIVE_TEST] Camera backend preference: "
3179 << cameraBackendPreferenceToString(options.backend_preference) << "\n"
3180 << "[LIVE_TEST] Auto operator ack: "
3181 << (options.auto_ack ? "ON" : "OFF") << "\n"
3182 << "[LIVE_TEST] Physical button = single-button control\n"
3183 << "[LIVE_TEST] Controls: space/button=control, 0/1/2/3=mode/routine, esc=quit\n"
3184 << "[LIVE_TEST] Manual mode keys: d/a=base left/right, w/s=forward/back, i/k=up/down, l/j=open/close\n"
3185 << "[LIVE_TEST] Focus keys: +/-=adjust focus (Pi Camera Module 3 only, -=autofocus)\n"
3186 << "[LIVE_TEST] Starting in DISARMED setup mode\n"
3187 << "[LIVE_TEST] Guardian thresholds: freeze after "
3188 << kLiveFreezeBadFrameThreshold
3189 << " consecutive bad frames, recover after "
3190 << kLiveRecoverGoodFrameThreshold
3191 << " consecutive good frames.\n"
3192 << "[LIVE_TEST] Modes: 0=manual, 1=surgery cut, 2=base scan, 3=grip pulse\n"
3193 << "[LIVE_TEST] Focus debug enabled: FOCUS_SCORE (Laplacian variance), "
3194 "higher usually means sharper marker edges.\n";
3195
3196 MotionChannelMap channel_map{};
3197 channel_map.base = kBaseServoChannel;
3198 channel_map.lower = kLowerServoChannel;
3199 channel_map.upper = kUpperServoChannel;
3200 channel_map.gripper = kGripServoChannel;
3201
3202 if (!motion_controller_.initialise(kDefaultI2cDevicePath,
3203 kDefaultPca9685Address,
3204 kDefaultPwmFrequencyHz,
3205 channel_map)) {
3206 std::cerr << "[LIVE_TEST] Motion controller initialization failed: "
3207 << motion_controller_.lastErrorString() << std::endl;
3208 return 1;
3209 }
3210
3211 MotionControllerHardwareAdapter hardware(motion_controller_);
3212 VisionConfig vision_config;
3213 vision_config.expectedMarkerId = options.expected_marker_id;
3214 VisionProcessor vision_processor(vision_config);
3215 CameraCapture camera_capture(
3217 std::cout << "[LIVE_TEST] Camera backend active: "
3218 << camera_capture.backendImplementation() << " ("
3219 << camera_capture.backendName() << ")" << std::endl;
3220
3221 std::unique_ptr<GuardianStateMachine> guardian;
3222 std::unique_ptr<RobotInterlock> interlock;
3223 bool guardian_armed = false;
3224 bool motion_faulted = false;
3225 bool motion_gate_open = false;
3227 FreezeReason pending_freeze_reason = FreezeReason::UNKNOWN_FAULT;
3228 SafetyState current_vision_state = SafetyState::SAFE;
3229 bool frame_is_safe = false;
3230 bool waiting_for_ack = false;
3231 bool waiting_for_ack_announced = false;
3232 bool home_pose_staged = false;
3233 std::string current_pose_name = "HOME";
3234 std::size_t selected_routine_index = 1;
3235 std::size_t next_routine_step_index = 0;
3236 SmokeJointOffsets current_pose_offsets = kSmokeHomePose;
3237 bool current_pose_offsets_known = false;
3238 SmokeJointOffsets pose_slew_target = kSmokeHomePose;
3239 bool pose_slew_active = false;
3240 std::chrono::steady_clock::time_point next_pose_slew_due =
3241 std::chrono::steady_clock::now();
3242 bool freeze_command_pending = false;
3243 std::optional<std::chrono::steady_clock::time_point> freeze_command_due;
3244 bool freeze_waiting_for_retract_logged = false;
3245 ControllerEventQueue control_events;
3246 CppTimerCallback live_step_timer;
3247 bool live_step_timer_started = false;
3248 RuntimeLatencyMetrics latency_metrics;
3249 constexpr std::size_t kVisionHistoryLimit = 180;
3250 constexpr std::size_t kEventHistoryLimit = 64;
3251 std::deque<double> vision_us_history;
3252 std::deque<double> unsafe_detect_history_ms;
3253 std::deque<double> freeze_pipeline_history_ms;
3254 std::deque<double> freeze_cmd_history_ms;
3255 std::deque<double> total_stop_history_ms;
3256 std::deque<double> ack_resume_history_ms;
3257 auto appendHistorySample = [&](std::deque<double>& history,
3258 double value,
3259 std::size_t limit) {
3260 history.push_back(value);
3261 if (history.size() > limit) {
3262 history.pop_front();
3263 }
3264 };
3265 auto copyHistory = [&](const std::deque<double>& history) {
3266 return std::vector<double>(history.begin(), history.end());
3267 };
3268 std::optional<std::chrono::steady_clock::time_point>
3269 pending_unsafe_capture_timestamp;
3270 std::optional<std::chrono::steady_clock::time_point>
3271 pending_unsafe_decision_timestamp;
3272 std::optional<std::chrono::steady_clock::time_point> ack_request_timestamp;
3273
3274 auto applyPoseOffsets = [&](const SmokeJointOffsets& offsets,
3275 const char* context) -> bool {
3276 bool target_clamped = false;
3277 const MeArmJointTargets targets = makeDemoTargets(offsets, target_clamped);
3278 if (!motion_controller_.setTargets(targets)) {
3279 motion_faulted = true;
3280 std::cerr << "[LIVE_TEST] failed to set pose (" << context << "): "
3281 << motion_controller_.lastErrorString() << std::endl;
3282 return false;
3283 }
3284 return true;
3285 };
3286
3287 auto stagePose = [&](const DemoPoseStep& step) -> bool {
3288 bool clamped = false;
3289 const SmokeJointOffsets target_offsets =
3290 clampDemoOffsets(step.offsets, clamped);
3291
3292 if (!current_pose_offsets_known) {
3293 current_pose_offsets = target_offsets;
3294 current_pose_offsets_known = true;
3295 if (!applyPoseOffsets(current_pose_offsets, "initial")) {
3296 return false;
3297 }
3298 }
3299
3300 pose_slew_target = target_offsets;
3301 pose_slew_active =
3302 (current_pose_offsets.base != pose_slew_target.base) ||
3303 (current_pose_offsets.lower != pose_slew_target.lower) ||
3304 (current_pose_offsets.upper != pose_slew_target.upper) ||
3305 (current_pose_offsets.grip != pose_slew_target.grip);
3306 next_pose_slew_due = std::chrono::steady_clock::now();
3307
3308 if (!pose_slew_active) {
3309 if (!applyPoseOffsets(pose_slew_target, "hold")) {
3310 return false;
3311 }
3312 }
3313
3314 current_pose_name = step.name;
3315 std::cout << "[LIVE_TEST] pose=" << step.name;
3316 if (clamped) {
3317 std::cout << " [clamped]";
3318 }
3319 if (pose_slew_active) {
3320 std::cout << " [slew]";
3321 }
3322 std::cout << " wait=1s" << std::endl;
3323 return true;
3324 };
3325
3326 auto advancePoseSlew = [&]() -> bool {
3327 if (!pose_slew_active) {
3328 return true;
3329 }
3330
3331 const auto now = std::chrono::steady_clock::now();
3332 if (now < next_pose_slew_due) {
3333 return true;
3334 }
3335
3336 auto stepTowards = [&](int current, int target) {
3337 if (current < target) {
3338 return std::min(current + kDemoSlewStepDegrees, target);
3339 }
3340 if (current > target) {
3341 return std::max(current - kDemoSlewStepDegrees, target);
3342 }
3343 return current;
3344 };
3345
3346 SmokeJointOffsets next_offsets = current_pose_offsets;
3347 next_offsets.base = stepTowards(next_offsets.base, pose_slew_target.base);
3348 next_offsets.lower = stepTowards(next_offsets.lower, pose_slew_target.lower);
3349 next_offsets.upper = stepTowards(next_offsets.upper, pose_slew_target.upper);
3350 next_offsets.grip = stepTowards(next_offsets.grip, pose_slew_target.grip);
3351
3352 if (!applyPoseOffsets(next_offsets, "slew")) {
3353 return false;
3354 }
3355
3356 current_pose_offsets = next_offsets;
3357 pose_slew_active =
3358 (current_pose_offsets.base != pose_slew_target.base) ||
3359 (current_pose_offsets.lower != pose_slew_target.lower) ||
3360 (current_pose_offsets.upper != pose_slew_target.upper) ||
3361 (current_pose_offsets.grip != pose_slew_target.grip);
3362 next_pose_slew_due = now + kDemoSlewStepInterval;
3363 return true;
3364 };
3365
3366 auto stopLiveStepTimer = [&]() {
3367 if (live_step_timer_started) {
3368 live_step_timer.stop();
3369 live_step_timer_started = false;
3370 }
3371 };
3372
3373 auto startLiveStepTimer = [&]() -> bool {
3374 if (live_step_timer_started) {
3375 return true;
3376 }
3377
3378 live_step_timer.registerEventCallback(
3379 [&]() { control_events.pushLiveStepReady(); });
3380 try {
3381 live_step_timer.startms(static_cast<long>(kDemoStepDwell.count()), PERIODIC);
3382 } catch (const char* exception) {
3383 motion_faulted = true;
3384 std::cerr << "[LIVE_TEST] routine timer failed: " << exception
3385 << std::endl;
3386 return false;
3387 } catch (...) {
3388 motion_faulted = true;
3389 std::cerr << "[LIVE_TEST] routine timer failed" << std::endl;
3390 return false;
3391 }
3392
3393 live_step_timer_started = true;
3394 return true;
3395 };
3396
3397 auto announceWaitingState = [&]() {
3398 const bool should_wait =
3399 frame_is_safe && guardian_armed &&
3400 guardian->getState() == GuardianState::FROZEN_UNSAFE;
3401
3402 if (should_wait && !waiting_for_ack_announced) {
3403 std::cout << "[LIVE_TEST] waiting for continue" << std::endl;
3404 waiting_for_ack_announced = true;
3405 }
3406
3407 waiting_for_ack = should_wait;
3408 if (!should_wait) {
3409 waiting_for_ack_announced = false;
3410 }
3411 };
3412
3413 auto selectRoutine = [&](std::size_t routine_index) -> bool {
3414 const LiveRoutineDefinition routine = getLiveRoutineDefinition(routine_index);
3415 stopLiveStepTimer();
3416 selected_routine_index = routine_index;
3417 next_routine_step_index = 0;
3418 std::cout << "[LIVE_TEST] routine " << routine.number << " selected: "
3419 << routine.name << std::endl;
3420
3421 if (guardian_armed && motion_gate_open && frame_is_safe) {
3422 if (routine.auto_progress) {
3423 if (!stagePose(kDemoHomeStep)) {
3424 return false;
3425 }
3426 home_pose_staged = true;
3427 if (!startLiveStepTimer()) {
3428 return false;
3429 }
3430 } else {
3431 std::cout << "[LIVE_TEST] manual mode active: auto routine paused."
3432 << std::endl;
3433 }
3434 }
3435
3436 return true;
3437 };
3438
3439 auto resetEnforcementState = [&]() {
3440 pending_reason = FreezeReason::UNKNOWN_FAULT;
3441 pending_freeze_reason = FreezeReason::UNKNOWN_FAULT;
3442 freeze_command_pending = false;
3443 freeze_command_due.reset();
3444 freeze_waiting_for_retract_logged = false;
3445 guardian = std::make_unique<GuardianStateMachine>(
3446 kLiveFreezeBadFrameThreshold,
3447 kLiveRecoverGoodFrameThreshold);
3448 interlock = std::make_unique<RobotInterlock>(hardware);
3449
3450 guardian->setOnFreezeCallback([&]() {
3451 const auto freeze_callback_start = std::chrono::steady_clock::now();
3452 if (pending_unsafe_decision_timestamp.has_value()) {
3453 latency_metrics.freeze_pipeline_ms = elapsedMilliseconds(
3454 *pending_unsafe_decision_timestamp,
3455 freeze_callback_start);
3456 appendHistorySample(freeze_pipeline_history_ms,
3457 static_cast<double>(
3458 *latency_metrics.freeze_pipeline_ms),
3459 kEventHistoryLimit);
3460 }
3461 stopLiveStepTimer();
3462 next_routine_step_index = 0;
3463 pending_freeze_reason = pending_reason;
3464 if (guardian_armed) {
3465 std::cout << "[LIVE_TEST] unsafe received: retracting to safe pose"
3466 << std::endl;
3467 if (!stagePose(kSurgeryRetractStep)) {
3468 motion_faulted = true;
3469 }
3470 }
3471 freeze_command_pending = true;
3472 freeze_command_due =
3473 std::chrono::steady_clock::now() + kSurgeryRetractDwell;
3474 freeze_waiting_for_retract_logged = false;
3475 motion_gate_open = false;
3476 waiting_for_ack = false;
3477 waiting_for_ack_announced = false;
3478 std::cout
3479 << "[LIVE_TEST] freeze pending: will hold after retract dwell"
3480 << std::endl;
3481 });
3482
3483 guardian->setOnClearFreezeCallback([&]() {
3484 interlock->onControlEvent(ControlEvent::ALLOW_MOTION);
3485 const auto resume_callback_end = std::chrono::steady_clock::now();
3486 if (ack_request_timestamp.has_value()) {
3487 latency_metrics.ack_to_resume_ms = elapsedMilliseconds(
3488 *ack_request_timestamp,
3489 resume_callback_end);
3490 appendHistorySample(ack_resume_history_ms,
3491 static_cast<double>(
3492 *latency_metrics.ack_to_resume_ms),
3493 kEventHistoryLimit);
3494 }
3495 ack_request_timestamp.reset();
3496 if (interlock->state() == InterlockState::FAULT) {
3497 motion_faulted = true;
3498 return;
3499 }
3500 if (getLiveRoutineDefinition(selected_routine_index).auto_progress) {
3501 if (!startLiveStepTimer()) {
3502 motion_faulted = true;
3503 return;
3504 }
3505 }
3506 motion_gate_open = true;
3507 std::cout << "[LIVE_TEST] resume" << std::endl;
3508 logLiveLatencySample("resume", latency_metrics);
3509 });
3510
3511 guardian->setOnStateChangeCallback([&](GuardianState from, GuardianState to) {
3512 std::cout << "[GUARDIAN] " << guardian->stateToString(from) << " -> "
3513 << guardian->stateToString(to) << std::endl;
3514 });
3515 };
3516
3517 resetEnforcementState();
3518
3519 auto processPendingFreezeCommand = [&]() -> bool {
3520 if (!freeze_command_pending || !freeze_command_due.has_value()) {
3521 return true;
3522 }
3523
3524 const auto now = std::chrono::steady_clock::now();
3525 if (now < *freeze_command_due) {
3526 return true;
3527 }
3528 if (pose_slew_active) {
3529 if (!freeze_waiting_for_retract_logged) {
3530 std::cout << "[LIVE_TEST] freeze pending: waiting for retract "
3531 "completion"
3532 << std::endl;
3533 freeze_waiting_for_retract_logged = true;
3534 }
3535 return true;
3536 }
3537
3538 freeze_command_pending = false;
3539 freeze_command_due.reset();
3540 freeze_waiting_for_retract_logged = false;
3541 const auto freeze_cmd_start = std::chrono::steady_clock::now();
3542 interlock->onControlEvent(ControlEvent::FREEZE_NOW, pending_freeze_reason);
3543 const auto freeze_cmd_end = std::chrono::steady_clock::now();
3544 latency_metrics.freeze_cmd_ms = elapsedMilliseconds(freeze_cmd_start,
3545 freeze_cmd_end);
3546 appendHistorySample(freeze_cmd_history_ms,
3547 static_cast<double>(*latency_metrics.freeze_cmd_ms),
3548 kEventHistoryLimit);
3549 if (pending_unsafe_capture_timestamp.has_value()) {
3550 latency_metrics.total_stop_ms = elapsedMilliseconds(
3551 *pending_unsafe_capture_timestamp,
3552 freeze_cmd_end);
3553 appendHistorySample(total_stop_history_ms,
3554 static_cast<double>(*latency_metrics.total_stop_ms),
3555 kEventHistoryLimit);
3556 }
3557 pending_unsafe_capture_timestamp.reset();
3558 pending_unsafe_decision_timestamp.reset();
3559 std::cout << "[LIVE_TEST] freeze: "
3560 << freezeReasonToString(pending_freeze_reason) << std::endl;
3561 logLiveLatencySample("freeze", latency_metrics);
3562
3563 if (interlock->state() == InterlockState::FAULT) {
3564 motion_faulted = true;
3565 return false;
3566 }
3567 return true;
3568 };
3569
3570 PhysicalButtonModule button_module;
3571 std::cout << "[LIVE_TEST] Physical button module: "
3572 << (button_module.available() ? "configured" : "disabled");
3573 const char* button_module_status =
3574 button_module.available() ? button_module.statusString()
3575 : button_module.lastErrorString();
3576 if (button_module_status != nullptr &&
3577 std::string(button_module_status) != "no error" &&
3578 std::string(button_module_status) != "no status") {
3579 std::cout << " (" << button_module_status << ")";
3580 }
3581 std::cout << std::endl;
3582 if (!button_module.available()) {
3583 std::cout << "[LIVE_TEST] Configure ARGUS_BUTTON_ACK_GPIO to enable the "
3584 "physical single-button control."
3585 << std::endl;
3586 }
3587
3588 auto freezeMotionBeforeModeChange = [&](const char* action_label) -> bool {
3589 interlock->onControlEvent(ControlEvent::FREEZE_NOW, FreezeReason::UNKNOWN_FAULT);
3590 if (interlock->state() == InterlockState::FAULT) {
3591 motion_faulted = true;
3592 std::cerr << "[LIVE_TEST] Unable to freeze motion before " << action_label
3593 << ": " << motion_controller_.lastErrorString() << std::endl;
3594 return false;
3595 }
3596
3597 return true;
3598 };
3599
3600 auto requestArm = [&]() -> bool {
3601 if (guardian_armed) {
3602 std::cout << "[LIVE_TEST] ARM request ignored: guardian already armed."
3603 << std::endl;
3604 return true;
3605 }
3606
3607 if (!frame_is_safe) {
3608 std::cout << "[LIVE_TEST] ARM request rejected: current observed condition is "
3609 "unsafe (vision="
3610 << safetyStateToString(current_vision_state) << ")." << std::endl;
3611 return true;
3612 }
3613
3614 if (!freezeMotionBeforeModeChange("arming")) {
3615 return false;
3616 }
3617
3618 resetEnforcementState();
3619 if (!stagePose(kDemoHomeStep)) {
3620 return false;
3621 }
3622 home_pose_staged = true;
3623 next_routine_step_index = 0;
3624
3625 interlock->onControlEvent(ControlEvent::FREEZE_NOW, FreezeReason::UNKNOWN_FAULT);
3626 if (interlock->state() == InterlockState::FAULT) {
3627 motion_faulted = true;
3628 std::cerr << "[LIVE_TEST] Unable to prepare motion path for arm: "
3629 << motion_controller_.lastErrorString() << std::endl;
3630 return false;
3631 }
3632
3633 interlock->operatorAcknowledge();
3634 interlock->onControlEvent(ControlEvent::ALLOW_MOTION);
3635 if (interlock->state() == InterlockState::FAULT) {
3636 motion_faulted = true;
3637 std::cerr << "[LIVE_TEST] Unable to enable motion on arm: "
3638 << motion_controller_.lastErrorString() << std::endl;
3639 return false;
3640 }
3641
3642 stopLiveStepTimer();
3643 if (getLiveRoutineDefinition(selected_routine_index).auto_progress) {
3644 if (!startLiveStepTimer()) {
3645 return false;
3646 }
3647 }
3648 guardian_armed = true;
3649 motion_gate_open = true;
3650 waiting_for_ack = false;
3651 waiting_for_ack_announced = false;
3652 ack_request_timestamp.reset();
3653 pending_unsafe_capture_timestamp.reset();
3654 pending_unsafe_decision_timestamp.reset();
3655 freeze_command_pending = false;
3656 freeze_command_due.reset();
3657 freeze_waiting_for_retract_logged = false;
3658 std::cout << "[LIVE_TEST] ARM accepted: guardian enforcement is now ACTIVE."
3659 << std::endl;
3660 return true;
3661 };
3662
3663 auto requestDisarm = [&]() -> bool {
3664 if (!guardian_armed) {
3665 std::cout << "[LIVE_TEST] DISARM request ignored: guardian already disarmed."
3666 << std::endl;
3667 return true;
3668 }
3669
3670 if (!freezeMotionBeforeModeChange("disarming")) {
3671 return false;
3672 }
3673
3674 guardian_armed = false;
3675 motion_gate_open = false;
3676 waiting_for_ack = false;
3677 waiting_for_ack_announced = false;
3678 home_pose_staged = false;
3679 ack_request_timestamp.reset();
3680 pending_unsafe_capture_timestamp.reset();
3681 pending_unsafe_decision_timestamp.reset();
3682 freeze_command_pending = false;
3683 freeze_command_due.reset();
3684 freeze_waiting_for_retract_logged = false;
3685 pose_slew_active = false;
3686 stopLiveStepTimer();
3687 next_routine_step_index = 0;
3688 current_pose_name = "HOLD";
3689 resetEnforcementState();
3690 std::cout << "[LIVE_TEST] DISARM accepted: guardian enforcement is now INACTIVE "
3691 "(setup/observation mode)."
3692 << std::endl;
3693 return true;
3694 };
3695
3696 auto requestAcknowledge = [&]() -> bool {
3697 if (!guardian_armed) {
3698 std::cout << "[LIVE_TEST] ACK request ignored: guardian is disarmed."
3699 << std::endl;
3700 return true;
3701 }
3702
3703 if (freeze_command_pending) {
3704 std::cout << "[LIVE_TEST] ACK request ignored: retract in progress."
3705 << std::endl;
3706 return true;
3707 }
3708
3709 if (!frame_is_safe) {
3710 std::cout << "[LIVE_TEST] ACK request ignored: current observed condition is "
3711 "unsafe (vision="
3712 << safetyStateToString(current_vision_state) << ")." << std::endl;
3713 return true;
3714 }
3715
3716 if (guardian->getState() != GuardianState::FROZEN_UNSAFE) {
3717 std::cout << "[LIVE_TEST] ACK request ignored: guardian is not frozen."
3718 << std::endl;
3719 return true;
3720 }
3721
3722 guardian->operatorAcknowledge();
3723 interlock->operatorAcknowledge();
3724 ack_request_timestamp = std::chrono::steady_clock::now();
3725 std::cout << "[LIVE_TEST] Operator acknowledge requested." << std::endl;
3726 return true;
3727 };
3728
3729 auto requestContinue = [&]() -> bool {
3730 if (!guardian_armed) {
3731 return requestArm();
3732 }
3733 if (guardian->getState() == GuardianState::FROZEN_UNSAFE) {
3734 return requestAcknowledge();
3735 }
3736 if (guardian->getState() == GuardianState::RESET_PENDING) {
3737 std::cout << "[LIVE_TEST] CONTINUE ignored: waiting for recovery"
3738 << std::endl;
3739 return true;
3740 }
3741 return requestDisarm();
3742 };
3743
3744 auto requestFromButton = [&](PhysicalButtonEvent event) -> bool {
3745 std::cout << "[BUTTON] " << PhysicalButtonModule::eventToString(event)
3746 << std::endl;
3747 switch (event) {
3750 return requestContinue();
3752 return requestDisarm();
3753 }
3754 return true;
3755 };
3756
3757 auto requestManualNudge = [&](int delta_base,
3758 int delta_lower,
3759 int delta_upper,
3760 int delta_grip,
3761 const char* label) -> bool {
3762 const LiveRoutineDefinition routine =
3763 getLiveRoutineDefinition(selected_routine_index);
3764 if (routine.auto_progress) {
3765 return true;
3766 }
3767
3768 if (!guardian_armed || !motion_gate_open || waiting_for_ack ||
3769 freeze_command_pending ||
3770 guardian->getState() != GuardianState::SAFE_MONITORING) {
3771 std::cout << "[LIVE_TEST] manual nudge ignored: control not available"
3772 << std::endl;
3773 return true;
3774 }
3775
3776 if (!current_pose_offsets_known) {
3777 current_pose_offsets = kSmokeHomePose;
3778 current_pose_offsets_known = true;
3779 pose_slew_target = current_pose_offsets;
3780 }
3781
3782 SmokeJointOffsets target_offsets =
3783 pose_slew_active ? pose_slew_target : current_pose_offsets;
3784 target_offsets.base += delta_base;
3785 target_offsets.lower += delta_lower;
3786 target_offsets.upper += delta_upper;
3787 target_offsets.grip += delta_grip;
3788
3789 bool clamped = false;
3790 target_offsets = clampDemoOffsets(target_offsets, clamped);
3791 pose_slew_target = target_offsets;
3792 pose_slew_active =
3793 (current_pose_offsets.base != pose_slew_target.base) ||
3794 (current_pose_offsets.lower != pose_slew_target.lower) ||
3795 (current_pose_offsets.upper != pose_slew_target.upper) ||
3796 (current_pose_offsets.grip != pose_slew_target.grip);
3797 next_pose_slew_due = std::chrono::steady_clock::now();
3798 current_pose_name = "Manual";
3799
3800 if (pose_slew_active) {
3801 std::cout << "[LIVE_TEST] manual " << label;
3802 if (clamped) {
3803 std::cout << " [clamped]";
3804 }
3805 std::cout << " [slew]" << std::endl;
3806 }
3807 return true;
3808 };
3809
3810 FrameEvent latest_frame_event;
3811 bool latest_frame_available = false;
3812 std::uint64_t frame_index = 0;
3813 const auto live_watchdog_clock_start = std::chrono::steady_clock::now();
3814 auto liveWatchdogTickMs = [&]() -> std::uint32_t {
3815 const auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
3816 std::chrono::steady_clock::now() - live_watchdog_clock_start)
3817 .count();
3818 const auto clamped_ms = std::min<std::uint64_t>(
3819 static_cast<std::uint64_t>(elapsed_ms),
3820 static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max()));
3821 return static_cast<std::uint32_t>(clamped_ms);
3822 };
3823 bool processed_any_frame = false;
3824 int consecutive_capture_failures = 0;
3825 double focus_score = 0.0;
3826 const char* focus_quality = "BLURRY";
3827
3828 auto handleControlEvent =
3829 [&](const ControllerEvent& event) -> ControllerEventDisposition {
3830 switch (event.kind) {
3831 case ControllerEventKind::ButtonInput:
3832 return requestFromButton(*event.button_event)
3833 ? ControllerEventDisposition::Consumed
3834 : ControllerEventDisposition::Abort;
3835 case ControllerEventKind::LiveStepReady: {
3836 if (!guardian_armed || !motion_gate_open || waiting_for_ack ||
3837 pose_slew_active || freeze_command_pending) {
3838 return ControllerEventDisposition::Deferred;
3839 }
3840
3841 const LiveRoutineDefinition routine =
3842 getLiveRoutineDefinition(selected_routine_index);
3843 if (!routine.auto_progress || routine.steps == nullptr ||
3844 routine.step_count == 0) {
3845 return ControllerEventDisposition::Consumed;
3846 }
3847 const DemoPoseStep& step = routine.steps[next_routine_step_index];
3848 if (!stagePose(step)) {
3849 return ControllerEventDisposition::Abort;
3850 }
3851
3852 next_routine_step_index =
3853 (next_routine_step_index + 1) % routine.step_count;
3854 return ControllerEventDisposition::Consumed;
3855 }
3856 case ControllerEventKind::DemoStepReady:
3857 return ControllerEventDisposition::Consumed;
3858 case ControllerEventKind::FrameCaptureFailed:
3859 ++consecutive_capture_failures;
3860 std::cerr << "[LIVE_TEST] Frame capture failed ("
3861 << consecutive_capture_failures
3862 << "/30). Retrying..." << std::endl;
3863 if (consecutive_capture_failures >= 30) {
3864 return ControllerEventDisposition::Abort;
3865 }
3866 return ControllerEventDisposition::Consumed;
3867 case ControllerEventKind::FrameAvailable: {
3868 consecutive_capture_failures = 0;
3869 processed_any_frame = true;
3870 latest_frame_event = *event.frame_event;
3871 latest_frame_available = !latest_frame_event.image_data.empty();
3872 ++frame_index;
3873
3874 const SafetyResult result = vision_processor.process(
3875 latest_frame_event.image_data,
3876 latest_frame_event.capture_timestamp);
3877 latency_metrics.vision_us = result.processing_time.count();
3878 appendHistorySample(vision_us_history,
3879 static_cast<double>(latency_metrics.vision_us),
3880 kVisionHistoryLimit);
3881 focus_score = computeFocusScore(latest_frame_event.image_data);
3882 focus_quality = focusQualityLabel(focus_score);
3883
3884 current_vision_state = result.state;
3885 frame_is_safe = (current_vision_state == SafetyState::SAFE);
3886 if (!frame_is_safe) {
3887 pending_reason = mapSafetyToFreezeReason(current_vision_state);
3888 }
3889
3890 const GuardianState guardian_state_before_update =
3891 guardian->getState();
3892 if (guardian_armed &&
3893 guardian_state_before_update ==
3895 if (!frame_is_safe) {
3896 if (!pending_unsafe_capture_timestamp.has_value()) {
3897 latency_metrics.unsafe_detect_ms.reset();
3898 latency_metrics.freeze_pipeline_ms.reset();
3899 latency_metrics.freeze_cmd_ms.reset();
3900 latency_metrics.total_stop_ms.reset();
3901 latency_metrics.ack_to_resume_ms.reset();
3902 pending_unsafe_capture_timestamp =
3903 latest_frame_event.capture_timestamp;
3904 pending_unsafe_decision_timestamp = result.timestamp;
3905 latency_metrics.unsafe_detect_ms = elapsedMilliseconds(
3906 latest_frame_event.capture_timestamp,
3907 result.timestamp);
3908 appendHistorySample(unsafe_detect_history_ms,
3909 static_cast<double>(
3910 *latency_metrics
3911 .unsafe_detect_ms),
3912 kEventHistoryLimit);
3913 logLiveLatencySample("unsafe_detect",
3914 latency_metrics);
3915 }
3916 } else {
3917 pending_unsafe_capture_timestamp.reset();
3918 pending_unsafe_decision_timestamp.reset();
3919 }
3920 }
3921 if (guardian_armed) {
3922 guardian->processFrame(frame_is_safe ? FrameStatus::FRAME_GOOD
3924 }
3925 announceWaitingState();
3926
3927 if (guardian_armed && options.auto_ack &&
3928 !freeze_command_pending &&
3929 guardian->getState() == GuardianState::FROZEN_UNSAFE) {
3930 ack_request_timestamp = std::chrono::steady_clock::now();
3931 guardian->operatorAcknowledge();
3932 interlock->operatorAcknowledge();
3933 std::cout
3934 << "[LIVE_TEST] Operator acknowledge sent automatically"
3935 << std::endl;
3936 }
3937
3938 if (guardian_armed) {
3939 interlock->guardianHeartbeat(liveWatchdogTickMs());
3940 }
3941
3942 return ControllerEventDisposition::Consumed;
3943 }
3944 }
3945 return ControllerEventDisposition::Consumed;
3946 };
3947
3948 cv::namedWindow(kLiveCameraWindowName, cv::WINDOW_AUTOSIZE);
3949 cv::namedWindow(kLiveStatusWindowName, cv::WINDOW_AUTOSIZE);
3950
3951 // Add simplified colour tuning sliders to the status window.
3952 // Three controls only: hue, saturation, brightness.
3953 VisionConfig dynamicConfig = vision_config;
3954 constexpr const char* kSliderHue = "Hue";
3955 constexpr const char* kSliderSaturation = "Saturation";
3956 constexpr const char* kSliderBrightness = "Brightness";
3957 constexpr const char* kSliderPixelThreshold = "Pixel threshold";
3958 constexpr int kHueHalfWidth = 12;
3959 constexpr int kDefaultTuneHue = 85;
3960 constexpr int kDefaultTuneSaturation = 255;
3961 constexpr int kDefaultTuneBrightness = 164;
3962 constexpr int kDefaultTunePixelThreshold = 100;
3963
3964 int tuning_hue_center = kDefaultTuneHue;
3965 int tuning_saturation = kDefaultTuneSaturation;
3966 int tuning_brightness = kDefaultTuneBrightness;
3967 int tuning_pixel_threshold = kDefaultTunePixelThreshold;
3968
3969 auto applySimpleColourTuning = [&]() {
3970 tuning_hue_center = std::clamp(tuning_hue_center, 0, 179);
3971 tuning_saturation = std::clamp(tuning_saturation, 0, 255);
3972 tuning_brightness = std::clamp(tuning_brightness, 0, 255);
3973
3974 cv::setTrackbarPos(kSliderHue, kLiveStatusWindowName, tuning_hue_center);
3975 cv::setTrackbarPos(kSliderSaturation, kLiveStatusWindowName, tuning_saturation);
3976 cv::setTrackbarPos(kSliderBrightness, kLiveStatusWindowName, tuning_brightness);
3977 tuning_pixel_threshold = std::clamp(tuning_pixel_threshold, 0, 500);
3978 cv::setTrackbarPos(kSliderPixelThreshold,
3979 kLiveStatusWindowName,
3980 tuning_pixel_threshold);
3981
3982 dynamicConfig.depthHueLower1 =
3983 normaliseHue(tuning_hue_center - kHueHalfWidth);
3984 dynamicConfig.depthHueUpper1 =
3985 normaliseHue(tuning_hue_center + kHueHalfWidth);
3986 dynamicConfig.depthHueLower2 = 255;
3987 dynamicConfig.depthHueUpper2 = 0;
3988 dynamicConfig.depthSatMin = tuning_saturation;
3989 dynamicConfig.depthSatMax = 255;
3990 dynamicConfig.depthValMin = tuning_brightness;
3991 dynamicConfig.depthValMax = 255;
3992 dynamicConfig.depthPixelThreshold = tuning_pixel_threshold;
3993 };
3994 auto sliderSnapshot = [&]() {
3995 return std::array<int, 4>{
3996 tuning_hue_center,
3997 tuning_saturation,
3998 tuning_brightness,
3999 tuning_pixel_threshold,
4000 };
4001 };
4002
4003 cv::createTrackbar(kSliderHue,
4004 kLiveStatusWindowName,
4005 &tuning_hue_center,
4006 179);
4007 cv::createTrackbar(kSliderSaturation,
4008 kLiveStatusWindowName,
4009 &tuning_saturation,
4010 255);
4011 cv::createTrackbar(kSliderBrightness,
4012 kLiveStatusWindowName,
4013 &tuning_brightness,
4014 255);
4015 cv::createTrackbar(kSliderPixelThreshold,
4016 kLiveStatusWindowName,
4017 &tuning_pixel_threshold,
4018 500);
4019
4020 applySimpleColourTuning();
4021 vision_processor.updateConfig(dynamicConfig);
4022 std::array<int, 4> last_slider_snapshot = sliderSnapshot();
4023
4024 struct LiveStatusSnapshot {
4025 bool guardian_armed = false;
4026 bool scene_safe = false;
4027 bool waiting_for_continue = false;
4028 int routine_number = 0;
4029 std::string vision_state;
4030 std::string guardian_state;
4031 std::string interlock_state;
4032 std::string motion_controller_state;
4033 std::string freeze_reason;
4034 };
4035
4036 LiveStatusSnapshot last_live_status;
4037 bool live_status_known = false;
4038 auto maybeLogLiveStatus = [&](const LiveStatusSnapshot& snapshot) {
4039 const bool changed =
4040 !live_status_known ||
4041 snapshot.guardian_armed != last_live_status.guardian_armed ||
4042 snapshot.scene_safe != last_live_status.scene_safe ||
4043 snapshot.waiting_for_continue !=
4044 last_live_status.waiting_for_continue ||
4045 snapshot.routine_number != last_live_status.routine_number ||
4046 snapshot.vision_state != last_live_status.vision_state ||
4047 snapshot.guardian_state != last_live_status.guardian_state ||
4048 snapshot.interlock_state != last_live_status.interlock_state ||
4049 snapshot.motion_controller_state !=
4050 last_live_status.motion_controller_state ||
4051 snapshot.freeze_reason != last_live_status.freeze_reason;
4052 if (!changed) {
4053 return;
4054 }
4055
4056 std::cout << "[LIVE_TEST] status: armed="
4057 << (snapshot.guardian_armed ? "YES" : "NO")
4058 << " safe=" << (snapshot.scene_safe ? "YES" : "NO")
4059 << " waiting="
4060 << (snapshot.waiting_for_continue ? "YES" : "NO")
4061 << " routine=" << snapshot.routine_number
4062 << " vision=" << snapshot.vision_state
4063 << " guardian=" << snapshot.guardian_state
4064 << " interlock=" << snapshot.interlock_state
4065 << " motion_ctrl=" << snapshot.motion_controller_state
4066 << " freeze_reason=" << snapshot.freeze_reason << std::endl;
4067
4068 last_live_status = snapshot;
4069 live_status_known = true;
4070 };
4071
4072 std::atomic<bool> capture_stop_requested{false};
4073 std::thread capture_thread([&]() {
4074 while (!capture_stop_requested.load(std::memory_order_relaxed)) {
4075 FrameEvent captured_frame;
4076 if (camera_capture.waitForNextFrame(captured_frame)) {
4077 control_events.pushFrame(std::move(captured_frame));
4078 continue;
4079 }
4080
4081 control_events.pushFrameCaptureFailed();
4082
4083 std::string timer_error;
4084 if (!waitForCppTimerDelay(kCaptureRetryBackoff, timer_error)) {
4085 control_events.pushFrameCaptureFailed();
4086 break;
4087 }
4088 }
4089 });
4090 std::atomic<bool> button_stop_requested{false};
4091 std::thread button_thread;
4092 if (button_module.available()) {
4093 button_thread = std::thread([&]() {
4094 while (!button_stop_requested.load(std::memory_order_relaxed)) {
4095 PhysicalButtonEvent button_event;
4096 if (button_module.waitForEvent(button_event,
4097 std::chrono::milliseconds(250))) {
4098 control_events.pushButton(button_event);
4099 }
4100 }
4101 });
4102 }
4103
4104 constexpr int kLiveStatusWidth = 430;
4105 constexpr int kLiveMetricsWidth = 430;
4106 cv::Mat status_frame;
4107 cv::Mat metrics_frame;
4108
4109 while (true) {
4110 if (interlock->state() == InterlockState::FAULT) {
4111 motion_faulted = true;
4112 std::cerr << "[LIVE_TEST] Interlock entered FAULT state (motion controller: "
4113 << motion_controller_.lastErrorString() << ")" << std::endl;
4114 break;
4115 }
4116 (void)control_events.waitForEvents(std::chrono::milliseconds(5));
4117
4118 applySimpleColourTuning();
4119 const std::array<int, 4> current_slider_snapshot = sliderSnapshot();
4120 if (current_slider_snapshot != last_slider_snapshot) {
4121 vision_processor.updateConfig(dynamicConfig);
4122 last_slider_snapshot = current_slider_snapshot;
4123 }
4124
4125 std::string guardian_state_text = "DISARMED_SETUP";
4126 std::string interlock_state_text = "DISARMED";
4127 std::string freeze_reason_text = "N/A";
4128
4129 if (!control_events.drain(handleControlEvent)) {
4130 motion_faulted = true;
4131 break;
4132 }
4133
4134 if (guardian_armed) {
4135 interlock->watchdogCheck(liveWatchdogTickMs(),
4136 kLiveInterlockWatchdogMaxDelayMs);
4137 if (interlock->state() == InterlockState::FROZEN &&
4138 interlock->freezeReason() == FreezeReason::WATCHDOG_TIMEOUT) {
4139 motion_faulted = true;
4140 std::cerr << "[LIVE_TEST] Watchdog timeout: no guardian heartbeat "
4141 "observed in "
4142 << kLiveInterlockWatchdogMaxDelayMs
4143 << " ms. Aborting run." << std::endl;
4144 break;
4145 }
4146 }
4147
4148 if (!advancePoseSlew()) {
4149 motion_faulted = true;
4150 break;
4151 }
4152
4153 if (!processPendingFreezeCommand()) {
4154 motion_faulted = true;
4155 break;
4156 }
4157
4158 if (guardian_armed) {
4159 guardian_state_text = guardian->getCurrentStateString();
4160 interlock_state_text = interlockStateToString(interlock->state());
4161 freeze_reason_text = freezeReasonToString(interlock->freezeReason());
4162 }
4163
4164 maybeLogLiveStatus(LiveStatusSnapshot{
4165 guardian_armed,
4166 frame_is_safe,
4167 waiting_for_ack,
4168 getLiveRoutineDefinition(selected_routine_index).number,
4169 safetyStateToString(current_vision_state),
4170 guardian_state_text,
4171 interlock_state_text,
4172 motionControllerStateToString(motion_controller_.outputState()),
4173 freeze_reason_text});
4174
4175 if (interlock->state() == InterlockState::FAULT) {
4176 motion_faulted = true;
4177 std::cerr << "[LIVE_TEST] Motion control fault detected: "
4178 << motion_controller_.lastErrorString() << std::endl;
4179 break;
4180 }
4181
4182 cv::Mat display_frame;
4183 if (latest_frame_available) {
4184 display_frame = latest_frame_event.image_data.clone();
4185 } else {
4186 display_frame = makePlaceholderFrame(640, 480);
4187 }
4188 const bool decision_is_safe = guardian_armed
4189 ? ((current_vision_state == SafetyState::SAFE) &&
4190 (guardian->getState() ==
4192 interlock->motionAllowed())
4193 : (current_vision_state == SafetyState::SAFE);
4194 std::string ui_state_label = "Setup";
4195 std::string ui_state_description = "Waiting for a safe scene";
4196 std::string next_action = "Make scene safe";
4197 if (!guardian_armed && frame_is_safe) {
4198 ui_state_label = "Ready";
4199 ui_state_description = "Scene is safe and ready to arm";
4200 next_action = "Press control to start";
4201 } else if (guardian_armed && motion_gate_open && decision_is_safe) {
4202 ui_state_label = "Running";
4203 ui_state_description = "Guard active, motion allowed";
4204 next_action = "Press control to stop";
4205 } else if (guardian_armed &&
4206 guardian->getState() == GuardianState::FROZEN_UNSAFE) {
4207 ui_state_label = "Frozen";
4208 ui_state_description = "Unsafe condition detected, motion stopped";
4209 next_action = frame_is_safe ? "Press control to resume"
4210 : "Clear workspace";
4211 } else if (guardian_armed &&
4212 guardian->getState() == GuardianState::RESET_PENDING) {
4213 ui_state_label = "Waiting";
4214 ui_state_description = "Safe again, waiting for recovery";
4215 next_action = "Waiting for recovery";
4216 } else if (guardian_armed && frame_is_safe) {
4217 ui_state_label = "Ready";
4218 ui_state_description = "Guard armed, motion blocked";
4219 next_action = "Press control";
4220 }
4221
4222 const cv::Scalar focus_color =
4223 (focus_score < 60.0) ? cv::Scalar(60, 60, 200)
4224 : ((focus_score < 180.0) ? cv::Scalar(50, 180, 230)
4225 : cv::Scalar(60, 170, 80));
4226 const std::string routine_label =
4227 std::to_string(
4228 getLiveRoutineDefinition(selected_routine_index).number) +
4229 " " + getLiveRoutineDefinition(selected_routine_index).name;
4230 const std::string footer_info =
4231 std::to_string(frameWidth(display_frame)) + "x" +
4232 std::to_string(frameHeight(display_frame)) + " | " +
4233 camera_capture.backendName();
4234
4235 SupervisoryUiModel live_ui{};
4236 live_ui.mode_title = "Live test";
4237 live_ui.state_label = ui_state_label;
4238 live_ui.state_description = ui_state_description;
4239 live_ui.state_color =
4240 guardian_armed && guardian->getState() == GuardianState::FROZEN_UNSAFE
4241 ? cv::Scalar(60, 60, 200)
4242 : (frame_is_safe ? cv::Scalar(60, 170, 80)
4243 : cv::Scalar(170, 140, 60));
4244 live_ui.motion_label = motion_gate_open ? "Allowed" : "Blocked";
4245 live_ui.motion_color =
4246 motion_gate_open ? cv::Scalar(60, 170, 80)
4247 : (frame_is_safe ? cv::Scalar(50, 180, 230)
4248 : cv::Scalar(60, 60, 200));
4249 live_ui.operator_prompt = "space/button";
4250 live_ui.next_action = next_action;
4251 const FreezeReason interlock_freeze_reason =
4252 guardian_armed ? interlock->freezeReason() : FreezeReason::NONE;
4253 live_ui.freeze_reason =
4254 guardian_armed ? freezeReasonToUiString(interlock_freeze_reason) : "N/A";
4255 live_ui.footer_info = footer_info;
4256 live_ui.latency = latency_metrics;
4257 live_ui.forbidden_colour_label = describeForbiddenColour(dynamicConfig);
4258 live_ui.forbidden_colour_thresholds =
4259 describeForbiddenColourThresholds(dynamicConfig);
4260 live_ui.forbidden_colour_swatch = forbiddenColourSwatchBgr(dynamicConfig);
4261 live_ui.vision_latency_history_us = copyHistory(vision_us_history);
4262 live_ui.unsafe_detect_history_ms = copyHistory(unsafe_detect_history_ms);
4263 live_ui.freeze_pipeline_history_ms =
4264 copyHistory(freeze_pipeline_history_ms);
4265 live_ui.freeze_cmd_history_ms = copyHistory(freeze_cmd_history_ms);
4266 live_ui.total_stop_history_ms = copyHistory(total_stop_history_ms);
4267 live_ui.ack_resume_history_ms = copyHistory(ack_resume_history_ms);
4268 live_ui.status_rows = {
4269 {"Vision", safetyStateToUiString(current_vision_state),
4270 severityColor(safetyStateToString(current_vision_state))},
4271 {"Guardian",
4272 guardian_armed ? guardianStateToUiString(guardian->getState())
4273 : "Disarmed (setup)",
4274 severityColor(guardian_state_text)},
4275 {"Interlock",
4276 guardian_armed ? interlockStateToUiString(interlock->state())
4277 : "Disarmed",
4278 severityColor(interlock_state_text)},
4279 {"Routine", routine_label, cv::Scalar(25, 25, 25)},
4280 {"Pose", current_pose_name, cv::Scalar(25, 25, 25)},
4281 {"Ready to arm", frame_is_safe ? "Yes" : "No",
4282 frame_is_safe ? cv::Scalar(60, 170, 80) : cv::Scalar(60, 60, 200)},
4283 };
4284 live_ui.show_focus = true;
4285 live_ui.focus_label =
4286 formatFocusScore(focus_score) + " (" + std::string(focus_quality) + ")";
4287 live_ui.focus_color = focus_color;
4288 live_ui.focus_fraction = std::clamp(focus_score / 240.0, 0.0, 1.0);
4289 live_ui.camera_hud_text =
4290 guardian_armed && guardian->getState() == GuardianState::FROZEN_UNSAFE
4291 ? "Unsafe: stopped"
4292 : (guardian_armed && motion_gate_open && decision_is_safe)
4293 ? "Running"
4294 : (!guardian_armed && frame_is_safe) ? "Ready to arm"
4295 : "Setup";
4296 live_ui.camera_hud_color =
4297 guardian_armed && guardian->getState() == GuardianState::FROZEN_UNSAFE
4298 ? cv::Scalar(60, 60, 200)
4299 : (guardian_armed && motion_gate_open ? cv::Scalar(60, 170, 80)
4300 : cv::Scalar(170, 140, 60));
4301 live_ui.camera_bottom_left = "CAM:0 · 640x480";
4302 live_ui.camera_bottom_right = "30fps";
4303 live_ui.show_frozen_overlay =
4304 guardian_armed && guardian->getState() == GuardianState::FROZEN_UNSAFE;
4305 live_ui.frozen_overlay_title = "Unsafe";
4306 live_ui.frozen_overlay_subtitle = "Motion frozen: clear workspace";
4307 live_ui.show_waiting_overlay =
4308 guardian_armed && guardian->getState() == GuardianState::RESET_PENDING;
4309 live_ui.waiting_overlay_text = "Workspace safe: press control to resume";
4310 live_ui.emphasise_danger =
4311 guardian_armed && guardian->getState() == GuardianState::FROZEN_UNSAFE;
4312
4313 cv::Mat camera_frame = display_frame.clone();
4314 drawCameraOverlay(camera_frame, live_ui);
4315
4316 const int status_height = std::max(frameHeight(camera_frame), 720);
4317 if (status_frame.empty() || status_frame.rows != status_height ||
4318 status_frame.cols != kLiveStatusWidth) {
4319 status_frame = cv::Mat(status_height,
4320 kLiveStatusWidth,
4321 CV_8UC3,
4322 cv::Scalar(245, 245, 245));
4323 } else {
4324 status_frame.setTo(cv::Scalar(245, 245, 245));
4325 }
4326 drawStatusDashboard(status_frame, live_ui);
4327
4328 const int metrics_height = std::max(frameHeight(camera_frame), 560);
4329 if (metrics_frame.empty() || metrics_frame.rows != metrics_height ||
4330 metrics_frame.cols != kLiveMetricsWidth) {
4331 metrics_frame = cv::Mat(metrics_height,
4332 kLiveMetricsWidth,
4333 CV_8UC3,
4334 cv::Scalar(245, 245, 245));
4335 } else {
4336 metrics_frame.setTo(cv::Scalar(245, 245, 245));
4337 }
4338 drawMetricsDashboard(metrics_frame, live_ui);
4339
4340 cv::imshow(kLiveCameraWindowName, camera_frame);
4341 cv::imshow(kLiveStatusWindowName, status_frame);
4342 cv::imshow(kLiveMetricsWindowName, metrics_frame);
4343 const int key = cv::waitKey(1);
4344 if (key == 27) {
4345 std::cout << "[LIVE_TEST] Exit requested from display window (esc)."
4346 << std::endl;
4347 break;
4348 }
4349
4350 int normalized_key = key;
4351 if (normalized_key >= 0 && normalized_key <= 255) {
4352 normalized_key = std::tolower(normalized_key);
4353 }
4354
4355 if (normalized_key == ' ') {
4356 if (!requestContinue()) {
4357 break;
4358 }
4359 }
4360
4361 // Handle focus control with + and - keys
4362 if (normalized_key == '+' || normalized_key == '=') {
4363 float current_focus = camera_capture.getFocusPosition();
4364 if (current_focus < 0.0f) {
4365 current_focus = 0.0f; // Start from closest if in autofocus mode
4366 }
4367 float new_focus = std::min(1.0f, current_focus + 0.1f);
4368 camera_capture.setFocusPosition(new_focus);
4369 std::cout << "[LIVE_TEST] Focus position increased to " << std::fixed
4370 << std::setprecision(2) << new_focus << " (0.0=close, 1.0=far)" << std::endl;
4371 } else if (normalized_key == '-' || normalized_key == '_') {
4372 float current_focus = camera_capture.getFocusPosition();
4373 if (current_focus < 0.0f) {
4374 current_focus = 1.0f; // Start from farthest if in autofocus mode
4375 }
4376 float new_focus = current_focus - 0.1f;
4377 if (new_focus < 0.0f) {
4378 new_focus = -1.0f; // Jump to autofocus when going below 0.0
4379 }
4380 camera_capture.setFocusPosition(new_focus);
4381 std::cout << "[LIVE_TEST] Focus position decreased to " << std::fixed
4382 << std::setprecision(2) << new_focus << " ("
4383 << (new_focus < 0.0f ? "autofocus" : "0.0=close, 1.0=far") << ")" << std::endl;
4384 }
4385
4386 std::size_t requested_routine_index = 0;
4387 if (liveRoutineIndexFromKey(normalized_key, requested_routine_index)) {
4388 if (!selectRoutine(requested_routine_index)) {
4389 break;
4390 }
4391 }
4392
4393 if (!getLiveRoutineDefinition(selected_routine_index).auto_progress) {
4394 switch (normalized_key) {
4395 case 'd':
4396 if (!requestManualNudge(-kLiveManualNudgeDegrees, 0, 0, 0,
4397 "BASE LEFT")) {
4398 break;
4399 }
4400 break;
4401 case 'a':
4402 if (!requestManualNudge(kLiveManualNudgeDegrees, 0, 0, 0,
4403 "BASE RIGHT")) {
4404 break;
4405 }
4406 break;
4407 case 'w':
4408 if (!requestManualNudge(0, 0, kLiveManualNudgeDegrees, 0,
4409 "FORWARD")) {
4410 break;
4411 }
4412 break;
4413 case 's':
4414 if (!requestManualNudge(0, 0, -kLiveManualNudgeDegrees, 0,
4415 "BACKWARD")) {
4416 break;
4417 }
4418 break;
4419 case 'i':
4420 if (!requestManualNudge(0, kLiveManualNudgeDegrees, 0, 0, "UP")) {
4421 break;
4422 }
4423 break;
4424 case 'k':
4425 if (!requestManualNudge(0, -kLiveManualNudgeDegrees, 0, 0,
4426 "DOWN")) {
4427 break;
4428 }
4429 break;
4430 case 'l':
4431 if (!requestManualNudge(0, 0, 0, kLiveManualNudgeDegrees, "OPEN")) {
4432 break;
4433 }
4434 break;
4435 case 'j':
4436 if (!requestManualNudge(0, 0, 0, -kLiveManualNudgeDegrees,
4437 "CLOSE")) {
4438 break;
4439 }
4440 break;
4441 default:
4442 break;
4443 }
4444 }
4445 }
4446
4447 capture_stop_requested.store(true, std::memory_order_relaxed);
4448 if (capture_thread.joinable()) {
4449 capture_thread.join();
4450 }
4451 button_stop_requested.store(true, std::memory_order_relaxed);
4452 if (button_thread.joinable()) {
4453 button_thread.join();
4454 }
4455
4456 cv::destroyWindow(kLiveCameraWindowName);
4457 cv::destroyWindow(kLiveStatusWindowName);
4458 stopLiveStepTimer();
4459 motion_controller_.shutdown();
4460
4461 if (!processed_any_frame) {
4462 std::cerr << "[LIVE_TEST] No frames processed. Check camera availability."
4463 << std::endl;
4464 return 1;
4465 }
4466
4467 if (motion_faulted) {
4468 return 1;
4469 }
4470
4471 std::cerr << "[LIVE_TEST] Frame stream ended." << std::endl;
4472 return 0;
4473}
Top-level runtime mode controller for ARGUS.
Camera frame acquisition interface for ARGUS.
Finite-state safety supervisor used by ARGUS.
@ FRAME_BAD
Frame classified as unsafe.
@ FRAME_GOOD
Frame classified as safe.
GuardianState
Internal FSM states.
@ SAFE_MONITORING
Normal operation; monitoring incoming frame status.
@ RESET_PENDING
Operator acknowledged; waiting for stable good frames.
@ FROZEN_UNSAFE
Unsafe condition latched; motion remains blocked.
MotionOutputState
High-level state of motion output availability.
@ UNINITIALISED
Hardware not initialised.
@ DISABLED
Initialised but outputs disabled.
@ FAULT
Fault state; output path blocked until reset/re-init.
@ ENABLED
Outputs enabled and can accept target writes.
Debounced physical operator-button input module.
PhysicalButtonEvent
Semantic operator requests produced by button input.
@ DISARM_REQUEST
Request to disarm/stop controlled motion.
@ ACK_REQUEST
Request acknowledge/continue after freeze.
@ ARM_REQUEST
Request to arm/start controlled motion.
Safety interlock gate between guardian decisions and robot motion.
FreezeReason
Latched freeze/fault reason.
@ UNKNOWN_FAULT
Unspecified fault reason.
@ MARKER_LOST
Vision lost expected marker.
@ POSITION_ERROR
Position-related safety violation.
@ VISION_TIMEOUT
Vision heartbeat/timing failure.
@ DEPTH_EXCEEDED
Forbidden depth-layer/colour detected.
@ NONE
No freeze reason currently latched.
@ MARKER_OUT_OF_ROI
Marker/tool moved outside allowed ROI.
@ WATCHDOG_TIMEOUT
Guardian heartbeat timeout.
InterlockState
Interlock state machine output states.
@ FROZEN
Motion blocked due to safety freeze.
@ FAULT
Motion blocked due to hardware/path fault.
@ SAFE
Motion allowed.
@ ALLOW_MOTION
Attempt to allow motion when safety conditions permit.
@ FREEZE_NOW
Immediately freeze/deny motion.
SafetyState
Outcome of a single vision safety evaluation.
Definition Types.hpp:22
@ OUTSIDE_ALLOWED_ZONE
Vision safety evaluator for the ARGUS pipeline.
Orchestrates runtime modes for ARGUS.
int runMotionSmokeTest(const MotionSmokeTestOptions &options)
Run motion-only smoke tests using the selected joint scope.
int runMotionHomePose()
Move the arm to the configured home pose and exit.
SmokeJoint
Joint selector used by motion smoke-test mode.
@ Base
Run smoke test for base joint only.
@ Upper
Run smoke test for upper joint only.
@ All
Run the full all-joint smoke sequence.
@ Lower
Run smoke test for lower joint only.
@ Grip
Run smoke test for gripper joint only.
int runCameraBackendCheck(const LiveTestOptions &options)
Run camera backend validation/check mode.
int runInteractiveServoConsole()
Run interactive servo console mode.
int runLiveMarkerTest(const LiveTestOptions &options)
Run live safety supervision mode.
int runServoCalibration()
Run interactive raw-pulse servo calibration mode.
int runButtonTest()
Run physical-button diagnostics.
AppController() noexcept
Construct the application controller.
Camera capture facade with pluggable backend implementations.
BackendPreference
Backend selection policy.
@ Auto
Auto-select backend based on platform/runtime constraints.
@ OpenCvVideoCapture
Force OpenCV VideoCapture backend.
@ Libcamera2OpenCv
Force libcamera2opencv callback backend.
std::string backendImplementation() const
Return active backend implementation family.
Event-driven safety state machine with callback hooks.
void operatorAcknowledge()
Inject an operator acknowledge event.
void setOnStateChangeCallback(std::function< void(GuardianState, GuardianState)> callback)
Register callback fired on state transitions.
void setOnClearFreezeCallback(std::function< void()> callback)
Register callback fired when freeze is cleared.
void setOnFreezeCallback(std::function< void()> callback)
Register callback fired when freeze is commanded.
void processFrame(FrameStatus status)
Convenience wrapper that maps frame status to an FSM event.
void printStatus() const
Print a status snapshot for debugging.
Low-level motion output driver for the PCA9685 servo board.
const char * lastErrorString() const noexcept
Get human-readable last error string.
bool initialise(const char *i2c_device_path="/dev/i2c-1", std::uint8_t device_address=0x40, float pwm_frequency_hz=50.0f, MotionChannelMap channel_map={}) noexcept
Initialise PCA9685 output path.
bool setTargets(const MeArmJointTargets &targets) noexcept
Write joint targets to hardware when motion is enabled.
void shutdown() noexcept
Disable outputs and close hardware resources.
static constexpr std::uint16_t kMaxPulseTicks
Maximum 12-bit pulse value.
bool enable() noexcept
Enable motion output after successful initialisation.
GPIO character-device based physical button module.
static const char * eventToString(PhysicalButtonEvent event) noexcept
Convert semantic event enum to readable string.
const char * statusString() const noexcept
Get compact status summary string.
bool available() const noexcept
Check whether at least one input channel is available.
const char * lastErrorString() const noexcept
Get last error string.
bool waitForEvent(PhysicalButtonEvent &event, std::chrono::milliseconds timeout) noexcept
Wait for next debounced event using blocking GPIO edge reads.
Abstract hardware contract used by RobotInterlock.
virtual bool freezeMotion() noexcept=0
Immediately stop motion output.
virtual bool enableMotion() noexcept=0
Re-enable motion output after safe/ack conditions.
SafetyResult process(const cv::Mat &frame, std::chrono::steady_clock::time_point captureTimestamp)
Processes a single camera frame and evaluates tool safety.
void updateConfig(const VisionConfig &newConfig)
Updates the safety configuration at runtime.
Options used by live test and camera backend check modes.
bool auto_ack
Auto-send acknowledge after freeze for testing.
CameraCapture::BackendPreference backend_preference
Requested camera backend policy.
int expected_marker_id
Expected marker ID (legacy CLI option).
int camera_index
Camera index passed to CameraCapture.
Options for the motion smoke-test mode.
SmokeJoint joint
Joint subset to smoke-test.
Construction options for camera capture.
Single captured frame and its timing metadata.
cv::Mat image_data
Captured image.
std::chrono::steady_clock::time_point capture_timestamp
Monotonic capture time.
Raw PWM tick targets for each MeArm joint.
std::uint16_t upper_ticks
Upper target pulse ticks.
std::uint16_t lower_ticks
Lower target pulse ticks.
std::uint16_t base_ticks
Base target pulse ticks.
std::uint16_t gripper_ticks
Gripper target pulse ticks.
Mapping of logical MeArm joints to PCA9685 channels.
std::uint8_t base
Base joint PWM channel.
Output of VisionProcessor::process() for each camera frame.
Definition Types.hpp:51
SafetyState state
The safety decision made for this frame.
Definition Types.hpp:59
std::chrono::microseconds processing_time
How long VisionProcessor took to process this frame.
Definition Types.hpp:76
std::chrono::steady_clock::time_point timestamp
The time at which this result was produced (end of processing).
Definition Types.hpp:67
All tunable safety thresholds and zone boundaries for VisionProcessor.
int depthValMax
Maximum value threshold.
int depthHueLower1
Lower hue bound for pink/magenta.
int depthPixelThreshold
Minimum number of HSV-matching pixels required to confirm the forbidden layer is exposed.
int depthHueUpper2
Disabled - see depthHueLower2 above.
int expectedMarkerId
The ArUco marker ID the system expects to detect.
int depthHueLower2
Disabled - lower > upper produces empty mask2.
int depthSatMax
Maximum saturation threshold.
int depthHueUpper1
Upper hue bound for pink/magenta.