649 Commits

Author SHA1 Message Date
f188997192 Merge pull request 'feat: RPLIDAR A1 mount bracket (Issue #596)' (#604) from sl-mechanical/issue-596-rplidar-mount into main 2026-03-14 15:54:40 -04:00
7e5f673f7d Merge pull request 'feat: WebUI gamepad teleop panel (Issue #598)' (#603) from sl-webui/issue-598-gamepad-teleop into main 2026-03-14 15:54:36 -04:00
816d165db4 feat: RPLIDAR A1 mount bracket (Issue #596) 2026-03-14 14:58:41 -04:00
cbcae34b79 feat: WebUI gamepad teleoperation panel (Issue #598)
- Standalone ui/gamepad_panel.{html,js,css} — no build step
- Web Gamepad API integration: L-stick=linear, R-stick=angular
  - LT trigger scales speed down (fine control)
  - B/Circle button toggles E-stop; Start button resumes
  - Live raw axis bars and button state in sidebar
- Virtual dual joystick (left=drive, right=steer) via Pointer Capture API
  - Deadzone ring drawn on canvas; configurable 0–40%
  - Touch and mouse support
- WASD/Arrow keyboard input (W/S=forward/reverse, A/D=turn, Space=E-stop)
- Speed limiter sliders: linear (0–1.0 m/s), angular (0–2.0 rad/s)
- Configurable deadzone slider (0–40%)
- E-stop: latches zero-velocity command, blinking overlay, resume button
- Publishes geometry_msgs/Twist to /cmd_vel at 20 Hz via rosbridge WebSocket
- Input priority: gamepad > keyboard > virtual sticks
- Live command display (m/s, rad/s) with color feedback
- Pub rate display (Hz) in sidebar
- localStorage WS URL persistence, auto-reconnect on load
- Mobile-responsive: sidebar hidden ≤800px, right stick hidden ≤560px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 14:58:41 -04:00
061189670a Merge pull request 'feat: STM32 watchdog and fault recovery handler (Issue #565)' (#583) from sl-firmware/issue-565-fault-handler into main 2026-03-14 13:54:22 -04:00
8fbe7c0033 feat: STM32 watchdog and fault recovery handler (Issue #565)
- New src/fault_handler.c + include/fault_handler.h:
  - HardFault/MemManage/BusFault/UsageFault naked ISR stubs with
    Cortex-M7 stack-frame capture (R0-R3, LR, PC, xPSR, CFSR, HFSR,
    MMFAR, BFAR, SP) and NVIC_SystemReset()
  - .noinit SRAM capture ring survives soft reset; persisted to flash
    sector 7 (0x08060000, 8x64-byte slots) on subsequent boot
  - MPU Region 0 stack guard (32 B at __stack_end, no-access) ->
    MemManage fault detected as FAULT_STACK_OVF
  - Brownout detect via RCC_CSR_BORRSTF on boot -> FAULT_BROWNOUT
  - Watchdog reset detection delegates to existing watchdog.c
  - LED blink codes on LED2 (PC14, active-low) for 10 s post-recovery:
    HARDFAULT=3, WATCHDOG=2, BROWNOUT=1, STACK_OVF=4 fast blinks
  - fault_led_tick(), fault_log_read(), fault_log_get_count(),
    fault_get_last_type(), fault_log_clear(), FAULT_ASSERT() macro
- jlink.h: add JLINK_CMD_FAULT_LOG_GET (0x0F), JLINK_TLM_FAULT_LOG
  (0x86), jlink_tlm_fault_log_t (20 bytes), fault_log_req in JLinkState,
  jlink_send_fault_log() declaration
- jlink.c: dispatch JLINK_CMD_FAULT_LOG_GET; implement
  jlink_send_fault_log() (26-byte CRC16-XModem framed response)
- main.c: call fault_handler_init() first in main(); send fault log
  TLM on boot if prior fault recorded; fault_led_tick() in main loop;
  handle fault_log_req flag to respond to Jetson queries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 13:37:14 -04:00
15ff5acca7 Merge pull request 'feat: Visual odometry from RealSense stereo ORB (Issue #586)' (#593) from sl-perception/issue-586-visual-odom into main 2026-03-14 13:32:56 -04:00
f2743198e5 Merge pull request 'feat: WebUI map view (Issue #587)' (#591) from sl-webui/issue-587-map-view into main 2026-03-14 13:32:50 -04:00
6512c805be Merge pull request 'feat: Motor current monitoring (Issue #584)' (#594) from sl-controls/issue-584-motor-current into main 2026-03-14 13:32:30 -04:00
1da1d50171 Merge pull request 'feat: Phone video bridge (Issue #585)' (#592) from sl-android/issue-585-video-bridge into main 2026-03-14 13:32:24 -04:00
6a8b6a679e Merge pull request 'feat: Integrate UWB tag display + ESP-NOW + e-stop (salty/uwb-tag-display-wireless)' (#590) from sl-uwb/issue-merge-uwb-tag-display into main 2026-03-14 13:32:19 -04:00
ddf8332cd7 Merge pull request 'feat: Battery holder bracket (Issue #588)' (#589) from sl-mechanical/issue-588-battery-holder into main 2026-03-14 13:32:16 -04:00
e9429e6177 Merge pull request 'feat: ROS2 launch orchestrator for full SaltyBot bringup (Issue #577)' (#582) from sl-jetson/issue-577-bringup-launch into main 2026-03-14 13:32:10 -04:00
2b06161cb4 feat: Motor current monitoring and overload protection (Issue #584)
Adds ADC-based motor current sensing with configurable overload threshold,
soft PWM limiting, hard cutoff on sustained overload, and auto-recovery.

Changes:
- include/motor_current.h: MotorCurrentState enum (NORMAL/SOFT_LIMIT/COOLDOWN),
  thresholds (5A hard, 4A soft, 2s overload, 10s cooldown), full API
- src/motor_current.c: reads battery_adc_get_current_ma() each tick (reuses
  existing ADC3 IN13/PC3 DMA sampling); linear PWM scale in soft-limit zone
  (scale256 fixed-point); fault counter + one-tick fault_pending flag for
  main-loop fault log integration; telemetry at MOTOR_CURR_TLM_HZ (5 Hz)
- include/pid_flash.h: add pid_sched_entry_t (16 bytes), pid_sched_flash_t
  (128 bytes at 0x0807FF40), PID_SCHED_MAX_BANDS=6, pid_flash_load_schedule(),
  pid_flash_save_all() — fixes missing types needed by jlink.h (Issue #550)
- src/pid_flash.c: implement flash_write_words() helper, pid_flash_load_schedule(),
  pid_flash_save_all() — single sector-7 erase covers both schedule and PID records
- include/jlink.h: add JLINK_TLM_MOTOR_CURRENT (0x86), jlink_tlm_motor_current_t
  (8 bytes: current_ma, limit_pct, state, fault_count), jlink_send_motor_current_tlm()
- src/jlink.c: implement jlink_send_motor_current_tlm() (14-byte frame)

Motor overload state machine:
  MC_NORMAL     : current_ma < 4000 mA — full PWM authority
  MC_SOFT_LIMIT : 4000-5000 mA — linear reduction (0% at 4A → 100% at 5A)
  MC_COOLDOWN   : >5A sustained 2s → zero output for 10s then NORMAL

Main-loop integration:
  motor_current_tick(now_ms);
  if (motor_current_fault_pending()) fault_log_append(FAULT_MOTOR_OVERCURRENT);
  cmd = motor_current_apply_limit(balance_pid_output());
  motor_current_send_tlm(now_ms);

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 12:25:29 -04:00
c1b82608d5 feat: Visual odometry from RealSense stereo ORB (Issue #586)
Adds stereo ORB-based visual odometry to saltybot_visual_odom package.

New modules:
- orb_stereo_matcher.py: ORB feature detection (cv2.ORB_create) with BFMatcher
  NORM_HAMMING + Lowe ratio test for temporal matching (infra1 prev→curr).
  Stereo scale method matches infra1↔infra2 under epipolar row constraint
  (|Δrow|≤2px), computes depth = baseline_m * fx / disparity.
- stereo_orb_node.py: StereoOrbNode subscribes to infra1+infra2+depth
  (ApproximateTimeSynchronizer 3-topic), detects/matches ORB temporally,
  estimates SE(3) via Essential matrix (5-point RANSAC) using StereoVO,
  recovers metric scale from D435i aligned depth (primary) or stereo
  baseline disparity (fallback). Publishes nav_msgs/Odometry on
  /saltybot/visual_odom and broadcasts TF2 odom→camera_link. Baseline
  auto-updated from infra2 camera_info Tx (overrides parameter).
- config/stereo_orb_params.yaml, launch/stereo_orb.launch.py
- setup.py: adds stereo_orb entrypoint, installs launch+config dirs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 12:21:58 -04:00
sl-android
08bc23f6df feat: Phone video streaming bridge (Issue #585)
Phone side — phone/video_bridge.py:
- MJPEG streaming server for Android/Termux phone camera
- Dual camera backends: OpenCV VideoCapture (V4L2) with automatic
  fallback to termux-camera-photo for unmodified Android
- WebSocket server (ws://0.0.0.0:8765) — binary JPEG frames + JSON
  info/error control messages; supports multiple concurrent clients
- HTTP server (http://0.0.0.0:8766):
    /stream    — multipart/x-mixed-replace MJPEG
    /snapshot  — single JPEG
    /health    — JSON stats (frame count, dropped, resolution, fps)
- Thread-safe single-slot FrameBuffer; CaptureThread rate-limited with
  wall-clock accounting for capture latency
- Flags: --ws-port, --http-port, --width, --height, --fps, --quality,
  --device, --camera-id, --no-http, --debug

Jetson side — saltybot_phone/phone_camera_node.py:
- ROS2 node: receives JPEG frames, publishes:
    /saltybot/phone/camera            sensor_msgs/Image (bgr8)
    /saltybot/phone/camera/compressed sensor_msgs/CompressedImage
    /saltybot/phone/camera/info       std_msgs/String (stream metadata)
- WebSocket client (primary); HTTP MJPEG polling fallback on WS failure
- Auto-reconnect loop (default 3 s) for both transports
- Latency warning when frame age > latency_warn_ms (default 200 ms)
- 10 s diagnostics log: received/published counts + last frame age
- Registered as phone_camera_node console script in setup.py
- Added to phone_bringup.py launch with phone_host / phone_cam_enabled args

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 12:20:28 -04:00
5dac6337e6 feat: WebUI map view (Issue #587)
Standalone 3-file 2D map panel (ui/map_panel.{html,js,css}).
No build step. Open directly or serve ui/ directory.

Canvas layers (drawn every animation frame):
  - Grid lines (1m spacing) + world-origin axis cross + 1m scale bar
  - RPLIDAR scan dots (/scan, green, cbor-compressed, 100ms throttle)
  - Safety zone rings: danger 0.30m (red dashed) + warn 1.00m (amber dashed)
  - 100-position breadcrumb trail with fading cyan polyline + dots every 5 pts
  - UWB anchor markers (amber diamond + label, user-configured)
  - Robot marker: circle + forward arrow, red when e-stopped

Interactions:
  - Mouse wheel zoom (zooms around cursor)
  - Click+drag pan
  - Pinch-to-zoom (touch, two-finger)
  - Auto-center toggle (robot stays centered when on)
  - Zoom +/- buttons, Reset view button
  - Clear trail button
  - Mouse hover shows world coords (m) in bottom-left HUD

ROS topics:
  SUB /saltybot/pose/fused        geometry_msgs/PoseStamped   50ms throttle
  SUB /scan                       sensor_msgs/LaserScan       100ms + cbor
  SUB /saltybot/safety_zone/status std_msgs/String (JSON)     200ms throttle

Sidebar:
  - Robot position (x, y m) + heading (°)
  - Safety zone: forward zone (CLEAR/WARN/DANGER), closest obstacle (m), e-stop
  - UWB anchor manager: add/remove anchors with x/y/label, persisted localStorage
  - Topic reference

E-stop banner: pulsing red overlay when /saltybot/safety_zone/status estop_active=true

Mobile-responsive: sidebar hidden on <700px, canvas fills viewport.
WS URL persisted in localStorage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 12:20:04 -04:00
sl-uwb
4b8d1b2ff7 feat: Integrate UWB tag display + ESP-NOW + e-stop (salty/uwb-tag-display-wireless)
Integrates Tee's additions to the DS-TWR tag firmware (esp32/uwb_tag/).
Base is our DS-TWR initiator from Issue #545; extensions added:

OLED display (SSD1306 128×64, I2C SDA=4 SCL=5):
- Big distance readout (nearest anchor, auto m/mm)
- Per-anchor range rows with link-age indicator
- Signal strength bars (RSSI)
- Uptime + sequence counter
- Full-screen E-STOP warning when button held

ESP-NOW wireless (peer-to-peer, no AP required):
- 20-byte broadcast packet: magic, tag_id, msg_type, anchor_id,
  range_mm, rssi_dbm, timestamp_ms, battery_pct, flags, seq_num
- MSG_RANGE (0x10) on every successful TWR
- MSG_ESTOP (0x20) at 10 Hz while button held; 3× clear on release
- MSG_HEARTBEAT (0x30) at 1 Hz

Emergency stop (GPIO 0 / BOOT button, active LOW):
- Blocks ranging while active
- 10 Hz ESP-NOW e-stop TX, serial +ESTOP:ACTIVE / +ESTOP:CLEAR
- 3× clear packets on release

Build: adds Adafruit SSD1306 + GFX libraries to platformio.ini.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 12:19:31 -04:00
5556c06153 feat: Battery holder bracket (Issue #588) 2026-03-14 12:18:37 -04:00
5a1290a8f9 Merge pull request 'feat: UWB anchor mount bracket (Issue #564)' (#569) from sl-mechanical/issue-564-uwb-anchor-mount into main 2026-03-14 12:15:43 -04:00
7b75cdad1a feat: UWB anchor mount bracket (Issue #564) 2026-03-14 12:15:12 -04:00
b09017c949 Merge pull request 'feat: UWB-IMU EKF fusion for robust indoor localization (Issue #573)' (#581) from sl-uwb/issue-573-uwb-imu-fusion into main 2026-03-14 12:14:05 -04:00
1726558a7a Merge pull request 'feat: RPLIDAR safety zone detector (Issue #575)' (#580) from sl-perception/issue-575-safety-zone into main 2026-03-14 12:14:01 -04:00
5a3f4d1df6 Merge pull request 'feat: WebUI event log panel (Issue #576)' (#579) from sl-webui/issue-576-event-log into main 2026-03-14 12:13:56 -04:00
b2f01b42f3 Merge pull request 'feat: Termux sensor dashboard (Issue #574)' (#578) from sl-android/issue-574-sensor-dashboard into main 2026-03-14 12:13:51 -04:00
a7eb2ba3e5 Merge pull request 'feat: PID gain scheduling for speed-dependent balance (Issue #550)' (#560) from sl-controls/issue-550-pid-scheduling into main 2026-03-14 12:13:44 -04:00
4035b4cfc3 feat: ROS2 launch orchestrator for full SaltyBot bringup (Issue #577)
Adds saltybot_bringup.launch.py with ordered startup groups (drivers→
perception→navigation→UI), timer-based health gates, configurable
profiles (minimal/full/debug), and estop on Ctrl-C shutdown.

Also adds launch_profiles.py dataclass module and 53-test coverage for
profile hierarchy, timing gates, safety bounds, and to_dict serialization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:57:57 -04:00
sl-uwb
7708c63698 feat: UWB-IMU EKF fusion for robust indoor localization (Issue #573)
EKF fusing UWB position (10Hz) with IMU accel+gyro (200Hz) for
SaltyBot indoor localization with UWB dropout resilience.

Package: saltybot_uwb_imu_fusion

- ekf_math.py: 6-state EKF [x,y,θ,vx,vy,ω], IMU predict + UWB update
  - IMU as process input: body-frame accel rotated to world via heading
  - Jacobian F for nonlinear rotation effect
  - Process noise Q from continuous white-noise model
  - UWB 2D position update, heading update from quaternion
  - Accel bias estimation (low-pass)
  - Velocity damping during UWB dropout (>2s threshold)
- ekf_node.py: ROS2 node subscribing to /imu/data (200Hz) + /saltybot/uwb/pose
  or /uwb/bearing (10Hz)
  - Publishes /saltybot/pose/fused (PoseStamped)
  - Publishes /saltybot/pose/fused_cov (PoseWithCovarianceStamped)
  - Broadcasts base_link → map TF2 at IMU rate
  - Suppresses output after max_dead_reckoning_s without UWB
- 14/14 unit tests passing (predict, update, dropout, PD covariance)
- Launch: ros2 launch saltybot_uwb_imu_fusion uwb_imu_fusion.launch.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:55:43 -04:00
131d85a0d3 feat: RPLIDAR safety zone detector (Issue #575)
Add saltybot_safety_zone — ROS2 Python node that processes the RPLIDAR
A1M8 /scan into three concentric 360° safety zones, latches an e-stop
when DANGER is detected in the forward arc, and overrides /cmd_vel to
zero while the latch is active.

Zone thresholds (default):
  DANGER  < 0.30 m — latching e-stop in forward arc
  WARN    < 1.00 m — advisory (published in sector data)
  CLEAR   otherwise

Sector grid:
  36 sectors of 10° each (sector 0 = robot forward, CCW positive).
  Per-sector: angle_deg, zone, min_range_m, in_forward_arc flag.

E-stop behaviour:
  - Latches after estop_debounce_frames (2) consecutive DANGER scans
    in the forward arc (configurable ±30°, or all-arcs mode).
  - While latched: zero Twist published to /cmd_vel every scan + every
    incoming /cmd_vel_input message is blocked.
  - Clear only via service (obstacle must be gone):
    /saltybot/safety_zone/clear_estop  (std_srvs/Trigger)

Published topics:
  /saltybot/safety_zone          String/JSON  every scan
    — per-sector {sector, angle_deg, zone, min_range_m, forward}
    — estop_active, estop_reason, danger_sectors[], warn_sectors[]
  /saltybot/safety_zone/status   String/JSON  10 Hz
    — forward_zone, closest_obstacle_m, danger/warn counts
  /cmd_vel                       Twist        zero when e-stopped

Subscribed topics:
  /scan           LaserScan  — RPLIDAR A1M8
  /cmd_vel_input  Twist      — upstream velocity (pass-through / block)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:54:52 -04:00
44691742c8 feat: WebUI event log panel (Issue #576)
Standalone 3-file filterable real-time event log (no build step).

Files:
  ui/event_log_panel.html  — layout, toolbar, empty state
  ui/event_log_panel.js    — rosbridge subscriptions, ring buffer, render
  ui/event_log_panel.css   — dark-theme, responsive grid layout

Features:
- 1000-entry ring buffer (oldest dropped when full, FIFO)
- Subscribes /rosout (rcl_interfaces/msg/Log) + /saltybot/events (std_msgs/String JSON)
- Severity filter buttons: DEBUG / INFO / WARN / ERROR / FATAL / EVENT (toggle on/off)
- Node name filter: select dropdown populated from seen nodes
- Live text search with <mark> highlight, Ctrl+F shortcut, Esc to clear
- Auto-scroll to latest entry; pauses on mouse hover (messages still buffered)
- Manual pause/resume button; detects user scroll-up and stops auto-scroll
- CSV export of current filtered view with timestamp (filename includes ISO date)
- Clear all entries button
- Color-coded by severity: left border stripe + text color per level
- Entry columns: timestamp (ms precision) | severity | node | message
- [system] entries for connect/disconnect events
- WS URL persisted in localStorage
- Responsive: node column hidden on narrow screens

ROS topics:
  SUB /rosout               rcl_interfaces/msg/Log  (all nodes)
  SUB /saltybot/events      std_msgs/String (JSON: {level,node,msg})

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:54:13 -04:00
sl-android
814624045a feat: Termux sensor dashboard (Issue #574)
Add phone/sensor_dashboard.py — publishes phone sensors to SaltyBot MQTT:

- IMU  → saltybot/phone/imu     @ 5 Hz  (accelerometer + gyroscope via
  termux-sensor -s <name> -n 1)
- GPS  → saltybot/phone/gps     @ 1 Hz  (lat/lon/alt/accuracy/speed/bearing
  via termux-location; GPS→network fallback on cold start)
- Battery → saltybot/phone/battery @ 1 Hz (pct/charging/temp/health/plugged
  via termux-battery-status)
- paho-mqtt with loop_start() + on_connect/on_disconnect callbacks for
  automatic reconnect (exponential back-off, max 60 s)
- Each sensor runs in its own daemon thread (SensorPoller); rate enforced
  by wall-clock sleep accounting for read latency
- 30 s status log: per-poller publish/error counts + MQTT state
- Flags: --broker, --port, --imu-hz, --gps-hz, --bat-hz, --qos,
  --no-imu, --no-gps, --no-battery, --debug

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:53:04 -04:00
929c9ecd74 feat: UWB anchor mount bracket (Issue #564) 2026-03-14 11:51:50 -04:00
8592361095 feat: PID gain scheduling for speed-dependent balance (Issue #550)
Implements a speed-dependent PID gain scheduler that interpolates Kp/Ki/Kd
across a configurable table of velocity breakpoints, replacing the fixed
single-gain PID used previously.

Changes:
- include/pid_flash.h: add pid_sched_entry_t (16-byte entry), pid_sched_flash_t
  (128-byte record at 0x0807FF40), pid_flash_load_schedule(), pid_flash_save_all()
  (atomic single-sector erase for both schedule and single-PID records)
- src/pid_flash.c: implement load_schedule and save_all; single erase covers
  both records at 0x0807FF40 (schedule) and 0x0807FFC0 (single PID)
- include/pid_schedule.h: API header -- init, get_gains, apply, set/get table,
  flash_save, active_band_idx, get_default_table
- src/pid_schedule.c: linear interpolation between sorted speed-band entries;
  integrator reset on band transition; default 3-band table (0/0.3/0.8 m/s)
- include/jlink.h: add SCHED_GET (0x0C), SCHED_SET (0x0D), SCHED_SAVE (0x0E)
  commands; TLM_SCHED (0x85); jlink_tlm_sched_t; JLinkSchedSetBuf;
  sched_get_req, sched_save_req fields in JLinkState; include pid_flash.h
- src/jlink.c: dispatch SCHED_GET/SET/SAVE; implement jlink_send_sched_telemetry,
  jlink_get_sched_set; add JLinkSchedSetBuf static buffer
- test/test_pid_schedule.c: 48 unit tests -- all passing (gcc host build)

Flash layout (sector 7):
  0x0807FF40  pid_sched_flash_t (128 bytes) -- schedule
  0x0807FFC0  pid_flash_t       ( 64 bytes) -- single PID (existing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:51:11 -04:00
35440b7463 Merge pull request 'feat: ROS2 sensor health monitor (Issue #566)' (#572) from sl-jetson/issue-566-health-monitor into main 2026-03-14 11:49:55 -04:00
d36b79371d Merge pull request 'feat: ESP32 UWB Pro anchor firmware — DS-TWR responder (Issue #544)' (#570) from sl-uwb/issue-544-anchor-firmware into main 2026-03-14 11:49:51 -04:00
3b0b9d0f16 Merge pull request 'feat: UWB tag firmware (Issue #545)' (#568) from sl-perception/issue-546-uwb-ros2 into main 2026-03-14 11:49:43 -04:00
4116232b27 Merge pull request 'feat: WebUI diagnostics dashboard (Issue #562)' (#567) from sl-webui/issue-562-diagnostics into main 2026-03-14 11:49:39 -04:00
c7dcce18c2 feat: UWB anchor mount bracket wall/ceiling design (Issue #564) 2026-03-14 11:47:07 -04:00
8e03a209be feat: ROS2 sensor health monitor (Issue #566)
Add sensor_health_node to saltybot_health_monitor package. Monitors 8
sensor topics for staleness, publishing DiagnosticArray on
/saltybot/diagnostics and MQTT JSON on saltybot/health.

Sensors monitored (configurable thresholds):
  /camera/color/image_raw, /camera/depth/image_rect_raw,
  /camera/color/camera_info, /scan, /imu/data,
  /saltybot/uwb/range, /saltybot/battery, /saltybot/motor_daemon/status

Each sensor: OK/WARN/ERROR based on topic age vs warn_s/error_s thresholds.
Critical sensors (camera, lidar, imu, motor_daemon) escalate overall status.

Files added:
  sensor_health_node.py — SensorWatcher + SensorHealthNode
  config/sensor_health_params.yaml — per-sensor thresholds
  launch/sensor_health.launch.py
  test/test_sensor_health.py — 35 tests, all passing

setup.py/package.xml updated: sensor_msgs, diagnostic_msgs deps + new entry point.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:47:01 -04:00
sl-uwb
a4879b6b3f feat: ESP32 UWB Pro anchor firmware — DS-TWR responder (Issue #544)
Anchor firmware for Makerfabs ESP32 UWB Pro (DW3000 chip). Two anchors
mount on SaltyBot (port/starboard), USB-connected to Jetson Orin.

- DS-TWR responder: Poll→Resp→Final with ±10cm accuracy
- Streams +RANGE:<id>,<mm>,<rssi_dbm> on Serial 115200
- AT command interface: AT+RANGE?, AT+RANGE_ADDR=, AT+ID?
- ANCHOR_ID 0/1 set at build time (env:anchor0 / env:anchor1)
- PlatformIO config for Makerfabs MaUWB_DW3000 library
- udev rules for /dev/uwb-anchor0 /dev/uwb-anchor1 USB symlinks
- Pin map: SCK=18 MISO=19 MOSI=23 CS=21 RST=27 IRQ=34

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:45:29 -04:00
2180b61440 feat: ROS2 UWB position node (Issue #546)
Add saltybot_uwb_position — ROS2 Python package that reads JSON range
measurements from an ESP32 DW3000 UWB tag over USB serial, trilaterates
the robot's absolute position from 3+ fixed infrastructure anchors, and
publishes position + TF2 to the rest of the stack.

Serial protocol (one JSON line per frame):
  Full frame: {"ts":…, "ranges": [{"id":0,"d_mm":1500,"rssi":-65}, …]}
  Per-anchor: {"id":0, "d_mm":1500, "rssi":-65.0}
  Accepts both "d_mm" and "range_mm" field names.

Trilateration (trilateration.py, numpy, no ROS deps):
  Linear least-squares: linearise sphere equations around anchor 0,
  solve (N-1)x2 (2D) or (N-1)x3 (3D) system via np.linalg.lstsq.
  2D mode (default): robot_z fixed, needs >=3 anchors.
  3D mode (solve_z=true): full 3D, needs >=4 anchors.

Outlier rejection:
  After initial solve, compute per-anchor residual |r_meas - r_pred|.
  Reject anchors with residual > outlier_threshold_m (0.4 m default).
  Re-solve with inliers if >= min_anchors remain.
  Track consecutive outlier strikes; flag in /status after N strikes.

Kalman filter (KalmanFilter3D, constant-velocity, 6-state, numpy):
  Predict-only coasting when anchors drop below minimum.
  Q=0.05, R=0.10 (tunable).

Topics:
  /saltybot/uwb/pose       PoseStamped  10 Hz Kalman-filtered position
  /saltybot/uwb/range/<id> UwbRange     on arrival, raw per-anchor ranges
  /saltybot/uwb/status     String/JSON  10 Hz state+residuals+flags

TF2: uwb_link -> map (identity rotation)

Anchor config: flat float arrays in YAML.
Default layout: 4-anchor 5x5m room at 2m height.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:43:22 -04:00
c2d9adad25 feat: WebUI diagnostics dashboard (Issue #562)
Standalone 3-file diagnostics dashboard (ui/diagnostics_panel.{html,js,css}).
No build step — serve the ui/ directory directly. roslib.js via CDN.

Panels:
- Battery: voltage (V), SOC (%), current (A) with large readouts + gauge bars
  + 2-minute sparkline history canvas, 4S LiPo thresholds
- Temperatures: CPU/GPU (Jetson tegrastats) + Board/STM32 + Motor L/R
  color-coded temp boxes with mini progress bars (green<60 amber<75 red>75°C)
- Motor current: per-wheel current gauge bars + CMD value + balance_state label
  Thresholds: warn 8A / crit 12A
- Resources: RAM / GPU memory / Disk — gauge bars with used/total display
  Thresholds: warn 80% / crit 95%
- WiFi / Network: RSSI signal bars (5-level) + dBm readout + latency (ms)
  MQTT broker status via mqtt_connected KeyValue
- ROS2 node health: full DiagnosticArray node list with OK/WARN/ERROR/STALE
  badges, per-node message preview, MutationObserver count badge

Features:
- Auto 2 Hz refresh via rosbridge subscriptions (throttle_rate: 500ms)
- Pulsing refresh indicator dot on each update
- System status bar: HEALTHY/DEGRADED/FAULT/STALE badge + battery/thermal/net
- Alert thresholds: red/amber/green for every metric
- Responsive CSS grid: 3-col → 2-col → 1-col via media queries
- WS URL persisted in localStorage

ROS topics:
  SUB /diagnostics              diagnostic_msgs/DiagnosticArray
  SUB /saltybot/balance_state   std_msgs/String (JSON)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:41:43 -04:00
76668d8346 Merge pull request 'feat: RPLIDAR A1 mount bracket (Issue #561)' (#563) from sl-mechanical/issue-561-rplidar-mount into main 2026-03-14 11:41:10 -04:00
d8e5490a0e feat: RPLIDAR A1 mount bracket (Issue #561) 2026-03-14 11:40:17 -04:00
6409360428 Merge pull request 'feat: Pan/tilt gimbal servo driver for ST3215 bus servos (Issue #547)' (#559) from sl-firmware/issue-547-gimbal-servo into main 2026-03-14 11:40:02 -04:00
6c5ecc9e00 Merge pull request 'feat: ROS2 gimbal control node (Issue #548)' (#558) from sl-jetson/issue-548-gimbal-ros2 into main 2026-03-14 11:39:58 -04:00
df6b79d676 Merge pull request 'feat: WebUI gimbal control panel (Issue #551)' (#557) from sl-webui/issue-551-gimbal-webui into main 2026-03-14 11:36:49 -04:00
0dbd64a6f4 Merge pull request 'feat: Camera gimbal mount for RealSense D435i (Issue #552)' (#556) from sl-mechanical/issue-552-gimbal-mount into main 2026-03-14 11:36:31 -04:00
8e21201dd4 Merge pull request 'feat: Person-following head tracking (Issue #549)' (#555) from sl-perception/issue-549-head-tracking into main 2026-03-14 11:36:29 -04:00
80e3b23aec Merge pull request 'feat: Phone voice command interface (Issue #553)' (#554) from sl-android/issue-553-voice-command into main 2026-03-14 11:36:27 -04:00