291dd689f8
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 08:41:26 -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
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
93756f5248
feat: Add FC↔Orin UART verification (Issue #362 )
...
Implements UART bridge verification between Flight Controller (STM32F722)
and Jetson Orin.
Changes:
1. jetson/scripts/uart_test.py (12.7 KB)
- Opens /dev/ttyTHS1 at 921600 baud
- Sends jlink binary test frames (PING, VERSION, ECHO)
- Verifies CRC16-CCITT frame integrity
- Logs transactions with timestamps
- JSON result export and optional MQTT publishing
2. jetson/ros2_ws/src/saltybot_bridge/launch/uart_bridge.launch.py
- ROS2 launch file for serial_bridge_node on UART port
- Configurable port (default /dev/ttyTHS1), baud rate (921600)
- Bridges FC telemetry to /saltybot/imu, /saltybot/balance_state
- Publishes diagnostics to /diagnostics
Usage:
Test: sudo python3 jetson/scripts/uart_test.py
Launch: ros2 launch saltybot_bridge uart_bridge.launch.py
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 12:17:56 -05:00
ea26cda76a
feat(bridge): battery management node (Issue #125 )
...
Add battery_node.py:
- Subscribes /saltybot/telemetry/battery (JSON from stm32_cmd_node)
- Publishes sensor_msgs/BatteryState on /saltybot/battery at 1 Hz
- SoC source priority: STM32 fuel gauge soc_pct field → fallback to
3S LiPo voltage curve (12-point lookup with linear interpolation)
- Charging detection: current_ma < -100 mA threshold
- Alert levels: WARNING (20%)→speed 60%, CRITICAL (10%)→speed 30%,
EMERGENCY (5%)→zero /cmd_vel + /saltybot/arm(False) disarm
- /saltybot/battery/alert JSON topic on threshold crossings
- /saltybot/speed_limit Float32 (0.0-1.0) for nav speed capping
- SQLite history logging: /var/log/saltybot/battery.db, 7-day retention
- Hourly prune timer to keep DB bounded
Add test_battery.py (70+ tests, no ROS2 runtime):
- SoC lookup: all curve points, interpolation, clamping, 3S/4S packs
- Alert level thresholds and transitions for all levels
- Speed factor assignments per alert level
- Charging detection logic
- sensor_msgs/BatteryState field population
- SQLite insert/retrieve/prune (in-memory and on-disk)
- JSON telemetry parsing: normal, charging, soc_pct=0 fallback
Add battery_params.yaml, battery.launch.py.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 09:27:06 -05:00
972bc8cb27
chore(bridge): update setup.py for stm32_cmd_node and battery_node entries
...
Add stm32_cmd_node and battery_node console_scripts, new launch/config files
to data_files list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 09:17:13 -05:00
30be0d8cd3
feat(bridge): binary STM32 command protocol (Issue #119 )
...
Add stm32_protocol.py — pure-Python binary frame codec:
- Frame: STX(0x02)+TYPE+LEN+PAYLOAD+CRC16-CCITT(BE)+ETX(0x03)
- CRC covers TYPE+LEN+PAYLOAD; polynomial 0x1021, init 0xFFFF
- Encoders: HEARTBEAT, SPEED_STEER(-1000..+1000 int16), ARM, SET_MODE, PID_UPDATE
- Telemetry decoders: ImuFrame, BatteryFrame, MotorRpmFrame, ArmStateFrame, ErrorFrame
- FrameParser: streaming byte-by-byte state machine, resync on corrupt data
Add stm32_cmd_node.py — ROS2 bidirectional bridge node:
- /cmd_vel → SPEED_STEER at up to 50 Hz
- HEARTBEAT timer (default 200ms); STM32 watchdog fires at 500ms
- Jetson-side watchdog: no /cmd_vel for 500ms → send SPEED_STEER(0,0)
- /saltybot/arm service (SetBool) → ARM frame
- /saltybot/pid_update topic → PID_UPDATE frame
- Telemetry RX: IMU→/saltybot/imu, BATTERY→/saltybot/telemetry/battery,
MOTOR_RPM→/saltybot/telemetry/motor_rpm, ARM_STATE→/saltybot/arm_state,
ERROR→/saltybot/error
- Auto-reconnect on USB disconnect (serial.SerialException caught)
- Zero-speed + disarm sent on node shutdown
- /diagnostics with serial health, frame counts, last cmd age
Add test_stm32_protocol.py (60+ tests):
- CRC16-CCITT correctness, known test vectors
- All encoder output structures and payload values
- FrameParser: all telemetry types, bad CRC, bad ETX, resync, streaming,
oversized payload, frame counters, reset
Add test_stm32_cmd_node.py (30+ tests):
- MockSerial: TX/RX byte-level testing without hardware
- Speed/steer clamping, scaling, frame structure
- Watchdog fires/doesn't fire relative to timeout
- CRC error counted, resync after garbage
Add stm32_cmd_params.yaml, stm32_cmd.launch.py.
Update package.xml (add std_srvs, geometry_msgs deps).
Update setup.py (add stm32_cmd_node entry point + new config/launch).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 09:17:04 -05:00
d41a9dfe10
feat(safety): remote e-stop over 4G MQTT (Issue #63 )
...
STM32 firmware:
- safety.h/c: EstopSource enum, safety_remote_estop/clear/get/active()
CDC 'E'=ESTOP_REMOTE, 'F'=ESTOP_CELLULAR_TIMEOUT, 'Z'=clear latch
- usbd_cdc_if: cdc_estop_request/cdc_estop_clear_request volatile flags
- status: status_update() +remote_estop param; both LEDs fast-blink 200ms
- main.c: immediate motor cutoff highest-priority; arming gated by
!safety_remote_estop_active(); motor estop auto-clear gated; telemetry
'es' field 0-4; status_update() updated to 5 args
Safety: IMMEDIATE motor cutoff, latched until explicit Z + DISARMED,
cannot re-arm via MQTT alone (requires RC arm hold). IWDG-safe.
Jetson bridge:
- remote_estop_node.py: paho-mqtt + pyserial, cellular watchdog 5s
- estop_params.yaml, remote_estop.launch.py
- setup.py / package.xml: register node + paho-mqtt dep
- docker-compose.yml: remote-estop service
- test_remote_estop.py: kill/clear/watchdog/latency unit tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 04:55:54 -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
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
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