8985934f29
Merge pull request 'fix: Bump arm pitch threshold to 20° (Issue #678 )' ( #679 ) from sl-firmware/issue-678-pitch-threshold into main
2026-03-18 07:48:49 -04:00
0a2f336eb8
Merge pull request 'feat: Orin CAN bus bridge — CANable 2.0 (Issue #674 )' ( #675 ) from sl-jetson/issue-674-can-bus-orin into main
2026-03-17 21:41:29 -04:00
5e82878083
feat: bxCAN integration for VESC motor control and Orin comms (Issue #674 )
...
- can_driver: add filter bank 15 (all ext IDs → FIFO1) and widen bank 14
to accept all standard IDs; add can_driver_send_ext/std and ext/std
frame callbacks (can_driver_set_ext_cb / can_driver_set_std_cb)
- vesc_can: VESC 29-bit extended CAN protocol driver — send RPM to IDs 56
and 68 (FSESC 6.7 Pro Mini Dual), parse STATUS/STATUS_4/STATUS_5
big-endian payloads, alive timeout, JLINK_TLM_VESC_STATE at 1 Hz
- orin_can: Orin↔FC standard CAN protocol — HEARTBEAT/DRIVE/MODE/ESTOP
commands in, FC_STATUS + FC_VESC broadcast at 10 Hz
- jlink: add JLINK_TLM_VESC_STATE (0x8E), jlink_tlm_vesc_state_t (22 bytes),
jlink_send_vesc_state_tlm()
- main: wire vesc_can_init/orin_can_init; replace can_driver_send_cmd with
vesc_can_send_rpm; inject Orin CAN speed/steer into balance PID; add
Orin CAN estop/clear handling; add orin_can_broadcast at 10 Hz
- test: 56-test host-side suite for vesc_can; test/stubs/stm32f7xx_hal.h
minimal HAL stub for all future host-side tests
Safety: balance PID runs independently on Mamba — if Orin CAN link drops
(orin_can_is_alive() == false) the robot continues balancing in-place.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:41:19 -04:00
92c0628c62
feat: Orin CANable 2.0 bridge for Mamba and VESC CAN bus (Issue #674 )
...
Adds slcan setup script and saltybot_can_bridge ROS2 package implementing
full CAN bus integration between the Orin and the Mamba motor controller /
VESC motor controllers via a CANable 2.0 USB dongle (slcan interface).
- jetson/scripts/setup_can.sh: slcand-based bring-up/tear-down for slcan0
at 500 kbps with error handling (already up, device missing, retry)
- saltybot_can_bridge/mamba_protocol.py: CAN message ID constants and
encode/decode helpers for velocity, mode, e-stop, IMU, battery, VESC state
- saltybot_can_bridge/can_bridge_node.py: ROS2 node subscribing to /cmd_vel
and /estop, publishing /can/imu, /can/battery, /can/vesc/{left,right}/state
and /can/connection_status; background reader thread, watchdog zero-vel,
auto-reconnect every 5 s on CAN error
- config/can_bridge_params.yaml: default params (slcan0, VESC IDs 56/68,
Mamba ID 1, 0.5 s command timeout)
- test/test_can_bridge.py: 30 unit tests covering encode/decode round-trips
and edge cases — all pass without ROS2 or CAN hardware
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:40:07 -04:00
56c59f60fe
fix: add __stack_end defsym for fault_handler MPU guard (Issue #678 )
...
STM32Cube ld script provides _estack but not __stack_end. Define
__stack_end = _estack - 0x1000 (_Min_Stack_Size) via --defsym so
fault_mpu_guard_init() and fault_mem_c() can locate the stack bottom.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:39:44 -04:00
7f67fc6abe
Merge pull request 'fix: remap CAN from CAN2/PB12-13 to CAN1/PB8-9 (Issue #676 )' ( #677 ) from sl-firmware/issue-597-can-driver into main
2026-03-17 21:39:29 -04:00
ea5203b67d
fix: bump arm pitch threshold 10°→20° (Issue #678 )
...
Mamba is mounted at ~12° on the frame, causing all three arm-interlock
checks to block arming. Raise fabsf(bal.pitch_deg) < 10.0f to 20.0f
at lines 375, 512, 532 (JLink arm, RC arm rising-edge, CDC arm).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:38:02 -04:00
14c80dc33c
fix: remap CAN from CAN2/PB12-13 to CAN1/PB8-9 (Issue #676 )
...
Mamba F722S MK2 does not expose PB12/PB13 externally. Waveshare CAN
module is wired to the SCL (PB8) and SDA (PB9) header pads.
Changes in can_driver_init():
- Drop __HAL_RCC_CAN2_CLK_ENABLE() — CAN1 needs no slave clock
- GPIO: GPIO_PIN_12/13 → GPIO_PIN_8/9, GPIO_AF9_CAN2 → GPIO_AF9_CAN1
- Instance: CAN2 → CAN1
- Filter bank: 14 → 0 (CAN1 master banks start at 0; bank 14 is the
CAN2 slave-start boundary, unused here)
I2C1 is free: BME280 has been moved to I2C2 (PB10/PB11), so PB8/PB9
are available for CAN1 without any peripheral conflict.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 20:31:59 -04:00
Sebastien Vayrette
d9b4b10b90
Merge remote-tracking branch 'origin/sl-perception/issue-646-vesc-odometry'
2026-03-17 11:27:45 -04:00
Sebastien Vayrette
a96fd91ed7
Merge remote-tracking branch 'origin/sl-firmware/issue-645-vesc-telemetry'
2026-03-17 11:27:36 -04:00
Sebastien Vayrette
bf8df6af8f
Merge remote-tracking branch 'origin/sl-controls/issue-644-vesc-can-driver'
2026-03-17 11:27:26 -04:00
d8b25bad77
feat: VESC CAN odometry for nav2 (Issue #646 )
...
Replace single-motor vesc_odometry_bridge with dual-CAN differential
drive odometry for left (CAN 61) and right (CAN 79) VESC motors.
New files:
- diff_drive_odom.py: pure-Python kinematics (eRPM→wheel vel, exact arc
integration, heading wrap), no ROS deps, fully unit-tested
- test/test_vesc_odometry.py: 20 unit tests (straight, arc, spin,
invert_right, guard conditions) — all pass
- config/vesc_odometry_params.yaml: configurable wheel_radius,
wheel_separation, motor_poles, invert_right, covariance tuning
Updated:
- vesc_odometry_bridge.py: rewritten as VESCCANOdometryNode; subscribes
to /vesc/can_61/state and /vesc/can_79/state (std_msgs/String JSON);
publishes /odom and /saltybot/wheel_odom (nav_msgs/Odometry) + TF
odom→base_link with proper 6×6 covariance matrices
- odometry_bridge.launch.py: updated to launch vesc_can_odometry with
vesc_odometry_params.yaml
- setup.py: added vesc_can_odometry entry point + config install
- pose_fusion_node.py: added optional wheel_odom_topic subscriber that
feeds DiffDriveOdometry velocities into EKF via update_vo_velocity
- pose_fusion_params.yaml: added use_wheel_odom, wheel_odom_topic,
sigma_wheel_vel_m_s, sigma_wheel_omega_r_s parameters
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:54:19 -04:00
b2c9f368f6
feat: VESC CAN telemetry for dual motors (Issue #645 )
...
New saltybot_vesc_telemetry ROS2 package — SocketCAN (python-can, can0)
telemetry for dual FSESC 6.7 Pro (FW 6.6) on CAN IDs 61 (left) and 79 (right).
- vesc_can_protocol.py: STATUS/STATUS_4/STATUS_5 frame parsers, VescState
dataclass, GET_VALUES request builder (CAN_PACKET_PROCESS_SHORT_BUFFER)
- vesc_telemetry_node.py: ROS2 node; background CAN RX thread; publishes
/vesc/left/state, /vesc/right/state, /vesc/combined (JSON String msgs),
/diagnostics (DiagnosticArray); overcurrent/overtemp/fault alerting;
configurable poll rate 10-50 Hz (default 20 Hz)
- test_vesc_telemetry.py: 31 unit tests, all passing (no ROS/CAN required)
- config/vesc_telemetry_params.yaml, launch file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:53:09 -04:00
a506989af6
feat: CANable 2.0 bringup with udev rule and systemd service (Issue #643 )
...
- udev: 70-canable.rules — gs_usb VID/PID 1d50:606f, names iface can0 and brings it up at 500 kbps on plug-in
- systemd: can-bringup.service — oneshot service bound to sys-subsystem-net-devices-can0.device
- scripts: can_setup.sh — manual up/down/verify helper; candump verify for VESC IDs 61 (0x3D) and 79 (0x4F)
- install_systemd.sh updated to install can-bringup.service and all udev rules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:49:21 -04:00
1d87899270
feat: VESC SocketCAN dual-motor driver IDs 61/79 (Issue #644 )
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:47:57 -04:00
0fcad75cb4
Merge pull request 'feat: Encoder odometry and wheel speed feedback (Issue #632 )' ( #642 ) from sl-controls/issue-632-encoder-odom into main
2026-03-15 17:29:54 -04:00
5aadf4b5c8
Merge pull request 'feat: Jetson Orin system monitor ROS2 node (Issue #631 )' ( #640 ) from sl-jetson/issue-631-system-monitor into main
2026-03-15 17:29:50 -04:00
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
779f9d00e2
feat: Encoder odometry and wheel speed feedback (Issue #632 )
...
- TIM2 (32-bit) left encoder, TIM3 (16-bit) right encoder in mode 3
- RPM calculation with int16 clamp; 16-bit wrap handled via signed delta
- Differential-drive odometry: x/y/theta Euler-forward integration
- Flash config (sector 7, 0x0807FF00) for ticks_per_rev/wheel_diam/base
- JLINK_TLM_ODOM (0x8C) at 50 Hz: rpm_l/r, x_mm, y_mm, theta_cdeg, speed_mmps
- 75/75 unit tests passing (TEST_HOST build)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 16:34:38 -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