# Plan: SaltyRover STM32 Firmware Variant ## Context SaltyRover is the 4-wheel-drive variant of SaltyBot. It uses the same STM32F722 MCU, CRSF RC, USB CDC, safety systems, and IMU — but replaces the balance PID loop with direct 4-wheel independent motor commands. Two hoverboard ESCs are used: front axle on USART2 (same as balance bot), rear axle on UART5 (PC12/PD2). saltyrover-dev is 16 commits behind main and lacks: mode_manager, remote e-stop (PR #69), jetson_cmd updates, and some CDC additions. We'll apply all relevant current main-equivalent features on the new branch. ## Branch - Source: `origin/saltyrover-dev` - Branch name: `sl-firmware/rover-firmware` - PR target: `saltyrover-dev` ## Files to Create ### `include/rover_driver.h` Public API for the 4-wheel rover motor layer: ```c typedef struct { int16_t fl, fr, rl, rr; /* last wheel commands (-1000..+1000) */ bool armed; bool estop; uint32_t last_cmd_ms; /* HAL_GetTick() of last set_cmd() call */ } rover_driver_t; void rover_driver_init(rover_driver_t *r); void rover_driver_set_cmd(rover_driver_t *r, int16_t fl, int16_t fr, int16_t rl, int16_t rr, uint32_t now); void rover_driver_update(rover_driver_t *r, uint32_t now); /* call at 50Hz */ void rover_driver_arm(rover_driver_t *r); void rover_driver_disarm(rover_driver_t *r); void rover_driver_estop(rover_driver_t *r); void rover_driver_estop_clear(rover_driver_t *r); ``` ### `src/rover_driver.c` Implementation — two hoverboard ESC control: - `rover_driver_init()`: calls existing `hoverboard_init()` (USART2 front), adds UART5 init for rear (HAL_UART_Init at 115200, PC12/PD2) - `rover_driver_update()` at 50Hz: - estop or disarmed → send (0,0) to both ESCs - armed → compute `front_spd=(fl+fr)/2`, `front_str=(fr-fl)/2`, `rear_spd=(rl+rr)/2`, `rear_str=(rr-rl)/2`; clamp each to ±ROVER_SPEED_MAX - If `now - last_cmd_ms > ROVER_CMD_TIMEOUT_MS` → zero commands (safety fallback) - Calls `hoverboard_send(front_spd, front_str)` for USART2 - Calls local `hoverboard2_send(rear_spd, rear_str)` for UART5 (static function in rover_driver.c, same 8-byte protocol) ## Files to Modify ### `include/config.h` Add rover-specific constants section: ```c /* --- SaltyRover 4WD --- */ #define ROVER_MAX_TILT_DEG 45.0f /* ESC estop angle (vs 25° balance cutoff) */ #define ROVER_CMD_TIMEOUT_MS 500 /* Zero wheels if no Jetson cmd for 500ms */ #define ROVER_SPEED_MAX 800 /* Max per-wheel command (ESC units) */ ``` ### `include/safety.h` + `src/safety.c` Apply PR #69 remote-estop additions (same as main): `EstopSource` enum, `safety_remote_estop/clear/get/active()`, `s_estop_source` static. These are already on main but not saltyrover-dev — include them here. ### `lib/USB_CDC/src/usbd_cdc_if.c` + `include/usbd_cdc_if.h` Apply PR #69 CDC additions: `cdc_estop_request`, `cdc_estop_clear_request`, cases 'E'/'F'/'Z'. Add rover JSON command handling: ```c volatile uint8_t rover_json_ready = 0; volatile char rover_json_buf[80]; // In CDC_Receive: case '{': { uint32_t n = *len < 79 ? *len : 79; for (uint32_t i = 0; i < n; i++) rover_json_buf[i] = (char)buf[i]; rover_json_buf[n] = '\0'; rover_json_ready = 1; jetson_hb_tick = HAL_GetTick(); /* JSON cmd refreshes heartbeat */ break; } ``` Export in header: `extern volatile uint8_t rover_json_ready; extern volatile char rover_json_buf[80];` ### `src/main.c` Full replacement of the main loop. Keep all init (clock, USB, IMU, CRSF, I2C sensors, safety), strip `balance_t`, replace with `rover_driver_t`. Key loop logic: **Per-iteration (1ms):** 1. `safety_refresh()` — IWDG feed 2. `mpu6000_read(&imu)` — IMU read 3. `mode_manager_update(&mode, now)` — RC liveness + CH6 mode 4. Remote e-stop block (from PR #69): `cdc_estop_request` → `rover_driver_estop()` 5. Tilt watchdog: `if (fabsf(imu.pitch) > ROVER_MAX_TILT_DEG) rover_driver_estop()` 6. RC CH5 arm/disarm (same hold interlock; pitch check relaxed to `< ROVER_MAX_TILT_DEG`) 7. USB arm/disarm (A/D commands) 8. Parse `rover_json_ready`: `sscanf(rover_json_buf, ...)` for `drive4` cmd → `rover_driver_set_cmd()` 9. RC direct drive fallback (MANUAL mode): CH3→speed all 4 wheels, CH4→differential steer **50Hz ESC block:** ```c if (rover.armed && !rover.estop) { // If Jetson active: use stored fl/fr/rl/rr // If RC MANUAL: compute fl=fr=rl=rr=speed, add steer diff rover_driver_update(&rover, now); } else { rover_driver_update(&rover, now); // sends zeros } ``` **50Hz telemetry:** ```json {"p":,"r":,"s":,"fl":,"fr":,"rl":,"rr":,"md":,"es":,"txc":,"rxc":} ``` Optional fields: heading, altitude/temp/pressure (same as balance bot). **Status LEDs:** Same `status_update(now, imu_ok, rover.armed, |pitch|>ROVER_MAX_TILT_DEG, safety_remote_estop_active())` **Arm guard:** `|pitch_deg| < ROVER_MAX_TILT_DEG` (45°) instead of `< 10.0f` ## Key Architectural Differences vs Balance Bot | | Balance Bot | SaltyRover | |---|---|---| | PID loop | 1kHz balance PID | None | | Motor cmd | `bal.motor_cmd` | Individual fl/fr/rl/rr | | ESCs | 1× USART2 | 2× USART2 + UART5 | | Tilt cutoff | 25° (balance state) | 45° (simple estop only) | | Drive cmd | `C,\n` | `{"cmd":"drive4",...}` JSON | | Arm pitch guard | `< 10°` | `< 45°` | | State machine | DISARMED/ARMED/TILT_FAULT | armed bool + estop bool | ## Test Plan 1. Build: `pio run -e saltyrover` (or equivalent) — confirms compile 2. Hardware: send `{"cmd":"drive4","fl":200,"fr":200,"rl":200,"rr":200}` → all 4 wheels spin forward 3. Estop: send `E\n` → wheels stop, LED fast-blinks 200ms; `Z\n` + RC arm → resumes 4. Tilt: tilt rover >45° → wheels cut immediately; no cut at 30° 5. RC manual: CH5 arm + CH3/CH4 → wheels respond; CH5 disarm → stop 6. Telemetry: verify `"fl"/"fr"/"rl"/"rr"` in JSON stream, `"es"` field changes on estop ## Critical Files - `src/main.c` — rover main loop (full rewrite of loop body) - `include/rover_driver.h` — new file - `src/rover_driver.c` — new file (UART5 init + two-ESC update) - `include/config.h` — rover constants - `include/safety.h` + `src/safety.c` — remote estop additions - `lib/USB_CDC/src/usbd_cdc_if.c` — JSON '{' case + estop flags - `lib/USB_CDC/include/usbd_cdc_if.h` — export new flags