fbc88f5c2a
fix: correct rclpy logger calls to use f-strings (pre-existing bugs)
...
rclpy RcutilsLogger.info/warning/debug() do not accept printf-style
positional format args. Also fix p["use_phone_timestamp"] → p["use_phone_ts"]
key mismatch in __init__ log line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:20:58 -04:00
0122957b6b
feat: Add iOS phone GPS MQTT-to-ROS2 bridge topic (Issue #681 )
...
- Add _TOPIC_IOS_GPS = 'saltybot/ios/gps' constant
- Subscribe to saltybot/ios/gps in _on_mqtt_connect
- Dispatch to _handle_ios_gps() in _dispatch()
- _handle_ios_gps(): same logic as _handle_gps(), frame_id='ios_gps',
publishes to /saltybot/ios/gps via self._ios_gps_pub
- Add rx/pub/err/last_rx_ts counters for the new topic
- Add /saltybot/ios/gps to rosbridge_params.yaml topics_glob
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:20:58 -04:00
416a393134
fix: correct delay_between_messages type to float in rosbridge_params
...
rclpy expects DOUBLE for this param; integer 0 raises InvalidParameterTypeException.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 10:58:09 -04:00
60f500c206
fix: add phone bridge and GPS topics to rosbridge whitelist (Issue #681 )
...
Add /saltybot/phone/gps, /saltybot/phone/imu, /saltybot/phone/battery,
/saltybot/phone/bridge/status, /gps/fix, /gps/vel to topics_glob so
the browser GPS dashboard can receive phone-bridged GPS data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 10:56:06 -04:00
a1233dbd04
fix: scrub remaining Mamba references in can_bridge and e2e test protocol files
...
- balance_protocol.py: Mamba→Orin / Mamba→VESC comments → ESP32-S3 BALANCE
- can_bridge_node.py: docstring and inline comments
- __init__.py: package description
- protocol_defs.py: all Mamba references in docstring and comments
- test_fc_vesc_broadcast.py, test_drive_command.py: test comments
Zero Mamba/STM32F722/BlackPill/stm32_protocol/mamba_protocol references
now exist outside legacy/stm32/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 09:00:44 -04:00
fa75c442a7
feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only
...
Archive STM32 firmware to legacy/stm32/:
- src/, include/, lib/USB_CDC/, platformio.ini, test stubs, flash_firmware.py
- test/test_battery_adc.c, test_hw_button.c, test_pid_schedule.c, test_vesc_can.c, test_can_watchdog.c
- USB_CDC_BUG.md
Rename: stm32_protocol → esp32_protocol, mamba_protocol → balance_protocol,
stm32_cmd_node → esp32_cmd_node, stm32_cmd_params → esp32_cmd_params,
stm32_cmd.launch.py → esp32_cmd.launch.py,
test_stm32_protocol → test_esp32_protocol, test_stm32_cmd_node → test_esp32_cmd_node
Content cleanup across all files:
- Mamba F722S → ESP32-S3 BALANCE
- BlackPill → ESP32-S3 IO
- STM32F722/F7xx → ESP32-S3
- stm32Mode/Version/Port → esp32Mode/Version/Port
- STM32 State/Mode labels → ESP32 State/Mode
- Jetson Nano → Jetson Orin Nano Super
- /dev/stm32 → /dev/esp32
- stm32_bridge → esp32_bridge
- STM32 HAL → ESP-IDF
docs/SALTYLAB.md:
- Update "Drone FC Details" to describe ESP32-S3 BALANCE board (Waveshare ESP32-S3 Touch LCD 1.28)
- Replace verbose "Self-Balancing Control" STM32 section with brief note pointing to SAUL-TEE-SYSTEM-REFERENCE.md
TEAM.md: Update Embedded Firmware Engineer role to ESP32-S3 / ESP-IDF
No new functionality — cleanup only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 09:00:38 -04:00
fda6ab99ff
feat(arch): align CAN/UART bridges with SAUL-TEE-SYSTEM-REFERENCE.md spec
...
Update CAN and serial bridge code to match authoritative protocol spec
from docs/SAUL-TEE-SYSTEM-REFERENCE.md §5-6 (hal, 2026-04-04).
mamba_protocol.py (CAN, Orin ↔ ESP32 BALANCE):
- 0x300 DRIVE: [speed:i16][steer:i16][mode:u8][flags:u8][_:u16] — combined frame
- 0x301 ARM: [arm:u8]
- 0x302 PID: [kp:f16][ki:f16][kd:f16][_:u16] — half-float gains
- 0x303 ESTOP: [0xE5] — magic byte cut
- 0x400 ATTITUDE: [pitch:f16][speed:f16][yaw_rate:f16][state:u8][flags:u8]
- 0x401 BATTERY: [vbat_mv:u16][fault_code:u8][rssi:i8]
- Add VESC STATUS1/4/5 decode helpers; VESC IDs 56 (left) / 68 (right)
can_bridge_node.py:
- /cmd_vel → encode_drive_cmd (speed/steer int16, MODE_DRIVE)
- /estop → encode_estop_cmd (magic 0xE5); clear → DISARM
- /saltybot/arm → encode_arm_cmd (new subscription)
- Watchdog sends DRIVE(0,0,MODE_IDLE) when /cmd_vel silent
- ATTITUDE (0x400) → /saltybot/attitude + /saltybot/balance_state JSON
- BATTERY (0x401) → /can/battery BatteryState
- VESC STATUS1 frames → /can/vesc/left|right/state
stm32_cmd_node.py — rewritten for inter-board protocol API:
- Imports from updated stm32_protocol (BAUD_RATE=460800, new frame types)
- RX: RcChannels → /saltybot/rc_channels, SensorData → /saltybot/sensors
- TX: encode_led_cmd, encode_output_cmd from /saltybot/leds + /saltybot/outputs
- HEARTBEAT (0x20) timer replaces old SPEED_STEER/ARM logic
stm32_cmd_params.yaml: serial_port=/dev/esp32-io, baud=460800
stm32_cmd.launch.py: updated defaults and description
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:59:45 -04:00
308be74330
feat(arch): implement SAUL-TEE ESP32 protocol specs from hal reference doc
...
Spec source: docs/SAUL-TEE-SYSTEM-REFERENCE.md (hal, 2026-04-04)
stm32_protocol.py — rewritten for inter-board UART protocol (ESP32 BALANCE ↔ IO):
- Frame: [0xAA][LEN][TYPE][PAYLOAD][CRC8] @ 460800 baud (was STX/ETX/CRC16)
- CRC-8 poly 0x07 over LEN+TYPE+PAYLOAD
- New message types: RC_CHANNELS(0x01), SENSORS(0x02), LED_CMD(0x10),
OUTPUT_CMD(0x11), MOTOR_CMD(0x12), HEARTBEAT(0x20)
mamba_protocol.py — updated CAN IDs and frame formats:
- Orin→BALANCE: DRIVE(0x300) f32×2 LE, MODE(0x301), ESTOP(0x302), LED(0x303)
- BALANCE→Orin: FC_STATUS(0x400) pitch/vbat/state, FC_VESC(0x401) rpm/current
- VESC node IDs: Left=56, Right=68 (authoritative per §8)
- VESC extended frames: STATUS1(cmd=9), STATUS4(cmd=16), STATUS5(cmd=27)
- Replaced old MAMBA_CMD_*/MAMBA_TELEM_* constants
can_bridge_node.py — updated to use new IDs:
- ORIN_CMD_DRIVE/MODE/ESTOP replace MAMBA_CMD_VELOCITY/MODE/ESTOP
- FC_STATUS handler: publishes pitch→/can/imu, vbat_mv→/can/battery
- FC_VESC handler: publishes rpm/cur→/can/vesc/left|right/state
- VESC STATUS1 extended frames decoded per node ID (56/68)
- Removed PID CAN command (not in new spec)
CLAUDE.md — updated with ESP32-S3 BALANCE/IO hardware summary + key protocols
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:59:28 -04:00
f71dad5344
feat(arch): migrate all STM32/Mamba/BlackPill refs to ESP32 BALANCE/IO + fix roslib@1.4.0
...
Architecture change (2026-04-03): Mamba F722S (STM32F722) and BlackPill
replaced by ESP32 BALANCE (PID loop) and ESP32 IO (motors/sensors/comms).
- Update CLAUDE.md, docs, chassis BOM/ASSEMBLY, pinout, power-budget,
wiring-diagram, TEAM.md, AUTONOMOUS_ARMING.md, docker-compose
- Update all ROS2 package comments, config labels, launch args
(stm32_port→esp32_port, /dev/stm32-bridge→/dev/esp32-bridge)
- Update WebUI: stm32Mode→esp32Mode, stm32Version→esp32Version,
"STM32 State/Mode" labels → "ESP32 State/Mode" (ControlMode, SettingsPanel)
- Add TODO(esp32-migration) markers on stm32_protocol.py and mamba_protocol.py
binary frame layouts — pending ESP32 protocol spec from max
- Fix roslib CDN 1.3.0→1.4.0 in all 11 HTML panels (fixes ROS2 Humble
rosbridge "Received a message without an op" incompatibility)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:25:24 -04:00
de4d1bbe3a
feat: PID tuning interface via CAN/ROS2 (Issue #693 )
...
- Mamba (STM32): add ORIN_CAN_ID_PID_SET (0x305) handler in orin_can.c.
Receives kp/ki/kd as uint16*100 (BE), applies to running balance loop,
replies with FC_PID_ACK (0x405) echoing clamped gains. Gains persist in
RAM until reboot; not saved to flash.
- Jetson: expose pid/kp, pid/ki, pid/kd as ROS2 parameters in
can_bridge_node. Parameter changes trigger encode_pid_set_cmd() and
send CAN frame 0x305 immediately. ACK frame 0x405 logged at DEBUG.
- mamba_protocol.py: add ORIN_CAN_ID_PID_SET / FC_PID_ACK IDs,
PidGains dataclass, encode_pid_set_cmd(), decode_pid_ack().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:39:00 -04:00
d235c414e0
Merge pull request 'feat: SLAM map persistence for AMCL (Issue #696 )' ( #705 ) from sl-perception/issue-696-slam-map-persistence into main
2026-03-20 17:38:29 -04:00
2b3f3584a9
Merge pull request 'feat: End-to-end CAN integration tests (Issue #695 )' ( #703 ) from sl-jetson/issue-695-can-e2e-test into main
2026-03-20 17:38:25 -04:00
2d60aab79c
feat: SLAM map persistence for AMCL (Issue #696 )
...
- New map_persistence.launch.py: launches map_saver_server lifecycle node
(nav2_map_server) + saltybot_map_saver helper node + lifecycle_manager.
Configurable map_dir (default /mnt/nvme/saltybot/maps) and map_name.
- New map_saver_node.py: ROS2 node providing /saltybot/save_map (Trigger
service) that calls nav2_map_server map_saver_cli. On startup logs whether
a saved map is present. Auto-saves map on shutdown (auto_save_on_shutdown).
- New config/map_saver_params.yaml: map_saver_server params
(save_map_timeout=5s, free/occupied thresholds, transient-local QoS).
- nav2_slam_bringup.launch.py: adds map_dir + map_name args; includes
map_persistence.launch.py so map_saver_server runs during SLAM sessions.
- nav2_amcl_bringup.launch.py: adds map_dir arg; auto-detects saved map at
/mnt/nvme/saltybot/maps/saltybot_map.yaml at launch time and uses it as
the AMCL map; falls back to placeholder if not found.
- setup.py: registers map_persistence.launch.py, map_saver_params.yaml,
map_saver_node console_scripts entry point.
- test_nav2_amcl.py: 21 new tests covering params, launch syntax,
node service/shutdown behaviour, SLAM bringup inclusion, AMCL auto-detect.
Workflow:
1. ros2 launch saltybot_nav2_slam nav2_slam_bringup.launch.py (build map)
2. ros2 service call /saltybot/save_map std_srvs/srv/Trigger {} (save)
3. ros2 launch saltybot_nav2_slam nav2_amcl_bringup.launch.py (auto-loads)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:27:52 -04:00
6d59baa30e
feat: End-to-end CAN integration tests (Issue #695 )
...
Add saltybot_can_e2e_test package with 64 tests covering the full
Orin↔Mamba↔VESC CAN pipeline: drive commands, heartbeat timeout,
e-stop escalation, mode switching, and FC_VESC status broadcasts.
Tests run with plain pytest — no ROS2 or real CAN hardware required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:25:23 -04:00
1ec4d3fc58
feat: WebSocket bridge for CAN monitor dashboard (Issue #697 )
...
rosbridge config:
- rosbridge_params.yaml: add /saltybot/barometer, /vesc/left/state,
/vesc/right/state to topics_glob whitelist (were missing, blocked
the CAN monitor panel from receiving data)
- can_monitor.launch.py: new lightweight launch — rosbridge only,
whitelist scoped to the 5 CAN monitor topics, port overridable via
launch arg (ros2 launch saltybot_bringup can_monitor.launch.py port:=9091)
can_monitor_panel.js auto-reconnect:
- Exponential backoff: 2s → 3s → 4.5s → ... → 30s cap (×1.5 factor)
- Countdown displayed in conn-label ("Retry in Xs…") during wait
- Backoff resets to 2s on successful connection
- Manual CONNECT / Enter resets backoff and cancels pending timer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:23:27 -04:00
fdda6fe5ee
Merge pull request 'feat: Nav2 AMCL integration (Issue #655 )' ( #664 ) from sl-perception/issue-655-nav2-integration into main
2026-03-18 07:57:02 -04:00
3457919c7a
Merge pull request 'feat: UWB geofence speed limiting (Issue #657 )' ( #663 ) from sl-uwb/issue-657-geofence-speed-limit into main
2026-03-18 07:56:53 -04:00
7eb3f187e2
feat: Smooth velocity controller (Issue #652 )
...
Adds velocity_smoother_node.py with configurable accel/decel ramps,
e-stop bypass, and optional jerk limiting. VESC driver updated to
subscribe /cmd_vel_smoothed instead of /cmd_vel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 07:56:16 -04:00
sl-android
a50dbe7e56
feat: VESC CAN telemetry MQTT relay (Issue #656 )
...
Add vesc_mqtt_relay_node.py to saltybot_phone: subscribes to
/vesc/left/state, /vesc/right/state, /vesc/combined ROS2 topics and
publishes JSON telemetry to saltybot/phone/vesc_{left,right,combined}
MQTT topics at 5 Hz per motor. 32 unit tests, no ROS2/paho required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 07:56:02 -04:00
06101371ff
fix: Use correct VESC topic names /vesc/left|right/state (Issue #670 )
...
- VESCCANOdometryNode subscriptions now use left_state_topic/right_state_topic
params (defaulting to /vesc/left/state and /vesc/right/state) instead of
building /vesc/can_<id>/state from CAN IDs — those topics never existed
- Update right_can_id default: 79 → 68 (Mamba F722S architecture update)
- Update vesc_odometry_params.yaml: CAN IDs 61/79 → 56/68; add explicit
left_state_topic and right_state_topic entries; remove stale can_N comments
- All IDs remain fully configurable via ROS2 params
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 07:55:04 -04:00
ee16bae9fb
fix: Make VESC CAN IDs configurable, default 56/68 (Issue #667 )
...
FSESC 6.7 Pro Mini Dual uses CAN IDs 56/68, not 61/79. Updates all
driver, telemetry, and odometry bridge files to use correct defaults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 07:50:20 -04:00
70fa404437
Merge pull request 'fix: Standardize VESC topic naming (Issue #669 )' ( #671 ) from sl-jetson/issue-669-vesc-topic-fix into main
2026-03-18 07:49:20 -04:00
9ed678ca35
feat: IMU mount angle cal, CAN telemetry, LED override (Issues #680 , #672 , #685 )
...
Issue #680 — IMU mount angle calibration:
- imu_cal_flash.h/.c: store pitch/roll offsets in flash sector 7
(0x0807FF00, 64 bytes; preserves PID records across sector erase)
- mpu6000_set_mount_offset(): subtracts offsets from pitch/roll output
- mpu6000_has_mount_offset(): reports cal_status=2 to Orin
- 'O' CDC command: capture current pitch/roll → save to flash → ACK JSON
- Load offsets on boot; report in printf log
CAN telemetry correction (Tee: production has no USB to Orin):
- FC_IMU (0x402): pitch/roll/yaw/cal_status/balance_state at 50 Hz
- orin_can_broadcast_imu() rate-limited to ORIN_IMU_TLM_HZ (50 Hz)
- FC_BARO (0x403): pressure_pa/temp_x10/alt_cm at 1 Hz (Issue #672 )
- orin_can_broadcast_baro() rate-limited to ORIN_BARO_TLM_HZ (1 Hz)
Issue #685 — LED CAN override:
- ORIN_CAN_ID_LED_CMD (0x304): pattern/brightness/duration_ms from Orin
- orin_can_led_override volatile state + orin_can_led_updated flag
- main.c: apply pattern to LED state machine on each LED_CMD received
Orin side:
- saltybot_can_node.py: production SocketCAN bridge — reads 0x400-0x403,
publishes /saltybot/imu, /saltybot/balance_state, /saltybot/barometer;
subscribes /cmd_vel → 0x301 DRIVE; /saltybot/leds → 0x304 LED_CMD;
sends 0x300 HEARTBEAT at 5 Hz; sends 0x303 ESTOP on shutdown
- setup.py: register saltybot_can_node entry point + uart_bridge launch
Fix: re-apply --defsym __stack_end=_estack-0x1000 linker fix to branch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 22:49:21 -04:00
06db56103f
feat: Enable VESC driver telemetry publishing (Issue #681 )
...
vesc_driver_node.py:
- Add VescState dataclass with to_dict() serialization
- Add CAN_PACKET_STATUS/STATUS_4/STATUS_5 (9/16/27) RX constants
- Add FAULT_NAMES lookup (11 VESC FW 6.6 fault codes)
- Add background CAN RX thread (_rx_loop / _dispatch_frame) that
parses STATUS broadcast frames using struct.unpack
- Add publishers for /saltybot/vesc/left and /saltybot/vesc/right
(std_msgs/String JSON) at configurable telemetry_rate_hz (default 10 Hz)
- Combine watchdog + publish into single timer callback
- Proper thread cleanup in destroy_node()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 22:49:06 -04:00
05ba557dca
fix: Move lines=[] above lock in _read_cb() (Issue #683 )
...
UnboundLocalError when _ser is None — lines was only assigned inside
the else branch. Move initialisation to function scope so the for-loop
outside the lock always has a valid list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 22:36:20 -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
7d2d41ba9f
fix: Standardize VESC topic naming to /vesc/left|right/state (Issue #669 )
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 15:18:43 -04:00
b74307c58a
feat: Nav2 AMCL integration with VESC odometry + LiDAR (Issue #655 )
...
AMCL-based autonomous navigation on pre-built static maps, wired to
VESC CAN differential-drive odometry (/odom, Issue #646 ) and RPLiDAR
(/scan) as the primary sensor sources.
New files (saltybot_nav2_slam):
- config/amcl_nav2_params.yaml — complete Nav2 + AMCL parameter file
with inline global/local costmap configs (required by nav2_bringup):
· AMCL: DifferentialMotionModel, 500–3000 particles, z-weights=1.0,
odom_frame=/odom, scan_topic=/scan
· Global costmap: static_layer + obstacle_layer (LiDAR) +
inflation_layer (0.55m radius)
· Local costmap: 4m rolling window, obstacle_layer (LiDAR) +
inflation_layer, global_frame=odom
· DWB controller: 1.0 m/s max, diff-drive constrained (vy=0)
· NavFn A* planner
· Recovery: spin + backup + wait
· Lifecycle managers for localization and navigation
- launch/nav2_amcl_bringup.launch.py — orchestrates:
1. sensors.launch.py (RealSense + RPLIDAR, conditional)
2. odometry_bridge.launch.py (VESC CAN → /odom)
3. nav2_bringup localization_launch.py (map_server + AMCL)
4. nav2_bringup navigation_launch.py (full nav stack)
Exposes: map, use_sim_time, autostart, params_file, include_sensors
- maps/saltybot_map.yaml — placeholder map descriptor (0.05m/cell)
- maps/saltybot_map.pgm — 200×200 P5 PGM, all free space (10m×10m)
- test/test_nav2_amcl.py — 38 unit tests (no ROS2 required):
params structure, z-weight sum, costmap layers, DWB/NavFn validity,
recovery behaviors, PGM format, launch file syntax checks
Updated:
- saltybot_bringup/launch/nav2.launch.py — adds nav_mode argument:
nav_mode:=slam (default, existing RTAB-Map behaviour unchanged)
nav_mode:=amcl (new, delegates to nav2_amcl_bringup.launch.py)
- saltybot_nav2_slam/setup.py — installs new launch, config, maps
- saltybot_nav2_slam/package.xml — adds nav2_amcl, nav2_map_server,
nav2_behaviors, dwb_core, nav2_navfn_planner exec_depends
All 58 tests pass (38 new + 20 from Issue #646 ).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 11:39:08 -04:00
sl-uwb
9d2b19104f
feat: UWB geofence speed limiting (Issue #657 )
...
Add saltybot_uwb_geofence ROS2 package — Jetson-side node that subscribes
to /saltybot/pose/authoritative (UWB+IMU fused PoseWithCovarianceStamped),
enforces configurable polygon speed-limit zones (YAML), and publishes
speed-limited /cmd_vel_limited with smooth ramp transitions.
Emergency boundary: if robot exits outer polygon, cmd_vel is zeroed and
/saltybot/geofence_violation (Bool) is latched True, triggering the
existing e-stop cascade. Publishes /saltybot/geofence/status (JSON).
Pure-geometry helpers (zone_checker.py) have no ROS2 dependency;
35 unit tests pass (pytest). ESP32 UWB firmware untouched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 11:36:37 -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
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
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
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
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
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
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
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
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
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
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
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