688 Commits

Author SHA1 Message Date
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
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
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