5f0affcd79
feat: Jetson Orin system monitor ROS2 node (Issue #631 )
...
New package saltybot_system_monitor:
- jetson_stats.py: pure-Python data layer (JetsonStats, CpuCore,
TegrastatsParser, JtopReader, TegrastatsReader, MockReader,
AlertChecker, AlertThresholds) — no ROS2 dependency
- system_monitor_node.py: ROS2 node publishing /saltybot/system/stats
(JSON) and /saltybot/diagnostics (DiagnosticArray) at 1 Hz
- Alerts: CPU/GPU >85% WARN (+10% ERROR), temp >80°C, disk/RAM >90%,
power >30 W; each alert produces a DiagnosticStatus entry
- Stats source priority: jtop > tegrastats > mock (auto-detected)
- config/system_monitor.yaml: all thresholds and rate tunable via params
- launch/system_monitor.launch.py: single-node launch with config arg
- test/test_system_monitor.py: 50+ pytest tests, ROS2-free
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 16:36:04 -04:00
4c7fa938a5
Merge pull request 'feat: UWB accuracy analyzer (Issue #634 )' ( #641 ) from sl-uwb/issue-634-uwb-logger into main
2026-03-15 16:30:16 -04:00
45332f1a8b
Merge pull request 'feat: UART command protocol for Jetson-STM32 (Issue #629 )' ( #639 ) from sl-firmware/issue-629-uart-protocol into main
2026-03-15 16:30:09 -04:00
af46b15391
Merge pull request 'feat: ArUco docking detection (Issue #627 )' ( #638 ) from sl-perception/issue-627-aruco-docking into main
2026-03-15 16:30:04 -04:00
e1d605dba7
Merge pull request 'feat: WebUI main dashboard (Issue #630 )' ( #637 ) from sl-webui/issue-630-main-dashboard into main
2026-03-15 16:30:00 -04:00
c8c8794daa
Merge pull request 'feat: Termux voice command interface (Issue #633 )' ( #636 ) from sl-android/issue-633-voice-commands into main
2026-03-15 16:29:56 -04:00
b5862ef529
Merge pull request 'feat: Cable management tray (Issue #628 )' ( #635 ) from sl-mechanical/issue-628-cable-tray into main
2026-03-15 16:29:52 -04:00
sl-uwb
343e53081a
feat: UWB position logger and accuracy analyzer (Issue #634 )
...
saltybot_uwb_logger_msgs (new package):
- AccuracyReport.msg: n_samples, mean/bias/std (x,y,2D), CEP50, CEP95,
RMSE, max_error, per-anchor range stats, test_id, duration_s
- StartAccuracyTest.srv: request (truth_x/y_m, n_samples, timeout_s,
test_id) → response (success, message, test_id)
saltybot_uwb_logger (new package):
- accuracy_stats.py: compute_stats() + RangeAccum — pure numpy, no ROS2
CEP50/CEP95 = 50th/95th percentile of 2-D error; bias, std, RMSE, max
- logger_node.py: /uwb_logger ROS2 node
Subscribes:
/saltybot/pose/fused → fused_pose_<DATE>.csv (ts, x, y, heading)
/saltybot/uwb/pose → uwb_pose_<DATE>.csv (ts, x, y)
/uwb/ranges → uwb_ranges_<DATE>.csv (ts, anchor_id, range_m,
raw_mm, rssi, tag_id)
Service /saltybot/uwb/start_accuracy_test:
Collects N fused-pose samples at known (truth_x, truth_y) in background
thread. On completion or timeout: publishes AccuracyReport on
/saltybot/uwb/accuracy_report + writes accuracy_<test_id>.json.
Per-anchor range stats included. CSV flushed every 5 s.
Tests: 16/16 passing (test/test_accuracy_stats.py, no ROS2/hardware)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:44:21 -04:00
602fbc6ab3
feat: UART command protocol for Jetson-STM32 (Issue #629 )
...
Implements binary command protocol on UART5 (PC12/PD2) at 115200 baud
for Jetson→STM32 communication. Frame: STX+LEN+CMD+PAYLOAD+CRC8+ETX.
Commands: SET_VELOCITY (RPM direct to CAN), GET_STATUS, SET_PID, ESTOP,
CLEAR_ESTOP. DMA1_Stream0_Channel4 circular 256-byte RX ring. ACK/NACK
inline; STATUS pushed at 10 Hz. Heartbeat timeout 500 ms (UART_PROT_HB_TIMEOUT_MS).
NOTE: Spec requested USART1 @ 115200; USART1 occupied by JLink @ 921600.
Implemented on UART5 instead; note in code comments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:41:00 -04:00
1fd935b87e
feat: ArUco marker detection for docking (Issue #627 )
...
New package saltybot_aruco_detect — DICT_4X4_50 ArUco detection from
RealSense D435i RGB, pose estimation, PoseArray + dock target output.
aruco_math.py (pure Python, no ROS2): rot_mat_to_quat (Shepperd),
rvec_to_quat (Rodrigues + cv2 fallback), tvec_distance, tvec_yaw_rad,
MarkerPose dataclass with lazy-cached distance_m/yaw_rad/lateral_m/quat.
aruco_detect_node.py (ROS2 node 'aruco_detect'):
Subscribes: /camera/color/image_raw (30Hz BGR8) + /camera/color/camera_info.
Converts to greyscale, cv2.aruco.ArucoDetector.detectMarkers().
estimatePoseSingleMarkers (legacy API) with solvePnP(IPPE_SQUARE) fallback.
Dock target: closest marker in dock_marker_ids (default=[42], empty=any),
filtered to max_dock_range_m (3.0m).
Publishes: /saltybot/aruco/markers (PoseArray — all detected, camera frame),
/saltybot/aruco/dock_target (PoseStamped — closest dock candidate,
position.z=forward, position.x=lateral), /saltybot/aruco/viz (MarkerArray
— SPHERE + TEXT per marker, dock in red), /saltybot/aruco/status (JSON
10Hz — detected_count, dock_distance_m, dock_yaw_deg, dock_lateral_m).
Optional debug image with drawDetectedMarkers + drawFrameAxes.
corner_refinement=CORNER_REFINE_SUBPIX.
config/aruco_detect_params.yaml, launch/aruco_detect.launch.py.
test/test_aruco_math.py: 22 unit tests (rotation/quat math, distance,
yaw sign/magnitude, MarkerPose accessors + caching).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:37:22 -04:00
b6c6dbd838
feat: WebUI main dashboard with panel launcher (Issue #630 )
...
Replaces ui/index.html (old USB-serial HUD) with a full rosbridge
dashboard. Adds ui/dashboard.{css,js}.
Top bar:
- Robot name + ⚡ SALTYBOT logo
- Live battery % + voltage with fill bar (4S LiPo: 12.0V–16.8V)
- Safety state from /saltybot/safety_zone/status (GREEN/AMBER/RED)
- E-stop state display
- Drive mode display
- ROS uptime counter
- rosbridge WS input + CONNECT button
Panel grid (auto-fill responsive):
- MAP VIEW (#587 ) — /saltybot/pose/fused liveness dot
- GAMEPAD TELEOP (#598 ) — /cmd_vel activity indicator
- DIAGNOSTICS (#562 ) — /diagnostics liveness dot
- EVENT LOG (#576 ) — /rosout liveness dot
- SETTINGS (#614 ) — param service (config state, no topic)
- GIMBAL (#551 ) — /gimbal/state liveness dot
Each card shows: icon, title, issue #, description, topic chips,
and a LIVE/IDLE/OFFLINE status badge updated every second. Cards
open the linked standalone panel in the same tab.
Auto-detect rosbridge:
- Probes: page hostname:9090, localhost:9090, saltybot.local:9090
- Progress dots per candidate (trying/ok/fail)
- Falls back to manual URL entry
- Saves last successful URL to localStorage
Bottom bar:
- ⛔ E-STOP button (latches, publishes zero Twist to /cmd_vel)
Space bar shortcut from dashboard
- RESUME button
- Drive mode switcher: MANUAL / AUTO / FOLLOW / DOCK
(publishes to /saltybot/drive_mode std_msgs/String)
- Session timer (HH:MM:SS since page load)
Info strip: rosbridge URL · msg rate · latency (5s ping via
/rosapi/get_time) · robot IP
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:35:56 -04:00
sl-android
26bf4ab8d3
feat: Add Termux voice command interface (Issue #633 )
...
phone/voice_cmd.py — listens via termux-speech-to-text, parses commands
(go forward/back, turn left/right, stop, e-stop, go to waypoint, speed
up/down, status) and publishes structured JSON to saltybot/phone/voice_cmd.
TTS confirmation via termux-tts-speak. Manual text fallback via --text flag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:35:27 -04:00
cb802ee76f
feat: Cable management tray (Issue #628 )
2026-03-15 14:33:49 -04:00
0e8758e9e1
Merge pull request 'feat: Battery voltage telemetry + LVC (Issue #613 )' ( #626 ) from sl-firmware/issue-613-battery-voltage into main
2026-03-15 13:29:32 -04:00
7785a16bff
feat: Battery voltage telemetry and LVC (Issue #613 )
...
- Add include/lvc.h + src/lvc.c: 3-stage low voltage cutoff state machine
WARNING 21.0V: MELODY_LOW_BATTERY buzzer, full motor power
CRITICAL 19.8V: double-beep every 10s, 50% motor power scaling
CUTOFF 18.6V: MELODY_ERROR one-shot, motors disabled + latched
200mV hysteresis on recovery; CUTOFF latched until reboot
- Add JLINK_TLM_LVC (0x8B, 4 bytes): voltage_mv, percent, protection_state
jlink_send_lvc_tlm() frame encoder in jlink.c
- Wire into main.c:
lvc_init() at startup; lvc_tick() each 1kHz loop tick
lvc_is_cutoff() triggers safety_arm_cancel + balance_disarm + motor_driver_estop
lvc_get_power_scale() applied to ESC speed command (100/50/0%)
1Hz JLINK_TLM_LVC telemetry with fuel-gauge percent field
- Add LVC thresholds to config.h (LVC_WARNING/CRITICAL/CUTOFF/HYSTERESIS_MV)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 11:04:38 -04:00
68568b2b66
Merge pull request 'feat: WebUI settings panel (Issue #614 )' ( #622 ) from sl-webui/issue-614-settings-panel into main
2026-03-15 11:03:04 -04:00
38df5b4ebb
Merge pull request 'feat: GPS waypoint logger (Issue #617 )' ( #620 ) from sl-android/issue-617-waypoint-logger into main
2026-03-15 11:02:58 -04:00
fea550c851
Merge pull request 'feat: ROS2 bag recording manager (Issue #615 )' ( #625 ) from sl-jetson/issue-615-bag-recorder into main
2026-03-15 11:02:37 -04:00
13b17a11e1
Merge pull request 'feat: Steering PID controller (Issue #616 )' ( #624 ) from sl-controls/issue-616-steering-pid into main
2026-03-15 11:02:33 -04:00
96d13052b4
Merge pull request 'feat: RealSense obstacle detection (Issue #611 )' ( #623 ) from sl-perception/issue-611-obstacle-detect into main
2026-03-15 11:02:29 -04:00
a01fa091d4
Merge pull request 'feat: ESP-NOW to ROS2 serial relay node (Issue #618 )' ( #621 ) from sl-uwb/issue-618-espnow-relay into main
2026-03-15 11:02:21 -04:00
62aab7164e
Merge pull request 'feat: Jetson Orin Nano mount bracket (Issue #612 )' ( #619 ) from sl-mechanical/issue-612-jetson-mount into main
2026-03-15 11:02:14 -04:00
7e12dab4ae
feat: ROS2 bag recording manager (Issue #615 )
...
Upgrades saltybot_bag_recorder (Issue #488 ) with:
- Motion-triggered auto-record: subscribes /cmd_vel, starts on non-zero
velocity, stops after 30s idle timeout (configurable)
- Auto-split at 1 GB or 10 min via subprocess restart
- USB/NVMe storage selection: ordered priority list, picks first path
with >= 2 GB free (/media/usb0 -> /media/usb1 -> /mnt/nvme -> ~/bags)
- Disk monitoring: warns at 70%, triggers cleanup of bags >7 days at 80%
- JSON status on /saltybot/bag_recorder/status at 1 Hz
- Services: /saltybot/bag_recorder/{start,stop,split}
(legacy /saltybot/{start,stop}_recording kept for compatibility)
- bag_policy.py: pure-Python MotionState, DiskInfo, StorageSelector,
BagPolicy — ROS2-free, fully unit-testable
- 76 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:12:40 -04:00
1e69337ffd
feat: Steering PID for differential drive (Issue #616 )
...
Closed-loop yaw-rate controller that converts Jetson Twist.angular.z
to a differential wheel speed offset using IMU gyro Z as feedback.
- include/steering_pid.h + src/steering_pid.c: PID with anti-windup
(integral clamped to ±200 counts) and rate limiter (10 counts/ms
max output change) to protect balance PID from sudden steering steps.
JLINK_TLM_STEERING (0x8A) telemetry at 10 Hz.
- include/mpu6000.h + src/mpu6000.c: expose yaw_rate (board_gz) in
IMUData so callers have direct bias-corrected gyro Z feedback.
- include/jlink.h + src/jlink.c: add JLINK_TLM_STEERING (0x8A),
jlink_tlm_steering_t (8 bytes), jlink_send_steering_tlm().
- test/test_steering_pid.c: 78 unit tests (host build with gcc),
all passing.
Usage (main loop):
steering_pid_set_target(&s, jlink_state.steer * STEER_OMEGA_SCALE);
int16_t steer_out = steering_pid_update(&s, imu.yaw_rate, dt);
motor_driver_update(&motor, balance_cmd, steer_out, now_ms);
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:11:05 -04:00
82ad626a94
feat: RealSense depth obstacle detection (Issue #611 )
...
New package saltybot_obstacle_detect — RANSAC ground plane fitting on
D435i depth images with 2D grid BFS obstacle clustering.
ground_plane.py (pure Python + numpy):
fit_ground_plane(pts, n_iter=50, inlier_thresh_m=0.06): RANSAC over 3D
point cloud in camera optical frame (+Z forward). Samples 3 points, fits
plane via cross-product, counts inliers, refines via SVD on best inlier
set. Orients normal toward -Y (upward in world). Returns (normal, d).
height_above_plane(pts, plane): signed h = d - n·p (h>0 = above ground).
obstacle_mask(pts, plane, min_h, max_h): min_obstacle_h_m < h < max_h.
ground_mask(pts, plane, thresh): inlier classification.
obstacle_clusterer.py (pure Python + numpy):
cluster_obstacles(pts, heights, cell_m=0.30, min_pts=5): projects
obstacle 3D points onto (X,Z) bird's-eye plane, discretises into grid
cells, runs 4-connected BFS flood-fill, returns ObstacleCluster list
sorted by forward distance. ObstacleCluster: centroid(3), radius_m,
height_m, n_pts + distance_m/lateral_m properties.
obstacle_detect_node.py (ROS2 node 'obstacle_detect'):
- Subscribes: /camera/depth/camera_info (latched, once),
/camera/depth/image_rect_raw (BEST_EFFORT, 30Hz float32 depth).
- Pipeline: stride downsample (default 8x → 80x60) → back-project to
3D → RANSAC ground plane (temporally blended α=0.3) → obstacle mask
(min_h=0.05m, max_h=0.80m) → BFS clustering → alert classification.
- Publishes:
/saltybot/obstacles (MarkerArray): SPHERE markers colour-coded
DANGER(red)/WARN(yellow)/CLEAR(green) + distance TEXT labels.
/saltybot/obstacles/cloud (PointCloud2): xyz float32 non-ground pts.
/saltybot/obstacles/alert (String JSON): alert_level, closest_m,
obstacle_count, per-obstacle {x,y,z,radius_m,height_m,level}.
- Safety zone integration (depth_estop_enabled=false by default):
DANGER → zero Twist to depth_estop_topic (/cmd_vel_input) feeds
into safety_zone's cmd_vel chain for independent depth e-stop.
config/obstacle_detect_params.yaml: all tuneable parameters with comments.
launch/obstacle_detect.launch.py: single node with params_file arg.
test/test_ground_plane.py: 10 unit tests (RANSAC correctness, normal
orientation, height computation, inlier/obstacle classification).
test/test_obstacle_clusterer.py: 8 unit tests (single/dual cluster,
distance sort, empty, min_pts filter, centroid accuracy, range clip).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:09:23 -04:00
921eaba8b3
feat: WebUI settings and configuration panel (Issue #614 )
...
Standalone ui/settings_panel.{html,js,css} — no build step.
Sections / tabs:
- PID: balance_controller (Kp/Ki/Kd/i_clamp/rate),
adaptive_pid (kp/ki/kd per load profile, output bounds)
- Speed: tank_driver (max_linear_vel, max_angular_vel, slip_factor),
smooth_velocity_controller (accel/decel limits),
battery_speed_limiter (speed factors)
- Safety: safety_zone (danger_range_m, warn_range_m, forward_arc_deg,
debounce, min_valid_range, publish_rate),
power_supervisor_node (battery % thresholds, speed factors),
lidar_avoidance (e-stop distance, safety zone sizes)
- Sensors: boolean toggles (estop_all_arcs, lidar_enabled, uwb_enabled),
uwb_imu_fusion weights and publish rate
- System: live /diagnostics subscriber (CPU/GPU/board/motor temps,
RAM/GPU/disk usage, WiFi RSSI+latency, MQTT status, last-update),
/rosapi/nodes node list
ROS2 parameter services (rcl_interfaces/srv/GetParameters +
SetParameters) via rosbridge WebSocket. Each section has independent
↓ LOAD (get_parameters) and ↑ APPLY (set_parameters) buttons with
success/error status feedback.
Presets: save/load/delete named snapshots of all values to
localStorage. Reset-to-defaults button restores built-in defaults.
Changed fields highlighted in amber (slider thumb + input border).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:08:47 -04:00
sl-uwb
65e0009118
feat: ESP-NOW to ROS2 serial relay (Issue #618 )
...
New ROS2 package saltybot_uwb_espnow_relay:
- packet.py: EspNowPacket dataclass + FrameReader stateful decoder
- Parses 20-byte ESP-NOW packets: MAGIC, tag_id, msg_type, anchor_id,
range_mm (int32 LE), rssi_dbm (float32), timestamp_ms, battery_pct,
flags (bit0=estop), seq_num
- Serial framing: STX(0x02) + LEN(0x14) + DATA[20] + XOR-CRC(1)
- Sync recovery: re-hunts STX after bad LEN or CRC; byte-by-byte capable
- relay_node.py: /espnow_relay ROS2 node
- Reads from USB serial in background thread (auto-reconnects on error)
- MSG_RANGE (0x10): publishes UwbRange on /uwb/espnow/ranges
- MSG_ESTOP (0x20): publishes std_msgs/Bool on /uwb/espnow/estop
and /saltybot/estop (latched True for estop_latch_s after last packet)
- MSG_HEARTBEAT (0x30): publishes EspNowHeartbeat on /uwb/espnow/heartbeat
- Range validity gating: min_range_m / max_range_m params
- 16/16 unit tests passing (test/test_packet.py, no ROS2/hardware needed)
saltybot_uwb_msgs: add EspNowHeartbeat.msg
(tag_id, battery_pct, seq_num, timestamp_ms + std_msgs/Header)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:08:19 -04:00
sl-android
9b1f3ddaf0
feat: GPS waypoint logger and route planner (Issue #617 )
...
Add phone/waypoint_logger.py — interactive Termux CLI for recording,
managing, and publishing GPS waypoints:
GPS acquisition
- termux-location with gps/network/passive provider selection
- Falls back to network provider on GPS timeout
- Optional --live-gps flag: subscribes to saltybot/phone/gps MQTT
topic (sensor_dashboard.py stream) to avoid redundant GPS calls
Waypoint operations
- Record: acquires GPS fix, prompts for name + tags, appends to route
- List: table with lat/lon/alt/accuracy/tags + inter-waypoint
distance (haversine) and bearing (8-point compass)
- Delete: by index with confirmation prompt
- Clear: entire route with confirmation
- Rename: route name
Persistence
- Routes saved as JSON to ~/saltybot_route.json (configurable)
- Auto-loads on startup; survives session restarts
MQTT publish (saltybot/phone/route, QoS 1, retained)
- Full waypoint list with metadata
- nav2_poses array: flat-earth x/y (metres from origin),
quaternion yaw facing next waypoint (last faces prev)
- Compatible with Nav2 FollowWaypoints action input
Geo maths
- haversine_m(): great-circle distance
- bearing_deg(): initial bearing with 8-point compass label
- flat_earth_xy(): ENU metres for Nav2 pose export (<1% error <100km)
Flags: --broker, --port, --file, --route, --provider, --live-gps,
--no-mqtt, --debug
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:05:57 -04:00
837c42a00d
feat: Jetson Orin Nano mount bracket (Issue #612 )
2026-03-15 10:04:37 -04:00
c0bb4f6276
Merge pull request 'feat: CAN bus driver for BLDC motor controllers (Issue #597 )' ( #610 ) from sl-firmware/issue-597-can-driver into main
2026-03-14 16:27:36 -04:00
2996d18ace
feat: CAN bus driver for BLDC motor controllers (Issue #597 )
...
- Add can_driver.h / can_driver.c: CAN2 on PB12/PB13 (AF9) at 500 kbps
APB1=54 MHz, PSC=6, BS1=13tq, BS2=4tq, SJW=1tq → 18tq/bit, SP 77.8%
Filter bank 14 (SlaveStartFilterBank=14); 32-bit mask; FIFO0
Accept std IDs 0x200–0x21F (left/right feedback frames)
TX: velocity+torque cmd (0x100+nid, DLC=4) at 100 Hz via main loop
RX: velocity/current/position/temp/fault feedback (0x200+nid, DLC=8)
AutoBusOff=ENABLE for HW recovery; can_driver_process() drains FIFO0
- Add JLINK_TLM_CAN_STATS (0x89, 16 bytes) + JLINK_CMD_CAN_STATS_GET (0x10)
Also add JLINK_TLM_SLOPE (0x88) + jlink_tlm_slope_t missing from Issue #600
- Wire into main.c: init after jlink_init; 100Hz TX loop (differential drive
speed_rpm ± steer_rpm/2); CAN enable follows arm state; 1Hz stats telemetry
- Add CAN_RPM_SCALE=10 and CAN_TLM_HZ=1 to config.h
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:58:13 -04:00
bb5eff1382
Merge pull request 'feat: MQTT-to-ROS2 phone sensor bridge (Issue #601 )' ( #605 ) from sl-android/issue-601-mqtt-ros2-bridge into main
2026-03-14 15:55:22 -04:00
8b1d6483cf
Merge pull request 'feat: Slope tilt compensation (Issue #600 )' ( #609 ) from sl-controls/issue-600-slope-compensation into main
2026-03-14 15:55:01 -04:00
6c00d6a321
Merge pull request 'feat: UWB anchor auto-calibration via inter-anchor ranging + MDS (Issue #602 )' ( #608 ) from sl-uwb/issue-602-anchor-calibration into main
2026-03-14 15:54:56 -04:00
2460ba27c7
Merge pull request 'feat: Nav2 with UWB localization (Issue #599 )' ( #607 ) from sl-jetson/issue-599-nav2-uwb into main
2026-03-14 15:54:52 -04:00
2367e08140
Merge pull request 'feat: Multi-sensor pose fusion (Issue #595 )' ( #606 ) from sl-perception/issue-595-pose-fusion into main
2026-03-14 15:54:48 -04:00
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
be4966b01d
feat: Tilt compensation for slopes (Issue #600 )
...
Adds a slow-adapting terrain slope estimator (IIR tau=5s) that decouples
the robot's balance offset from genuine ground incline. The balance
controller subtracts the slope estimate from measured pitch so the PID
balances around the slope surface rather than absolute vertical.
- include/slope_estimator.h + src/slope_estimator.c: first-order IIR
filter clamped to ±15°; JLINK_TLM_SLOPE (0x88) telemetry at 1 Hz
- include/jlink.h + src/jlink.c: add JLINK_TLM_SLOPE (0x88),
jlink_tlm_slope_t (4 bytes), jlink_send_slope_tlm()
- include/balance.h + src/balance.c: integrate slope_estimator into
balance_t; update, reset on tilt-fault and disarm
- test/test_slope_estimator.c: 35 unit tests, all passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:07:05 -04:00
sl-uwb
82cc223fb8
feat: Add AT+PEER_RANGE= command for inter-anchor calibration (Issue #602 )
...
- peer_range_once(): DS-TWR initiator role toward a peer anchor
(POLL → RESP → FINAL, one-sided range estimate Ra - Da/2)
- AT+PEER_RANGE=<id>: returns +PEER_RANGE:<my>,<peer>,<mm>,<rssi>
or +PEER_RANGE:ERR,<peer>,TIMEOUT
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:06:29 -04:00
5f03e4cbef
feat: Tilt compensation for slopes (Issue #600 )
...
Adds a slow-adapting terrain slope estimator (IIR tau=5s) that decouples
the robot's balance offset from genuine ground incline. The balance
controller subtracts the slope estimate from measured pitch so the PID
balances around the slope surface rather than absolute vertical.
- include/slope_estimator.h + src/slope_estimator.c: first-order IIR
filter clamped to ±15°; JLINK_TLM_SLOPE (0x88) telemetry at 1 Hz
- include/jlink.h + src/jlink.c: add JLINK_TLM_SLOPE (0x88),
jlink_tlm_slope_t (4 bytes), jlink_send_slope_tlm()
- include/balance.h + src/balance.c: integrate slope_estimator into
balance_t; update, reset on tilt-fault and disarm
- test/test_slope_estimator.c: 35 unit tests, all passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:04:58 -04:00
sl-uwb
587ca8a98e
feat: UWB anchor auto-calibration via inter-anchor ranging + MDS (Issue #602 )
...
Anchor firmware (esp32/uwb_anchor/src/main.cpp):
- Add peer_range_once(peer_id) — DS-TWR initiator role toward a peer anchor
- Add AT+PEER_RANGE=<id> command: triggers inter-anchor ranging and returns
+PEER_RANGE:<my_id>,<peer_id>,<range_mm>,<rssi_dbm> (or ERR,TIMEOUT)
ROS2 package saltybot_uwb_calibration_msgs:
- CalibrateAnchors.srv: request (anchor_ids[], n_samples) →
response (positions_x/y/z[], residual_rms_m, anchor_positions_json)
ROS2 package saltybot_uwb_calibration:
- mds_math.py: classical MDS (double-centred D², eigendecomposition),
anchor_frame_align() to fix anchor-0 at origin / anchor-1 on +X
- calibration_node.py: /saltybot/uwb/calibrate_anchors service —
opens anchor serial ports, rounds-robin AT+PEER_RANGE= for all pairs,
builds N×N distance matrix, runs MDS, returns JSON anchor positions
- 12/12 unit tests passing (test/test_mds_math.py)
- Supports ≥ 4 anchors; 5× averaged ranging per pair by default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:03:53 -04:00
40b0917c33
feat: Nav2 integration with UWB localization (Issue #599 )
...
New package saltybot_nav2_uwb replacing AMCL-based localization with
UWB-IMU EKF fused pose. Key components:
- uwb_pose_bridge_node: subscribes /saltybot/pose/fused_cov (from EKF),
computes map→odom TF via T_map_odom = T_map_base × inv(T_odom_base),
broadcasts at 20 Hz. Publishes /initialpose on first valid pose.
- waypoint_sequencer.py: pure-Python state machine (IDLE→RUNNING→
SUCCEEDED/ABORTED/CANCELED) for sequential waypoint execution.
- waypoint_follower_node: action server on /saltybot/nav/follow_waypoints
(nav2_msgs/FollowWaypoints), sends each goal to Nav2 NavigateToPose
in sequence; JSON topic /saltybot/nav/waypoints for operator use.
- nav2_uwb_params.yaml: DWB controller capped at 1.0 m/s, global+local
costmap with /scan (RPLIDAR), rolling-window global costmap (no static
map needed), AMCL removed from lifecycle manager.
- nav2_uwb.launch.py: bridge (t=0) → Nav2 (t=2s) → waypoint follower
(t=4s) with LogInfo markers.
- 65 unit tests passing (waypoint dataclass, sequencer state machine,
2-D TF maths, progress tracking).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:02:26 -04:00
c76d5b0dd7
feat: Multi-sensor pose fusion node (Issue #595 )
...
New package saltybot_pose_fusion — EKF fusing UWB+IMU absolute pose,
visual odometry velocity, and raw IMU into a single authoritative pose.
pose_fusion_ekf.py (pure Python, no ROS2 deps):
PoseFusionEKF — state [x, y, θ, vx, vy, ω], 6-state EKF.
- predict_imu(ax_body, ay_body, omega, dt): body-frame IMU predict step
with Jacobian F, bias-compensated accel, process noise Q.
- update_uwb_position(x, y, sigma_m): absolute position measurement
(H=[1,0,0,0,0,0; 0,1,0,0,0,0]) from UWB+IMU fused stream.
- update_uwb_heading(heading_rad, sigma_rad): heading measurement.
- update_vo_velocity(vx_body, omega, ...): VO velocity measurement —
body-frame vx rotated to world via cos/sin(θ), updates [vx,vy,ω] state.
- Joseph-form covariance update for numerical stability.
- Dual dropout clocks: uwb_dropout_s, vo_dropout_s (reset on each update).
- Velocity damping when uwb_dropout_s > 2s.
- Sensor weight parameters: sigma_uwb_pos_m, sigma_uwb_head_rad,
sigma_vo_vel_m_s, sigma_vo_omega_r_s, sigma_imu_accel/gyro,
sigma_vel_drift, dropout_vel_damp.
pose_fusion_node.py (ROS2 node 'pose_fusion'):
- Subscribes: /imu/data (Imu, 200Hz → predict), /saltybot/pose/fused_cov
(PoseWithCovarianceStamped, 10Hz → position+heading update, σ extracted
from message covariance when use_uwb_covariance=true), /saltybot/visual_odom
(Odometry, 30Hz → velocity update, σ from twist covariance).
- Publishes: /saltybot/pose/authoritative (PoseWithCovarianceStamped),
/saltybot/pose/status (String JSON, 10Hz).
- TF2: map→base_link broadcast at IMU rate.
- Suppresses output when uwb_dropout_s > uwb_dropout_max_s (10s).
- Warns (throttled) on UWB/VO dropout.
config/pose_fusion_params.yaml: sensor weights + dropout thresholds.
launch/pose_fusion.launch.py: single node launch with params_file arg.
test/test_pose_fusion_ekf.py: 13 unit tests — init, predict, UWB/VO
updates, dropout reset, covariance shape/convergence, sigma override.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:00:54 -04:00
sl-android
c62444cc0e
chore: Register mqtt_ros2_bridge entry point and paho-mqtt dep (Issue #601 )
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 14:59:41 -04:00
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
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