9#include "CppTimerStdFuncCallback.h"
22#include <condition_variable>
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;
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;
99class MotionControllerHardwareAdapter final :
public RobotHardware {
101 explicit MotionControllerHardwareAdapter(
MotionController& motion_controller) noexcept
102 : motion_controller_(motion_controller) {}
105 motion_controller_.freeze();
107 std::cerr <<
"[MOTION] freezeMotion() failed: "
108 << motion_controller_.lastErrorString() << std::endl;
115 if (!motion_controller_.enable()) {
116 std::cerr <<
"[MOTION] enableMotion() failed: "
117 << motion_controller_.lastErrorString() << std::endl;
127void handleInteractiveServoSignal(
int) {
128 g_interactive_servo_stop_requested = 1;
131bool waitForCppTimerDelay(std::chrono::nanoseconds delay,
132 std::string& error_message) {
133 if (delay.count() <= 0) {
134 error_message.clear();
139 std::condition_variable condition;
142 CppTimerCallback timer;
143 timer.registerEventCallback([&]() {
144 std::lock_guard<std::mutex> lock(mutex);
146 condition.notify_one();
150 timer.startns(
static_cast<long>(delay.count()), ONESHOT);
151 }
catch (
const char* exception) {
152 error_message = exception;
155 error_message =
"CppTimer start failed";
159 std::unique_lock<std::mutex> lock(mutex);
160 condition.wait(lock, [&]() {
return fired; });
162 error_message.clear();
166double computeFocusScore(
const cv::Mat& image) {
172 if (image.channels() == 1) {
175 cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
179 cv::Laplacian(gray, laplacian, CV_64F);
182 cv::meanStdDev(laplacian, mean, stddev);
183 return stddev[0] * stddev[0];
186const char* focusQualityLabel(
double focus_score) {
187 if (focus_score < 60.0) {
190 if (focus_score < 180.0) {
196std::string formatFocusScore(
double focus_score) {
197 std::ostringstream oss;
198 oss << std::fixed << std::setprecision(1) << focus_score;
202int normaliseHue(
int hue) {
203 int value = hue % 180;
210int hueBandMidpoint(
int lower,
int upper) {
211 const int l = normaliseHue(lower);
212 const int u = normaliseHue(upper);
217 const int wrapped_span = (u + 180) - l;
218 return normaliseHue(l + (wrapped_span / 2));
221std::string describeHueBand(
int lower,
int upper) {
222 const int l = normaliseHue(lower);
223 const int u = normaliseHue(upper);
225 return std::to_string(l) +
"-" + std::to_string(u);
227 return std::to_string(l) +
"-179 + 0-" + std::to_string(u);
230std::string hueFamilyName(
int hue_midpoint) {
231 const int h = normaliseHue(hue_midpoint);
232 if (h <= 8 || h >= 170) {
253 return "Pink/Magenta";
256std::string describeForbiddenColour(
const VisionConfig& config) {
258 return hueFamilyName(hue_mid);
261std::string describeForbiddenColourThresholds(
const VisionConfig& config) {
263 " | S " + std::to_string(config.
depthSatMin) +
"-" +
270cv::Scalar forbiddenColourSwatchBgr(
const VisionConfig& config) {
277 cv::Mat hsv(1, 1, CV_8UC3, cv::Scalar(hue, sat, val));
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]);
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;
293enum class ControllerEventKind {
301enum class ControllerEventDisposition {
307struct ControllerEvent {
308 ControllerEventKind kind;
309 std::optional<PhysicalButtonEvent> button_event;
310 std::optional<FrameEvent> frame_event;
313class ControllerEventQueue {
316 std::lock_guard<std::mutex> lock(mutex_);
317 queue_.push_back(ControllerEvent{
318 ControllerEventKind::ButtonInput,
322 condition_.notify_one();
325 void pushDemoStepReady() {
326 std::lock_guard<std::mutex> lock(mutex_);
327 if (demo_step_pending_) {
330 demo_step_pending_ =
true;
331 queue_.push_back(ControllerEvent{
332 ControllerEventKind::DemoStepReady,
336 condition_.notify_one();
339 void pushLiveStepReady() {
340 std::lock_guard<std::mutex> lock(mutex_);
341 if (live_step_pending_) {
344 live_step_pending_ =
true;
345 queue_.push_back(ControllerEvent{
346 ControllerEventKind::LiveStepReady,
350 condition_.notify_one();
354 std::lock_guard<std::mutex> lock(mutex_);
355 queue_.push_back(ControllerEvent{
356 ControllerEventKind::FrameAvailable,
358 std::move(frame_event),
360 condition_.notify_one();
363 void pushFrameCaptureFailed() {
364 std::lock_guard<std::mutex> lock(mutex_);
365 queue_.push_back(ControllerEvent{
366 ControllerEventKind::FrameCaptureFailed,
370 condition_.notify_one();
373 bool waitForEvents(std::chrono::milliseconds timeout) {
374 std::unique_lock<std::mutex> lock(mutex_);
375 if (!queue_.empty()) {
379 if (timeout.count() <= 0) {
383 return condition_.wait_for(lock, timeout, [&]() {
return !queue_.empty(); });
386 template <
typename Handler>
387 bool drain(Handler&& handler) {
388 std::deque<ControllerEvent> drained;
390 std::lock_guard<std::mutex> lock(mutex_);
391 drained.swap(queue_);
392 demo_step_pending_ =
false;
393 live_step_pending_ =
false;
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) {
403 if (disposition == ControllerEventDisposition::Deferred) {
404 deferred.push_back(event);
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);
415 case ControllerEventKind::DemoStepReady:
416 if (!demo_step_pending_) {
417 demo_step_pending_ =
true;
418 queue_.push_back(event);
421 case ControllerEventKind::LiveStepReady:
422 if (!live_step_pending_) {
423 live_step_pending_ =
true;
424 queue_.push_back(event);
427 case ControllerEventKind::FrameAvailable:
428 case ControllerEventKind::FrameCaptureFailed:
429 queue_.push_back(event);
433 condition_.notify_one();
441 std::condition_variable condition_;
442 std::deque<ControllerEvent> queue_;
443 bool demo_step_pending_ =
false;
444 bool live_step_pending_ =
false;
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)
453std::string formatLatencyMilliseconds(
const std::optional<long long>& value) {
454 return value.has_value() ? std::to_string(*value) :
"N/A";
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)
466 << formatLatencyMilliseconds(metrics.freeze_cmd_ms)
468 << formatLatencyMilliseconds(metrics.total_stop_ms)
469 <<
" ack_to_resume_ms="
470 << formatLatencyMilliseconds(metrics.ack_to_resume_ms)
474int uiPlainFontFace() {
475#ifdef CV_VERSION_MAJOR
476 return cv::FONT_HERSHEY_PLAIN;
478 return cv::FONT_HERSHEY_SIMPLEX;
482int frameWidth(
const cv::Mat& frame) {
483#ifdef CV_VERSION_MAJOR
491int frameHeight(
const cv::Mat& frame) {
492#ifdef CV_VERSION_MAJOR
500cv::Mat makePlaceholderFrame(
int width,
int height) {
501#ifdef CV_VERSION_MAJOR
502 return cv::Mat::zeros(height, width, CV_8UC3);
510void drawRectangle(cv::Mat& frame,
512 cv::Point bottom_right,
513 const cv::Scalar& color,
515#ifdef CV_VERSION_MAJOR
516 cv::rectangle(frame, top_left, bottom_right, color, thickness);
526void drawLine(cv::Mat& frame,
529 const cv::Scalar& color,
531#ifdef CV_VERSION_MAJOR
532 cv::line(frame, from, to, color, thickness);
542struct SupervisoryUiRow {
545 cv::Scalar value_color;
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;
586const char* safetyStateToUiString(
SafetyState state) {
591 return "Tool not detected";
593 return "Outside allowed zone";
595 return "Excessive speed";
597 return "Invalid orientation";
599 return "Depth exceeded";
608 return "Safe (monitoring)";
610 return "Frozen (unsafe)";
612 return "Reset (pending)";
631const char* freezeReasonToUiString(
FreezeReason reason) {
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";
649 return "Unknown fault";
653bool isFreezeReasonActiveForUi(
const std::string& freeze_reason_text) {
654 return freeze_reason_text !=
"N/A" && freeze_reason_text !=
"None" &&
655 freeze_reason_text !=
"NONE";
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);
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);
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);
678 return cv::Scalar(25, 25, 25);
681void drawPanel(cv::Mat& frame,
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);
690const cv::Mat& argusLogoImage() {
692 static bool loaded =
false;
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",
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);
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) {
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));
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);
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) {
740 cv::Mat dst_roi = frame(cv::Rect(draw_x, draw_y, draw_w, draw_h));
741 if (resized_logo.channels() == 4) {
743 cv::cvtColor(resized_logo, src_bgr, cv::COLOR_BGRA2BGR);
745 std::vector<cv::Mat> channels;
746 cv::split(resized_logo, channels);
748 channels[3].convertTo(alpha_f, CV_32FC1, 1.0 / 255.0);
752 src_bgr.convertTo(src_f, CV_32FC3, 1.0 / 255.0);
753 dst_roi.convertTo(dst_f, CV_32FC3, 1.0 / 255.0);
756 std::vector<cv::Mat> alpha_channels(3, alpha_f);
757 cv::merge(alpha_channels, alpha3);
759 src_f.mul(alpha3) + dst_f.mul(cv::Scalar::all(1.0) - alpha3);
760 blended.convertTo(dst_roi, CV_8UC3, 255.0);
764 if (resized_logo.channels() == 3) {
765 resized_logo.copyTo(dst_roi);
772void drawSparkline(cv::Mat& frame,
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);
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()) {
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);
795 if (max_value - min_value < 1e-6) {
796 max_value = min_value + 1.0;
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));
815 cv::Point(x_prev, y_prev),
816 cv::Point(x_curr, y_curr),
822void drawSupervisoryGui(cv::Mat& frame,
const SupervisoryUiModel& model) {
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;
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);
851 cv::Point(width - 1, header_height),
855 cv::Point(0, header_height),
856 cv::Point(width - 1, header_height + state_bar_height),
860 cv::Point(0, panel_top),
861 cv::Point(left_x2, panel_bottom),
865 cv::Point(right_x1, panel_top),
866 cv::Point(width - 1, panel_bottom),
870 cv::Point(camera_x1, camera_y1),
871 cv::Point(camera_x2, camera_y2),
880 if (!drawArgusLogo(frame, cv::Rect(12, 6, 29, 27))) {
884 cv::FONT_HERSHEY_SIMPLEX,
893 cv::FONT_HERSHEY_SIMPLEX,
900 cv::FONT_HERSHEY_SIMPLEX,
906 cv::Point(width - 135, 20),
909 cv::Scalar(60, 170, 80),
913 cv::Point(width - 135, 34),
921 cv::Point(16, header_height + 20),
927 model.state_description,
928 cv::Point(84, header_height + 20),
929 cv::FONT_HERSHEY_SIMPLEX,
934 "Control: " + model.operator_prompt,
935 cv::Point(width - 240, header_height + 20),
936 cv::FONT_HERSHEY_SIMPLEX,
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) {
948 cv::Point(card_margin, card_y),
949 cv::Point(card_margin + card_width, card_y + height_px),
952 painter(card_margin, card_y);
953 card_y += height_px + 10;
956 drawLeftCard(82, model.state_color, [&](
int x,
int y0) {
959 cv::Point(x + 12, y0 + 16),
960 cv::FONT_HERSHEY_SIMPLEX,
966 cv::Point(x + 12, y0 + 48),
967 cv::FONT_HERSHEY_SIMPLEX,
972 model.state_description,
973 cv::Point(x + 12, y0 + 66),
974 cv::FONT_HERSHEY_SIMPLEX,
980 drawLeftCard(62, model.motion_color, [&](
int x,
int y0) {
983 cv::Point(x + 12, y0 + 16),
984 cv::FONT_HERSHEY_SIMPLEX,
990 cv::Point(x + 12, y0 + 44),
997 drawLeftCard(62, panel_border, [&](
int x,
int y0) {
1000 cv::Point(x + 12, y0 + 16),
1001 cv::FONT_HERSHEY_SIMPLEX,
1007 cv::Point(x + 12, y0 + 44),
1008 cv::FONT_HERSHEY_SIMPLEX,
1014 if (isFreezeReasonActiveForUi(model.freeze_reason)) {
1015 drawLeftCard(62, cv::Scalar(60, 60, 200), [&](
int x,
int y0) {
1018 cv::Point(x + 12, y0 + 16),
1019 cv::FONT_HERSHEY_SIMPLEX,
1021 cv::Scalar(60, 60, 200),
1024 model.freeze_reason,
1025 cv::Point(x + 12, y0 + 44),
1028 severityColor(model.freeze_reason),
1034 model.camera_hud_text,
1035 cv::Point(camera_x1 + 12, camera_y1 + 20),
1038 model.camera_hud_color,
1041 "FOCUS " + model.focus_label,
1042 cv::Point(camera_x2 - 145, camera_y1 + 20),
1048 model.camera_bottom_left,
1049 cv::Point(camera_x1 + 12, camera_y2 - 10),
1055 model.camera_bottom_right,
1056 cv::Point(camera_x2 - 90, camera_y2 - 10),
1062 int rx = right_x1 + 12;
1063 int ry = panel_top + 18;
1064 auto drawRightSectionTitle = [&](
const std::string& title) {
1068 cv::FONT_HERSHEY_SIMPLEX,
1074 cv::Point(right_x1 + 10, ry),
1075 cv::Point(width - 12, ry),
1081 drawRightSectionTitle(
"Subsystems");
1083 for (
const auto& row : model.status_rows) {
1087 cv::FONT_HERSHEY_SIMPLEX,
1102 if (model.show_focus) {
1104 drawRightSectionTitle(
"FOCUS");
1107 cv::Point(width - 24, ry + 16),
1108 cv::Scalar(235, 235, 235),
1110 const int bar_width = std::max(
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),
1133 drawRightSectionTitle(
"LATENCY");
1135 "Time to process frame: " + std::to_string(model.latency.vision_us) +
" us",
1143 "Time to detect unsafe: " +
1144 formatLatencyMilliseconds(model.latency.unsafe_detect_ms),
1152 "Time to stop motion: " +
1153 formatLatencyMilliseconds(model.latency.total_stop_ms),
1157 severityColor(formatLatencyMilliseconds(model.latency.total_stop_ms)),
1162 formatLatencyMilliseconds(model.latency.freeze_cmd_ms),
1171 formatLatencyMilliseconds(model.latency.ack_to_resume_ms),
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),
1185 model.frozen_overlay_title,
1186 cv::Point(camera_x1 + 80, camera_y1 + 110),
1187 cv::FONT_HERSHEY_SIMPLEX,
1189 cv::Scalar(60, 60, 200),
1192 model.frozen_overlay_subtitle,
1193 cv::Point(camera_x1 + 70, camera_y1 + 140),
1194 cv::FONT_HERSHEY_SIMPLEX,
1196 cv::Scalar(60, 60, 200),
1198 }
else if (model.show_waiting_overlay) {
1200 cv::Point(camera_x1 + 80, camera_y2 - 60),
1201 cv::Point(camera_x2 - 80, camera_y2 - 28),
1203 cv::Scalar(50, 180, 230));
1205 model.waiting_overlay_text,
1206 cv::Point(camera_x1 + 92, camera_y2 - 38),
1209 cv::Scalar(50, 180, 230),
1215 cv::Point(16, height - 14),
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,
1226 cv::Point(width - 3, height - 3),
1231void drawCameraOverlay(cv::Mat& frame,
const SupervisoryUiModel& model) {
1232 if (frame.empty()) {
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);
1242 model.camera_hud_text,
1246 model.camera_hud_color,
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);
1253 cv::Point(focus_x, 24),
1261 model.camera_bottom_left,
1262 cv::Point(12, height - 12),
1268 model.camera_bottom_right,
1269 cv::Point(std::max(12, width - 90), height - 12),
1275 if (model.show_frozen_overlay) {
1276 drawRectangle(frame,
1278 cv::Point(width - 7, height - 7),
1279 cv::Scalar(60, 60, 200),
1281 const int title_y = std::max(80, height / 3);
1283 model.frozen_overlay_title,
1284 cv::Point(24, title_y),
1285 cv::FONT_HERSHEY_SIMPLEX,
1287 cv::Scalar(60, 60, 200),
1290 model.frozen_overlay_subtitle,
1291 cv::Point(24, title_y + 34),
1292 cv::FONT_HERSHEY_SIMPLEX,
1294 cv::Scalar(60, 60, 200),
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);
1302 cv::Point(x1 + box_w, y1 + 34),
1304 cv::Scalar(50, 180, 230));
1306 model.waiting_overlay_text,
1307 cv::Point(x1 + 10, y1 + 22),
1310 cv::Scalar(50, 180, 230),
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,
1319 cv::Point(width - 3, height - 3),
1324void drawStatusDashboard(cv::Mat& frame,
const SupervisoryUiModel& model) {
1325 if (frame.empty()) {
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);
1340 frame.setTo(panel_fill);
1343 cv::Point(width - 1, header_height),
1347 cv::Point(0, header_height),
1348 cv::Point(width - 1, header_height + state_bar_height),
1357 if (!drawArgusLogo(frame, cv::Rect(10, 8, 29, 27))) {
1361 cv::FONT_HERSHEY_SIMPLEX,
1370 cv::FONT_HERSHEY_SIMPLEX,
1375 "Safety supervisor",
1377 cv::FONT_HERSHEY_SIMPLEX,
1383 cv::Point(width - 124, 31),
1391 cv::Point(12, header_height + 22),
1397 model.state_description,
1398 cv::Point(92, header_height + 22),
1399 cv::FONT_HERSHEY_SIMPLEX,
1404 "Control: " + model.operator_prompt,
1405 cv::Point(12, header_height + 34),
1406 cv::FONT_HERSHEY_SIMPLEX,
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) {
1418 cv::Point(card_margin, card_y),
1419 cv::Point(card_margin + card_width, card_y + height_px),
1422 painter(card_margin, card_y);
1423 card_y += height_px + 8;
1426 drawCard(76, model.state_color, [&](
int x,
int y0) {
1429 cv::Point(x + 10, y0 + 15),
1430 cv::FONT_HERSHEY_SIMPLEX,
1436 cv::Point(x + 10, y0 + 44),
1437 cv::FONT_HERSHEY_SIMPLEX,
1442 model.state_description,
1443 cv::Point(x + 10, y0 + 62),
1444 cv::FONT_HERSHEY_SIMPLEX,
1450 drawCard(56, model.motion_color, [&](
int x,
int y0) {
1453 cv::Point(x + 10, y0 + 15),
1454 cv::FONT_HERSHEY_SIMPLEX,
1460 cv::Point(x + 10, y0 + 40),
1467 drawCard(56, panel_border, [&](
int x,
int y0) {
1470 cv::Point(x + 10, y0 + 15),
1471 cv::FONT_HERSHEY_SIMPLEX,
1477 cv::Point(x + 10, y0 + 40),
1478 cv::FONT_HERSHEY_SIMPLEX,
1484 const bool freeze_reason_active = isFreezeReasonActiveForUi(model.freeze_reason);
1486 freeze_reason_active ? cv::Scalar(60, 60, 200) : panel_border,
1487 [&](int x, int y0) {
1490 cv::Point(x + 10, y0 + 15),
1491 cv::FONT_HERSHEY_SIMPLEX,
1493 freeze_reason_active ? cv::Scalar(60, 60, 200) : muted_text,
1496 model.freeze_reason,
1497 cv::Point(x + 10, y0 + 40),
1500 freeze_reason_active ? severityColor(model.freeze_reason)
1505 drawCard(72, panel_border, [&](
int x,
int y0) {
1508 cv::Point(x + 10, y0 + 15),
1509 cv::FONT_HERSHEY_SIMPLEX,
1514 cv::Point(x + 10, y0 + 24),
1515 cv::Point(x + 44, y0 + 54),
1516 model.forbidden_colour_swatch,
1519 model.forbidden_colour_label,
1520 cv::Point(x + 54, y0 + 39),
1526 model.forbidden_colour_thresholds,
1527 cv::Point(x + 10, y0 + 66),
1528 cv::FONT_HERSHEY_SIMPLEX,
1534 int rx = card_margin + 2;
1535 int ry = card_y + 12;
1536 auto drawSectionTitle = [&](
const std::string& title) {
1540 cv::FONT_HERSHEY_SIMPLEX,
1546 cv::Point(card_margin, ry),
1547 cv::Point(width - card_margin, ry),
1553 drawSectionTitle(
"Subsystems");
1554 for (
const auto& row : model.status_rows) {
1558 cv::FONT_HERSHEY_SIMPLEX,
1573 const cv::Scalar border_color =
1574 model.emphasise_danger ? cv::Scalar(60, 60, 200) : model.state_color;
1575 drawRectangle(frame,
1577 cv::Point(width - 3, height - 3),
1579 model.emphasise_danger ? 4 : 2);
1582void drawMetricsDashboard(cv::Mat& frame,
const SupervisoryUiModel& model) {
1583 if (frame.empty()) {
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);
1600 frame.setTo(panel_fill);
1603 cv::Point(width - 1, header_height),
1607 cv::Point(0, header_height),
1608 cv::Point(width - 1, header_height + state_bar_height),
1617 if (!drawArgusLogo(frame, cv::Rect(10, 8, 29, 27))) {
1621 cv::FONT_HERSHEY_SIMPLEX,
1630 cv::FONT_HERSHEY_SIMPLEX,
1637 cv::FONT_HERSHEY_SIMPLEX,
1643 cv::Point(width - 124, 31),
1650 "Focus and safety timing",
1651 cv::Point(12, header_height + 22),
1657 "Live signal and event timing",
1658 cv::Point(12, header_height + 34),
1659 cv::FONT_HERSHEY_SIMPLEX,
1664 const int card_margin = 12;
1665 const int card_width = width - (2 * card_margin);
1669 cv::Point(card_margin, y),
1670 cv::Point(card_margin + card_width, y + 62),
1675 cv::Point(card_margin + 10, y + 15),
1676 cv::FONT_HERSHEY_SIMPLEX,
1681 cv::Point(card_margin + 10, y + 22),
1682 cv::Point(width - 22, y + 38),
1683 cv::Scalar(235, 235, 235),
1685 const int bar_width = std::max(
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),
1697 model.show_focus ? model.focus_label :
"N/A",
1698 cv::Point(card_margin + 10, y + 56),
1701 model.show_focus ? model.focus_color : muted_text,
1706 "Vision processing",
1707 cv::Point(card_margin + 2, y + 8),
1708 cv::FONT_HERSHEY_SIMPLEX,
1714 cv::Point(card_margin, y),
1715 cv::Point(width - card_margin, y),
1720 cv::Point(card_margin, y),
1721 cv::Point(card_margin + card_width, y + 54),
1725 "Time to process frame: " + std::to_string(model.latency.vision_us) +
" us",
1726 cv::Point(card_margin + 10, y + 16),
1731 drawSparkline(frame,
1732 cv::Point(card_margin + 10, y + 22),
1733 cv::Point(width - 20, y + 46),
1734 model.vision_latency_history_us,
1740 cv::Point(card_margin + 2, y + 8),
1741 cv::FONT_HERSHEY_SIMPLEX,
1747 cv::Point(card_margin, y),
1748 cv::Point(width - card_margin, y),
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;
1763 cv::Point(x2, y + card_height),
1767 const std::string label_with_target =
1768 label +
" (target: <= " + std::to_string(good_threshold_ms) +
" ms)";
1771 cv::Point(x1 + 10, y + 15),
1772 cv::FONT_HERSHEY_SIMPLEX,
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;
1782 cv::Point(bar_x1, bar_y1),
1783 cv::Point(bar_x2, bar_y2),
1784 cv::Scalar(235, 235, 235),
1787 cv::Scalar status_color = muted_text;
1788 std::string status_text =
"No event";
1789 std::string value_text =
"N/A";
1792 if (value_ms.has_value()) {
1793 const long long value = *value_ms;
1794 value_text = std::to_string(value) +
" ms";
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;
1803 status_text =
"Slow";
1804 status_color = slow_color;
1807 const double fraction =
1808 std::clamp(
static_cast<double>(value) /
1809 static_cast<double>(moderate_threshold_ms),
1812 fill_width = std::max(
1814 static_cast<int>((bar_x2 - bar_x1 - 2) * fraction));
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),
1826 value_text +
" " + status_text,
1827 cv::Point(x1 + 10, y + 57),
1833 y += card_height + 8;
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);
1840 const cv::Scalar border_color =
1841 model.emphasise_danger ? cv::Scalar(60, 60, 200) : model.state_color;
1842 drawRectangle(frame,
1844 cv::Point(width - 3, frameHeight(frame) - 3),
1846 model.emphasise_danger ? 4 : 2);
1849const char* safetyStateToString(
SafetyState state) {
1854 return "TOOL_NOT_DETECTED";
1856 return "OUTSIDE_ALLOWED_ZONE";
1858 return "EXCESSIVE_SPEED";
1860 return "INVALID_ORIENTATION";
1862 return "DEPTH_EXCEEDED";
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";
1903 return "UNKNOWN_FAULT";
1907const char* cameraBackendPreferenceToString(
1909 switch (preference) {
1915 return "libcamera2opencv";
1936 return "UNINITIALISED";
1948struct SmokeJointOffsets {
1955struct SmokeJointSpec {
1956 const char* logical_name;
1957 const char* mearm_name;
1958 const char* visual_check;
1959 std::uint8_t channel;
1964struct SmokeJointRunPlan {
1965 std::array<std::size_t, MotionController::kServoCount> indices{};
1966 std::size_t count{0};
1969struct JointPulseCalibration {
1970 std::uint16_t neg90_ticks;
1971 std::uint16_t zero_ticks;
1972 std::uint16_t pos90_ticks;
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};
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},
1991constexpr std::array<JointPulseCalibration, MotionController::kServoCount>
1992 kJointPulseCalibration = {{
1999constexpr SmokeJointOffsets kSmokeHomePose{0, 0, 0, 0};
2000constexpr SmokeJointOffsets kSurgeryRetractPose{
2001 0, 35, -30, kSurgeryGripHoldOffset};
2003struct DemoPoseStep {
2005 SmokeJointOffsets offsets;
2008struct LiveRoutineDefinition {
2011 const DemoPoseStep* steps;
2012 std::size_t step_count;
2016constexpr DemoPoseStep kDemoHomeStep{
"Home", kSmokeHomePose};
2017constexpr DemoPoseStep kSurgeryRetractStep{
"Retract (safe)", kSurgeryRetractPose};
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}},
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}},
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}},
2049LiveRoutineDefinition getLiveRoutineDefinition(std::size_t index) {
2054 kLiveSurgeryCutSequence.data(),
2055 kLiveSurgeryCutSequence.size(),
2060 kLiveBaseScanSequence.data(),
2061 kLiveBaseScanSequence.size(),
2066 kLiveGripPulseSequence.data(),
2067 kLiveGripPulseSequence.size(),
2071 return {0,
"Manual",
nullptr, 0,
false};
2075bool liveRoutineIndexFromKey(
int key, std::size_t& index) {
2094std::string toLowerCopy(std::string text);
2113const char* smokeJointIndexToString(std::size_t index) {
2128bool smokeJointIndexFromName(
const std::string& name, std::size_t& index) {
2129 const std::string lower = toLowerCopy(name);
2130 if (lower ==
"base") {
2134 if (lower ==
"lower") {
2138 if (lower ==
"upper") {
2142 if (lower ==
"grip" || lower ==
"gripper") {
2150 SmokeJointRunPlan plan{};
2154 plan.indices[0] = 0;
2158 plan.indices[0] = 1;
2162 plan.indices[0] = 2;
2166 plan.indices[0] = 3;
2171 plan.indices[0] = 0;
2172 plan.indices[1] = 1;
2173 plan.indices[2] = 2;
2174 plan.indices[3] = 3;
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) {
2190SmokeJointOffsets clampOffsetsToWindow(
const SmokeJointOffsets& requested,
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);
2208SmokeJointOffsets clampSmokeOffsets(
const SmokeJointOffsets& requested,
2210 return clampOffsetsToWindow(requested,
2211 kSmokeBaseMinOffset,
2212 kSmokeBaseMaxOffset,
2213 kSmokeLowerMinOffset,
2214 kSmokeLowerMaxOffset,
2215 kSmokeUpperMinOffset,
2216 kSmokeUpperMaxOffset,
2217 kSmokeGripMinOffset,
2218 kSmokeGripMaxOffset,
2222SmokeJointOffsets clampDemoOffsets(
const SmokeJointOffsets& requested,
2224 return clampOffsetsToWindow(requested,
2227 kDemoLowerMinOffset,
2228 kDemoLowerMaxOffset,
2229 kDemoUpperMinOffset,
2230 kDemoUpperMaxOffset,
2236std::uint16_t logicalAngleToPulseTicks(std::size_t joint_index,
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) -
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(
2253 if (bounded != raw) {
2256 return static_cast<std::uint16_t
>(bounded);
2259std::uint16_t clampPulseTicks(
int raw_ticks,
bool& clamped) {
2260 const int bounded = std::clamp(raw_ticks,
2263 if (bounded != raw_ticks) {
2266 return static_cast<std::uint16_t
>(bounded);
2271 const SmokeJointOffsets bounded = clampSmokeOffsets(requested, clamped);
2273 logicalAngleToPulseTicks(0, bounded.base, clamped),
2274 logicalAngleToPulseTicks(1, bounded.lower, clamped),
2275 logicalAngleToPulseTicks(2, bounded.upper, clamped),
2276 logicalAngleToPulseTicks(3, bounded.grip, clamped)};
2281 const SmokeJointOffsets bounded = clampDemoOffsets(requested, clamped);
2283 logicalAngleToPulseTicks(0, bounded.base, clamped),
2284 logicalAngleToPulseTicks(1, bounded.lower, clamped),
2285 logicalAngleToPulseTicks(2, bounded.upper, clamped),
2286 logicalAngleToPulseTicks(3, bounded.grip, clamped)};
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
2300 std::ostringstream oss;
2309std::uint16_t pulseForJoint(
const MeArmJointTargets& targets, std::size_t index) {
2326 std::uint16_t ticks) {
2345bool parseCalibrationPoint(
const std::string& text,
2347 int& nominal_degrees) {
2348 const std::string lower = toLowerCopy(text);
2349 if (lower ==
"-90" || lower ==
"neg90" || lower ==
"minus90") {
2351 nominal_degrees = -90;
2354 if (lower ==
"0" || lower ==
"zero" || lower ==
"center" || lower ==
"centre") {
2356 nominal_degrees = 0;
2359 if (lower ==
"+90" || lower ==
"90" || lower ==
"pos90" || lower ==
"plus90") {
2361 nominal_degrees = 90;
2367std::string formatCalibrationMarks(std::size_t joint_index,
2368 const JointCalibrationMarks& marks) {
2369 std::ostringstream oss;
2370 oss << smokeJointIndexToString(joint_index) <<
": "
2372 if (marks.has_neg90) {
2373 oss << marks.neg90_ticks;
2378 if (marks.has_zero) {
2379 oss << marks.zero_ticks;
2384 if (marks.has_pos90) {
2385 oss << marks.pos90_ticks;
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";
2404std::string toLowerCopy(std::string text) {
2405 std::transform(text.begin(),
2408 [](
unsigned char ch) {
2409 return static_cast<char>(std::tolower(ch));
2423 std::cout <<
">>> ROBOTIC ARM: Emergency stop activated! <<<" << std::endl;
2426 std::cout <<
">>> ROBOTIC ARM: Motion resumed, system operational <<<"
2430 std::cout <<
">>> STATE CHANGE NOTIFICATION: System transitioned <<<"
2434 std::cout <<
"\n========== GUARDIAN STATE MACHINE TEST ==========\n"
2437 std::cout <<
"Scenario 1: Normal Operation" << std::endl;
2442 std::cout <<
"Scenario 2: Single Bad Frame" << std::endl;
2447 std::cout <<
"Scenario 3: freezeCount Consecutive Bad Frames (Freeze)"
2453 std::cout <<
"Scenario 4: Frames During Frozen State" << std::endl;
2458 std::cout <<
"Scenario 5: Operator Acknowledgment/Reset" << std::endl;
2462 std::cout <<
"Scenario 6: Bad Frame During Reset" << std::endl;
2467 std::cout <<
"Scenario 7: recoverCount Consecutive Good Frames (Clear Freeze)"
2474 std::cout <<
"========== TEST COMPLETE ==========\n" << std::endl;
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");
2485 std::cout <<
" (" << button_module.
statusString() <<
")";
2489 std::cout << std::endl;
2497 if (button_module.
waitForEvent(event, std::chrono::milliseconds(250))) {
2498 std::cout <<
"[BUTTON_TEST] event="
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";
2514 if (!motion_controller_.
initialise(kDefaultI2cDevicePath,
2515 kDefaultPca9685Address,
2516 kDefaultPwmFrequencyHz,
2517 kMotionChannelMap)) {
2518 std::cerr <<
"[CAL] init failed: "
2523 auto fail = [&](
const std::string& message) {
2524 std::cerr <<
"[CAL] " << message << std::endl;
2529 bool home_clamped =
false;
2531 makeSmokeTargets(kSmokeHomePose, home_clamped);
2533 if (!motion_controller_.
setTargets(current_targets)) {
2534 return fail(std::string(
"failed to stage home pulse set: ") +
2538 if (!motion_controller_.
enable()) {
2539 return fail(std::string(
"failed to enable motion: ") +
2543 std::array<JointCalibrationMarks, MotionController::kServoCount> marks{};
2544 std::cout <<
"[CAL] home " << formatTargets(current_targets) << std::endl;
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;
2550 auto restoreSignals = [&]() {
2551 std::signal(SIGINT, previous_sigint);
2552 std::signal(SIGTERM, previous_sigterm);
2556 const std::string& label) ->
bool {
2558 bool clamped =
false;
2564 if (!motion_controller_.
setTargets(bounded)) {
2565 (void)fail(std::string(
"failed to set ") + label +
": " +
2570 current_targets = bounded;
2571 std::cout <<
"[CAL] " << label <<
" " << formatTargets(current_targets);
2573 std::cout <<
" [clamped]";
2575 std::cout << std::endl;
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;
2585 out << buildCalibrationSummary(marks);
2587 std::cout <<
"[CAL] wrote " << path << std::endl;
2591 while (!g_interactive_servo_stop_requested) {
2592 std::cout <<
"cal> " << std::flush;
2595 if (!std::getline(std::cin, line)) {
2596 if (g_interactive_servo_stop_requested || std::cin.eof()) {
2600 return fail(
"stdin read failed");
2603 std::istringstream iss(line);
2604 std::string command;
2605 if (!(iss >> command)) {
2609 command = toLowerCopy(command);
2610 if (command ==
"exit" || command ==
"quit") {
2614 if (command ==
"help") {
2616 <<
"[CAL] commands: <joint> <ticks>, <joint> +/-<delta>, mark <joint> <-90|0|+90>, home, status, summary, write [path], exit"
2621 if (command ==
"home") {
2622 bool clamped =
false;
2624 makeSmokeTargets(kSmokeHomePose, clamped);
2625 if (!applyTargets(home_targets,
"home")) {
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])
2641 if (command ==
"summary") {
2642 std::cout << buildCalibrationSummary(marks);
2646 if (command ==
"write") {
2648 if (!(iss >> path)) {
2649 path =
"config/servo_calibration_latest.txt";
2651 (void)writeSummaryToFile(path);
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;
2663 std::size_t joint_index = 0;
2664 if (!smokeJointIndexFromName(joint_name, joint_index)) {
2665 std::cout <<
"[CAL] unknown joint: " << joint_name << std::endl;
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
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;
2686 joint_marks.has_zero =
true;
2687 joint_marks.zero_ticks = current_ticks;
2690 std::cout <<
"[CAL] marked " << smokeJointIndexToString(joint_index)
2691 <<
" " << point_label <<
"=" << current_ticks << std::endl;
2695 std::size_t joint_index = 0;
2696 if (!smokeJointIndexFromName(command, joint_index)) {
2697 std::cout <<
"[CAL] unknown command or joint: " << command << std::endl;
2701 std::string value_text;
2702 if (!(iss >> value_text)) {
2703 std::cout <<
"[CAL] expected: <joint> <ticks>" << std::endl;
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");
2714 }
catch (
const std::exception&) {
2715 std::cout <<
"[CAL] invalid tick value: " << value_text << std::endl;
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,
2726 clampPulseTicks(requested_ticks + raw_value,
2729 setPulseForJoint(requested,
2731 clampPulseTicks(raw_value, value_clamped));
2734 std::ostringstream label;
2735 label << smokeJointIndexToString(joint_index) <<
"="
2736 << pulseForJoint(requested, joint_index);
2737 if (!applyTargets(requested, label.str())) {
2744 std::cout << buildCalibrationSummary(marks);
2746 std::cout <<
"[CAL] done" << std::endl;
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";
2758 if (!motion_controller_.
initialise(kDefaultI2cDevicePath,
2759 kDefaultPca9685Address,
2760 kDefaultPwmFrequencyHz,
2761 kMotionChannelMap)) {
2762 std::cerr <<
"[SERVO] init failed: "
2767 auto fail = [&](
const std::string& message) {
2768 std::cerr <<
"[SERVO] " << message << std::endl;
2773 SmokeJointOffsets current_offsets = kSmokeHomePose;
2774 bool initial_clamped =
false;
2776 makeSmokeTargets(current_offsets, initial_clamped);
2778 if (!motion_controller_.
setTargets(home_targets)) {
2779 return fail(std::string(
"failed to stage home pose: ") +
2783 if (!motion_controller_.
enable()) {
2784 return fail(std::string(
"failed to enable motion: ") +
2788 std::cout <<
"[SERVO] home " << formatOffsets(current_offsets) << std::endl;
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;
2794 auto restoreSignals = [&]() {
2795 std::signal(SIGINT, previous_sigint);
2796 std::signal(SIGTERM, previous_sigterm);
2799 auto applyOffsets = [&](
const SmokeJointOffsets& requested,
2800 const std::string& label) ->
bool {
2801 bool clamped =
false;
2802 const SmokeJointOffsets bounded = clampSmokeOffsets(requested, clamped);
2805 if (!motion_controller_.
setTargets(targets)) {
2806 (void)fail(std::string(
"failed to set ") + label +
": " +
2811 current_offsets = bounded;
2812 std::cout <<
"[SERVO] " << label <<
" " << formatOffsets(current_offsets);
2814 std::cout <<
" [clamped]";
2816 std::cout << std::endl;
2820 while (!g_interactive_servo_stop_requested) {
2821 std::cout <<
"servo> " << std::flush;
2824 if (!std::getline(std::cin, line)) {
2825 if (g_interactive_servo_stop_requested) {
2828 if (std::cin.eof()) {
2832 return fail(
"stdin read failed");
2835 std::istringstream iss(line);
2836 std::string command;
2837 if (!(iss >> command)) {
2841 command = toLowerCopy(command);
2842 if (command ==
"exit" || command ==
"quit") {
2846 if (command ==
"help") {
2848 <<
"[SERVO] commands: base <deg>, lower <deg>, upper <deg>, grip <deg>, home, status, exit"
2853 if (command ==
"status") {
2854 std::cout <<
"[SERVO] " << formatOffsets(current_offsets)
2859 if (command ==
"home") {
2860 if (!applyOffsets(kSmokeHomePose,
"home")) {
2868 if (!(iss >> angle)) {
2869 std::cout <<
"[SERVO] expected: <joint> <angle>" << std::endl;
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;
2883 std::cout <<
"[SERVO] unknown joint: " << command << std::endl;
2887 std::ostringstream label;
2888 label << command <<
"=" << angle;
2889 if (!applyOffsets(requested, label.str())) {
2897 std::cout <<
"[SERVO] done" << std::endl;
2902 std::cout <<
"[HOME] setting all joints to 0" << std::endl;
2904 if (!motion_controller_.
initialise(kDefaultI2cDevicePath,
2905 kDefaultPca9685Address,
2906 kDefaultPwmFrequencyHz,
2907 kMotionChannelMap)) {
2908 std::cerr <<
"[HOME] init failed: "
2913 auto fail = [&](
const std::string& message) {
2914 std::cerr <<
"[HOME] " << message << std::endl;
2919 bool clamped =
false;
2921 makeSmokeTargets(kSmokeHomePose, clamped);
2923 if (!motion_controller_.
setTargets(home_targets)) {
2924 return fail(std::string(
"failed to stage home pose: ") +
2928 if (!motion_controller_.
enable()) {
2929 return fail(std::string(
"failed to enable motion: ") +
2933 std::cout <<
"[HOME] pose=HOME wait=2s";
2935 std::cout <<
" [clamped]";
2937 std::cout << std::endl;
2939 std::string timer_error;
2940 if (!waitForCppTimerDelay(kMotionHomeSettleDwell, timer_error)) {
2941 return fail(std::string(
"home dwell timer failed: ") + timer_error);
2945 std::cout <<
"[HOME] done" << std::endl;
2951 <<
"[SMOKE] joint=" << smokeJointSelectionToString(options.
joint)
2952 <<
" 0 -> -90 -> +90 -> 0"
2956 channel_map.
base = kBaseServoChannel;
2957 channel_map.lower = kLowerServoChannel;
2958 channel_map.upper = kUpperServoChannel;
2959 channel_map.gripper = kGripServoChannel;
2961 if (!motion_controller_.
initialise(kDefaultI2cDevicePath,
2962 kDefaultPca9685Address,
2963 kDefaultPwmFrequencyHz,
2965 std::cerr <<
"[SMOKE] init failed: "
2970 auto fail = [&](
const std::string& message) {
2971 std::cerr <<
"[SMOKE] " << message << std::endl;
2976 auto makeJointOffsets = [](std::size_t joint_index,
int joint_offset) {
2977 SmokeJointOffsets offsets{};
2978 switch (joint_index) {
2980 offsets.base = joint_offset;
2983 offsets.lower = joint_offset;
2986 offsets.upper = joint_offset;
2989 offsets.grip = joint_offset;
2997 auto stagePose = [&](
const std::string& label,
2998 const SmokeJointOffsets& requested) {
2999 bool clamped =
false;
3000 const SmokeJointOffsets bounded = clampSmokeOffsets(requested, clamped);
3003 if (!motion_controller_.
setTargets(targets)) {
3004 return fail(std::string(
"failed to stage pose ") + label +
": " +
3008 std::cout <<
"[SMOKE] " << label;
3010 std::cout <<
" [clamped]";
3012 std::cout << std::endl;
3013 std::cout <<
"[SMOKE] wait 3s" << std::endl;
3015 std::string timer_error;
3016 if (!waitForCppTimerDelay(kSmokeStepDwell, timer_error)) {
3017 return fail(std::string(
"smoke dwell timer failed: ") + timer_error);
3022 bool home_clamped =
false;
3024 makeSmokeTargets(kSmokeHomePose, home_clamped);
3026 if (!motion_controller_.
setTargets(home_targets)) {
3027 return fail(std::string(
"failed to stage home pose: ") +
3031 if (!motion_controller_.
enable()) {
3032 return fail(std::string(
"failed to enable motion: ") +
3036 std::cout <<
"[SMOKE] all -> 0" << std::endl;
3037 std::cout <<
"[SMOKE] wait 3s" << std::endl;
3039 std::string timer_error;
3040 if (!waitForCppTimerDelay(kSmokeStepDwell, timer_error)) {
3041 return fail(std::string(
"initial smoke dwell timer failed: ") +
3046 const SmokeJointRunPlan plan = makeSmokeJointRunPlan(options.
joint);
3047 if (plan.count == 0) {
3048 return fail(
"no smoke-test joint selected");
3051 auto runJointSweep = [&](std::size_t index) {
3052 const SmokeJointSpec& spec = kSmokeJointSpecs[index];
3053 std::cout <<
"[SMOKE] " << spec.logical_name << std::endl;
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";
3059 if (stagePose(home_label, makeJointOffsets(index, 0)) != 0) {
3062 if (stagePose(neg_label, makeJointOffsets(index, kSmokeNegativeStep)) != 0) {
3065 if (stagePose(pos_label, makeJointOffsets(index, kSmokePositiveStep)) != 0) {
3068 if (stagePose(home_label, makeJointOffsets(index, 0)) != 0) {
3075 for (std::size_t i = 0; i < plan.count; ++i) {
3076 if (runJointSweep(plan.indices[i]) != 0) {
3083 std::cout <<
"[SMOKE] done" << std::endl;
3088 constexpr int kValidationFrameTarget = 60;
3089 constexpr int kMaxConsecutiveFailures = 10;
3091 std::cout <<
"[CAMERA_CHECK] camera=" << options.
camera_index
3092 <<
" backend_request="
3094 <<
" frames=" << kValidationFrameTarget << std::endl;
3098 std::cout <<
"[CAMERA_CHECK] backend_active="
3100 << camera_capture.backendName() <<
")" << std::endl;
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;
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)) {
3116 ++consecutive_failures;
3117 std::cerr <<
"[CAMERA_CHECK] frame failure " << frame_failures
3118 <<
" (consecutive=" << consecutive_failures <<
")"
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()) {
3129 std::cout <<
"[CAMERA_CHECK] first_frame_ms="
3130 << elapsedMilliseconds(validation_start,
3131 *first_capture_timestamp)
3132 <<
" size=" << frame_width <<
"x" << frame_height
3138 if (frames_received == 0) {
3139 std::cerr <<
"[CAMERA_CHECK] no frames received" << std::endl;
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)
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))
3154 std::ostringstream fps_stream;
3155 fps_stream << std::fixed << std::setprecision(1) << approx_fps;
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;
3164 if (consecutive_failures >= kMaxConsecutiveFailures) {
3165 std::cerr <<
"[CAMERA_CHECK] failed after repeated capture errors"
3175 <<
"[LIVE_TEST] Starting live marker safety test mode\n"
3176 <<
"[LIVE_TEST] Camera index: " << options.
camera_index <<
"\n"
3178 <<
"[LIVE_TEST] Camera backend preference: "
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";
3197 channel_map.
base = kBaseServoChannel;
3198 channel_map.lower = kLowerServoChannel;
3199 channel_map.upper = kUpperServoChannel;
3200 channel_map.gripper = kGripServoChannel;
3202 if (!motion_controller_.
initialise(kDefaultI2cDevicePath,
3203 kDefaultPca9685Address,
3204 kDefaultPwmFrequencyHz,
3206 std::cerr <<
"[LIVE_TEST] Motion controller initialization failed: "
3211 MotionControllerHardwareAdapter hardware(motion_controller_);
3217 std::cout <<
"[LIVE_TEST] Camera backend active: "
3219 << camera_capture.backendName() <<
")" << std::endl;
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;
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,
3259 std::size_t limit) {
3260 history.push_back(value);
3261 if (history.size() > limit) {
3262 history.pop_front();
3265 auto copyHistory = [&](
const std::deque<double>& history) {
3266 return std::vector<double>(history.begin(), history.end());
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;
3274 auto applyPoseOffsets = [&](
const SmokeJointOffsets& offsets,
3275 const char* context) ->
bool {
3276 bool target_clamped =
false;
3278 if (!motion_controller_.
setTargets(targets)) {
3279 motion_faulted =
true;
3280 std::cerr <<
"[LIVE_TEST] failed to set pose (" << context <<
"): "
3287 auto stagePose = [&](
const DemoPoseStep& step) ->
bool {
3288 bool clamped =
false;
3289 const SmokeJointOffsets target_offsets =
3290 clampDemoOffsets(step.offsets, clamped);
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")) {
3300 pose_slew_target = target_offsets;
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();
3308 if (!pose_slew_active) {
3309 if (!applyPoseOffsets(pose_slew_target,
"hold")) {
3314 current_pose_name = step.name;
3315 std::cout <<
"[LIVE_TEST] pose=" << step.name;
3317 std::cout <<
" [clamped]";
3319 if (pose_slew_active) {
3320 std::cout <<
" [slew]";
3322 std::cout <<
" wait=1s" << std::endl;
3326 auto advancePoseSlew = [&]() ->
bool {
3327 if (!pose_slew_active) {
3331 const auto now = std::chrono::steady_clock::now();
3332 if (now < next_pose_slew_due) {
3336 auto stepTowards = [&](
int current,
int target) {
3337 if (current < target) {
3338 return std::min(current + kDemoSlewStepDegrees, target);
3340 if (current > target) {
3341 return std::max(current - kDemoSlewStepDegrees, target);
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);
3352 if (!applyPoseOffsets(next_offsets,
"slew")) {
3356 current_pose_offsets = next_offsets;
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;
3366 auto stopLiveStepTimer = [&]() {
3367 if (live_step_timer_started) {
3368 live_step_timer.stop();
3369 live_step_timer_started =
false;
3373 auto startLiveStepTimer = [&]() ->
bool {
3374 if (live_step_timer_started) {
3378 live_step_timer.registerEventCallback(
3379 [&]() { control_events.pushLiveStepReady(); });
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
3388 motion_faulted =
true;
3389 std::cerr <<
"[LIVE_TEST] routine timer failed" << std::endl;
3393 live_step_timer_started =
true;
3397 auto announceWaitingState = [&]() {
3398 const bool should_wait =
3399 frame_is_safe && guardian_armed &&
3402 if (should_wait && !waiting_for_ack_announced) {
3403 std::cout <<
"[LIVE_TEST] waiting for continue" << std::endl;
3404 waiting_for_ack_announced =
true;
3407 waiting_for_ack = should_wait;
3409 waiting_for_ack_announced =
false;
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;
3421 if (guardian_armed && motion_gate_open && frame_is_safe) {
3422 if (routine.auto_progress) {
3423 if (!stagePose(kDemoHomeStep)) {
3426 home_pose_staged =
true;
3427 if (!startLiveStepTimer()) {
3431 std::cout <<
"[LIVE_TEST] manual mode active: auto routine paused."
3439 auto resetEnforcementState = [&]() {
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);
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);
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"
3467 if (!stagePose(kSurgeryRetractStep)) {
3468 motion_faulted =
true;
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;
3479 <<
"[LIVE_TEST] freeze pending: will hold after retract dwell"
3483 guardian->setOnClearFreezeCallback([&]() {
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);
3495 ack_request_timestamp.reset();
3497 motion_faulted =
true;
3500 if (getLiveRoutineDefinition(selected_routine_index).auto_progress) {
3501 if (!startLiveStepTimer()) {
3502 motion_faulted =
true;
3506 motion_gate_open =
true;
3507 std::cout <<
"[LIVE_TEST] resume" << std::endl;
3508 logLiveLatencySample(
"resume", latency_metrics);
3512 std::cout <<
"[GUARDIAN] " << guardian->stateToString(from) <<
" -> "
3513 << guardian->stateToString(to) << std::endl;
3517 resetEnforcementState();
3519 auto processPendingFreezeCommand = [&]() ->
bool {
3520 if (!freeze_command_pending || !freeze_command_due.has_value()) {
3524 const auto now = std::chrono::steady_clock::now();
3525 if (now < *freeze_command_due) {
3528 if (pose_slew_active) {
3529 if (!freeze_waiting_for_retract_logged) {
3530 std::cout <<
"[LIVE_TEST] freeze pending: waiting for retract "
3533 freeze_waiting_for_retract_logged =
true;
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();
3543 const auto freeze_cmd_end = std::chrono::steady_clock::now();
3544 latency_metrics.freeze_cmd_ms = elapsedMilliseconds(freeze_cmd_start,
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,
3553 appendHistorySample(total_stop_history_ms,
3554 static_cast<double>(*latency_metrics.total_stop_ms),
3555 kEventHistoryLimit);
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);
3564 motion_faulted =
true;
3571 std::cout <<
"[LIVE_TEST] Physical button module: "
3572 << (button_module.
available() ?
"configured" :
"disabled");
3573 const char* button_module_status =
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 <<
")";
3581 std::cout << std::endl;
3583 std::cout <<
"[LIVE_TEST] Configure ARGUS_BUTTON_ACK_GPIO to enable the "
3584 "physical single-button control."
3588 auto freezeMotionBeforeModeChange = [&](
const char* action_label) ->
bool {
3591 motion_faulted =
true;
3592 std::cerr <<
"[LIVE_TEST] Unable to freeze motion before " << action_label
3600 auto requestArm = [&]() ->
bool {
3601 if (guardian_armed) {
3602 std::cout <<
"[LIVE_TEST] ARM request ignored: guardian already armed."
3607 if (!frame_is_safe) {
3608 std::cout <<
"[LIVE_TEST] ARM request rejected: current observed condition is "
3610 << safetyStateToString(current_vision_state) <<
")." << std::endl;
3614 if (!freezeMotionBeforeModeChange(
"arming")) {
3618 resetEnforcementState();
3619 if (!stagePose(kDemoHomeStep)) {
3622 home_pose_staged =
true;
3623 next_routine_step_index = 0;
3627 motion_faulted =
true;
3628 std::cerr <<
"[LIVE_TEST] Unable to prepare motion path for arm: "
3633 interlock->operatorAcknowledge();
3636 motion_faulted =
true;
3637 std::cerr <<
"[LIVE_TEST] Unable to enable motion on arm: "
3642 stopLiveStepTimer();
3643 if (getLiveRoutineDefinition(selected_routine_index).auto_progress) {
3644 if (!startLiveStepTimer()) {
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."
3663 auto requestDisarm = [&]() ->
bool {
3664 if (!guardian_armed) {
3665 std::cout <<
"[LIVE_TEST] DISARM request ignored: guardian already disarmed."
3670 if (!freezeMotionBeforeModeChange(
"disarming")) {
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)."
3696 auto requestAcknowledge = [&]() ->
bool {
3697 if (!guardian_armed) {
3698 std::cout <<
"[LIVE_TEST] ACK request ignored: guardian is disarmed."
3703 if (freeze_command_pending) {
3704 std::cout <<
"[LIVE_TEST] ACK request ignored: retract in progress."
3709 if (!frame_is_safe) {
3710 std::cout <<
"[LIVE_TEST] ACK request ignored: current observed condition is "
3712 << safetyStateToString(current_vision_state) <<
")." << std::endl;
3717 std::cout <<
"[LIVE_TEST] ACK request ignored: guardian is not frozen."
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;
3729 auto requestContinue = [&]() ->
bool {
3730 if (!guardian_armed) {
3731 return requestArm();
3734 return requestAcknowledge();
3737 std::cout <<
"[LIVE_TEST] CONTINUE ignored: waiting for recovery"
3741 return requestDisarm();
3750 return requestContinue();
3752 return requestDisarm();
3757 auto requestManualNudge = [&](
int delta_base,
3761 const char* label) ->
bool {
3762 const LiveRoutineDefinition routine =
3763 getLiveRoutineDefinition(selected_routine_index);
3764 if (routine.auto_progress) {
3768 if (!guardian_armed || !motion_gate_open || waiting_for_ack ||
3769 freeze_command_pending ||
3771 std::cout <<
"[LIVE_TEST] manual nudge ignored: control not available"
3776 if (!current_pose_offsets_known) {
3777 current_pose_offsets = kSmokeHomePose;
3778 current_pose_offsets_known =
true;
3779 pose_slew_target = current_pose_offsets;
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;
3789 bool clamped =
false;
3790 target_offsets = clampDemoOffsets(target_offsets, clamped);
3791 pose_slew_target = target_offsets;
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";
3800 if (pose_slew_active) {
3801 std::cout <<
"[LIVE_TEST] manual " << label;
3803 std::cout <<
" [clamped]";
3805 std::cout <<
" [slew]" << std::endl;
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)
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);
3823 bool processed_any_frame =
false;
3824 int consecutive_capture_failures = 0;
3825 double focus_score = 0.0;
3826 const char* focus_quality =
"BLURRY";
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;
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;
3847 const DemoPoseStep& step = routine.steps[next_routine_step_index];
3848 if (!stagePose(step)) {
3849 return ControllerEventDisposition::Abort;
3852 next_routine_step_index =
3853 (next_routine_step_index + 1) % routine.step_count;
3854 return ControllerEventDisposition::Consumed;
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;
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();
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);
3884 current_vision_state = result.
state;
3886 if (!frame_is_safe) {
3887 pending_reason = mapSafetyToFreezeReason(current_vision_state);
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 =
3904 pending_unsafe_decision_timestamp = result.
timestamp;
3905 latency_metrics.unsafe_detect_ms = elapsedMilliseconds(
3908 appendHistorySample(unsafe_detect_history_ms,
3909 static_cast<double>(
3912 kEventHistoryLimit);
3913 logLiveLatencySample(
"unsafe_detect",
3917 pending_unsafe_capture_timestamp.reset();
3918 pending_unsafe_decision_timestamp.reset();
3921 if (guardian_armed) {
3925 announceWaitingState();
3927 if (guardian_armed && options.
auto_ack &&
3928 !freeze_command_pending &&
3930 ack_request_timestamp = std::chrono::steady_clock::now();
3931 guardian->operatorAcknowledge();
3932 interlock->operatorAcknowledge();
3934 <<
"[LIVE_TEST] Operator acknowledge sent automatically"
3938 if (guardian_armed) {
3939 interlock->guardianHeartbeat(liveWatchdogTickMs());
3942 return ControllerEventDisposition::Consumed;
3945 return ControllerEventDisposition::Consumed;
3948 cv::namedWindow(kLiveCameraWindowName, cv::WINDOW_AUTOSIZE);
3949 cv::namedWindow(kLiveStatusWindowName, cv::WINDOW_AUTOSIZE);
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;
3964 int tuning_hue_center = kDefaultTuneHue;
3965 int tuning_saturation = kDefaultTuneSaturation;
3966 int tuning_brightness = kDefaultTuneBrightness;
3967 int tuning_pixel_threshold = kDefaultTunePixelThreshold;
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);
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);
3983 normaliseHue(tuning_hue_center - kHueHalfWidth);
3985 normaliseHue(tuning_hue_center + kHueHalfWidth);
3994 auto sliderSnapshot = [&]() {
3995 return std::array<int, 4>{
3999 tuning_pixel_threshold,
4003 cv::createTrackbar(kSliderHue,
4004 kLiveStatusWindowName,
4007 cv::createTrackbar(kSliderSaturation,
4008 kLiveStatusWindowName,
4011 cv::createTrackbar(kSliderBrightness,
4012 kLiveStatusWindowName,
4015 cv::createTrackbar(kSliderPixelThreshold,
4016 kLiveStatusWindowName,
4017 &tuning_pixel_threshold,
4020 applySimpleColourTuning();
4022 std::array<int, 4> last_slider_snapshot = sliderSnapshot();
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;
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;
4056 std::cout <<
"[LIVE_TEST] status: armed="
4057 << (snapshot.guardian_armed ?
"YES" :
"NO")
4058 <<
" safe=" << (snapshot.scene_safe ?
"YES" :
"NO")
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;
4068 last_live_status = snapshot;
4069 live_status_known =
true;
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));
4081 control_events.pushFrameCaptureFailed();
4083 std::string timer_error;
4084 if (!waitForCppTimerDelay(kCaptureRetryBackoff, timer_error)) {
4085 control_events.pushFrameCaptureFailed();
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);
4104 constexpr int kLiveStatusWidth = 430;
4105 constexpr int kLiveMetricsWidth = 430;
4106 cv::Mat status_frame;
4107 cv::Mat metrics_frame;
4111 motion_faulted =
true;
4112 std::cerr <<
"[LIVE_TEST] Interlock entered FAULT state (motion controller: "
4113 << motion_controller_.lastErrorString() <<
")" << std::endl;
4116 (void)control_events.waitForEvents(std::chrono::milliseconds(5));
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;
4125 std::string guardian_state_text =
"DISARMED_SETUP";
4126 std::string interlock_state_text =
"DISARMED";
4127 std::string freeze_reason_text =
"N/A";
4129 if (!control_events.drain(handleControlEvent)) {
4130 motion_faulted =
true;
4134 if (guardian_armed) {
4135 interlock->watchdogCheck(liveWatchdogTickMs(),
4136 kLiveInterlockWatchdogMaxDelayMs);
4139 motion_faulted =
true;
4140 std::cerr <<
"[LIVE_TEST] Watchdog timeout: no guardian heartbeat "
4142 << kLiveInterlockWatchdogMaxDelayMs
4143 <<
" ms. Aborting run." << std::endl;
4148 if (!advancePoseSlew()) {
4149 motion_faulted =
true;
4153 if (!processPendingFreezeCommand()) {
4154 motion_faulted =
true;
4158 if (guardian_armed) {
4159 guardian_state_text = guardian->getCurrentStateString();
4160 interlock_state_text = interlockStateToString(interlock->state());
4161 freeze_reason_text = freezeReasonToString(interlock->freezeReason());
4164 maybeLogLiveStatus(LiveStatusSnapshot{
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});
4176 motion_faulted =
true;
4177 std::cerr <<
"[LIVE_TEST] Motion control fault detected: "
4178 << motion_controller_.lastErrorString() << std::endl;
4182 cv::Mat display_frame;
4183 if (latest_frame_available) {
4184 display_frame = latest_frame_event.image_data.clone();
4186 display_frame = makePlaceholderFrame(640, 480);
4188 const bool decision_is_safe = guardian_armed
4190 (guardian->getState() ==
4192 interlock->motionAllowed())
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 &&
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 &&
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";
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 =
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();
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 =
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;
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))},
4272 guardian_armed ? guardianStateToUiString(guardian->getState())
4273 :
"Disarmed (setup)",
4274 severityColor(guardian_state_text)},
4276 guardian_armed ? interlockStateToUiString(interlock->state())
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)},
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 =
4292 : (guardian_armed && motion_gate_open && decision_is_safe)
4294 : (!guardian_armed && frame_is_safe) ?
"Ready to arm"
4296 live_ui.camera_hud_color =
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 =
4305 live_ui.frozen_overlay_title =
"Unsafe";
4306 live_ui.frozen_overlay_subtitle =
"Motion frozen: clear workspace";
4307 live_ui.show_waiting_overlay =
4309 live_ui.waiting_overlay_text =
"Workspace safe: press control to resume";
4310 live_ui.emphasise_danger =
4313 cv::Mat camera_frame = display_frame.clone();
4314 drawCameraOverlay(camera_frame, live_ui);
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,
4322 cv::Scalar(245, 245, 245));
4324 status_frame.setTo(cv::Scalar(245, 245, 245));
4326 drawStatusDashboard(status_frame, live_ui);
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,
4334 cv::Scalar(245, 245, 245));
4336 metrics_frame.setTo(cv::Scalar(245, 245, 245));
4338 drawMetricsDashboard(metrics_frame, live_ui);
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);
4345 std::cout <<
"[LIVE_TEST] Exit requested from display window (esc)."
4350 int normalized_key = key;
4351 if (normalized_key >= 0 && normalized_key <= 255) {
4352 normalized_key = std::tolower(normalized_key);
4355 if (normalized_key ==
' ') {
4356 if (!requestContinue()) {
4362 if (normalized_key ==
'+' || normalized_key ==
'=') {
4363 float current_focus = camera_capture.getFocusPosition();
4364 if (current_focus < 0.0f) {
4365 current_focus = 0.0f;
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;
4376 float new_focus = current_focus - 0.1f;
4377 if (new_focus < 0.0f) {
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;
4386 std::size_t requested_routine_index = 0;
4387 if (liveRoutineIndexFromKey(normalized_key, requested_routine_index)) {
4388 if (!selectRoutine(requested_routine_index)) {
4393 if (!getLiveRoutineDefinition(selected_routine_index).auto_progress) {
4394 switch (normalized_key) {
4396 if (!requestManualNudge(-kLiveManualNudgeDegrees, 0, 0, 0,
4402 if (!requestManualNudge(kLiveManualNudgeDegrees, 0, 0, 0,
4408 if (!requestManualNudge(0, 0, kLiveManualNudgeDegrees, 0,
4414 if (!requestManualNudge(0, 0, -kLiveManualNudgeDegrees, 0,
4420 if (!requestManualNudge(0, kLiveManualNudgeDegrees, 0, 0,
"UP")) {
4425 if (!requestManualNudge(0, -kLiveManualNudgeDegrees, 0, 0,
4431 if (!requestManualNudge(0, 0, 0, kLiveManualNudgeDegrees,
"OPEN")) {
4436 if (!requestManualNudge(0, 0, 0, -kLiveManualNudgeDegrees,
4447 capture_stop_requested.store(
true, std::memory_order_relaxed);
4448 if (capture_thread.joinable()) {
4449 capture_thread.join();
4451 button_stop_requested.store(
true, std::memory_order_relaxed);
4452 if (button_thread.joinable()) {
4453 button_thread.join();
4456 cv::destroyWindow(kLiveCameraWindowName);
4457 cv::destroyWindow(kLiveStatusWindowName);
4458 stopLiveStepTimer();
4459 motion_controller_.shutdown();
4461 if (!processed_any_frame) {
4462 std::cerr <<
"[LIVE_TEST] No frames processed. Check camera availability."
4467 if (motion_faulted) {
4471 std::cerr <<
"[LIVE_TEST] Frame stream ended." << std::endl;
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.
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.
@ ALLOW_MOTION
Attempt to allow motion when safety conditions permit.
@ FREEZE_NOW
Immediately freeze/deny motion.
SafetyState
Outcome of a single vision safety evaluation.
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.
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.
SafetyState state
The safety decision made for this frame.
std::chrono::microseconds processing_time
How long VisionProcessor took to process this frame.
std::chrono::steady_clock::time_point timestamp
The time at which this result was produced (end of processing).
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.