sl-android
dd13569413
feat: MQTT-to-ROS2 phone sensor bridge (Issue #601 )
...
Add saltybot_phone/mqtt_ros2_bridge_node.py — ROS2 node bridging the three
MQTT topics published by phone/sensor_dashboard.py into typed ROS2 messages:
saltybot/phone/imu → /saltybot/phone/imu sensor_msgs/Imu
saltybot/phone/gps → /saltybot/phone/gps sensor_msgs/NavSatFix
saltybot/phone/battery → /saltybot/phone/battery sensor_msgs/BatteryState
(status) → /saltybot/phone/bridge/status std_msgs/String
Key design:
- paho-mqtt loop_start() runs in dedicated network thread; on_message
enqueues (topic, payload) pairs into a thread-safe queue
- ROS2 timer drains queue at 50 Hz — all publishing stays on executor
thread, avoiding any rclpy threading concerns
- Timestamp alignment: uses ROS2 wall clock by default; opt-in
use_phone_timestamp param uses phone epoch ts when drift < warn_drift_s
- IMU: populates accel + gyro with diagonal covariance; orientation_cov[0]=-1
(unknown per REP-145)
- GPS: NavSatStatus.STATUS_FIX for gps/fused/network providers; full 3×3
position covariance from accuracy_m; COVARIANCE_TYPE_APPROXIMATED
- Battery: pct→percentage [0-1], temp Kelvin, health/status mapped from
Android health strings, voltage/current=NaN (unavailable on Android)
- Input validation: finite value checks on IMU, lat/lon range on GPS,
pct [0-100] on battery; bad messages logged at DEBUG and counted
- Status topic at 0.2 Hz: JSON {mqtt_connected, rx/pub/err counts,
age_s per sensor, queue_depth}
- Auto-reconnect via paho reconnect_delay_set (5 s → 20 s max)
Add launch/mqtt_bridge.launch.py with args: mqtt_host, mqtt_port,
reconnect_delay_s, use_phone_timestamp, warn_drift_s, imu_accel_cov,
imu_gyro_cov.
Register mqtt_ros2_bridge console script in setup.py.
Add python3-paho-mqtt exec_depend to package.xml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 14:59:02 -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
36643dd652
feat: Pan/tilt gimbal servo driver for ST3215 bus servos (Issue #547 )
...
- servo_bus.c/h: half-duplex USART3 driver for Feetech ST3215 servos at
1 Mbps; blocking TX/RX with CRC checksum; read/write position, torque
enable, speed; deg<->raw conversion (center=2048, 4096 counts/360°)
- gimbal.c/h: gimbal_t controller; 50 Hz feedback poll alternating pan/tilt
at 25 Hz each; clamps to ±GIMBAL_PAN/TILT_LIMIT_DEG soft limits
- jlink.c: dispatch JLINK_CMD_GIMBAL_POS (0x0B, 6-byte payload int16+int16+
uint16); jlink_send_gimbal_state() for JLINK_TLM_GIMBAL_STATE (0x84)
- main.c: servo_bus_init() + gimbal_init() on boot; gimbal_tick() in main
loop; gimbal_updated flag handler; GIMBAL_STATE telemetry at 50 Hz
- config.h: SERVO_BUS_UART/PORT/PIN/BAUD, GIMBAL_PAN/TILT_ID, GIMBAL_TLM_HZ,
GIMBAL_PAN/TILT_LIMIT_DEG
- jlink.h: CMD_GIMBAL_POS, TLM_GIMBAL_STATE, jlink_tlm_gimbal_state_t (10 B),
gimbal_updated/pan_x10/tilt_x10/speed volatile fields in JLinkState
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:38:06 -04:00
da6a17cdcb
feat: ROS2 gimbal control node (Issue #548 )
...
saltybot_gimbal ROS2 Python package for pan/tilt camera head control
via JLINK binary protocol over serial to STM32 (Issue #547 C side).
- gimbal_node.py: subscribes /saltybot/gimbal/cmd (Vector3: pan, tilt,
speed), publishes /saltybot/gimbal/state (JSON), /saltybot/gimbal/cmd_echo
- Services: /saltybot/gimbal/home (Trigger), /saltybot/gimbal/look_at
(Trigger + /saltybot/gimbal/look_at_target PointStamped)
- jlink_gimbal.py: JLINK codec matching jlink.h — CMD_GIMBAL_POS=0x0B,
TLM_GIMBAL_STATE=0x84, CRC16-CCITT, deg*10 encoding, speed register
- MotionAxis: trapezoidal velocity profile (configurable accel + speed)
- Configurable limits: pan ±150°, tilt ±45° (gimbal_params.yaml)
- Serial reconnect with configurable retry delay
- 48 unit tests — all passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:34:06 -04:00
cc3a65f4a4
feat: WebUI gimbal control panel (Issue #551 )
...
Adds a full gimbal control panel with live camera preview:
Standalone page (ui/gimbal_panel.html + .js + .css):
- Self-contained HTML page, no build step, served directly
- roslib.js via CDN, connects to rosbridge WebSocket
- 2-D canvas pan/tilt pad: click-drag + touch pointer capture
- Live camera stream (front/rear/left/right selector, base64 CompressedImage)
- FPS badge + angle overlay on video feed
- Preset positions: CENTER / LEFT / RIGHT / UP / DOWN
- Home button (0° / 0°)
- Person-tracking toggle → /gimbal/tracking_enabled
- Current angle display from /gimbal/state feedback
- WS URL persisted in localStorage
React component (GimbalPanel.jsx) + App.jsx integration:
- Same features in dashboard — TELEOP group → Gimbal tab
- Shares rosbridge connection from parent
- Mobile-responsive: stacks vertically on mobile, side-by-side on lg+
ROS topics:
PUB /gimbal/cmd geometry_msgs/Vector3
SUB /gimbal/state geometry_msgs/Vector3
PUB /gimbal/tracking_enabled std_msgs/Bool
SUB /camera/*/image_raw/compressed sensor_msgs/CompressedImage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:29:29 -04:00