6420e07487
feat: rosbridge WebSocket server for web UI (port 9090)
...
Adds rosbridge_suite to the Jetson stack so the browser dashboard can
subscribe to ROS2 topics via roslibjs over ws://jetson:9090.
docker-compose.yml
New service: saltybot-rosbridge
- Runs saltybot_bringup/launch/rosbridge.launch.py
- network_mode: host → port 9090 directly reachable on Jetson LAN
- Depends on saltybot-ros2, stm32-bridge, csi-cameras
saltybot_bringup/launch/rosbridge.launch.py
- rosbridge_websocket node (port 9090, params from rosbridge_params.yaml)
- 4× image_transport/republish nodes: compress CSI camera streams
/camera/<name>/image_raw → /camera/<name>/image_raw/compressed (JPEG 75%)
saltybot_bringup/config/rosbridge_params.yaml
Whitelisted topics:
/map /scan /tf /tf_static
/saltybot/imu /saltybot/balance_state
/cmd_vel
/person/*
/camera/*/image_raw/compressed
max_message_size: 10 MB (OccupancyGrid headroom)
saltybot_bringup/SENSORS.md
Added rosbridge connection section with roslibjs snippet,
topic reference table, bandwidth estimates, and throttle_rate tips.
saltybot_bringup/package.xml
Added exec_depend: rosbridge_server, image_transport,
image_transport_plugins (all already installed in Docker image).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 00:22:02 -05:00
3d2d317431
Merge pull request 'feat: bumper + frame crash protection (roll cage, base bumper, stem sleeves)' ( #56 ) from sl-mechanical/bumper-protection into main
2026-03-01 00:21:13 -05:00
beb0af7ce6
feat: bumper + frame crash protection for SaltyBot
...
Protects expensive sensors when robot tips over during testing.
Files added:
chassis/bumper_frame.scad — roll cage (25 mm stem collar + 4 inclined
posts + crown ring + TPU snap pad). Posts at
45/135/225/315° between cameras. Crown at
Z=148 mm above cage collar, 18 mm above RPLIDAR
top. RPLIDAR FOV unobstructed.
chassis/base_bumper.scad — clip-on bumper ring for 270×240 mm base plate.
50 mm tall, 15 mm standoff, rounded corners.
4× corner + 4× side clip brackets. TPU snap
edge caps. FC status LEDs remain visible.
chassis/stem_protector.scad — split TPU sleeve for 25 mm stem. Snap-fit
closure (no hardware). Install 3–4 sleeves
along stem at ~200 mm intervals.
chassis/bumper_BOM.md — full BOM, print settings, install heights,
mass estimate (~500 g total, balanced).
All parts print without supports. Fully removable (clip/snap/clamp).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 23:30:06 -05:00
fcd59ead80
Merge pull request 'feat: person-following control loop' ( #55 ) from sl-controls/person-follower into main
2026-02-28 23:25:40 -05:00
9014b6738d
Merge pull request 'feat: person detection + tracking (YOLOv8n TensorRT)' ( #54 ) from sl-jetson/person-detection into main
2026-02-28 23:25:22 -05:00
2ffd462223
Merge pull request 'feat: 4x IMX219 surround vision + Nav2 costmap layer (Phase 2c)' ( #52 ) from sl-perception/surround-vision into main
2026-02-28 23:25:17 -05:00
432d5cb267
feat: person-following control loop (Phase 2b)
...
Adds saltybot_follower ROS2 package — proportional person-following
controller that bridges sl-jetson's /person/target detections to Nav2
/cmd_vel, with the cmd_vel_bridge_node (PR #46 ) providing safety wrapping.
Controller features:
- Proportional control: linear.x ∝ distance error, angular.z ∝ bearing
- Follow distance: 1.5m default with ±0.3m dead zone (no jitter at target)
- Max speed: 0.5 m/s linear, 1.0 rad/s angular (conservative for balance)
- Obstacle override: zeroes forward cmd_vel when Nav2 local costmap
detects obstacle in forward corridor; turning still allowed
- Lost-target state machine:
FOLLOWING → person visible
STOPPING → lost > 2s, publish zero
SEARCHING → lost > 5s, slow rotation (0.3 rad/s) to re-acquire
- Mode integration: follow_enabled param (toggle via ros2 param set)
independently gates the controller; cmd_vel bridge gates on md=2
Deliverables:
saltybot_follower/person_follower_node.py — ROS2 node (314 lines)
config/person_follower_params.yaml — all params documented
launch/person_follower.launch.py — all params as launch args
test/test_person_follower.py — 53 pytest tests, no ROS2
package.xml / setup.py / setup.cfg — package metadata
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 23:22:49 -05:00
c44a30561a
feat: person detection + tracking (YOLOv8n TensorRT)
...
New package: saltybot_perception
person_detector_node.py:
- Subscribes /camera/color/image_raw + /camera/depth/image_rect_raw
(ApproximateTimeSynchronizer, slop=50ms)
- Subscribes /camera/color/camera_info for intrinsics
- YOLOv8n inference via TensorRT FP16 engine (Orin Nano 67 TOPS)
Falls back to ONNX Runtime when engine not found (dev/CI)
- Letterbox preprocessing (640x640), YOLOv8n post-process + NMS
- Median-window depth lookup at bbox centre (7x7 px)
- Back-projects 2D pixel + depth to 3D point in camera frame
- tf2 transform to base_link (fallback: camera_color_optical_frame)
- Publishes:
/person/detections vision_msgs/Detection2DArray all persons
/person/target geometry_msgs/PoseStamped tracked person 3D
/person/debug_image sensor_msgs/Image (optional)
tracker.py — SimplePersonTracker:
- Single-target IoU-based tracker
- Picks closest valid person (smallest depth) on first lock
- Re-associates across frames using IoU threshold
- Holds last known position for configurable duration (default 2s)
- Monotonically increasing track IDs
detection_utils.py — pure helpers (no ROS2 deps, testable standalone):
- nms(), letterbox(), remap_bbox(), get_depth_at(), pixel_to_3d()
scripts/build_trt_engine.py:
- Converts ONNX to TensorRT FP16 engine using TRT Python API
- Prints trtexec CLI alternative
- Includes YOLOv8n download instructions
config/person_detection_params.yaml:
- confidence_threshold: 0.40, min_depth: 0.5m, max_depth: 5.0m
- track_hold_duration: 2.0s, target_frame: base_link
launch/person_detection.launch.py:
- engine_path, onnx_path, publish_debug_image, target_frame overridable
Tests: 26/26 passing (test_tracker.py + test_postprocess.py)
- IoU computation, NMS suppression, tracker state machine,
depth filtering, hold duration, re-association, track ID
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 23:21:24 -05:00
dc01efe323
feat: 4x IMX219 surround vision + Nav2 camera obstacle layer (Phase 2c)
...
New ROS2 package saltybot_surround:
surround_costmap_node
- Subscribes to /camera/{front,left,rear,right}/image_raw
- Detects obstacles via Canny edge detection + ground projection
- Pinhole back-projection: pixel row → forward distance (d = h*fy/(v-cy))
- Rotates per-camera points to base_link frame using known camera yaws
- Publishes /surround_vision/obstacles (PointCloud2, 5 Hz)
- Catches chairs, glass walls, people that RPLIDAR misses
- Placeholder IMX219 fisheye calibration (hook for real cal via cv2.fisheye)
surround_vision_node
- IPM homography computed from camera height + pinhole model
- 4× bird's-eye patches composited into 240×240px 360° overhead view
- Publishes /surround_vision/birdseye (Image, 10 Hz)
- Robot footprint + compass overlay
surround_vision.launch.py
- Launches both nodes with surround_vision_params.yaml
- start_cameras arg: set false when csi-cameras container runs separately
Updated:
- jetson/config/nav2_params.yaml add surround_cameras PointCloud2 source
to local + global costmap obstacle_layer
- jetson/docker-compose.yml add saltybot-surround service
(depends_on: csi-cameras, start_cameras:=false)
- projects/saltybot/SLAM-SETUP-PLAN.md Phase 2c ✅ Done
Calibration TODO (run after hardware assembly):
ros2 run camera_calibration cameracalibrator --size 8x6 --square 0.025 \
image:=/camera/front/image_raw camera:=/camera/front
Replace placeholder K/D in surround_costmap_node._undistort()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 23:19:23 -05:00
5008e03cc4
Merge pull request 'feat: Orin Nano Super platform update + 4x IMX219 CSI cameras' ( #51 ) from sl-jetson/orin-platform-cameras into main
2026-02-28 23:08:49 -05:00
54d3e12c78
Merge pull request 'feat: Phase 2a URDF robot description + static TF for SLAM/Nav2' ( #50 ) from sl-firmware/robot-urdf into main
2026-02-28 23:08:46 -05:00
3755e235aa
feat: Orin Nano Super platform update + 4x IMX219 CSI cameras
...
Task A — Orin Nano Super platform update:
- docker-compose.yml: update header/comments, switch all service image tags
to jetson-orin, update devices to udev symlinks (/dev/rplidar,
/dev/stm32-bridge, i2c-7), add NVMe volume mounts (/mnt/nvme/saltybot),
update stm32-bridge to saltybot_bridge launch, add csi-cameras service
- docs/pinout.md: full rewrite for Orin Nano Super — i2c-7, ttyTHS0,
CSI-A/B connectors, M.2 NVMe slot, IMX219 15-pin FFC pinout, V4L2 nodes,
GStreamer test commands, updated udev rules
- docs/power-budget.md: full rewrite — 25W TDP, 8GB LPDDR5, 67 TOPS,
4-camera CSI bandwidth analysis, nvpmodel modes, Nano vs Orin comparison,
5V 6A PSU recommendation, 4S LiPo architecture
- scripts/setup-jetson.sh: full rewrite — JetPack 6 / Ubuntu 22.04,
nvidia-container-toolkit new keyring method, NVMe partition/format/fstab,
CSI driver check (imx219 modprobe), video group, jtop install, 8GB swap
Task B — saltybot_cameras ROS2 package:
- launch/csi_cameras.launch.py: 4x v4l2_camera nodes, namespace per camera
(front/left/rear/right), 640x480x30fps, includes TF launch automatically
- launch/camera_tf.launch.py: static TF for 4 cameras at 90deg intervals
on sensor_head_link (r=5cm offset), yaw 0/90/180/-90 deg
- package.xml, setup.py, setup.cfg, __init__.py, resource marker
- config/cameras_params.yaml: per-camera device/frame/offset configuration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:59:13 -05:00
3f627ac3c8
Merge pull request 'feat: Nav2 path planning + obstacle avoidance (Phase 2b)' ( #49 ) from sl-perception/nav2-integration into main
2026-02-28 22:58:49 -05:00
1c6c5b3c0b
Merge pull request 'feat: sensor head mounts — RPLIDAR, RealSense D435i, 4× IMX219' ( #48 ) from sl-mechanical/sensor-mounts into main
2026-02-28 22:58:43 -05:00
03045b063f
Merge pull request 'feat: Nav2 cmd_vel to STM32 autonomous drive bridge' ( #46 ) from sl-controls/cmd-vel-bridge into main
2026-02-28 22:58:42 -05:00
2f33421956
Merge pull request 'feat: Web UI overhaul — modern HUD dashboard ( #43 )' ( #45 ) from sl-firmware/web-ui-overhaul into main
2026-02-28 22:58:14 -05:00
b4bb6a44e0
feat: Phase 2a URDF robot description for SLAM and Nav2
...
Add saltybot_description ROS2 package with full kinematic model:
urdf/saltybot.urdf.xacro
- base_footprint → base_link (axle_height = 0.310m, from AXLE_HEIGHT SCAD)
- wheel_left/right_link (continuous, separation=0.600m, radius=0.1015m)
- imu_link (FC/MPU-6000 at x=+50mm forward, z=+12mm)
- stem_link (visual: 38.1mm EMT, 1.050m — from stem_battery_clamp.scad)
- sensor_head_link at top of stem
- laser (RPLIDAR A1M8, z=COL_H=36mm — frame matches slam_toolbox config)
- camera_link (RealSense D435i, ARM_R=50mm — matches realsense2_camera)
- camera_{front,left,rear,right}_link (IMX219, 10° down tilt, ARM_R=50mm
— positions match camera_tf.launch.py; that file superseded when live)
config/saltybot_properties.yaml
All dimensions from chassis/chassis_frame.scad + ASSEMBLY.md Rev A.
launch/robot_description.launch.py
Compiles xacro at launch time, runs robot_state_publisher.
Publishes /robot_description + /tf_static for all fixed joints.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:57:58 -05:00
772a70b545
feat: Nav2 path planning + obstacle avoidance (Phase 2b)
...
Integrates Nav2 autonomous navigation stack with RTAB-Map SLAM on Orin
Nano Super. No AMCL/map_server needed — RTAB-Map provides /map + TF.
New files:
- jetson/config/nav2_params.yaml DWB controller,
NavFn planner, RPLIDAR obstacle layer, RealSense voxel layer;
10Hz local / 5Hz global costmap; robot_radius 0.15m, max_vel 1.0 m/s
- jetson/ros2_ws/src/saltybot_bringup/launch/nav2.launch.py
wraps nav2_bringup navigation_launch with saltybot params + BT XML
- jetson/ros2_ws/src/saltybot_bringup/behavior_trees/
navigate_to_pose_with_recovery.xml BT: replan@1Hz, DWB follow,
recovery: clear maps → spin 90° → wait 5s → back up 0.30m
Updated:
- jetson/docker-compose.yml add saltybot-nav2 service
(depends_on: saltybot-ros2)
- jetson/ros2_ws/src/saltybot_bringup/setup.py install behavior_trees/*.xml
- jetson/ros2_ws/src/saltybot_bringup/package.xml add rtabmap_ros + nav2_bringup
- projects/saltybot/SLAM-SETUP-PLAN.md Phase 2b ✅ Done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:54:24 -05:00
23f2daa3cd
feat: sensor head mounts — stem collar, RPLIDAR, RealSense, 4× IMX219
...
Part of Phase 2 sensor mount designs for SaltyBot 25 mm mast.
Files added:
chassis/sensor_head.scad — split collar (25 mm OD stem) + octagonal platform
chassis/rplidar_mount.scad — anti-vibration ring for RPLIDAR A1M8 (Ø58 mm BC)
chassis/realsense_mount.scad — RealSense D435i arm bracket, 10° tilt, 1/4-20 nut
chassis/imx219_mount.scad — 4× IMX219 radial arms, 10° tilt, CSI ribbon slot
chassis/sensor_head_assembly.md — assembly diagram + fastener BOM + print settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:53:18 -05:00
a50f22d56b
feat: Nav2 cmd_vel to STM32 autonomous drive bridge
...
Adds cmd_vel_bridge_node — a standalone ROS2 node that subscribes to
Nav2 /cmd_vel and drives the STM32 over USB CDC with:
- Hard velocity limits (max_linear_vel=0.5 m/s, max_angular_vel=2.0 rad/s)
- Smooth ESC ramp (500 ESC-units/s, 50 Hz control loop)
- Deadman switch: zeros targets if /cmd_vel silent >500 ms
- Mode gate: sends drive only when STM32 reports md=2 (AUTONOMOUS)
- Telemetry RX → /saltybot/imu, /saltybot/balance_state, /diagnostics
- Heartbeat TX every 200 ms (H\n)
Deliverables:
saltybot_bridge/cmd_vel_bridge_node.py — node implementation
config/cmd_vel_bridge_params.yaml — tunable parameters
launch/cmd_vel_bridge.launch.py — standalone launch file
test/test_cmd_vel_bridge.py — 37 pytest unit tests (no ROS2)
setup.py — register node + new data files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:50:15 -05:00
6dc7aea32f
feat: modern HUD dashboard + telemetry expansion ( #43 )
...
ui/index.html — full dashboard rewrite:
- 3-column layout: LEFT telemetry gauges, CENTER 3D SaltyBot, RIGHT comms
- LEFT: artificial horizon (canvas, pitch/roll/ladder/roll-arc), yaw compass
tape, pitch/roll/yaw readouts, bidirectional motor bar, battery bar, BME280
environment section (auto-shows on data), MAG heading row
- CENTER: Three.js SaltyBot model (PR#41) with ground plane + animated
wheel rolling proportional to motor_cmd
- RIGHT: USB tx/rx packet counters, mode badge (MANUAL/ASSISTED/AUTO),
CRSF RSSI/LQ, dual RC stick overlay canvases (CH1–4), Jetson active dot
- BOTTOM: KP/KI/KD/SP/MAX sliders with APPLY + QUERY, collapsible log console
- Style: Tailwind CSS CDN, dark cyberpunk theme, neon cyan + orange accents
src/main.c — telemetry JSON additions:
- buf: 256 → 320 bytes (headroom for new fields)
- ja: Jetson active flag (0/1) via jetson_cmd_is_active()
- txc: TX telemetry frame counter (uint32, main-loop local)
- rxc: RX CDC packet counter (cdc_rx_count from usbd_cdc_if)
- ch1–ch4: CRSF channels mapped to µs (1000–2000) via crsf_to_range(),
appended alongside rssi/lq when RC is alive
lib/USB_CDC/src/usbd_cdc_if.c:
- cdc_rx_count: volatile uint32_t, incremented in CDC_Receive on every
packet; extern'd in main.c for telemetry
Closes #43 .
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:41:02 -05:00
2a12aac86f
Merge pull request 'feat: SLAM stack update for Jetson Orin Nano Super (67 TOPS, JetPack 6)' ( #36 ) from sl-perception/orin-slam-update into main
2026-02-28 21:58:07 -05:00
939800a9fb
Merge pull request 'feat: CRSF/ELRS RC integration (Phase 2)' ( #35 ) from sl-firmware/crsf-elrs into main
2026-02-28 21:58:05 -05:00
a118582374
Merge pull request 'feat: Gyro recalibration button in web UI ( #32 )' ( #39 ) from sl-firmware/gyro-recal-button into main
2026-02-28 21:58:04 -05:00
e7298996d0
Merge pull request 'feat: SaltyBot 3D robot model in web UI ( #37 )' ( #41 ) from sl-firmware/robot-3d-model into main
2026-02-28 21:57:55 -05:00
c6b7d5cadd
Merge pull request 'fix: Yaw inversion in web UI (P0 #38 )' ( #40 ) from sl-firmware/yaw-fix into main
2026-02-28 21:57:54 -05:00
fbfde24aba
feat: CRSF/ELRS RC integration — 16ch input with failsafe (#Phase2)
...
Protocol choice: implemented from spec (CRSFforArduino needs Arduino
framework; Betaflight extraction has deep scheduler dependencies).
Protocol verified against Betaflight src/main/rx/crsf.c + CRSF spec.
crsf.c:
- UART4 PA0=TX/PA1=RX (GPIO_AF8_UART4), 420000 baud 8N1, oversampling×8
APB1=54MHz → BRR=0x101 → 418604 baud (0.33% error, within spec)
- DMA1 Stream2 Channel4, circular 64-byte buffer, IDLE interrupt
DMA half/complete callbacks drain buffer; IDLE fires at frame boundary
- CRC8 DVB-S2 (polynomial 0xD5) validated on every frame
- Parser state machine: SYNC(0xC8)→LEN→DATA with length sanity check
- 11-bit channel unpack for all 16 channels from 22-byte payload
- RC channels frame (0x16): unpacks 16ch, updates last_rx_ms + armed
- Link stats frame (0x14): captures RSSI dBm, LQ%, SNR dB
crsf.h: added rssi_dbm, link_quality, snr fields to CRSFState
config.h: CRSF_ARM_THRESHOLD=1750, CRSF_STEER_MAX=400, CRSF_FAILSAFE_MS=300
main.c:
- crsf_init() called after motor_driver_init()
- RC failsafe: disarm if (now - last_rx_ms) > CRSF_FAILSAFE_MS, but only
after RC was first seen (last_rx_ms != 0) — USB-only mode unaffected
- RC arm: CH5 rising edge → safety_arm_start(); falling edge → disarm
Same ARMING_HOLD_MS interlock as USB arm command
- RC steer: CH1 → crsf_to_range() → ±CRSF_STEER_MAX → motor_driver steer
- RSSI/LQ: appended to JSON when safety_rc_alive() ("rssi","lq" fields)
ui/index.html: hidden RC RSSI row revealed on first packet with rssi/lq
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:54:58 -05:00
91fc54c3d7
feat: SaltyBot 3D robot model in web UI ( #37 )
...
Replace generic flat PCB with a standing two-wheeled balancing robot:
- Vertical navy body (1.2 tall) rising above wheel axle at y=0
- Two wheels with blue rim accents, aligned to axle
- Front display panel and status LED
- Sensor stem + head on top
Camera repositioned to frame the taller robot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:52:53 -05:00
36abbde93a
fix: correct yaw inversion in web UI (P0 #38 )
...
Remove erroneous negate on targetYaw — yaw was spinning opposite to
physical rotation. Update resetYaw() formula to match (+ instead of -).
Pitch and roll were unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:51:29 -05:00
bd30e2b40d
feat: gyro recalibration button in web UI ( #32 )
...
Add 'G' CDC command that disarms and re-runs gyro bias calibration.
safety_refresh() added to calibration loop (every 40ms) so IWDG
does not trip during the 1s blocking re-cal when watchdog is running.
GYRO CAL button in ui/index.html sends 'G' and shows status feedback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:50:28 -05:00
c5d6a72d39
feat: update SLAM stack for Jetson Orin Nano Super (67 TOPS, JetPack 6)
...
Platform upgrade: Jetson Nano 4GB → Orin Nano Super 8GB (March 1, 2026)
All Nano-era constraints removed — power/rate/resolution limits obsolete.
Dockerfile: l4t-jetpack:r36.2.0 (JetPack 6 / Ubuntu 22.04 / CUDA 12.x),
ROS2 Humble via native apt, added ros-humble-rtabmap-ros,
ros-humble-v4l2-camera for future IMX219 CSI (Phase 2c)
New: slam_rtabmap.launch.py — Orin primary SLAM entry point
RTAB-Map with subscribe_scan (RPLIDAR) + subscribe_rgbd (D435i)
Replaces slam_toolbox as docker-compose default
New: config/rtabmap_params.yaml — Orin-optimized
DetectionRate 10Hz, MaxFeatures 1000, Grid/3D true,
TimeThr 0 (no limit), Mem/STMSize 0 (unlimited)
Updated: config/realsense_d435i.yaml — 848x480x30, pointcloud enabled
Updated: config/slam_toolbox_params.yaml — 10Hz rate, 1s map interval
Updated: SLAM-SETUP-PLAN.md — full rewrite for Orin: arch diagram,
Phase 2c IMX219 plan (4x 160° CSI surround), 25W power budget
docker-compose.yml: image tag jetson-orin, default → slam_rtabmap.launch.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:46:27 -05:00
f867956b43
Merge pull request 'feat: Jetson command protocol — /cmd_vel to STM32 (Phase 2)' ( #34 ) from sl-jetson/command-protocol into main
2026-02-28 21:43:03 -05:00
14ac85bf57
Merge pull request 'feat: RC/Autonomous mode switch (Phase 2)' ( #33 ) from sl-controls/mode-switch into main
2026-02-28 21:43:00 -05:00
ad02d90b6b
Merge pull request 'feat: BME280 temp/humidity/pressure telemetry ( #30 )' ( #31 ) from sl-firmware/bme280-full into main
2026-02-28 21:42:54 -05:00
22aaeb02cf
feat: Jetson→STM32 command protocol — /cmd_vel to serial (Phase 2)
...
STM32 firmware (C):
- include/jetson_cmd.h: protocol constants (HB_TIMEOUT_MS=500,
SPEED_MAX_DEG=4°), API for jetson_cmd_process/is_active/steer/sp_offset
- src/jetson_cmd.c: main-loop parser for buffered C<spd>,<str> frames;
setpoint offset = speed/1000 * 4°; steer clamped ±1000
- lib/USB_CDC/src/usbd_cdc_if.c: add H (heartbeat) and C (drive cmd) to
CDC_Receive ISR — follows existing pattern: H updates jetson_hb_tick in
ISR, C copied to jetson_cmd_buf for main-loop sscanf (avoids sscanf in IRQ)
- src/main.c: integrate jetson_cmd — process buffered frame, apply setpoint
offset around balance_update(), inject steer into motor_driver_update()
only when heartbeat alive (fallback: steer=0, setpoint unchanged)
ROS2 (Python):
- saltybot_cmd_node.py: full bidirectional node — owns serial port, handles
telemetry RX → topics AND /cmd_vel TX → C<spd>,<str>\n + H\n heartbeat
200ms timer; sends C0,0\n on shutdown; speed/steer_scale configurable
- serial_bridge_node.py: add write_serial() helper for extensibility
- launch/bridge.launch.py: mode arg (bidirectional|rx_only) selects node
- config/bridge_params.yaml: heartbeat_period, speed_scale, steer_scale docs
- test/test_cmd.py: 13 tests — zero, full fwd/rev, turn clamping, combined
- setup.py: saltybot_cmd_node entry point
All 21 tests pass (8 parse + 13 cmd).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:07:15 -05:00
ea5e2dac72
feat: RC/autonomous mode manager with smooth handoff
...
Adds mode_manager.c/h: three operating modes selected by RC CH6 (3-pos
switch), smoothly interpolated over ~500ms to prevent jerky transitions.
Modes:
RC_MANUAL (blend=0.0) — RC CH4 steer + CH3 speed bias; balance PID active
RC_ASSISTED (blend=0.5) — 50/50 blend of RC and Jetson autonomous commands
AUTONOMOUS (blend=1.0) — Jetson steer only; RC CH5 still kills motors
Key design:
- Single `blend` float (0=RC, 1=auto) drives all lerp; MANUAL→AUTO takes
500ms, adjacent steps take ~250ms
- CH6 thresholds: <=600=MANUAL, >=1200=AUTONOMOUS, else ASSISTED
- CH4/CH3 read with ±30-count deadband around CRSF center (992)
- RC speed bias (CH3, ±300 counts) added to bal.motor_cmd AFTER PID
- RC CH5 kill: if rc_alive && !crsf_state.armed → disarm, regardless of mode
- Jetson steer fed via mode_manager_set_auto_cmd() → blended in get_steer()
- Telemetry: new "md" field (0/1/2) in USB JSON stream
- mode_manager_set_auto_cmd() API ready for Jetson serial bridge integration
config.h: CRSF channel indices, deadband, speed-bias max, blend timing.
Safe on USB-only build: CRSF stub keeps last_rx_ms=0 → rc_alive=false
→ RC inputs = 0, mode stays RC_MANUAL, CH5 kill never fires.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:06:26 -05:00
ca23407ceb
feat: BME280 full readout — temp, humidity, pressure telemetry ( #30 )
...
- bmp280.c: detect BME280 (chip_id 0x60) vs BMP280 (0x58) at init
- bmp280.c: read humidity calibration (dig_H1–H6) from 0xA1 and 0xE1–0xE7
- bmp280.c: set ctrl_hum (0xF2, osrs_h=×16) before ctrl_meas — hardware req
- bmp280.c: add bmp280_read_humidity() — float compensation (FPv5-SP FPU),
returns %RH × 10; -1 if chip is BMP280 or not initialised
- bmp280.h: add bmp280_read_humidity() declaration + timeout note
- main.c: baro_ok → baro_chip (stores chip_id for BME280 detection)
- main.c: telemetry adds t (°C×10), pa (hPa×10) for all barometers;
adds h (%RH×10) for BME280 only; alt unchanged
- ui/index.html: hidden TEMP/HUMIDITY/PRESSURE rows, revealed on first
packet containing t/h/pa fields; values shown with 1 dp
I2C hang safety: all HAL_I2C_Mem_Read/Write use 100ms timeouts, so
missing hardware (NAK) returns in <1ms, not after a hang.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 19:43:48 -05:00
76dc03db1b
Merge pull request 'docs: Multi-variant branch strategy ( #28 )' ( #29 ) from sl-firmware/branch-strategy into main
2026-02-28 18:56:10 -05:00
32f7620c34
docs: multi-variant branch strategy (issue #28 )
...
Document 6 variant branches (saltylab/dev, saltyrover/dev, saltytank/dev),
promotion rules, and agent targeting conventions. Remove resolved USB CDC
blocker note.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 18:49:41 -05:00
d37e9ab276
Merge pull request 'feat: Auto-detect magnetometer + barometer ( #24 )' ( #27 ) from sl-firmware/mag-baro-detect into main
2026-02-28 18:45:25 -05:00
e1b82941ea
Merge pull request 'fix: Status LEDs solid=OK blink=error ( #22 )' ( #26 ) from sl-firmware/status-leds into main
2026-02-28 18:45:24 -05:00
1bf30a0262
Merge pull request 'feat: Boot gyro calibration — eliminates yaw drift ( #21 , #23 )' ( #25 ) from sl-controls/gyro-calibration into main
2026-02-28 18:45:18 -05:00
e21526327b
feat: Auto-detect magnetometer + barometer ( #24 )
...
Shared I2C1 bus (i2c1.c/h, PB8=SCL PB9=SDA 100kHz):
- i2c1_init() called once in main() before sensor probes.
- hi2c1 exported globally; baro and mag drivers use it directly.
Barometer (bmp280.c):
- Probes I2C1 at 0x76 then 0x77 (covers both SDO options).
- bmp280_init() returns chip_id (0x58/0x60) on success, neg if absent.
- Added bmp280_pressure_to_alt_cm() — ISA barometric formula.
- Added bmp280.h (was missing).
Magnetometer (mag.c / mag.h):
- Auto-detects QMC5883L (0x0D, id=0xFF), HMC5883L (0x1E, id='H43'),
IST8310 (0x0E, id=0x10) in that order.
- mag_read_heading() returns degrees×10 (0–3599) or -1 if not ready.
- HMC5883L: correct XZY byte order applied.
- IST8310: single-measurement trigger mode.
main.c:
- i2c1_init() + bmp280_init() + mag_init() after all other inits.
- Both skip gracefully (baro_ok=0, mag_type=MAG_NONE) if not present.
- Telemetry JSON: incremental builder appends ",\"hd\":<n>" when mag
found and ",\"alt\":<n>" when baro found. No extra bytes when absent.
UI (index.html):
- HEADING and ALT rows hidden until first packet with that field.
- Heading shown in degrees, alt in metres (firmware sends cm).
Closes #24 .
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:48:53 -05:00
011f212056
fix: Status LEDs solid=OK blink=error ( #22 )
...
New LED behavior (active-low, PC15=LED1, PC14=LED2):
Disarmed, IMU OK : LED1 solid ON + LED2 off
Armed : LED1 solid ON + LED2 solid ON
Tilt fault : LED1 blink 1Hz + LED2 blink 1Hz
IMU error : LED1 blink 1Hz + LED2 solid ON
Rule: solid = good, slow blink (~1Hz) = needs attention.
Removed the confusing fast-blink-at-5Hz-for-error and the
baro-flash-every-5s patterns.
status_update() signature changed: baro_ok → armed + tilt_fault
so the LED pattern can reflect arm state directly.
Closes #22 .
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:44:33 -05:00
1c95243e52
feat: gyro bias calibration on boot — fixes yaw drift (issues #21 , #23 )
...
On boot, before the main loop, sample 1000 gyro readings (~1s) while
board is held still. Compute per-axis mean offset (sensor-frame raw LSBs)
and subtract from all subsequent readings in mpu6000_read().
- mpu6000_calibrate(): LED1+LED2 solid ON during 1s sample window,
resets filter state to zero once bias is known
- mpu6000_is_calibrated(): gate; main loop blocks arming and USB
streaming until calibration completes
- Bias subtracted in sensor frame before CW270 axis transform + scale,
so all three axes (pitch/roll/yaw rate) benefit
- config.h: GYRO_CAL_SAMPLES=1000
- No flash storage — recalibrate fresh each boot (bias varies with temp)
Closes #21 (3.5°/s yaw drift), #23 (gyro bias calibration on boot).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:42:34 -05:00
f02ed8172a
feat: gyro bias calibration on boot — fixes yaw drift (issues #21 , #23 )
...
On boot, before the main loop, sample 1000 gyro readings (~1s) while
board is held still. Compute per-axis mean offset (sensor-frame raw LSBs)
and subtract from all subsequent readings in mpu6000_read().
- mpu6000_calibrate(): LED1+LED2 solid ON during 1s sample window,
resets filter state to zero once bias is known
- mpu6000_is_calibrated(): gate; main loop blocks arming and USB
streaming until calibration completes
- Bias subtracted in sensor frame before CW270 axis transform + scale,
so all three axes (pitch/roll/yaw rate) benefit
- config.h: GYRO_CAL_SAMPLES=1000
- No flash storage — recalibrate fresh each boot (bias varies with temp)
Closes #21 (3.5°/s yaw drift), #23 (gyro bias calibration on boot).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:42:24 -05:00
c882467e1b
Merge pull request 'fix: IMU axis mapping for CW270 orientation ( #15 )' ( #20 ) from sl-firmware/fix-axis-orientation into main
2026-02-28 17:28:57 -05:00
93d50054a2
fix: correct IMU axis mapping for CW270 mount orientation (issue #15 )
...
The MAMBA F722S mounts MPU6000 at CW270 (clockwise 270°) which applies
rotation matrix R = [[0,1,0],[-1,0,0],[0,0,1]] to transform sensor axes
to board axes (Betaflight convention).
Firmware (mpu6000.c):
- accel_pitch: was atan2(ax, az) → now atan2(ay, az)
board_forward = sensor_Y, so ay drives pitch not ax
- accel_roll: was atan2(ay, az) → now atan2(-ax, az)
board_right = -sensor_X, so -ax drives roll not ay
- gyro_pitch_rate: was +raw.gx → now -raw.gx
board_gy (pitch) = -sensor_gx after R_CW270 transform
- gyro_roll_rate: raw.gy unchanged (board_gx = sensor_gy ✓)
- gyro_yaw_rate: raw.gz unchanged ✓
UI (index.html) rotation sign fixes:
- roll → -rotation.z: Three.js +z = CCW from camera = left bank;
our convention is right-bank-positive so negate
- yaw → -rotation.y: Three.js +y = CCW from above; sensor_Z points
down on MAMBA (az ≈ +1g when level) so gz+ = CW physical; negate
- pitch → +rotation.x: correct as-is (Three.js +x tilts nose up ✓)
Closes #15 .
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:23:02 -05:00
fb3c8c863a
Merge pull request 'feat: motor driver layer — differential drive, steer ramp, estop' ( #19 ) from sl-controls/motor-driver into main
2026-02-28 17:19:42 -05:00
a89297f1d4
Merge pull request 'feat(bd-a2j): Sensor driver integration — RealSense D435i + RPLIDAR A1M8' ( #17 ) from sl-perception/bd-a2j-sensor-drivers into main
2026-02-28 17:19:41 -05:00