267 Commits

Author SHA1 Message Date
seb
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
seb
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
seb
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
seb
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
cf0a5a3583 fix: IWDG reset during gyro recal — refresh at i=0 not i=39 (P0 #42)
i%40==39 fired the first IWDG refresh only after 40ms of calibration.
Combined with ~10ms of main loop overhead before entering calibrate(),
total elapsed since last refresh could exceed the 50ms IWDG window.

Change to i%40==0: first refresh fires at i=0 (<1ms after entry),
subsequent refreshes every 40ms — safely within the 50ms window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:04:27 -05:00
seb
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
seb
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
seb
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
seb
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
seb
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
seb
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
seb
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
seb
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
seb
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
seb
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
seb
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
seb
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
seb
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
seb
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
seb
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
seb
f2d2df030e Merge pull request 'feat: STM32 serial bridge — USB CDC to ROS2 topics' (#16) from sl-jetson/stm32-serial-bridge into main 2026-02-28 17:19:25 -05:00
80a41e5008 feat: motor driver layer — differential drive, steer ramp, estop
Adds motor_driver.c/h between the balance PID and the raw
hoverboard UART driver:

- Differential drive: balance_cmd → speed, steer_cmd → steer
- Steer-only ramping at MOTOR_STEER_RAMP_RATE (balance PID keeps
  full immediate authority — no ramp on speed channel)
- Headroom clamp: reduces steer so |speed|+|steer|<=MOTOR_CMD_MAX
  ensuring ESC never clips the balance command
- Emergency stop: latches on TILT_FAULT, clears on BALANCE_DISARMED;
  send path stays in 50Hz ESC tick to avoid flooding UART

main.c: replace bare hoverboard_send() with motor_driver_update();
config.h: MOTOR_CMD_MAX=1000, MOTOR_STEER_RAMP_RATE=20 counts/ms

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:15:40 -05:00
76067d6d89 feat(bd-a2j): RealSense D435i + RPLIDAR A1M8 ROS2 driver integration
Adds saltybot_bringup ROS2 package with four launch files:
  - realsense.launch.py  — D435i at 640x480x15fps, IMU unified topic
  - rplidar.launch.py    — RPLIDAR A1M8 via /dev/rplidar udev symlink
  - sensors.launch.py    — both sensors + static TF (base_link→laser/camera)
  - slam.launch.py       — sensors + slam_toolbox online_async (compose entry point)

Sensor config YAMLs (mounted at /config/ in container):
  - realsense_d435i.yaml  — Nano power-budget settings (15fps, no pointcloud)
  - rplidar_a1m8.yaml     — Standard scan mode, 115200 baud, laser frame
  - slam_toolbox_params.yaml — Nano-tuned (2Hz processing, 5cm resolution)

Fixes docker-compose volume mount: ./ros2_ws/src:/ros2_ws/src
(was ./ros2_ws:/ros2_ws/src — would have double-nested the src directory)

Topic reference and verification commands in SENSORS.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:14:21 -05:00
7c4e46aaa1 feat: STM32-to-Jetson ROS2 serial bridge node
saltybot_bridge ROS2 Python package (ament_python):
- serial_bridge_node.py: reads USB CDC JSON telemetry from STM32F722 at 50Hz
  Parses exact firmware format: {"p","r","e","ig","m","s","y"} (all ×10 ints)
  State enum: 0=DISARMED, 1=ARMED, 2=TILT_FAULT (matched to balance_state_t)
- Publishes sensor_msgs/Imu on /saltybot/imu (pitch/roll/yaw as angular_velocity)
- Publishes std_msgs/String on /saltybot/balance_state (full PID JSON diagnostics)
- Publishes diagnostic_msgs/DiagnosticArray on /diagnostics (OK/WARN/ERROR by state)
- Auto-reconnects on serial disconnect; IMU fault frames → ERROR diagnostic
- launch/bridge.launch.py with serial_port + baud_rate launch args
- config/bridge_params.yaml (921600 baud, /dev/ttyACM0)
- test/test_parse.py: 8 unit tests covering normal, fault, edge cases (all pass)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:11:02 -05:00
seb
b9c8bc1a36 Merge pull request 'fix: Roll axis + yaw telemetry (issues #12, #13)' (#14) from sl-firmware/fix-orientation-telemetry into main 2026-02-28 16:56:09 -05:00
seb
544a52686e Merge pull request 'feat: Prototype base plate — real hub motor axle measurements' (#11) from sl-mechanical/prototype-baseplate into main 2026-02-28 15:17:47 -05:00
6513b04e4e fix: correct roll axis mapping + add yaw telemetry (issues #12, #13)
Issue #12 — Roll displayed as pitch:
- Firmware was sending r=pitch_rate (wrong). Changed to r=roll_deg*10.
- mpu6000.c: add roll complementary filter (accel atan2(ay,az) +
  gyro gy integration, same COMP_ALPHA=0.98 as pitch).
- IMUData: add roll and yaw fields.
- UI: updateIMU() now uses data.r/10 for roll (not client-side filter
  that computed from ax/ay/az which firmware never sent).
- Three.js: roll -> rotation.z (banking), pitch -> rotation.x (tipping)
  — axes were already correct, fix was the firmware data.

Issue #13 — Add yaw telemetry:
- Firmware: gyro Z integration (gz * dt) → s_yaw, sent as y=yaw_deg*10.
  Gyro-only, drifts — no magnetometer.
- IMUData: yaw field added.
- UI: yaw -> rotation.y (spinning on vertical axis). Displayed in HUD.
- UI: YAW RESET button captures current yaw as new zero reference
  (client-side offset, no new firmware command needed).

Closes #12, #13.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 15:07:04 -05:00
914afbc1ca feat: vertical stem architecture — compact baseplate + battery carousel
ARCHITECTURE CHANGE: batteries no longer sit flat on the base plate.
They mount VERTICALLY on a central mast via a height-adjustable carousel.
CG is tuned by sliding the carousel up/down the stem.

Part A — prototype_baseplate.scad (Rev C):
- PLATE_DEPTH: 210mm → 130mm (no battery footprint constraint)
- Removed all battery tray geometry (holes, strap slots, expansion mounts)
- Added central stem socket: Ø38.6mm bore + 4x M5 flange bolt holes on Ø66mm BC
- Added stem_flange() module: laser-cut ring (qty 2, one each side of plate)
- Wiring pass-through slots flanking stem centre
- FC mount relocated to FC_X_OFFSET = -40mm (front of plate, clear of stem)
- New RENDER="stem_flange_2d" DXF export option

Part B — stem_battery_clamp.scad (new):
- Collar: two 3D-printed D-shaped halves, split at Y=0
  - Ø38.6mm bore (1.5" EMT / 6061-T6 tube)
  - 4x M6 clamping bolts + hex nut pockets
  - 1x M6 set screw per half for height/rotation lock
  - Arm attachment pads with M4 through-holes + nut pockets
- Arms: flat bars, laser-cut or printed, ARM_REACH=55mm
- Battery cradles: U-channel, open top, Velcro strap slots at 30% + 65% height
- BATT_COUNT param: 2 (180°), 3 (120°), or 4 (90°) radial batteries
- ARM_START_ANGLE chosen per BATT_COUNT to keep all arms clear of Y=0 split
- Battery ghosts in assembly for visualisation
- Full RENDER control: assembly / collar_half / arm / arm_2d / cradle
- Assembly sequence + CG tuning notes in file footer

BOM.md → Rev C:
- Part A table updated (5 laser-cut parts + stem tube)
- Part B table added (collar halves, arms, cradles, fasteners)
- Battery section: flat-deck layout replaced with vertical stem guide
- Fastener table updated to match new architecture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 14:57:30 -05:00
seb
257d6ccf26 Merge pull request 'fix(usb): MPU non-cacheable region + IWDG ordering fix (bd-3ulu)' (#10) from sl-firmware/bd-3ulu-usb-dcache-fix into main 2026-02-28 14:54:42 -05:00
5bb5c7f863 fix: update baseplate with real battery dimensions (420x88x56mm)
Replaces placeholder 185x72x52mm battery spec with caliper-verified
pack dimensions. 2 packs side-by-side is the default config.

Geometry impact:
- PLATE_DEPTH reduced to 210mm (2x88mm + 17mm margin each side)
- Battery zone: 420x176mm centred between motor forks (fits 600mm wheelbase)
- Mount holes repositioned: 4 per pack x 2 packs = 8 M4 holes
  at (±(BATT_L/2 - 18), ±BATT_W/2)
- Velcro strap slots: 25mm wide, pierce full plate depth at x=±BATT_L/4
- 4-pack expansion: optional M5 shelf bolt holes when BATT_PACKS=4
  (only viable 4-pack layout is 2+2 underdeck — analysed in BOM)
- Battery ghost in assembly preview shows 2-pack deck layout

4-pack analysis (added to BOM):
  in-line 840mm > wheelbase, side-by-side 352mm > plate depth
  → 2+2 underdeck shelf is the only viable 4-pack configuration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 14:46:50 -05:00
22d7b546f3 feat: prototype base plate with real hub motor measurements
Adds prototype_baseplate.scad — a laser-cuttable / CNC-routable flat
base plate for the self-balancing robot using caliper-verified axle
dimensions from the wiki (replaces placeholder values in PR #7):

  Axle base dia:     16.11 mm (was 14 mm)
  D-cut OD:          15.95 mm (new)
  D-cut flat chord:  13.00 mm (new)
  Total protrusion:  65.50 mm
  Bearing seat OD:   37.80 mm
  Tire OD:          254 mm (10x2.125")
  Axle CL height:   127 mm (was wrong 310 mm)

Design:
- Single flat plate (6 mm Al / 8 mm acrylic), 680x220 mm blank
- Open fork slots (16.51 mm, semicircular tip) at each axle end
- Bearing seat relief cutout prevents Ø37.8 mm collar binding on edge
- Two-piece dropout clamp: lower (round bore) + upper (D-cut bore)
- D-cut profile computed from chord geometry with 0.3 mm all-round clearance
- MAMBA F722S FC holes (30.5x30.5 mm M3), battery mount holes (M4)
- Lightening slots, corner radii via minkowski
- RENDER param switches between 3-D assembly and 2-D DXF projections
  for each of the three laser-cut parts

Updates BOM.md to Rev B: measurement delta table, prototype BOM section,
updated motor entry with verified axle spec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 14:43:26 -05:00