chore: remove all Mamba/STM32/BlackPill legacy hardware references #719

Closed
sl-jetson wants to merge 2 commits from sl-firmware/cleanup-legacy-hw into main
84 changed files with 392 additions and 1414 deletions
Showing only changes of commit bb35ddd56d - Show all commits

View File

@ -7,12 +7,7 @@ The robot can now be armed and operated autonomously from the Jetson without req
### Jetson Autonomous Arming ### Jetson Autonomous Arming
- Command: `A\n` (single byte 'A' followed by newline) - Command: `A\n` (single byte 'A' followed by newline)
<<<<<<< HEAD - Sent via USB Serial (CH343) to the ESP32-S3 firmware- Robot arms after ARMING_HOLD_MS (~500ms) safety hold period
- Sent via USB CDC to the ESP32 BALANCE firmware
=======
- Sent via USB Serial (CH343) to the ESP32-S3 firmware
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
- Robot arms after ARMING_HOLD_MS (~500ms) safety hold period
- Works even when RC is not connected or not armed - Works even when RC is not connected or not armed
### RC Arming (Optional Override) ### RC Arming (Optional Override)
@ -46,12 +41,7 @@ The robot can now be armed and operated autonomously from the Jetson without req
## Command Protocol ## Command Protocol
<<<<<<< HEAD ### From Jetson to ESP32-S3 (USB Serial (CH343))```
### From Jetson to ESP32 BALANCE (USB CDC)
=======
### From Jetson to ESP32-S3 (USB Serial (CH343))
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
```
A — Request arm (triggers safety hold, then motors enable) A — Request arm (triggers safety hold, then motors enable)
D — Request disarm (immediate motor stop) D — Request disarm (immediate motor stop)
E — Emergency stop (immediate motor cutoff, latched) E — Emergency stop (immediate motor cutoff, latched)
@ -60,12 +50,7 @@ H — Heartbeat (refresh timeout timer, every 500ms)
C<spd>,<str> — Drive command: speed, steer (also refreshes heartbeat) C<spd>,<str> — Drive command: speed, steer (also refreshes heartbeat)
``` ```
<<<<<<< HEAD ### From ESP32-S3 to Jetson (USB Serial (CH343))Motor commands are gated by `bal.state == BALANCE_ARMED`:
### From ESP32 BALANCE to Jetson (USB CDC)
=======
### From ESP32-S3 to Jetson (USB Serial (CH343))
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
Motor commands are gated by `bal.state == BALANCE_ARMED`:
- When ARMED: Motor commands sent every 20ms (50 Hz) - When ARMED: Motor commands sent every 20ms (50 Hz)
- When DISARMED: Zero sent every 20ms (prevents ESC timeout) - When DISARMED: Zero sent every 20ms (prevents ESC timeout)

View File

@ -1,34 +1,13 @@
# SaltyLab Firmware — Agent Playbook # SaltyLab Firmware — Agent Playbook
## Project ## Project
<<<<<<< HEAD
**SAUL-TEE** — 4-wheel wagon (870×510×550 mm, 23 kg).
Two ESP32-S3 boards + Jetson Orin via CAN. Full spec: `docs/SAUL-TEE-SYSTEM-REFERENCE.md`
| Board | Role |
|-------|------|
| **ESP32-S3 BALANCE** | QMI8658 IMU, PID balance, CAN→VESC (L:68 / R:56), GC9A01 LCD (Waveshare Touch LCD 1.28) |
| **ESP32-S3 IO** | TBS Crossfire RC, ELRS failover, BTS7960 motors, NFC/baro/ToF, WS2812 |
| **Jetson Orin** | AI/SLAM, CANable2 USB→CAN, cmds 0x3000x303, telemetry 0x4000x401 |
> **Legacy:** `src/` and `include/` = archived STM32 HAL — do not extend. New firmware in `esp32/`.
=======
Self-balancing two-wheeled robot: ESP32-S3 ESP32-S3 BALANCE, hoverboard hub motors, Jetson Orin Nano Super for AI/SLAM. Self-balancing two-wheeled robot: ESP32-S3 ESP32-S3 BALANCE, hoverboard hub motors, Jetson Orin Nano Super for AI/SLAM.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Team ## Team
| Agent | Role | Focus | | Agent | Role | Focus |
|-------|------|-------| |-------|------|-------|
<<<<<<< HEAD
| **sl-firmware** | Embedded Firmware Lead | ESP32-S3, ESP-IDF, QMI8658, CAN/UART protocol, BTS7960 |
| **sl-controls** | Control Systems Engineer | PID tuning, IMU fusion, balance loop, safety |
| **sl-perception** | Perception / SLAM Engineer | Jetson Orin, RealSense D435i, RPLIDAR, ROS2, Nav2 |
=======
| **sl-firmware** | Embedded Firmware Lead | ESP-IDF, USB Serial (CH343) debugging, SPI/UART, PlatformIO, DFU bootloader | | **sl-firmware** | Embedded Firmware Lead | ESP-IDF, USB Serial (CH343) debugging, SPI/UART, PlatformIO, DFU bootloader |
| **sl-controls** | Control Systems Engineer | PID tuning, IMU sensor fusion, real-time control loops, safety systems | | **sl-controls** | Control Systems Engineer | PID tuning, IMU sensor fusion, real-time control loops, safety systems |
| **sl-perception** | Perception / SLAM Engineer | Jetson Orin Nano Super, RealSense D435i, RPLIDAR, ROS2, Nav2 | | **sl-perception** | Perception / SLAM Engineer | Jetson Orin Nano Super, RealSense D435i, RPLIDAR, ROS2, Nav2 |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Status ## Status
USB Serial (CH343) TX bug resolved (PR #10 — DCache MPU non-cacheable region + IWDG ordering fix). USB Serial (CH343) TX bug resolved (PR #10 — DCache MPU non-cacheable region + IWDG ordering fix).

32
TEAM.md
View File

@ -1,54 +1,29 @@
# SaltyLab — Ideal Team # SaltyLab — Ideal Team
## Project ## Project
<<<<<<< HEAD
**SAUL-TEE** — 4-wheel wagon (870×510×550 mm, 23 kg).
Two ESP32-S3 boards (BALANCE + IO) + Jetson Orin. See `docs/SAUL-TEE-SYSTEM-REFERENCE.md`.
## Current Status
- **Hardware:** ESP32-S3 BALANCE (Waveshare Touch LCD 1.28, CH343 USB) + ESP32-S3 IO (bare devkit, JTAG USB)
- **Firmware:** ESP-IDF/PlatformIO target; legacy `src/` STM32 HAL archived
- **Comms:** UART 460800 baud inter-board; CANable2 USB→CAN for Orin; CAN 500 kbps to VESCs (L:68 / R:56)
=======
Self-balancing two-wheeled robot using a drone ESP32-S3 BALANCE (ESP32-S3), hoverboard hub motors, and eventually a Jetson Orin Nano Super for AI/SLAM. Self-balancing two-wheeled robot using a drone ESP32-S3 BALANCE (ESP32-S3), hoverboard hub motors, and eventually a Jetson Orin Nano Super for AI/SLAM.
## Current Status ## Current Status
- **Hardware:** Assembled — FC, motors, ESC, IMU, battery, RC all on hand - **Hardware:** Assembled — FC, motors, ESC, IMU, battery, RC all on hand
- **Firmware:** Balance PID + hoverboard ESC protocol written, but blocked by USB Serial (CH343) bug - **Firmware:** Balance PID + hoverboard ESC protocol written, but blocked by USB Serial (CH343) bug
- **Blocker:** USB Serial (CH343) TX stops working when peripheral inits (SPI/UART/GPIO) are added alongside USB on ESP32-S3 — see `legacy/stm32/USB_CDC_BUG.md` for historical context - **Blocker:** USB Serial (CH343) TX stops working when peripheral inits (SPI/UART/GPIO) are added alongside USB on ESP32-S3 — see `legacy/stm32/USB_CDC_BUG.md` for historical context
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
--- ---
## Roles Needed ## Roles Needed
### 1. Embedded Firmware Engineer (Lead) ### 1. Embedded Firmware Engineer (Lead)
**Must-have:** **Must-have:**
<<<<<<< HEAD
- Deep ESP32 (Arduino/ESP-IDF) or STM32 HAL experience
- USB OTG FS / CDC ACM debugging (TxState, endpoint management, DMA conflicts)
- SPI + UART + USB coexistence on ESP32
- PlatformIO or bare-metal ESP32 toolchain
- DFU bootloader implementation
=======
- Deep ESP-IDF experience (ESP32-S3 specifically) - Deep ESP-IDF experience (ESP32-S3 specifically)
- USB Serial (CH343) / UART debugging on ESP32-S3 - USB Serial (CH343) / UART debugging on ESP32-S3
- SPI + UART + USB coexistence on ESP32-S3 - SPI + UART + USB coexistence on ESP32-S3
- ESP-IDF / Arduino-ESP32 toolchain - ESP-IDF / Arduino-ESP32 toolchain
- OTA firmware update implementation - OTA firmware update implementation
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
**Nice-to-have:** **Nice-to-have:**
- ESP32-S3 peripheral coexistence (SPI + UART + USB) - ESP32-S3 peripheral coexistence (SPI + UART + USB)
- PID control loop tuning for balance robots - PID control loop tuning for balance robots
- FOC motor control (hoverboard ESC protocol) - FOC motor control (hoverboard ESC protocol)
<<<<<<< HEAD
**Why:** The immediate blocker is a USB peripheral conflict. Need someone who's debugged STM32 USB issues before — ESP32 firmware for the balance loop and I/O needs to be written from scratch.
=======
**Why:** The immediate blocker is a USB peripheral conflict on ESP32-S3. Need someone who's debugged ESP32-S3 USB Serial (CH343) issues before — this is not a software logic bug, it's a hardware peripheral interaction issue. **Why:** The immediate blocker is a USB peripheral conflict on ESP32-S3. Need someone who's debugged ESP32-S3 USB Serial (CH343) issues before — this is not a software logic bug, it's a hardware peripheral interaction issue.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
### 2. Control Systems / Robotics Engineer ### 2. Control Systems / Robotics Engineer
**Must-have:** **Must-have:**
- PID tuning for inverted pendulum / self-balancing systems - PID tuning for inverted pendulum / self-balancing systems
@ -83,12 +58,7 @@ Self-balancing two-wheeled robot using a drone ESP32-S3 BALANCE (ESP32-S3), hove
## Hardware Reference ## Hardware Reference
| Component | Details | | Component | Details |
|-----------|---------| |-----------|---------|
<<<<<<< HEAD | FC | ESP32-S3 BALANCE (ESP32-S3RET6, QMI8658) || Motors | 2x 8" pneumatic hoverboard hub motors |
| FC | ESP32 BALANCE (ESP32RET6, MPU6000) |
=======
| FC | ESP32-S3 BALANCE (ESP32-S3RET6, QMI8658) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| Motors | 2x 8" pneumatic hoverboard hub motors |
| ESC | Hoverboard ESC (EFeru FOC firmware) | | ESC | Hoverboard ESC (EFeru FOC firmware) |
| Battery | 36V pack | | Battery | 36V pack |
| RC | BetaFPV ELRS 2.4GHz TX + RX | | RC | BetaFPV ELRS 2.4GHz TX + RX |

View File

@ -56,16 +56,7 @@
3. Fasten 4× M4×12 SHCS. Torque 2.5 N·m. 3. Fasten 4× M4×12 SHCS. Torque 2.5 N·m.
4. Insert battery pack; route Velcro straps through slots and cinch. 4. Insert battery pack; route Velcro straps through slots and cinch.
<<<<<<< HEAD ### 7 FC mount (ESP32-S3 BALANCE)1. Place silicone anti-vibration grommets onto nylon M3 standoffs.
### 7 MCU mount (ESP32 BALANCE + ESP32 IO)
> ⚠️ **ARCHITECTURE CHANGE (2026-04-03):** ESP32 BALANCE retired. Two ESP32 boards replace it.
> Board dimensions and hole patterns TBD — await spec from max before machining mount plate.
=======
### 7 FC mount (ESP32-S3 BALANCE)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
1. Place silicone anti-vibration grommets onto nylon M3 standoffs.
2. Lower ESP32 BALANCE board onto standoffs; secure with M3×6 BHCS. Snug only. 2. Lower ESP32 BALANCE board onto standoffs; secure with M3×6 BHCS. Snug only.
3. Mount ESP32 IO board adjacent — exact placement TBD pending board dimensions. 3. Mount ESP32 IO board adjacent — exact placement TBD pending board dimensions.
4. Orient USB connectors toward front of robot for cable access. 4. Orient USB connectors toward front of robot for cable access.

View File

@ -41,12 +41,7 @@ PR #7 (`chassis_frame.scad`) used placeholder values. The table below records th
| 3 | Dropout clamp — upper | 2 | 8mm 6061-T6 Al | 90×70mm blank | D-cut bore; `RENDER="clamp_upper_2d"` | | 3 | Dropout clamp — upper | 2 | 8mm 6061-T6 Al | 90×70mm blank | D-cut bore; `RENDER="clamp_upper_2d"` |
| 4 | Stem flange ring | 2 | 6mm Al or acrylic | Ø82mm disc | One above + one below plate; `RENDER="stem_flange_2d"` | | 4 | Stem flange ring | 2 | 6mm Al or acrylic | Ø82mm disc | One above + one below plate; `RENDER="stem_flange_2d"` |
| 5 | Vertical stem tube | 1 | 38.1mm OD × 1.5mm wall 6061-T6 Al | 1050mm length | 1.5" EMT conduit is a drop-in alternative | | 5 | Vertical stem tube | 1 | 38.1mm OD × 1.5mm wall 6061-T6 Al | 1050mm length | 1.5" EMT conduit is a drop-in alternative |
<<<<<<< HEAD | 6 | FC standoff M3×6mm nylon | 4 | Nylon | — | ESP32-S3 BALANCE vibration isolation || 7 | Ø4mm × 16mm alignment pin | 8 | Steel dowel | — | Dropout clamp-to-plate alignment |
| 6 | MCU standoff M3×6mm nylon | 4 | Nylon | — | ESP32 BALANCE / IO board isolation (dimensions TBD) |
=======
| 6 | FC standoff M3×6mm nylon | 4 | Nylon | — | ESP32-S3 BALANCE vibration isolation |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| 7 | Ø4mm × 16mm alignment pin | 8 | Steel dowel | — | Dropout clamp-to-plate alignment |
### Battery Stem Clamp (`stem_battery_clamp.scad`) — Part B ### Battery Stem Clamp (`stem_battery_clamp.scad`) — Part B
@ -97,19 +92,10 @@ PR #7 (`chassis_frame.scad`) used placeholder values. The table below records th
| # | Part | Qty | Spec | Notes | | # | Part | Qty | Spec | Notes |
|---|------|-----|------|-------| |---|------|-----|------|-------|
<<<<<<< HEAD
| 13 | ESP32 BALANCE board | 1 | TBD — mount pattern TBD | PID balance loop; replaces ESP32 BALANCE |
| 13b | ESP32 IO board | 1 | TBD — mount pattern TBD | Motor/sensor/comms I/O |
| 14 | Nylon M3 standoff 6mm | 4 | F/F nylon | ESP32 board isolation |
| 15 | Anti-vibration grommet M3 | 4 | Ø6mm silicone | Under ESP32 mount pads |
| 16 | Jetson Orin module | 1 | 69.6×45mm module + carrier | 58×58mm M3 carrier hole pattern |
=======
| 13 | ESP32-S3 ESP32-S3 BALANCE FC | 1 | 36×36mm PCB, 30.5×30.5mm M3 mount | Oriented USB-C port toward front | | 13 | ESP32-S3 ESP32-S3 BALANCE FC | 1 | 36×36mm PCB, 30.5×30.5mm M3 mount | Oriented USB-C port toward front |
| 14 | Nylon M3 standoff 6mm | 4 | F/F nylon | FC vibration isolation | | 14 | Nylon M3 standoff 6mm | 4 | F/F nylon | FC vibration isolation |
| 15 | Anti-vibration grommet M3 | 4 | Ø6mm silicone | Under FC mount pads | | 15 | Anti-vibration grommet M3 | 4 | Ø6mm silicone | Under FC mount pads |
| 16 | Jetson Orin Nano Super B01 module | 1 | 69.6×45mm module + carrier | 58×58mm M3 carrier hole pattern | | 16 | Jetson Orin Nano Super B01 module | 1 | 69.6×45mm module + carrier | 58×58mm M3 carrier hole pattern || 17 | Nylon M3 standoff 8mm | 4 | F/F nylon | Jetson board standoffs |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| 17 | Nylon M3 standoff 8mm | 4 | F/F nylon | Jetson board standoffs |
--- ---

View File

@ -104,12 +104,7 @@ IP54-rated enclosures and sensor housings for all-weather outdoor robot operatio
| Component | Thermal strategy | Max junction | Enclosure budget | | Component | Thermal strategy | Max junction | Enclosure budget |
|-----------|-----------------|-------------|-----------------| |-----------|-----------------|-------------|-----------------|
| Jetson Orin NX | Al pad → lid → fan forced convection | 95 °C Tj | Target ≤ 60 °C case | | Jetson Orin NX | Al pad → lid → fan forced convection | 95 °C Tj | Target ≤ 60 °C case |
<<<<<<< HEAD | FC (ESP32-S3 BALANCE) | Passive; FC has own EMI shield | 85 °C | <60 °C ambient OK || ESC × 2 | Al pad lid | 100 °C Tj | Target 60 °C |
| FC (ESP32 BALANCE) | Passive; FC has own EMI shield | 85 °C | <60 °C ambient OK |
=======
| FC (ESP32-S3 BALANCE) | Passive; FC has own EMI shield | 85 °C | <60 °C ambient OK |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| ESC × 2 | Al pad → lid | 100 °C Tj | Target ≤ 60 °C |
| D435i | Passive; housing vent gap on rear cap | 45 °C surface | — | | D435i | Passive; housing vent gap on rear cap | 45 °C surface | — |
Fan spec: 40 mm, 12 V, ≥10 CFM at 0.1" H₂O static pressure. Fan spec: 40 mm, 12 V, ≥10 CFM at 0.1" H₂O static pressure.

View File

@ -4,24 +4,6 @@ You're working on **SaltyLab**, a self-balancing two-wheeled indoor robot. Read
## ⚠️ ARCHITECTURE — SAUL-TEE (finalised 2026-04-04) ## ⚠️ ARCHITECTURE — SAUL-TEE (finalised 2026-04-04)
<<<<<<< HEAD
Full hardware spec: `docs/SAUL-TEE-SYSTEM-REFERENCE.md` — **read it before writing firmware.**
| Board | Role |
|-------|------|
| **ESP32-S3 BALANCE** | Waveshare Touch LCD 1.28 (CH343 USB). QMI8658 IMU, PID loop, CAN→VESC L(68)/R(56), GC9A01 LCD |
| **ESP32-S3 IO** | Bare devkit (JTAG USB). TBS Crossfire RC (UART0), ELRS failover (UART2), BTS7960 motors, NFC/baro/ToF, WS2812, buzzer/horn/headlight/fan |
| **Jetson Orin** | CANable2 USB→CAN. Cmds on 0x3000x303, telemetry on 0x4000x401 |
```
Jetson Orin ──CANable2──► CAN 500kbps ◄───────────────────────┐
│ │
ESP32-S3 BALANCE ←─UART 460800─► ESP32-S3 IO
(QMI8658, PID loop) (BTS7960, RC, sensors)
│ CAN 500kbps
┌─────────┴──────────┐
VESC Left (ID 68) VESC Right (ID 56)
=======
A hoverboard-based balancing robot with two compute layers: A hoverboard-based balancing robot with two compute layers:
1. **ESP32-S3 BALANCE** — ESP32-S3 BALANCE (ESP32-S3RET6 + MPU6000 IMU). Runs a lean C balance loop at up to 8kHz. Talks UART to the hoverboard ESC. This is the safety-critical layer. 1. **ESP32-S3 BALANCE** — ESP32-S3 BALANCE (ESP32-S3RET6 + MPU6000 IMU). Runs a lean C balance loop at up to 8kHz. Talks UART to the hoverboard ESC. This is the safety-critical layer.
2. **Jetson Orin Nano Super** — AI brain. ROS2, SLAM, person tracking. Sends velocity commands to FC via UART. Not safety-critical — FC operates independently. 2. **Jetson Orin Nano Super** — AI brain. ROS2, SLAM, person tracking. Sends velocity commands to FC via UART. Not safety-critical — FC operates independently.
@ -33,9 +15,7 @@ Jetson (speed+steer via UART1) ←→ ELRS RC (UART3, kill switch)
ESP32-S3 BALANCE (MPU6000 IMU, PID balance) ESP32-S3 BALANCE (MPU6000 IMU, PID balance)
▼ UART2 ▼ UART2
Hoverboard ESC (FOC) → 2× 8" hub motors Hoverboard ESC (FOC) → 2× 8" hub motors```
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
```
Frame: `[0xAA][LEN][TYPE][PAYLOAD][CRC8]` Frame: `[0xAA][LEN][TYPE][PAYLOAD][CRC8]`
Legacy `src/` STM32 HAL code is **archived — do not extend.** Legacy `src/` STM32 HAL code is **archived — do not extend.**
@ -57,12 +37,7 @@ This is not a toy. 8" hub motors + 36V battery can crush fingers, break toes, an
## Repository Layout ## Repository Layout
``` ```
<<<<<<< HEAD firmware/ # ESP-IDF firmware (PlatformIO)├── src/
firmware/ # Legacy ESP32/STM32 HAL firmware (PlatformIO, archived)
=======
firmware/ # ESP-IDF firmware (PlatformIO)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
├── src/
│ ├── main.c # Entry point, clock config, main loop │ ├── main.c # Entry point, clock config, main loop
│ ├── icm42688.c # QMI8658-P SPI driver (backup IMU — currently broken) │ ├── icm42688.c # QMI8658-P SPI driver (backup IMU — currently broken)
│ ├── bmp280.c # Barometer driver (disabled) │ ├── bmp280.c # Barometer driver (disabled)
@ -108,25 +83,16 @@ PLATFORM.md # Hardware platform reference
## Hardware Quick Reference ## Hardware Quick Reference
<<<<<<< HEAD
### ESP32 BALANCE Flight Controller
| Spec | Value |
|------|-------|
| MCU | ESP32RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) |
=======
### ESP32-S3 BALANCE Flight Controller ### ESP32-S3 BALANCE Flight Controller
| Spec | Value | | Spec | Value |
|------|-------| |------|-------|
| MCU | ESP32-S3RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) | | MCU | ESP32-S3RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) || Primary IMU | MPU6000 (WHO_AM_I = 0x68) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| Primary IMU | MPU6000 (WHO_AM_I = 0x68) |
| IMU Bus | SPI1: PA5=SCK, PA6=MISO, PA7=MOSI, CS=PA4 | | IMU Bus | SPI1: PA5=SCK, PA6=MISO, PA7=MOSI, CS=PA4 |
| IMU EXTI | PC4 (data ready interrupt) | | IMU EXTI | PC4 (data ready interrupt) |
| IMU Orientation | CW270 (Betaflight convention) | | IMU Orientation | CW270 (Betaflight convention) |
| Secondary IMU | QMI8658-P (on same SPI1, CS unknown — currently non-functional) | | Secondary IMU | QMI8658-P (on same SPI1, CS unknown — currently non-functional) |
| Betaflight Target | DIAT-MAMBAF722_2022B | | Board Name | Waveshare ESP32-S3 Touch LCD 1.28 |
| USB | OTG FS (PA11/PA12), enumerates as /dev/cu.usbmodemSALTY0011 | | USB | OTG FS (PA11/PA12), enumerates as /dev/cu.usbmodemSALTY0011 |
| VID/PID | 0x0483/0x5740 | | VID/PID | 0x0483/0x5740 |
| LEDs | PC15 (LED1), PC14 (LED2), active low | | LEDs | PC15 (LED1), PC14 (LED2), active low |
@ -194,12 +160,7 @@ PLATFORM.md # Hardware platform reference
### Critical Lessons Learned (DON'T REPEAT THESE) ### Critical Lessons Learned (DON'T REPEAT THESE)
1. **SysTick_Handler with HAL_IncTick() is MANDATORY** — without it, HAL_Delay() and every HAL timeout hangs forever. This bricked us multiple times. 1. **SysTick_Handler with HAL_IncTick() is MANDATORY** — without it, HAL_Delay() and every HAL timeout hangs forever. This bricked us multiple times.
<<<<<<< HEAD 2. **DCache breaks SPI on ESP32-S3** — disable DCache or use cache-aligned DMA buffers with clean/invalidate. We disable it.3. **`-(int)0 == 0`** — checking `if (-result)` to detect errors doesn't work when result is 0 (success and failure look the same). Always use explicit error codes.
2. **DCache breaks SPI on ESP32** — disable DCache or use cache-aligned DMA buffers with clean/invalidate. We disable it.
=======
2. **DCache breaks SPI on ESP32-S3** — disable DCache or use cache-aligned DMA buffers with clean/invalidate. We disable it.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
3. **`-(int)0 == 0`** — checking `if (-result)` to detect errors doesn't work when result is 0 (success and failure look the same). Always use explicit error codes.
4. **NEVER auto-run untested code on_boot** — we bricked the NSPanel 3x doing this. Test manually first. 4. **NEVER auto-run untested code on_boot** — we bricked the NSPanel 3x doing this. Test manually first.
5. **USB Serial (CH343) needs ReceivePacket() primed in CDC_Init** — without it, the OUT endpoint never starts listening. No data reception. 5. **USB Serial (CH343) needs ReceivePacket() primed in CDC_Init** — without it, the OUT endpoint never starts listening. No data reception.
@ -210,19 +171,14 @@ The firmware supports reboot-to-DFU via USB command:
2. Firmware writes `0xDEADBEEF` to RTC backup register 0 2. Firmware writes `0xDEADBEEF` to RTC backup register 0
3. `NVIC_SystemReset()` — clean hardware reset 3. `NVIC_SystemReset()` — clean hardware reset
4. On boot, `checkForBootloader()` (called after `HAL_Init()`) reads the magic 4. On boot, `checkForBootloader()` (called after `HAL_Init()`) reads the magic
<<<<<<< HEAD 5. If magic found: clears it, remaps system memory, jumps to ESP32-S3 bootloader at `0x1FF00000`6. Board appears as DFU device, ready for `dfu-util` flash
5. If magic found: clears it, remaps system memory, jumps to ESP32 BALANCE bootloader at `0x1FF00000`
=======
5. If magic found: clears it, remaps system memory, jumps to ESP32-S3 bootloader at `0x1FF00000`
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
6. Board appears as DFU device, ready for `dfu-util` flash
### Build & Flash ### Build & Flash
```bash ```bash
cd firmware/ cd firmware/
python3 -m platformio run # Build python3 -m platformio run # Build
dfu-util -a 0 -s 0x08000000:leave -D .pio/build/f722/firmware.bin # Flash esptool.py --port /dev/esp32-balance write_flash 0x0 firmware.bin # Flash
``` ```
Dev machine: mbpm4 (seb@192.168.87.40), PlatformIO project at `~/Projects/saltylab-firmware/` Dev machine: mbpm4 (seb@192.168.87.40), PlatformIO project at `~/Projects/saltylab-firmware/`

View File

@ -1,11 +1,6 @@
# Face LCD Animation System (Issue #507) # Face LCD Animation System (Issue #507)
<<<<<<< HEAD
Implements expressive face animations on an ESP32 LCD display with 5 core emotions and smooth transitions.
=======
Implements expressive face animations on an ESP32-S3 LCD display with 5 core emotions and smooth transitions. Implements expressive face animations on an ESP32-S3 LCD display with 5 core emotions and smooth transitions.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Features ## Features
### Emotions ### Emotions
@ -86,12 +81,7 @@ STATUS → Echo current emotion + idle state
- Colors: Monochrome (1-bit) or RGB565 - Colors: Monochrome (1-bit) or RGB565
### Microcontroller ### Microcontroller
<<<<<<< HEAD - ESP32-S3xx (ESP32-S3 BALANCE)- Available UART: USART3 (PB10=TX, PB11=RX)
- ESP32xx (ESP32 BALANCE)
=======
- ESP32-S3xx (ESP32-S3 BALANCE)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
- Available UART: USART3 (PB10=TX, PB11=RX)
- Clock: 216 MHz - Clock: 216 MHz
## Animation Timing ## Animation Timing

View File

@ -102,11 +102,8 @@ balance loop, and drives the hoverboard ESC via UART. Jetson Orin Nano Super
sends velocity commands over UART1. ELRS receiver on UART3 provides RC sends velocity commands over UART1. ELRS receiver on UART3 provides RC
override and kill-switch capability. override and kill-switch capability.
The legacy STM32 firmware (Mamba F722S era) has been archived to
=======
The legacy STM32 firmware (STM32 era) has been archived to The legacy STM32 firmware (STM32 era) has been archived to
`legacy/stm32/` and is no longer built or deployed. `legacy/stm32/` and is no longer built or deployed.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## LED Subsystem (ESP32-C3) ## LED Subsystem (ESP32-C3)

View File

@ -14,13 +14,7 @@
│ ORIN NANO SUPER │ │ ORIN NANO SUPER │
│ (Top Plate — 25W) │ │ (Top Plate — 25W) │
│ │ │ │
<<<<<<< HEAD │ USB-C ──── ESP32-S3 CDC (/dev/esp32-bridge, 921600 baud) ││ USB-A1 ─── RealSense D435i (USB 3.1) │
│ USB-A ──── CANable2 USB-CAN adapter (slcan0, 500 kbps) │
│ USB-A ──── ESP32-S3 IO (/dev/esp32-io, 460800 baud) │
=======
│ USB-C ──── ESP32-S3 CDC (/dev/esp32-bridge, 921600 baud) │
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ USB-A1 ─── RealSense D435i (USB 3.1) │
│ USB-A2 ─── RPLIDAR A1M8 (via CP2102 adapter, 115200) │ │ USB-A2 ─── RPLIDAR A1M8 (via CP2102 adapter, 115200) │
│ USB-C* ─── SIM7600A 4G/LTE modem (ttyUSB0-2, AT cmds + PPP) │ │ USB-C* ─── SIM7600A 4G/LTE modem (ttyUSB0-2, AT cmds + PPP) │
│ USB ─────── Leap Motion Controller (hand/gesture tracking) │ │ USB ─────── Leap Motion Controller (hand/gesture tracking) │
@ -38,14 +32,8 @@
│ 500 kbps │ │ 500 kbps │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────┐
<<<<<<< HEAD
│ ESP32-S3 BALANCE │
│ (Waveshare Touch LCD 1.28, Middle Plate) │
=======
│ ESP32-S3 BALANCE (FC) │ │ ESP32-S3 BALANCE (FC) │
│ (Middle Plate — foam mounted) │ │ (Middle Plate — foam mounted) ││ │
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ │
│ CAN bus ──── CANable2 → Orin (primary link, ISO 11898) │ │ CAN bus ──── CANable2 → Orin (primary link, ISO 11898) │
│ UART0 ──── Orin UART fallback (460800 baud, 3.3V) │ │ UART0 ──── Orin UART fallback (460800 baud, 3.3V) │
│ UART1 ──── VESC Left (CAN ID 56) via UART/CAN bridge │ │ UART1 ──── VESC Left (CAN ID 56) via UART/CAN bridge │
@ -77,29 +65,16 @@
## Wire-by-Wire Connections ## Wire-by-Wire Connections
<<<<<<< HEAD
### 1. Orin <-> ESP32-S3 BALANCE (Primary: CAN Bus via CANable2)
=======
### 1. Orin ↔ FC (Primary: USB Serial (CH343)) ### 1. Orin ↔ FC (Primary: USB Serial (CH343))
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| From | To | Wire | Notes | | From | To | Wire | Notes |
|------|----|------|-------| |------|----|------|-------|
| Orin USB-A | CANable2 USB | USB cable | SocketCAN slcan0 @ 500 kbps | | Orin USB-A | CANable2 USB | USB cable | SocketCAN slcan0 @ 500 kbps |
| CANable2 CAN-H | ESP32-S3 BALANCE CAN-H | twisted pair | ISO 11898 differential | | CANable2 CAN-H | ESP32-S3 BALANCE CAN-H | twisted pair | ISO 11898 differential |
| CANable2 CAN-L | ESP32-S3 BALANCE CAN-L | twisted pair | ISO 11898 differential | | CANable2 CAN-L | ESP32-S3 BALANCE CAN-L | twisted pair | ISO 11898 differential |
<<<<<<< HEAD
- Interface: SocketCAN `slcan0`, 500 kbps
- Device node: `/dev/canable2` (via udev, symlink to ttyUSBx)
- Protocol: CAN frames --- ORIN_CMD_DRIVE (0x300), ORIN_CMD_MODE (0x301), ORIN_CMD_ESTOP (0x302)
- Telemetry: BALANCE_STATUS (0x400), BALANCE_VESC (0x401), BALANCE_IMU (0x402), BALANCE_BATTERY (0x403)
=======
- Device: `/dev/ttyACM0` → symlink `/dev/esp32-bridge` - Device: `/dev/ttyACM0` → symlink `/dev/esp32-bridge`
- Baud: 921600, 8N1 - Baud: 921600, 8N1
- Protocol: JSON telemetry (FC→Orin), ASCII commands (Orin→FC) - Protocol: JSON telemetry (FC→Orin), ASCII commands (Orin→FC)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
### 2. Orin <-> ESP32-S3 BALANCE (Fallback: Hardware UART) ### 2. Orin <-> ESP32-S3 BALANCE (Fallback: Hardware UART)
| Orin Pin | Signal | ESP32-S3 Pin | Notes | | Orin Pin | Signal | ESP32-S3 Pin | Notes |
@ -164,34 +139,6 @@ BATTERY (36V) ──┬── VESC Left (36V direct -> BLDC left motor)
| CANable2 | USB-CAN | USB-A | `/dev/canable2` -> `slcan0` | | CANable2 | USB-CAN | USB-A | `/dev/canable2` -> `slcan0` |
<<<<<<< HEAD
## FC UART Summary (MAMBA F722S — OBSOLETE)
| Interface | Pins | Baud/Rate | Assignment | Notes |
|-----------|------|-----------|------------|-------|
| UART0 | GPIO17=RX, GPIO18=TX | 460800 | Orin UART fallback | 3.3V, cross-connect |
| UART1 | GPIO19=RX, GPIO20=TX | 115200 | Debug serial | Optional |
| CAN (TWAI) | GPIO21=H, GPIO22=L | 500 kbps | CAN bus (VESCs + Orin) | SN65HVD230 transceiver |
| I2C | GPIO4=SDA, GPIO5=SCL | 400 kHz | QMI8658 IMU (addr 0x6B) | Onboard |
| SPI | GPIO36=MOSI, GPIO37=SCLK, GPIO35=CS | 40 MHz | GC9A01 LCD (onboard) | 240x240 round |
| USB CDC | USB-C | 460800 | Orin USB fallback | /dev/esp32-balance |
## CAN Frame ID Map
| CAN ID | Direction | Name | Contents |
|--------|-----------|------|----------|
| 0x300 | Orin -> BALANCE | ORIN_CMD_DRIVE | left_rpm_f32, right_rpm_f32 (8 bytes LE) |
| 0x301 | Orin -> BALANCE | ORIN_CMD_MODE | mode byte (0=IDLE, 1=DRIVE, 2=ESTOP) |
| 0x302 | Orin -> BALANCE | ORIN_CMD_ESTOP | flags byte (bit0=stop, bit1=clear) |
| 0x400 | BALANCE -> Orin | BALANCE_STATUS | pitch x10:i16, motor_cmd:u16, vbat_mv:u16, state:u8, flags:u8 |
| 0x401 | BALANCE -> Orin | BALANCE_VESC | l_rpm x10:i16, r_rpm x10:i16, l_cur x10:i16, r_cur x10:i16 |
| 0x402 | BALANCE -> Orin | BALANCE_IMU | pitch x100:i16, roll x100:i16, yaw x100:i16, ax x100:i16, ay x100:i16, az x100:i16 |
| 0x403 | BALANCE -> Orin | BALANCE_BATTERY | vbat_mv:u16, current_ma:i16, soc_pct:u8 |
| 0x900+ID | VESC Left -> | VESC_STATUS_1 | erpm:i32, current x10:i16, duty x1000:i16 |
| 0x910+ID | VESC Right -> | VESC_STATUS_1 | erpm:i32, current x10:i16, duty x1000:i16 |
VESC Left CAN ID = 56 (0x38), VESC Right CAN ID = 68 (0x44).
=======
## FC UART Summary (ESP32-S3 BALANCE) ## FC UART Summary (ESP32-S3 BALANCE)
| UART | Pins | Baud | Assignment | Notes | | UART | Pins | Baud | Assignment | Notes |
@ -203,8 +150,6 @@ VESC Left CAN ID = 56 (0x38), VESC Right CAN ID = 68 (0x44).
| UART5 | PC12=TX, PD2=RX | 115200 | Debug serial | Optional | | UART5 | PC12=TX, PD2=RX | 115200 | Debug serial | Optional |
| USART6 | PC6=TX, PC7=RX | 921600 | Jetson UART | Fallback link | | USART6 | PC6=TX, PC7=RX | 921600 | Jetson UART | Fallback link |
| USB Serial (CH343) | USB-C | 921600 | Jetson primary | `/dev/esp32-bridge` | | USB Serial (CH343) | USB-C | 921600 | Jetson primary | `/dev/esp32-bridge` |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
### 7. ReSpeaker 2-Mic HAT (on Orin 40-pin header) ### 7. ReSpeaker 2-Mic HAT (on Orin 40-pin header)
@ -263,14 +208,7 @@ VESC Left CAN ID = 56 (0x38), VESC Right CAN ID = 68 (0x44).
| Device | Interface | Power Draw | | Device | Interface | Power Draw |
|--------|-----------|------------| |--------|-----------|------------|
<<<<<<< HEAD | ESP32-S3 FC (CDC) | USB-C | ~0.5W (data only, FC on 5V bus) || RealSense D435i | USB-A | ~1.5W (3.5W peak) |
| CANable2 USB-CAN | USB-A | ~0.5W |
| ESP32-S3 BALANCE | USB-C | ~0.8W (WiFi off) |
| ESP32-S3 IO | USB-C | ~0.5W |
=======
| ESP32-S3 FC (CDC) | USB-C | ~0.5W (data only, FC on 5V bus) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| RealSense D435i | USB-A | ~1.5W (3.5W peak) |
| RPLIDAR A1M8 | USB-A | ~2.6W (motor on) | | RPLIDAR A1M8 | USB-A | ~2.6W (motor on) |
| SIM7600A | USB | ~1W idle, 3W TX peak | | SIM7600A | USB | ~1W idle, 3W TX peak |
| Leap Motion | USB-A | ~0.5W | | Leap Motion | USB-A | ~0.5W |
@ -294,25 +232,14 @@ Orin Nano Super delivers up to 25W --- USB peripherals are well within budget.
└──────┬───────┘ └──────┬───────┘
│ UART │ UART
┌────────────▼────────────┐ ┌────────────▼────────────┐
<<<<<<< HEAD │ ESP32-S3 BALANCE │ │ │
│ ESP32-S3 BALANCE │
│ (Waveshare LCD 1.28) │
=======
│ ESP32-S3 BALANCE │
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ │
│ QMI8658 -> Balance PID │ │ QMI8658 -> Balance PID │
│ RC -> Mode Manager │ │ RC -> Mode Manager │
│ Safety Monitor │ │ Safety Monitor │
│ │ │ │
└──┬──────────┬───────────┘ └──┬──────────┬───────────┘
<<<<<<< HEAD
CAN 500kbps─┘ └───── CAN bus / UART fallback
=======
USART2 ─────┘ └───── USB Serial (CH343) / USART6 USART2 ─────┘ └───── USB Serial (CH343) / USART6
26400 baud 921600 baud 26400 baud 921600 baud │ │
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ │
┌────┴────────────┐ ▼ ┌────┴────────────┐ ▼
│ CAN bus (500k) │ ┌───────────────────┐ │ CAN bus (500k) │ ┌───────────────────┐
├─ VESC Left 56 │ │ Orin Nano Super │ ├─ VESC Left 56 │ │ Orin Nano Super │

View File

@ -14,12 +14,7 @@ Self-balancing robot: Jetson Orin Nano Super dev environment for ROS2 Humble + S
| Nav | Nav2 | | Nav | Nav2 |
| Depth camera | Intel RealSense D435i | | Depth camera | Intel RealSense D435i |
| LiDAR | RPLIDAR A1M8 | | LiDAR | RPLIDAR A1M8 |
<<<<<<< HEAD
| MCU bridge | ESP32 (USB CDC @ 921600) |
=======
| MCU bridge | ESP32-S3 (USB Serial (CH343) @ 921600) | | MCU bridge | ESP32-S3 (USB Serial (CH343) @ 921600) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Quick Start ## Quick Start
```bash ```bash
@ -46,12 +41,7 @@ bash scripts/build-and-run.sh shell
``` ```
jetson/ jetson/
├── Dockerfile # L4T base + ROS2 Humble + SLAM packages ├── Dockerfile # L4T base + ROS2 Humble + SLAM packages
<<<<<<< HEAD ├── docker-compose.yml # Multi-service stack (ROS2, RPLIDAR, D435i, ESP32-S3)├── README.md # This file
├── docker-compose.yml # Multi-service stack (ROS2, RPLIDAR, D435i, ESP32 BALANCE)
=======
├── docker-compose.yml # Multi-service stack (ROS2, RPLIDAR, D435i, ESP32-S3)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
├── README.md # This file
├── docs/ ├── docs/
│ ├── pinout.md # GPIO/I2C/UART pinout reference │ ├── pinout.md # GPIO/I2C/UART pinout reference
│ └── power-budget.md # Power budget analysis (10W envelope) │ └── power-budget.md # Power budget analysis (10W envelope)

View File

@ -34,12 +34,7 @@ Recovery behaviors are triggered when Nav2 encounters navigation failures (path
The emergency stop system (Issue #459, `saltybot_emergency` package) runs independently of Nav2 and takes absolute priority. The emergency stop system (Issue #459, `saltybot_emergency` package) runs independently of Nav2 and takes absolute priority.
<<<<<<< HEAD
Recovery behaviors cannot interfere with E-stop because the emergency system operates at the motor driver level on the ESP32 BALANCE firmware.
=======
Recovery behaviors cannot interfere with E-stop because the emergency system operates at the motor driver level on the ESP32-S3 firmware. Recovery behaviors cannot interfere with E-stop because the emergency system operates at the motor driver level on the ESP32-S3 firmware.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Behavior Tree Sequence ## Behavior Tree Sequence
Recovery runs in a round-robin fashion with up to 6 retry cycles. Recovery runs in a round-robin fashion with up to 6 retry cycles.

View File

@ -12,12 +12,7 @@
# /scan — RPLIDAR A1M8 (obstacle layer) # /scan — RPLIDAR A1M8 (obstacle layer)
# /camera/depth/color/points — RealSense D435i (voxel layer) # /camera/depth/color/points — RealSense D435i (voxel layer)
# #
<<<<<<< HEAD
# Output: /cmd_vel (Twist) — ESP32 bridge consumes this topic.
=======
# Output: /cmd_vel (Twist) — ESP32-S3 bridge consumes this topic. # Output: /cmd_vel (Twist) — ESP32-S3 bridge consumes this topic.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
bt_navigator: bt_navigator:
ros__parameters: ros__parameters:
use_sim_time: false use_sim_time: false

View File

@ -97,12 +97,7 @@ services:
rgb_camera.profile:=640x480x30 rgb_camera.profile:=640x480x30
" "
<<<<<<< HEAD # ── ESP32-S3 bridge node (bidirectional serial<->ROS2) ──────────────────────── esp32-bridge:
# ── ESP32 bridge node (bidirectional serial<->ROS2) ────────────────────────
=======
# ── ESP32-S3 bridge node (bidirectional serial<->ROS2) ────────────────────────
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
esp32-bridge:
image: saltybot/ros2-humble:jetson-orin image: saltybot/ros2-humble:jetson-orin
build: build:
context: . context: .
@ -212,14 +207,8 @@ services:
" "
<<<<<<< HEAD
# -- Remote e-stop bridge (MQTT over 4G -> ESP32 CDC) ----------------------
# Subscribes to saltybot/estop MQTT topic. {"kill":true} -> 'E\r\n' to ESP32 BALANCE.
=======
# -- Remote e-stop bridge (MQTT over 4G -> ESP32-S3 CDC) ---------------------- # -- Remote e-stop bridge (MQTT over 4G -> ESP32-S3 CDC) ----------------------
# Subscribes to saltybot/estop MQTT topic. {"kill":true} -> 'E\r\n' to ESP32-S3. # Subscribes to saltybot/estop MQTT topic. {"kill":true} -> 'E\r\n' to ESP32-S3. # Cellular watchdog: 5s MQTT drop in AUTO mode -> 'F\r\n' (ESTOP_CELLULAR_TIMEOUT).
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# Cellular watchdog: 5s MQTT drop in AUTO mode -> 'F\r\n' (ESTOP_CELLULAR_TIMEOUT).
remote-estop: remote-estop:
image: saltybot/ros2-humble:jetson-orin image: saltybot/ros2-humble:jetson-orin
build: build:

View File

@ -1,10 +1,5 @@
# Jetson Orin Nano Super — GPIO / I2C / UART / CSI Pinout Reference # Jetson Orin Nano Super — GPIO / I2C / UART / CSI Pinout Reference
<<<<<<< HEAD
## Self-Balancing Robot: ESP32 Bridge + RealSense D435i + RPLIDAR A1M8 + 4× IMX219
=======
## Self-Balancing Robot: ESP32-S3 Bridge + RealSense D435i + RPLIDAR A1M8 + 4× IMX219 ## Self-Balancing Robot: ESP32-S3 Bridge + RealSense D435i + RPLIDAR A1M8 + 4× IMX219
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
Last updated: 2026-02-28 Last updated: 2026-02-28
JetPack version: 6.x (L4T R36.x / Ubuntu 22.04) JetPack version: 6.x (L4T R36.x / Ubuntu 22.04)
@ -47,50 +42,26 @@ i2cdetect -l
--- ---
<<<<<<< HEAD
## 1. ESP32 Bridge (USB CDC — Primary)
The ESP32 BALANCE acts as a real-time motor + IMU controller. Communication is via **USB CDC serial**.
=======
## 1. ESP32-S3 Bridge (USB Serial (CH343) — Primary) ## 1. ESP32-S3 Bridge (USB Serial (CH343) — Primary)
The ESP32-S3 acts as a real-time motor + IMU controller. Communication is via **USB Serial (CH343) serial**. The ESP32-S3 acts as a real-time motor + IMU controller. Communication is via **USB Serial (CH343) serial**.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
### USB Serial (CH343) Connection ### USB Serial (CH343) Connection
| Connection | Detail | | Connection | Detail |
|-----------|--------| |-----------|--------|
<<<<<<< HEAD
| Interface | USB on ESP32 BALANCE board → USB-A on Jetson |
| Device node | `/dev/ttyACM0` → symlink `/dev/esp32-bridge` (via udev) |
| Baud rate | 921600 (configured in ESP32 BALANCE firmware) |
=======
| Interface | USB Micro-B on ESP32-S3 dev board → USB-A on Jetson | | Interface | USB Micro-B on ESP32-S3 dev board → USB-A on Jetson |
| Device node | `/dev/ttyACM0` → symlink `/dev/esp32-bridge` (via udev) | | Device node | `/dev/ttyACM0` → symlink `/dev/esp32-bridge` (via udev) |
| Baud rate | 921600 (configured in ESP32-S3 firmware) | | Baud rate | 921600 (configured in ESP32-S3 firmware) || Protocol | JSON telemetry RX + ASCII command TX (see bridge docs) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| Protocol | JSON telemetry RX + ASCII command TX (see bridge docs) |
| Power | Powered via robot 5V bus (data-only via USB) | | Power | Powered via robot 5V bus (data-only via USB) |
### Hardware UART (Fallback — 40-pin header) ### Hardware UART (Fallback — 40-pin header)
<<<<<<< HEAD | Jetson Pin | Signal | ESP32-S3 Pin | Notes ||-----------|--------|-----------|-------|
| Jetson Pin | Signal | ESP32 Pin | Notes |
=======
| Jetson Pin | Signal | ESP32-S3 Pin | Notes |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
|-----------|--------|-----------|-------|
| Pin 8 (TXD0) | TX → | PA10 (UART1 RX) | Cross-connect TX→RX | | Pin 8 (TXD0) | TX → | PA10 (UART1 RX) | Cross-connect TX→RX |
| Pin 10 (RXD0) | RX ← | PA9 (UART1 TX) | Cross-connect RX→TX | | Pin 10 (RXD0) | RX ← | PA9 (UART1 TX) | Cross-connect RX→TX |
| Pin 6 (GND) | GND | GND | Common ground **required** | | Pin 6 (GND) | GND | GND | Common ground **required** |
**Jetson device node:** `/dev/ttyTHS0` **Jetson device node:** `/dev/ttyTHS0`
**Baud rate:** 921600, 8N1 **Baud rate:** 921600, 8N1
<<<<<<< HEAD
**Voltage level:** 3.3V — both Jetson Orin and ESP32 are 3.3V GPIO
=======
**Voltage level:** 3.3V — both Jetson Orin and ESP32-S3 are 3.3V GPIO **Voltage level:** 3.3V — both Jetson Orin and ESP32-S3 are 3.3V GPIO
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
```bash ```bash
# Verify UART # Verify UART
ls /dev/ttyTHS0 ls /dev/ttyTHS0
@ -99,15 +70,6 @@ sudo usermod -aG dialout $USER
picocom -b 921600 /dev/ttyTHS0 picocom -b 921600 /dev/ttyTHS0
``` ```
<<<<<<< HEAD
**ROS2 topics (ESP32 bridge node):**
| ROS2 Topic | Direction | Content |
|-----------|-----------|---------
| `/saltybot/imu` | ESP32 BALANCE→Jetson | IMU data (accel, gyro) at 50Hz |
| `/saltybot/balance_state` | ESP32 BALANCE→Jetson | Motor cmd, pitch, state |
| `/cmd_vel` | Jetson→ESP32 BALANCE | Velocity commands → `C<spd>,<str>\n` |
| `/saltybot/estop` | Jetson→ESP32 BALANCE | Emergency stop |
=======
**ROS2 topics (ESP32-S3 bridge node):** **ROS2 topics (ESP32-S3 bridge node):**
| ROS2 Topic | Direction | Content | | ROS2 Topic | Direction | Content |
|-----------|-----------|--------- |-----------|-----------|---------
@ -115,8 +77,6 @@ picocom -b 921600 /dev/ttyTHS0
| `/saltybot/balance_state` | ESP32-S3→Jetson | Motor cmd, pitch, state | | `/saltybot/balance_state` | ESP32-S3→Jetson | Motor cmd, pitch, state |
| `/cmd_vel` | Jetson→ESP32-S3 | Velocity commands → `C<spd>,<str>\n` | | `/cmd_vel` | Jetson→ESP32-S3 | Velocity commands → `C<spd>,<str>\n` |
| `/saltybot/estop` | Jetson→ESP32-S3 | Emergency stop | | `/saltybot/estop` | Jetson→ESP32-S3 | Emergency stop |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
--- ---
## 2. RealSense D435i (USB 3.1) ## 2. RealSense D435i (USB 3.1)
@ -300,12 +260,7 @@ sudo mkdir -p /mnt/nvme
|------|------|----------| |------|------|----------|
| USB-A (top, blue) | USB 3.1 Gen 1 | RealSense D435i | | USB-A (top, blue) | USB 3.1 Gen 1 | RealSense D435i |
| USB-A (bottom) | USB 2.0 | RPLIDAR (via USB-UART adapter) | | USB-A (bottom) | USB 2.0 | RPLIDAR (via USB-UART adapter) |
<<<<<<< HEAD | USB-C | USB 3.1 Gen 1 (+ DP) | ESP32-S3 CDC or host flash || Micro-USB | Debug/flash | JetPack flash only |
| USB-C | USB 3.1 Gen 1 (+ DP) | ESP32 CDC or host flash |
=======
| USB-C | USB 3.1 Gen 1 (+ DP) | ESP32-S3 CDC or host flash |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| Micro-USB | Debug/flash | JetPack flash only |
--- ---
@ -315,18 +270,10 @@ sudo mkdir -p /mnt/nvme
|-------------|----------|---------|----------| |-------------|----------|---------|----------|
| 3 | SDA1 | 3.3V | I2C data (i2c-7) | | 3 | SDA1 | 3.3V | I2C data (i2c-7) |
| 5 | SCL1 | 3.3V | I2C clock (i2c-7) | | 5 | SCL1 | 3.3V | I2C clock (i2c-7) |
<<<<<<< HEAD
| 8 | TXD0 | 3.3V | UART TX → ESP32 BALANCE (fallback) |
| 10 | RXD0 | 3.3V | UART RX ← ESP32 BALANCE (fallback) |
| USB-A ×2 | — | 5V | D435i, RPLIDAR |
| USB-C | — | 5V | ESP32 CDC |
=======
| 8 | TXD0 | 3.3V | UART TX → ESP32-S3 (fallback) | | 8 | TXD0 | 3.3V | UART TX → ESP32-S3 (fallback) |
| 10 | RXD0 | 3.3V | UART RX ← ESP32-S3 (fallback) | | 10 | RXD0 | 3.3V | UART RX ← ESP32-S3 (fallback) |
| USB-A ×2 | — | 5V | D435i, RPLIDAR | | USB-A ×2 | — | 5V | D435i, RPLIDAR |
| USB-C | — | 5V | ESP32-S3 CDC | | USB-C | — | 5V | ESP32-S3 CDC || CSI-A (J5) | MIPI CSI-2 | — | Cameras front + left |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| CSI-A (J5) | MIPI CSI-2 | — | Cameras front + left |
| CSI-B (J8) | MIPI CSI-2 | — | Cameras rear + right | | CSI-B (J8) | MIPI CSI-2 | — | Cameras rear + right |
| M.2 Key M | PCIe Gen3 ×4 | — | NVMe SSD | | M.2 Key M | PCIe Gen3 ×4 | — | NVMe SSD |
@ -343,12 +290,7 @@ Apply stable device names:
KERNEL=="ttyUSB*", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", \ KERNEL=="ttyUSB*", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", \
SYMLINK+="rplidar", MODE="0666" SYMLINK+="rplidar", MODE="0666"
<<<<<<< HEAD # ESP32-S3 USB Serial (CH343) (STMicroelectronics)KERNEL=="ttyACM*", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", \
# ESP32 USB CDC (STMicroelectronics)
=======
# ESP32-S3 USB Serial (CH343) (STMicroelectronics)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
KERNEL=="ttyACM*", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", \
SYMLINK+="esp32-bridge", MODE="0666" SYMLINK+="esp32-bridge", MODE="0666"
# Intel RealSense D435i # Intel RealSense D435i

View File

@ -56,12 +56,7 @@ sudo jtop
|-----------|----------|------------|----------|-----------|-------| |-----------|----------|------------|----------|-----------|-------|
| RealSense D435i | 0.3 | 1.5 | 3.5 | USB 3.1 | Peak during boot/init | | RealSense D435i | 0.3 | 1.5 | 3.5 | USB 3.1 | Peak during boot/init |
| RPLIDAR A1M8 | 0.4 | 2.6 | 3.0 | USB (UART adapter) | Motor spinning | | RPLIDAR A1M8 | 0.4 | 2.6 | 3.0 | USB (UART adapter) | Motor spinning |
<<<<<<< HEAD | ESP32-S3 bridge | 0.0 | 0.0 | 0.0 | USB Serial (CH343) | Self-powered from robot 5V || 4× IMX219 cameras | 0.2 | 2.0 | 2.4 | MIPI CSI-2 | ~0.5W per camera active |
| ESP32 bridge | 0.0 | 0.0 | 0.0 | USB CDC | Self-powered from robot 5V |
=======
| ESP32-S3 bridge | 0.0 | 0.0 | 0.0 | USB Serial (CH343) | Self-powered from robot 5V |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| 4× IMX219 cameras | 0.2 | 2.0 | 2.4 | MIPI CSI-2 | ~0.5W per camera active |
| **Peripheral Subtotal** | **0.9** | **6.1** | **8.9** | | | | **Peripheral Subtotal** | **0.9** | **6.1** | **8.9** | | |
### Total System (from Jetson 5V barrel jack) ### Total System (from Jetson 5V barrel jack)
@ -155,12 +150,7 @@ LiPo 4S (16.8V max)
├─► DC-DC Buck → 5V 6A ──► Jetson Orin barrel jack (30W) ├─► DC-DC Buck → 5V 6A ──► Jetson Orin barrel jack (30W)
│ (e.g., XL4016E1) │ (e.g., XL4016E1)
<<<<<<< HEAD ├─► DC-DC Buck → 5V 3A ──► ESP32-S3 + logic 5V rail │
├─► DC-DC Buck → 5V 3A ──► ESP32 + logic 5V rail
=======
├─► DC-DC Buck → 5V 3A ──► ESP32-S3 + logic 5V rail
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
└─► Hoverboard ESC ──► Hub motors (48V loop) └─► Hoverboard ESC ──► Hub motors (48V loop)
``` ```

View File

@ -11,12 +11,7 @@ reconnect_delay: 2.0 # seconds between reconnect attempts on serial disconne
# ── saltybot_cmd_node (bidirectional) only ───────────────────────────────────── # ── saltybot_cmd_node (bidirectional) only ─────────────────────────────────────
# Heartbeat: H\n sent every heartbeat_period seconds. # Heartbeat: H\n sent every heartbeat_period seconds.
<<<<<<< HEAD # ESP32-S3 reverts steer to 0 after JETSON_HB_TIMEOUT_MS (500ms) without heartbeat.heartbeat_period: 0.2 # seconds (= 200ms)
# ESP32 BALANCE reverts steer to 0 after JETSON_HB_TIMEOUT_MS (500ms) without heartbeat.
=======
# ESP32-S3 reverts steer to 0 after JETSON_HB_TIMEOUT_MS (500ms) without heartbeat.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
heartbeat_period: 0.2 # seconds (= 200ms)
# Twist → ESC command scaling # Twist → ESC command scaling
# speed = clamp(linear.x * speed_scale, -1000, 1000) [m/s → ESC units] # speed = clamp(linear.x * speed_scale, -1000, 1000) [m/s → ESC units]

View File

@ -1,10 +1,5 @@
# cmd_vel_bridge_params.yaml # cmd_vel_bridge_params.yaml
<<<<<<< HEAD # Configuration for cmd_vel_bridge_node — Nav2 /cmd_vel → ESP32-S3 autonomous drive.#
# Configuration for cmd_vel_bridge_node — Nav2 /cmd_vel → ESP32 BALANCE autonomous drive.
=======
# Configuration for cmd_vel_bridge_node — Nav2 /cmd_vel → ESP32-S3 autonomous drive.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
#
# Run with: # Run with:
# ros2 launch saltybot_bridge cmd_vel_bridge.launch.py # ros2 launch saltybot_bridge cmd_vel_bridge.launch.py
# Or override individual params: # Or override individual params:
@ -18,12 +13,7 @@ timeout: 0.05 # serial readline timeout (s)
reconnect_delay: 2.0 # seconds between reconnect attempts reconnect_delay: 2.0 # seconds between reconnect attempts
# ── Heartbeat ────────────────────────────────────────────────────────────────── # ── Heartbeat ──────────────────────────────────────────────────────────────────
<<<<<<< HEAD # ESP32-S3 jetson_cmd module reverts steer to 0 after JETSON_HB_TIMEOUT_MS (500ms).# Keep heartbeat well below that threshold.
# ESP32 BALANCE jetson_cmd module reverts steer to 0 after JETSON_HB_TIMEOUT_MS (500ms).
=======
# ESP32-S3 jetson_cmd module reverts steer to 0 after JETSON_HB_TIMEOUT_MS (500ms).
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# Keep heartbeat well below that threshold.
heartbeat_period: 0.2 # seconds (200ms) heartbeat_period: 0.2 # seconds (200ms)
# ── Velocity limits ──────────────────────────────────────────────────────────── # ── Velocity limits ────────────────────────────────────────────────────────────
@ -58,9 +48,4 @@ ramp_rate: 500 # ESC units/second
# ── Deadman switch ───────────────────────────────────────────────────────────── # ── Deadman switch ─────────────────────────────────────────────────────────────
# If /cmd_vel is not received for this many seconds, target speed/steer are # If /cmd_vel is not received for this many seconds, target speed/steer are
# zeroed immediately. The ramp then drives the robot to a stop. # zeroed immediately. The ramp then drives the robot to a stop.
<<<<<<< HEAD # 500ms matches the ESP32-S3 jetson heartbeat timeout for consistency.cmd_vel_timeout: 0.5 # seconds
# 500ms matches the ESP32 BALANCE jetson heartbeat timeout for consistency.
=======
# 500ms matches the ESP32-S3 jetson heartbeat timeout for consistency.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
cmd_vel_timeout: 0.5 # seconds

View File

@ -1,21 +1,3 @@
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/config/stm32_cmd_params.yaml
# stm32_cmd_params.yaml — Configuration for stm32_cmd_node (ESP32-S3 IO bridge)
# Connects to ESP32-S3 IO board via USB-CDC @ 460800 baud.
# Frame format: [0xAA][LEN][TYPE][PAYLOAD][CRC8]
# Spec: docs/SAUL-TEE-SYSTEM-REFERENCE.md §5
# ── Serial port ────────────────────────────────────────────────────────────────
# Use /dev/esp32-io if udev rule is applied (see jetson/docs/udev-rules.md).
# ESP32-S3 IO appears as USB-JTAG/Serial device; no external UART bridge needed.
serial_port: /dev/esp32-io
baud_rate: 460800
reconnect_delay: 2.0 # seconds between reconnect attempts
# ── Heartbeat ─────────────────────────────────────────────────────────────────
# HEARTBEAT (0x20) sent every heartbeat_period.
# ESP32 IO watchdog fires if no heartbeat for ~500 ms.
heartbeat_period: 0.2 # 200 ms → well within 500 ms watchdog
=======
# esp32_cmd_params.yaml — Configuration for esp32_cmd_node (Issue #119) # esp32_cmd_params.yaml — Configuration for esp32_cmd_node (Issue #119)
# Binary-framed Jetson↔ESP32-S3 bridge at 921600 baud. # Binary-framed Jetson↔ESP32-S3 bridge at 921600 baud.
@ -46,4 +28,3 @@ watchdog_timeout: 0.5 # 500ms
# Tune speed_scale to set the physical top speed. # Tune speed_scale to set the physical top speed.
speed_scale: 1000.0 speed_scale: 1000.0
steer_scale: -500.0 steer_scale: -500.0
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/config/esp32_cmd_params.yaml

View File

@ -6,12 +6,7 @@ Two deployment modes:
1. Full bidirectional (recommended for Nav2): 1. Full bidirectional (recommended for Nav2):
ros2 launch saltybot_bridge bridge.launch.py mode:=bidirectional ros2 launch saltybot_bridge bridge.launch.py mode:=bidirectional
Starts saltybot_cmd_node owns serial port, handles both RX telemetry Starts saltybot_cmd_node owns serial port, handles both RX telemetry
<<<<<<< HEAD
and TX /cmd_vel ESP32 BALANCE commands + heartbeat.
=======
and TX /cmd_vel ESP32-S3 commands + heartbeat. and TX /cmd_vel ESP32-S3 commands + heartbeat.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
2. RX-only (telemetry monitor, no drive commands): 2. RX-only (telemetry monitor, no drive commands):
ros2 launch saltybot_bridge bridge.launch.py mode:=rx_only ros2 launch saltybot_bridge bridge.launch.py mode:=rx_only
Starts serial_bridge_node telemetry RX only. Use when you want to Starts serial_bridge_node telemetry RX only. Use when you want to
@ -69,12 +64,7 @@ def generate_launch_description():
DeclareLaunchArgument("mode", default_value="bidirectional", DeclareLaunchArgument("mode", default_value="bidirectional",
description="bidirectional | rx_only"), description="bidirectional | rx_only"),
DeclareLaunchArgument("serial_port", default_value="/dev/ttyACM0", DeclareLaunchArgument("serial_port", default_value="/dev/ttyACM0",
<<<<<<< HEAD description="ESP32-S3 USB CDC device node"), DeclareLaunchArgument("baud_rate", default_value="921600"),
description="ESP32 USB CDC device node"),
=======
description="ESP32-S3 USB CDC device node"),
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
DeclareLaunchArgument("baud_rate", default_value="921600"),
DeclareLaunchArgument("speed_scale", default_value="1000.0", DeclareLaunchArgument("speed_scale", default_value="1000.0",
description="m/s → ESC units (linear.x scale)"), description="m/s → ESC units (linear.x scale)"),
DeclareLaunchArgument("steer_scale", default_value="-500.0", DeclareLaunchArgument("steer_scale", default_value="-500.0",

View File

@ -1,19 +1,9 @@
""" """
<<<<<<< HEAD
cmd_vel_bridge.launch.py Nav2 cmd_vel ESP32 BALANCE autonomous drive bridge.
=======
cmd_vel_bridge.launch.py Nav2 cmd_vel ESP32-S3 autonomous drive bridge. cmd_vel_bridge.launch.py Nav2 cmd_vel ESP32-S3 autonomous drive bridge.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Starts cmd_vel_bridge_node, which owns the serial port exclusively and provides: Starts cmd_vel_bridge_node, which owns the serial port exclusively and provides:
- /cmd_vel subscription with velocity limits + smooth ramp - /cmd_vel subscription with velocity limits + smooth ramp
- Deadman switch (zero speed if /cmd_vel silent > cmd_vel_timeout) - Deadman switch (zero speed if /cmd_vel silent > cmd_vel_timeout)
<<<<<<< HEAD - Mode gate (drives only when ESP32-S3 is in AUTONOMOUS mode, md=2) - Telemetry RX /saltybot/imu, /saltybot/balance_state, /diagnostics
- Mode gate (drives only when ESP32 BALANCE is in AUTONOMOUS mode, md=2)
=======
- Mode gate (drives only when ESP32-S3 is in AUTONOMOUS mode, md=2)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
- Telemetry RX /saltybot/imu, /saltybot/balance_state, /diagnostics
- /saltybot/cmd publisher (observability) - /saltybot/cmd publisher (observability)
Do NOT run alongside serial_bridge_node or saltybot_cmd_node on the same port. Do NOT run alongside serial_bridge_node or saltybot_cmd_node on the same port.
@ -80,21 +70,11 @@ def generate_launch_description():
description="Full path to cmd_vel_bridge_params.yaml (overrides inline args)"), description="Full path to cmd_vel_bridge_params.yaml (overrides inline args)"),
DeclareLaunchArgument( DeclareLaunchArgument(
"serial_port", default_value="/dev/ttyACM0", "serial_port", default_value="/dev/ttyACM0",
<<<<<<< HEAD description="ESP32-S3 USB CDC device node"), DeclareLaunchArgument(
description="ESP32 USB CDC device node"),
=======
description="ESP32-S3 USB CDC device node"),
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
DeclareLaunchArgument(
"baud_rate", default_value="921600"), "baud_rate", default_value="921600"),
DeclareLaunchArgument( DeclareLaunchArgument(
"heartbeat_period",default_value="0.2", "heartbeat_period",default_value="0.2",
<<<<<<< HEAD description="Heartbeat interval (s); must be < ESP32-S3 HB timeout (0.5s)"), DeclareLaunchArgument(
description="Heartbeat interval (s); must be < ESP32 BALANCE HB timeout (0.5s)"),
=======
description="Heartbeat interval (s); must be < ESP32-S3 HB timeout (0.5s)"),
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
DeclareLaunchArgument(
"max_linear_vel", default_value="0.5", "max_linear_vel", default_value="0.5",
description="Hard speed cap before scaling (m/s)"), description="Hard speed cap before scaling (m/s)"),
DeclareLaunchArgument( DeclareLaunchArgument(

View File

@ -1,16 +1,3 @@
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/launch/stm32_cmd.launch.py
"""stm32_cmd.launch.py — Launch the ESP32-S3 IO auxiliary bridge node.
Connects to ESP32-S3 IO board via USB-CDC @ 460800 baud (inter-board protocol).
Handles RC monitoring, sensor data, LED/output commands.
Primary drive path uses CAN (can_bridge_node / saltybot_can_node), not this node.
Spec: docs/SAUL-TEE-SYSTEM-REFERENCE.md §5
Usage:
ros2 launch saltybot_bridge stm32_cmd.launch.py
ros2 launch saltybot_bridge stm32_cmd.launch.py serial_port:=/dev/ttyACM0
=======
"""esp32_cmd.launch.py — Launch the binary-framed ESP32-S3 command node (Issue #119). """esp32_cmd.launch.py — Launch the binary-framed ESP32-S3 command node (Issue #119).
Usage: Usage:
@ -21,9 +8,7 @@ Usage:
ros2 launch saltybot_bridge esp32_cmd.launch.py serial_port:=/dev/ttyACM1 ros2 launch saltybot_bridge esp32_cmd.launch.py serial_port:=/dev/ttyACM1
# Custom velocity scales: # Custom velocity scales:
ros2 launch saltybot_bridge esp32_cmd.launch.py speed_scale:=800.0 steer_scale:=-400.0 ros2 launch saltybot_bridge esp32_cmd.launch.py speed_scale:=800.0 steer_scale:=-400.0"""
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/launch/esp32_cmd.launch.py
"""
import os import os
from ament_index_python.packages import get_package_share_directory from ament_index_python.packages import get_package_share_directory

View File

@ -2,12 +2,7 @@
uart_bridge.launch.py FCOrin UART bridge (Issue #362) uart_bridge.launch.py FCOrin UART bridge (Issue #362)
Launches serial_bridge_node configured for Jetson Orin UART port. Launches serial_bridge_node configured for Jetson Orin UART port.
<<<<<<< HEAD
Bridges Flight Controller (ESP32) telemetry from /dev/ttyTHS1 into ROS2.
=======
Bridges Flight Controller (ESP32-S3) telemetry from /dev/ttyTHS1 into ROS2. Bridges Flight Controller (ESP32-S3) telemetry from /dev/ttyTHS1 into ROS2.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Published topics (same as USB CDC bridge): Published topics (same as USB CDC bridge):
/saltybot/imu sensor_msgs/Imu pitch/roll/yaw as angular velocity /saltybot/imu sensor_msgs/Imu pitch/roll/yaw as angular velocity
/saltybot/balance_state std_msgs/String (JSON) full PID diagnostics /saltybot/balance_state std_msgs/String (JSON) full PID diagnostics
@ -24,12 +19,7 @@ Usage:
Prerequisites: Prerequisites:
- Flight Controller connected to /dev/ttyTHS1 @ 921600 baud - Flight Controller connected to /dev/ttyTHS1 @ 921600 baud
<<<<<<< HEAD - ESP32-S3 firmware transmitting JSON telemetry frames (50 Hz) - ROS2 environment sourced (source install/setup.bash)
- ESP32 BALANCE firmware transmitting JSON telemetry frames (50 Hz)
=======
- ESP32-S3 firmware transmitting JSON telemetry frames (50 Hz)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
- ROS2 environment sourced (source install/setup.bash)
Note: Note:
/dev/ttyTHS1 is the native UART1 on Jetson Orin. Verify connectivity: /dev/ttyTHS1 is the native UART1 on Jetson Orin. Verify connectivity:

View File

@ -14,12 +14,7 @@ Alert levels (SoC thresholds):
5% EMERGENCY publish zero /cmd_vel, disarm, log + alert 5% EMERGENCY publish zero /cmd_vel, disarm, log + alert
SoC source priority: SoC source priority:
<<<<<<< HEAD 1. soc_pct field from ESP32-S3 BATTERY telemetry (fuel gauge or lookup on ESP32-S3) 2. Voltage-based lookup table (3S LiPo curve) if soc_pct == 0 and voltage known
1. soc_pct field from ESP32 BATTERY telemetry (fuel gauge or lookup on ESP32 BALANCE)
=======
1. soc_pct field from ESP32-S3 BATTERY telemetry (fuel gauge or lookup on ESP32-S3)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
2. Voltage-based lookup table (3S LiPo curve) if soc_pct == 0 and voltage known
Parameters (config/battery_params.yaml): Parameters (config/battery_params.yaml):
db_path /var/log/saltybot/battery.db db_path /var/log/saltybot/battery.db
@ -324,12 +319,7 @@ class BatteryNode(Node):
self._speed_limit_pub.publish(msg) self._speed_limit_pub.publish(msg)
def _execute_safe_stop(self) -> None: def _execute_safe_stop(self) -> None:
<<<<<<< HEAD """Send zero /cmd_vel and disarm the ESP32-S3.""" self.get_logger().fatal("EMERGENCY: publishing zero /cmd_vel and disarming")
"""Send zero /cmd_vel and disarm the ESP32 BALANCE."""
=======
"""Send zero /cmd_vel and disarm the ESP32-S3."""
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
self.get_logger().fatal("EMERGENCY: publishing zero /cmd_vel and disarming")
# Publish zero velocity # Publish zero velocity
zero_twist = Twist() zero_twist = Twist()
self._cmd_vel_pub.publish(zero_twist) self._cmd_vel_pub.publish(zero_twist)

View File

@ -1,10 +1,5 @@
""" """
<<<<<<< HEAD
cmd_vel_bridge_node Nav2 /cmd_vel ESP32 BALANCE drive command bridge.
=======
cmd_vel_bridge_node Nav2 /cmd_vel ESP32-S3 drive command bridge. cmd_vel_bridge_node Nav2 /cmd_vel ESP32-S3 drive command bridge.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Extends the basic saltybot_cmd_node with four additions required for safe Extends the basic saltybot_cmd_node with four additions required for safe
autonomous operation on a self-balancing robot: autonomous operation on a self-balancing robot:
@ -16,28 +11,16 @@ autonomous operation on a self-balancing robot:
3. Deadman switch if /cmd_vel is silent for cmd_vel_timeout seconds, 3. Deadman switch if /cmd_vel is silent for cmd_vel_timeout seconds,
zero targets immediately (Nav2 node crash / planner zero targets immediately (Nav2 node crash / planner
stall robot coasts to stop rather than running away). stall robot coasts to stop rather than running away).
<<<<<<< HEAD 4. Mode gate only issue non-zero drive commands when ESP32-S3 reports md=2 (AUTONOMOUS). In any other mode (RC_MANUAL,
4. Mode gate only issue non-zero drive commands when ESP32 BALANCE reports
=======
4. Mode gate only issue non-zero drive commands when ESP32-S3 reports
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
md=2 (AUTONOMOUS). In any other mode (RC_MANUAL,
RC_ASSISTED) Jetson cannot override the RC pilot. RC_ASSISTED) Jetson cannot override the RC pilot.
On mode re-entry current ramp state resets to 0 so On mode re-entry current ramp state resets to 0 so
acceleration is always smooth from rest. acceleration is always smooth from rest.
Serial protocol (C<speed>,<steer>\\n / H\\n same as saltybot_cmd_node): Serial protocol (C<speed>,<steer>\\n / H\\n same as saltybot_cmd_node):
C<spd>,<str>\\n drive command. speed/steer: -1000..+1000 integers. C<spd>,<str>\\n drive command. speed/steer: -1000..+1000 integers.
<<<<<<< HEAD
H\\n heartbeat. ESP32 BALANCE reverts steer to 0 after 500ms silence.
Telemetry (50 Hz from ESP32 BALANCE):
=======
H\\n heartbeat. ESP32-S3 reverts steer to 0 after 500ms silence. H\\n heartbeat. ESP32-S3 reverts steer to 0 after 500ms silence.
Telemetry (50 Hz from ESP32-S3): Telemetry (50 Hz from ESP32-S3): Same RX/publish pipeline as saltybot_cmd_node.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Same RX/publish pipeline as saltybot_cmd_node.
The "md" field (0=MANUAL,1=ASSISTED,2=AUTO) is parsed for the mode gate. The "md" field (0=MANUAL,1=ASSISTED,2=AUTO) is parsed for the mode gate.
Topics published: Topics published:
@ -164,12 +147,7 @@ class CmdVelBridgeNode(Node):
self._open_serial() self._open_serial()
# ── Timers ──────────────────────────────────────────────────────────── # ── Timers ────────────────────────────────────────────────────────────
<<<<<<< HEAD # Telemetry read at 100 Hz (ESP32-S3 sends at 50 Hz) self._read_timer = self.create_timer(0.01, self._read_cb)
# Telemetry read at 100 Hz (ESP32 BALANCE sends at 50 Hz)
=======
# Telemetry read at 100 Hz (ESP32-S3 sends at 50 Hz)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
self._read_timer = self.create_timer(0.01, self._read_cb)
# Control loop at 50 Hz: ramp + deadman + mode gate + send # Control loop at 50 Hz: ramp + deadman + mode gate + send
self._control_timer = self.create_timer(1.0 / _CONTROL_HZ, self._control_cb) self._control_timer = self.create_timer(1.0 / _CONTROL_HZ, self._control_cb)
# Heartbeat TX # Heartbeat TX
@ -256,12 +234,7 @@ class CmdVelBridgeNode(Node):
speed = self._current_speed speed = self._current_speed
steer = self._current_steer steer = self._current_steer
<<<<<<< HEAD # Send to ESP32-S3 frame = f"C{speed},{steer}\n".encode("ascii")
# Send to ESP32 BALANCE
=======
# Send to ESP32-S3
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
frame = f"C{speed},{steer}\n".encode("ascii")
if not self._write(frame): if not self._write(frame):
self.get_logger().warn( self.get_logger().warn(
"Cannot send cmd — serial not open", "Cannot send cmd — serial not open",
@ -278,12 +251,7 @@ class CmdVelBridgeNode(Node):
# ── Heartbeat TX ────────────────────────────────────────────────────────── # ── Heartbeat TX ──────────────────────────────────────────────────────────
def _heartbeat_cb(self): def _heartbeat_cb(self):
<<<<<<< HEAD """H\\n keeps ESP32-S3 jetson_cmd heartbeat alive regardless of mode.""" self._write(b"H\n")
"""H\\n keeps ESP32 BALANCE jetson_cmd heartbeat alive regardless of mode."""
=======
"""H\\n keeps ESP32-S3 jetson_cmd heartbeat alive regardless of mode."""
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
self._write(b"H\n")
# ── Telemetry RX ────────────────────────────────────────────────────────── # ── Telemetry RX ──────────────────────────────────────────────────────────
@ -404,12 +372,7 @@ class CmdVelBridgeNode(Node):
diag.header.stamp = stamp diag.header.stamp = stamp
status = DiagnosticStatus() status = DiagnosticStatus()
status.name = "saltybot/balance_controller" status.name = "saltybot/balance_controller"
<<<<<<< HEAD status.hardware_id = "esp32s322" status.message = f"{state_label} [{mode_label}]"
status.hardware_id = "esp32"
=======
status.hardware_id = "esp32s322"
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
status.message = f"{state_label} [{mode_label}]"
status.level = ( status.level = (
DiagnosticStatus.OK if state == 1 else DiagnosticStatus.OK if state == 1 else
DiagnosticStatus.WARN if state == 0 else DiagnosticStatus.WARN if state == 0 else
@ -436,20 +399,11 @@ class CmdVelBridgeNode(Node):
status = DiagnosticStatus() status = DiagnosticStatus()
status.level = DiagnosticStatus.ERROR status.level = DiagnosticStatus.ERROR
status.name = "saltybot/balance_controller" status.name = "saltybot/balance_controller"
<<<<<<< HEAD
status.hardware_id = "esp32"
status.message = f"IMU fault errno={errno}"
diag.status.append(status)
self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32 BALANCE IMU fault: errno={errno}")
=======
status.hardware_id = "esp32s322" status.hardware_id = "esp32s322"
status.message = f"IMU fault errno={errno}" status.message = f"IMU fault errno={errno}"
diag.status.append(status) diag.status.append(status)
self._diag_pub.publish(diag) self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32-S3 IMU fault: errno={errno}") self.get_logger().error(f"ESP32-S3 IMU fault: errno={errno}")
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
# ── Lifecycle ───────────────────────────────────────────────────────────── # ── Lifecycle ─────────────────────────────────────────────────────────────
def destroy_node(self): def destroy_node(self):

View File

@ -1,31 +1,15 @@
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
"""stm32_cmd_node.py — Orin ↔ ESP32-S3 IO auxiliary bridge node.
=======
"""esp32_cmd_node.py — Full bidirectional binary-framed ESP32-S3↔Jetson bridge. """esp32_cmd_node.py — Full bidirectional binary-framed ESP32-S3↔Jetson bridge.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
Connects to the ESP32-S3 IO board via USB-CDC (/dev/esp32-io) using the Connects to the ESP32-S3 IO board via USB-CDC (/dev/esp32-io) using the
inter-board binary protocol (docs/SAUL-TEE-SYSTEM-REFERENCE.md §5). inter-board binary protocol (docs/SAUL-TEE-SYSTEM-REFERENCE.md §5).
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
This node is NOT the primary drive path (that is CAN via can_bridge_node).
It handles auxiliary I/O: RC monitoring, sensor data, LED/output control.
=======
TX commands (Jetson ESP32-S3): TX commands (Jetson ESP32-S3):
SPEED_STEER 50 Hz from /cmd_vel subscription SPEED_STEER 50 Hz from /cmd_vel subscription
HEARTBEAT 200 ms timer (ESP32-S3 watchdog fires at 500 ms) HEARTBEAT 200 ms timer (ESP32-S3 watchdog fires at 500 ms)
ARM via /saltybot/arm service ARM via /saltybot/arm service
SET_MODE via /saltybot/set_mode service SET_MODE via /saltybot/set_mode service
PID_UPDATE via /saltybot/pid_update topic PID_UPDATE via /saltybot/pid_update topic
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
Frame format: [0xAA][LEN][TYPE][PAYLOAD][CRC8] @ 460800 baud Frame format: [0xAA][LEN][TYPE][PAYLOAD][CRC8] @ 460800 baud
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
RX from ESP32 IO:
RC_CHANNELS (0x01) /saltybot/rc_channels (std_msgs/String JSON)
SENSORS (0x02) /saltybot/sensors (std_msgs/String JSON)
=======
RX telemetry (ESP32-S3 Jetson): RX telemetry (ESP32-S3 Jetson):
IMU /saltybot/imu (sensor_msgs/Imu) IMU /saltybot/imu (sensor_msgs/Imu)
BATTERY /saltybot/telemetry/battery (std_msgs/String JSON) BATTERY /saltybot/telemetry/battery (std_msgs/String JSON)
@ -33,20 +17,11 @@ RX telemetry (ESP32-S3 → Jetson):
ARM_STATE /saltybot/arm_state (std_msgs/String JSON) ARM_STATE /saltybot/arm_state (std_msgs/String JSON)
ERROR /saltybot/error (std_msgs/String JSON) ERROR /saltybot/error (std_msgs/String JSON)
All frames /diagnostics (diagnostic_msgs/DiagnosticArray) All frames /diagnostics (diagnostic_msgs/DiagnosticArray)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
TX to ESP32 IO: TX to ESP32 IO:
LED_CMD (0x10) /saltybot/leds (std_msgs/String JSON) LED_CMD (0x10) /saltybot/leds (std_msgs/String JSON)
OUTPUT_CMD (0x11) /saltybot/outputs (std_msgs/String JSON) OUTPUT_CMD (0x11) /saltybot/outputs (std_msgs/String JSON)
HEARTBEAT (0x20) sent every heartbeat_period (keep IO watchdog alive) HEARTBEAT (0x20) sent every heartbeat_period (keep IO watchdog alive)
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
Parameters (config/stm32_cmd_params.yaml):
serial_port /dev/esp32-io
baud_rate 460800
reconnect_delay 2.0
heartbeat_period 0.2 (ESP32 IO watchdog fires at ~500 ms)
=======
Parameters (config/esp32_cmd_params.yaml): Parameters (config/esp32_cmd_params.yaml):
serial_port /dev/ttyACM0 serial_port /dev/ttyACM0
baud_rate 921600 baud_rate 921600
@ -54,9 +29,7 @@ Parameters (config/esp32_cmd_params.yaml):
heartbeat_period 0.2 (seconds) heartbeat_period 0.2 (seconds)
watchdog_timeout 0.5 (seconds no /cmd_vel send zero-speed) watchdog_timeout 0.5 (seconds no /cmd_vel send zero-speed)
speed_scale 1000.0 (linear.x m/s ESC units) speed_scale 1000.0 (linear.x m/s ESC units)
steer_scale -500.0 (angular.z rad/s ESC units, neg to flip convention) steer_scale -500.0 (angular.z rad/s ESC units, neg to flip convention)"""
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
"""
from __future__ import annotations from __future__ import annotations
@ -73,13 +46,7 @@ import serial
from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue
from std_msgs.msg import String from std_msgs.msg import String
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py from .esp32_protocol import ( FrameParser,
from .stm32_protocol import (
BAUD_RATE,
=======
from .esp32_protocol import (
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
FrameParser,
RcChannels, RcChannels,
SensorData, SensorData,
encode_heartbeat, encode_heartbeat,
@ -88,13 +55,8 @@ from .esp32_protocol import (
) )
class Stm32CmdNode(Node): class Esp32CmdNode(Node):
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
"""Orin ↔ ESP32-S3 IO auxiliary bridge node."""
=======
"""Binary-framed Jetson↔ESP32-S3 bridge node.""" """Binary-framed Jetson↔ESP32-S3 bridge node."""
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("esp32_cmd_node") super().__init__("esp32_cmd_node")
@ -138,13 +100,8 @@ class Stm32CmdNode(Node):
self._diag_timer = self.create_timer(1.0, self._publish_diagnostics) self._diag_timer = self.create_timer(1.0, self._publish_diagnostics)
self.get_logger().info( self.get_logger().info(
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
f"stm32_cmd_node started — {self._port_name} @ {self._baud} baud"
=======
f"esp32_cmd_node started — {port} @ {baud} baud | " f"esp32_cmd_node started — {port} @ {baud} baud | "
f"HB {int(self._hb_period * 1000)}ms | WD {int(self._wd_timeout * 1000)}ms" f"HB {int(self._hb_period * 1000)}ms | WD {int(self._wd_timeout * 1000)}ms" )
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
)
# ── Serial management ───────────────────────────────────────────────── # ── Serial management ─────────────────────────────────────────────────
@ -245,9 +202,6 @@ class Stm32CmdNode(Node):
type_code, _ = msg type_code, _ = msg
self.get_logger().debug(f"Unknown inter-board type 0x{type_code:02X}") self.get_logger().debug(f"Unknown inter-board type 0x{type_code:02X}")
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
# ── TX ────────────────────────────────────────────────────────────────
=======
elif isinstance(frame, ArmStateFrame): elif isinstance(frame, ArmStateFrame):
self._publish_arm_state(frame, now) self._publish_arm_state(frame, now)
@ -358,8 +312,6 @@ class Stm32CmdNode(Node):
"SPEED_STEER dropped — serial not open", "SPEED_STEER dropped — serial not open",
throttle_duration_sec=2.0, throttle_duration_sec=2.0,
) )
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
def _heartbeat_cb(self) -> None: def _heartbeat_cb(self) -> None:
self._write(encode_heartbeat()) self._write(encode_heartbeat())
@ -399,14 +351,8 @@ class Stm32CmdNode(Node):
diag = DiagnosticArray() diag = DiagnosticArray()
diag.header.stamp = self.get_clock().now().to_msg() diag.header.stamp = self.get_clock().now().to_msg()
status = DiagnosticStatus() status = DiagnosticStatus()
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
status.name = "saltybot/esp32_io_bridge"
status.hardware_id = "esp32-s3-io"
=======
status.name = "saltybot/esp32_cmd_node" status.name = "saltybot/esp32_cmd_node"
status.hardware_id = "esp32s322" status.hardware_id = "esp32s322"
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
port_ok = self._ser is not None and self._ser.is_open port_ok = self._ser is not None and self._ser.is_open
status.level = DiagnosticStatus.OK if port_ok else DiagnosticStatus.ERROR status.level = DiagnosticStatus.OK if port_ok else DiagnosticStatus.ERROR
status.message = "Serial OK" if port_ok else f"Disconnected: {self._port_name}" status.message = "Serial OK" if port_ok else f"Disconnected: {self._port_name}"
@ -436,7 +382,7 @@ class Stm32CmdNode(Node):
def main(args=None) -> None: def main(args=None) -> None:
rclpy.init(args=args) rclpy.init(args=args)
node = Stm32CmdNode() node = Esp32CmdNode()
try: try:
rclpy.spin(node) rclpy.spin(node)
except KeyboardInterrupt: except KeyboardInterrupt:

View File

@ -1,16 +1,8 @@
""" """
<<<<<<< HEAD
remote_estop_node.py -- Remote e-stop bridge: MQTT -> ESP32 USB CDC
{"kill": true} -> writes 'E\n' to ESP32 BALANCE (ESTOP_REMOTE, immediate motor cutoff)
{"kill": false} -> writes 'Z\n' to ESP32 BALANCE (clear latch, robot can re-arm)
=======
remote_estop_node.py -- Remote e-stop bridge: MQTT -> ESP32-S3 USB CDC remote_estop_node.py -- Remote e-stop bridge: MQTT -> ESP32-S3 USB CDC
{"kill": true} -> writes 'E\n' to ESP32-S3 (ESTOP_REMOTE, immediate motor cutoff) {"kill": true} -> writes 'E\n' to ESP32-S3 (ESTOP_REMOTE, immediate motor cutoff)
{"kill": false} -> writes 'Z\n' to ESP32-S3 (clear latch, robot can re-arm) {"kill": false} -> writes 'Z\n' to ESP32-S3 (clear latch, robot can re-arm)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Cellular watchdog: if MQTT link drops for > cellular_timeout_s while in Cellular watchdog: if MQTT link drops for > cellular_timeout_s while in
AUTO mode, automatically sends 'F\n' (ESTOP_CELLULAR_TIMEOUT). AUTO mode, automatically sends 'F\n' (ESTOP_CELLULAR_TIMEOUT).

View File

@ -9,13 +9,13 @@ back to FC over CAN.
CAN interface: SocketCAN (CANable USB adapter on vcan0 / can0) CAN interface: SocketCAN (CANable USB adapter on vcan0 / can0)
FC Orin (telemetry): FC Orin (telemetry):
0x400 FC_STATUS int16 pitch_x10, int16 motor_cmd, uint16 vbat_mv, 0x400 BALANCE_STATUS int16 pitch_x10, int16 motor_cmd, uint16 vbat_mv,
uint8 balance_state, uint8 flags (10 Hz) uint8 balance_state, uint8 flags (10 Hz)
0x401 FC_VESC int16 left_rpm_x10, int16 right_rpm_x10, 0x401 BALANCE_VESC int16 left_rpm_x10, int16 right_rpm_x10,
int16 left_cur_x10, int16 right_cur_x10 (10 Hz) int16 left_cur_x10, int16 right_cur_x10 (10 Hz)
0x402 FC_IMU int16 pitch_x10, int16 roll_x10, int16 yaw_x10, 0x402 BALANCE_IMU int16 pitch_x10, int16 roll_x10, int16 yaw_x10,
uint8 cal_status, uint8 balance_state (50 Hz) uint8 cal_status, uint8 balance_state (50 Hz)
0x403 FC_BARO int32 pressure_pa, int16 temp_x10, int16 alt_cm (1 Hz) 0x403 BALANCE_BARO int32 pressure_pa, int16 temp_x10, int16 alt_cm (1 Hz)
Orin FC (commands): Orin FC (commands):
0x300 HEARTBEAT uint32 sequence counter (5 Hz) 0x300 HEARTBEAT uint32 sequence counter (5 Hz)
@ -57,10 +57,10 @@ from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue
# ---- CAN frame IDs ------------------------------------------------ # ---- CAN frame IDs ------------------------------------------------
CAN_FC_STATUS = 0x400 CAN_BALANCE_STATUS = 0x400
CAN_FC_VESC = 0x401 CAN_BALANCE_VESC = 0x401
CAN_FC_IMU = 0x402 CAN_BALANCE_IMU = 0x402
CAN_FC_BARO = 0x403 CAN_BALANCE_BARO = 0x403
CAN_HEARTBEAT = 0x300 CAN_HEARTBEAT = 0x300
CAN_DRIVE = 0x301 CAN_DRIVE = 0x301
@ -216,11 +216,11 @@ class SaltybotCanNode(Node):
def _dispatch(self, can_id: int, data: bytes): def _dispatch(self, can_id: int, data: bytes):
now = self.get_clock().now().to_msg() now = self.get_clock().now().to_msg()
if can_id == CAN_FC_IMU and len(data) >= 8: if can_id == CAN_BALANCE_IMU and len(data) >= 8:
self._handle_fc_imu(data, now) self._handle_fc_imu(data, now)
elif can_id == CAN_FC_STATUS and len(data) >= 8: elif can_id == CAN_BALANCE_STATUS and len(data) >= 8:
self._handle_fc_status(data) self._handle_fc_status(data)
elif can_id == CAN_FC_BARO and len(data) >= 8: elif can_id == CAN_BALANCE_BARO and len(data) >= 8:
self._handle_fc_baro(data, now) self._handle_fc_baro(data, now)
# ── Frame handlers ─────────────────────────────────────────────── # ── Frame handlers ───────────────────────────────────────────────
@ -322,12 +322,7 @@ class SaltybotCanNode(Node):
diag.header.stamp = stamp diag.header.stamp = stamp
st = DiagnosticStatus() st = DiagnosticStatus()
st.name = "saltybot/balance_controller" st.name = "saltybot/balance_controller"
<<<<<<< HEAD st.hardware_id = "esp32s322" st.message = state_label
st.hardware_id = "esp32"
=======
st.hardware_id = "esp32s322"
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
st.message = state_label
st.level = (DiagnosticStatus.OK if state == 1 else st.level = (DiagnosticStatus.OK if state == 1 else
DiagnosticStatus.WARN if state == 0 else DiagnosticStatus.WARN if state == 0 else
DiagnosticStatus.ERROR) DiagnosticStatus.ERROR)

View File

@ -1,26 +1,8 @@
""" """
<<<<<<< HEAD saltybot_cmd_node full bidirectional ESP32-S3Jetson bridgeCombines telemetry RX (from serial_bridge_node) with drive command TX.
saltybot_cmd_node full bidirectional ESP32 BALANCEJetson bridge
=======
saltybot_cmd_node full bidirectional ESP32-S3Jetson bridge
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Combines telemetry RX (from serial_bridge_node) with drive command TX.
Owns /dev/ttyACM0 exclusively do NOT run alongside serial_bridge_node. Owns /dev/ttyACM0 exclusively do NOT run alongside serial_bridge_node.
<<<<<<< HEAD
RX path (50Hz from ESP32 BALANCE):
JSON telemetry /saltybot/imu, /saltybot/balance_state, /diagnostics
TX path:
/cmd_vel (geometry_msgs/Twist) C<speed>,<steer>\\n ESP32 BALANCE
Heartbeat timer (200ms) H\\n ESP32 BALANCE
Protocol:
H\\n heartbeat. ESP32 BALANCE reverts steer to 0 if gap > 500ms.
C<spd>,<str>\\n drive command. speed/steer: -1000..+1000 integers.
C command also refreshes ESP32 BALANCE heartbeat timer.
=======
RX path (50Hz from ESP32-S3): RX path (50Hz from ESP32-S3):
JSON telemetry /saltybot/imu, /saltybot/balance_state, /diagnostics JSON telemetry /saltybot/imu, /saltybot/balance_state, /diagnostics
@ -32,8 +14,6 @@ Protocol:
H\\n heartbeat. ESP32-S3 reverts steer to 0 if gap > 500ms. H\\n heartbeat. ESP32-S3 reverts steer to 0 if gap > 500ms.
C<spd>,<str>\\n drive command. speed/steer: -1000..+1000 integers. C<spd>,<str>\\n drive command. speed/steer: -1000..+1000 integers.
C command also refreshes ESP32-S3 heartbeat timer. C command also refreshes ESP32-S3 heartbeat timer.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Twist mapping (configurable via ROS2 params): Twist mapping (configurable via ROS2 params):
speed = clamp(linear.x * speed_scale, -1000, 1000) speed = clamp(linear.x * speed_scale, -1000, 1000)
steer = clamp(angular.z * steer_scale, -1000, 1000) steer = clamp(angular.z * steer_scale, -1000, 1000)
@ -118,12 +98,7 @@ class SaltybotCmdNode(Node):
self._open_serial() self._open_serial()
# ── Timers ──────────────────────────────────────────────────────────── # ── Timers ────────────────────────────────────────────────────────────
<<<<<<< HEAD # Telemetry read at 100Hz (ESP32-S3 sends at 50Hz) self._read_timer = self.create_timer(0.01, self._read_cb)
# Telemetry read at 100Hz (ESP32 BALANCE sends at 50Hz)
=======
# Telemetry read at 100Hz (ESP32-S3 sends at 50Hz)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
self._read_timer = self.create_timer(0.01, self._read_cb)
# Heartbeat TX at configured period (default 200ms) # Heartbeat TX at configured period (default 200ms)
self._hb_timer = self.create_timer(self._hb_period, self._heartbeat_cb) self._hb_timer = self.create_timer(self._hb_period, self._heartbeat_cb)
@ -288,12 +263,7 @@ class SaltybotCmdNode(Node):
diag.header.stamp = stamp diag.header.stamp = stamp
status = DiagnosticStatus() status = DiagnosticStatus()
status.name = "saltybot/balance_controller" status.name = "saltybot/balance_controller"
<<<<<<< HEAD status.hardware_id = "esp32s322" status.message = state_label
status.hardware_id = "esp32"
=======
status.hardware_id = "esp32s322"
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
status.message = state_label
if state == 1: if state == 1:
status.level = DiagnosticStatus.OK status.level = DiagnosticStatus.OK
elif state == 0: elif state == 0:
@ -320,20 +290,11 @@ class SaltybotCmdNode(Node):
status = DiagnosticStatus() status = DiagnosticStatus()
status.level = DiagnosticStatus.ERROR status.level = DiagnosticStatus.ERROR
status.name = "saltybot/balance_controller" status.name = "saltybot/balance_controller"
<<<<<<< HEAD
status.hardware_id = "esp32"
status.message = f"IMU fault errno={errno}"
diag.status.append(status)
self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32 BALANCE IMU fault: errno={errno}")
=======
status.hardware_id = "esp32s322" status.hardware_id = "esp32s322"
status.message = f"IMU fault errno={errno}" status.message = f"IMU fault errno={errno}"
diag.status.append(status) diag.status.append(status)
self._diag_pub.publish(diag) self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32-S3 IMU fault: errno={errno}") self.get_logger().error(f"ESP32-S3 IMU fault: errno={errno}")
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
# ── TX — command send ───────────────────────────────────────────────────── # ── TX — command send ─────────────────────────────────────────────────────
def _cmd_vel_cb(self, msg: Twist): def _cmd_vel_cb(self, msg: Twist):
@ -350,12 +311,7 @@ class SaltybotCmdNode(Node):
) )
def _heartbeat_cb(self): def _heartbeat_cb(self):
<<<<<<< HEAD """Send H\\n heartbeat. ESP32-S3 reverts steer to 0 if gap > 500ms.""" self._write(b"H\n")
"""Send H\\n heartbeat. ESP32 BALANCE reverts steer to 0 if gap > 500ms."""
=======
"""Send H\\n heartbeat. ESP32-S3 reverts steer to 0 if gap > 500ms."""
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
self._write(b"H\n")
# ── Lifecycle ───────────────────────────────────────────────────────────── # ── Lifecycle ─────────────────────────────────────────────────────────────

View File

@ -1,11 +1,6 @@
""" """
saltybot_bridge serial_bridge_node saltybot_bridge serial_bridge_node
<<<<<<< HEAD
ESP32 USB CDC ROS2 topic publisher
=======
ESP32-S3 USB CDC ROS2 topic publisher ESP32-S3 USB CDC ROS2 topic publisher
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Telemetry frame (50 Hz, newline-delimited JSON): Telemetry frame (50 Hz, newline-delimited JSON):
{"p":<pitch×10>,"r":<roll×10>,"e":<err×10>,"ig":<integral×10>, {"p":<pitch×10>,"r":<roll×10>,"e":<err×10>,"ig":<integral×10>,
"m":<motor_cmd -1000..1000>,"s":<state 0-2>,"y":<yaw×10>} "m":<motor_cmd -1000..1000>,"s":<state 0-2>,"y":<yaw×10>}
@ -33,12 +28,7 @@ from sensor_msgs.msg import Imu
from std_msgs.msg import String from std_msgs.msg import String
from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue
<<<<<<< HEAD # Balance state labels matching ESP32-S3 balance_state_t enum_STATE_LABEL = {0: "DISARMED", 1: "ARMED", 2: "TILT_FAULT"}
# Balance state labels matching ESP32 BALANCE balance_state_t enum
=======
# Balance state labels matching ESP32-S3 balance_state_t enum
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
_STATE_LABEL = {0: "DISARMED", 1: "ARMED", 2: "TILT_FAULT"}
# Sensor frame_id published in Imu header # Sensor frame_id published in Imu header
IMU_FRAME_ID = "imu_link" IMU_FRAME_ID = "imu_link"
@ -91,12 +81,7 @@ class SerialBridgeNode(Node):
# ── Open serial and start read timer ────────────────────────────────── # ── Open serial and start read timer ──────────────────────────────────
self._open_serial() self._open_serial()
<<<<<<< HEAD # Poll at 100 Hz — ESP32-S3 sends at 50 Hz, so we never miss a frame self._timer = self.create_timer(0.01, self._read_cb)
# Poll at 100 Hz — ESP32 BALANCE sends at 50 Hz, so we never miss a frame
=======
# Poll at 100 Hz — ESP32-S3 sends at 50 Hz, so we never miss a frame
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
self._timer = self.create_timer(0.01, self._read_cb)
self.get_logger().info( self.get_logger().info(
f"stm32_serial_bridge started — {port} @ {baud} baud" f"stm32_serial_bridge started — {port} @ {baud} baud"
@ -129,12 +114,7 @@ class SerialBridgeNode(Node):
def write_serial(self, data: bytes) -> bool: def write_serial(self, data: bytes) -> bool:
""" """
<<<<<<< HEAD Send raw bytes to ESP32-S3 over the open serial port. Returns False if port is not open (caller should handle gracefully).
Send raw bytes to ESP32 BALANCE over the open serial port.
=======
Send raw bytes to ESP32-S3 over the open serial port.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Returns False if port is not open (caller should handle gracefully).
Note: for bidirectional use prefer saltybot_cmd_node which owns TX natively. Note: for bidirectional use prefer saltybot_cmd_node which owns TX natively.
""" """
if self._ser is None or not self._ser.is_open: if self._ser is None or not self._ser.is_open:
@ -222,12 +202,7 @@ class SerialBridgeNode(Node):
""" """
Publish sensor_msgs/Imu. Publish sensor_msgs/Imu.
<<<<<<< HEAD The ESP32-S3 IMU gives Euler angles (pitch/roll from accelerometer+gyro fusion, yaw from gyro integration). We publish them as angular_velocity
The ESP32 BALANCE IMU gives Euler angles (pitch/roll from accelerometer+gyro
=======
The ESP32-S3 IMU gives Euler angles (pitch/roll from accelerometer+gyro
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
fusion, yaw from gyro integration). We publish them as angular_velocity
for immediate use by slam_toolbox / robot_localization. for immediate use by slam_toolbox / robot_localization.
Note: orientation quaternion is left zeroed (covariance [-1,...]) until Note: orientation quaternion is left zeroed (covariance [-1,...]) until
@ -284,12 +259,7 @@ class SerialBridgeNode(Node):
diag.header.stamp = stamp diag.header.stamp = stamp
status = DiagnosticStatus() status = DiagnosticStatus()
status.name = "saltybot/balance_controller" status.name = "saltybot/balance_controller"
<<<<<<< HEAD status.hardware_id = "esp32s322" status.message = state_label
status.hardware_id = "esp32"
=======
status.hardware_id = "esp32s322"
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
status.message = state_label
if state == 1: # ARMED if state == 1: # ARMED
status.level = DiagnosticStatus.OK status.level = DiagnosticStatus.OK
@ -317,20 +287,11 @@ class SerialBridgeNode(Node):
status = DiagnosticStatus() status = DiagnosticStatus()
status.level = DiagnosticStatus.ERROR status.level = DiagnosticStatus.ERROR
status.name = "saltybot/balance_controller" status.name = "saltybot/balance_controller"
<<<<<<< HEAD
status.hardware_id = "esp32"
status.message = f"IMU fault errno={errno}"
diag.status.append(status)
self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32 BALANCE reported IMU fault: errno={errno}")
=======
status.hardware_id = "esp32s322" status.hardware_id = "esp32s322"
status.message = f"IMU fault errno={errno}" status.message = f"IMU fault errno={errno}"
diag.status.append(status) diag.status.append(status)
self._diag_pub.publish(diag) self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32-S3 reported IMU fault: errno={errno}") self.get_logger().error(f"ESP32-S3 reported IMU fault: errno={errno}")
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
def destroy_node(self): def destroy_node(self):
self._close_serial() self._close_serial()
super().destroy_node() super().destroy_node()

View File

@ -29,12 +29,7 @@ setup(
zip_safe=True, zip_safe=True,
maintainer="sl-jetson", maintainer="sl-jetson",
maintainer_email="sl-jetson@saltylab.local", maintainer_email="sl-jetson@saltylab.local",
<<<<<<< HEAD description="ESP32-S3 USB CDC → ROS2 serial bridge for saltybot", license="MIT",
description="ESP32 USB CDC → ROS2 serial bridge for saltybot",
=======
description="ESP32-S3 USB CDC → ROS2 serial bridge for saltybot",
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
license="MIT",
tests_require=["pytest"], tests_require=["pytest"],
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [
@ -45,14 +40,8 @@ setup(
# Nav2 cmd_vel bridge: velocity limits + ramp + deadman + mode gate # Nav2 cmd_vel bridge: velocity limits + ramp + deadman + mode gate
"cmd_vel_bridge_node = saltybot_bridge.cmd_vel_bridge_node:main", "cmd_vel_bridge_node = saltybot_bridge.cmd_vel_bridge_node:main",
"remote_estop_node = saltybot_bridge.remote_estop_node:main", "remote_estop_node = saltybot_bridge.remote_estop_node:main",
<<<<<<< HEAD
# Binary-framed ESP32 BALANCE command node (Issue #119)
"stm32_cmd_node = saltybot_bridge.stm32_cmd_node:main",
=======
# Binary-framed ESP32-S3 command node (Issue #119) # Binary-framed ESP32-S3 command node (Issue #119)
"esp32_cmd_node = saltybot_bridge.esp32_cmd_node:main", "esp32_cmd_node = saltybot_bridge.esp32_cmd_node:main", # Battery management node (Issue #125)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
# Battery management node (Issue #125)
"battery_node = saltybot_bridge.battery_node:main", "battery_node = saltybot_bridge.battery_node:main",
# Production CAN bridge: FC telemetry RX + /cmd_vel TX over CAN (Issues #680, #672, #685) # Production CAN bridge: FC telemetry RX + /cmd_vel TX over CAN (Issues #680, #672, #685)
"saltybot_can_node = saltybot_bridge.saltybot_can_node:main", "saltybot_can_node = saltybot_bridge.saltybot_can_node:main",

View File

@ -1,10 +1,5 @@
""" """
<<<<<<< HEAD Unit tests for JetsonESP32-S3 command serialization logic.Tests Twistspeed/steer conversion and frame formatting.
Unit tests for JetsonESP32 BALANCE command serialization logic.
=======
Unit tests for JetsonESP32-S3 command serialization logic.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Tests Twistspeed/steer conversion and frame formatting.
Run with: pytest jetson/ros2_ws/src/saltybot_bridge/test/test_cmd.py Run with: pytest jetson/ros2_ws/src/saltybot_bridge/test/test_cmd.py
""" """

View File

@ -1,4 +1,4 @@
"""test_esp32_cmd_node.py — Unit tests for Stm32CmdNode with mock serial port. """test_esp32_cmd_node.py — Unit tests for Esp32CmdNode with mock serial port.
Tests: Tests:
- Serial open/close lifecycle - Serial open/close lifecycle

View File

@ -1,10 +1,5 @@
""" """
<<<<<<< HEAD Unit tests for ESP32-S3 telemetry parsing logic.Run with: pytest jetson/ros2_ws/src/saltybot_bridge/test/test_parse.py
Unit tests for ESP32 BALANCE telemetry parsing logic.
=======
Unit tests for ESP32-S3 telemetry parsing logic.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Run with: pytest jetson/ros2_ws/src/saltybot_bridge/test/test_parse.py
""" """
import json import json

View File

@ -19,12 +19,7 @@
# inflation_radius: 0.3m (robot_radius 0.15m + 0.15m padding) # inflation_radius: 0.3m (robot_radius 0.15m + 0.15m padding)
# DepthCostmapLayer in-layer inflation: 0.10m (pre-inflation before inflation_layer) # DepthCostmapLayer in-layer inflation: 0.10m (pre-inflation before inflation_layer)
# #
<<<<<<< HEAD
# Output: /cmd_vel (Twist) — ESP32 bridge consumes this topic.
=======
# Output: /cmd_vel (Twist) — ESP32-S3 bridge consumes this topic. # Output: /cmd_vel (Twist) — ESP32-S3 bridge consumes this topic.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
bt_navigator: bt_navigator:
ros__parameters: ros__parameters:
use_sim_time: false use_sim_time: false

View File

@ -2,12 +2,7 @@
# Master configuration for full stack bringup # Master configuration for full stack bringup
# ──────────────────────────────────────────────────────────────────────────── # ────────────────────────────────────────────────────────────────────────────
<<<<<<< HEAD # HARDWARE — ESP32-S3 Bridge & Motor Control# ────────────────────────────────────────────────────────────────────────────
# HARDWARE — ESP32 BALANCE Bridge & Motor Control
=======
# HARDWARE — ESP32-S3 Bridge & Motor Control
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# ────────────────────────────────────────────────────────────────────────────
saltybot_bridge_node: saltybot_bridge_node:
ros__parameters: ros__parameters:

View File

@ -39,12 +39,7 @@ Modes
UWB driver (2-anchor DW3000, publishes /uwb/target) UWB driver (2-anchor DW3000, publishes /uwb/target)
YOLOv8n person detection (TensorRT) YOLOv8n person detection (TensorRT)
Person follower with UWB+camera fusion Person follower with UWB+camera fusion
<<<<<<< HEAD cmd_vel bridge ESP32-S3 (deadman + ramp + AUTONOMOUS gate) rosbridge WebSocket (port 9090)
cmd_vel bridge ESP32 BALANCE (deadman + ramp + AUTONOMOUS gate)
=======
cmd_vel bridge ESP32-S3 (deadman + ramp + AUTONOMOUS gate)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
rosbridge WebSocket (port 9090)
outdoor outdoor
RPLIDAR + RealSense D435i sensors (no SLAM) RPLIDAR + RealSense D435i sensors (no SLAM)
@ -61,14 +56,8 @@ Modes
Launch sequence (wall-clock delays conservative for cold start) Launch sequence (wall-clock delays conservative for cold start)
t= 0s robot_description (URDF + TF tree) t= 0s robot_description (URDF + TF tree)
<<<<<<< HEAD
t= 0s ESP32 bridge (serial port owner must be first)
t= 2s cmd_vel bridge (consumes /cmd_vel, needs ESP32 bridge up)
=======
t= 0s ESP32-S3 bridge (serial port owner must be first) t= 0s ESP32-S3 bridge (serial port owner must be first)
t= 2s cmd_vel bridge (consumes /cmd_vel, needs ESP32-S3 bridge up) t= 2s cmd_vel bridge (consumes /cmd_vel, needs ESP32-S3 bridge up) t= 2s sensors (RPLIDAR + RealSense)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
t= 2s sensors (RPLIDAR + RealSense)
t= 4s UWB driver (independent serial device) t= 4s UWB driver (independent serial device)
t= 4s CSI cameras (optional, independent) t= 4s CSI cameras (optional, independent)
t= 5s audio_pipeline (Jabra SPEAK 810: wake word + STT + TTS; Issue #503) t= 5s audio_pipeline (Jabra SPEAK 810: wake word + STT + TTS; Issue #503)
@ -80,18 +69,10 @@ Launch sequence (wall-clock delays — conservative for cold start)
Safety wiring Safety wiring
<<<<<<< HEAD
ESP32 bridge must be up before cmd_vel bridge sends any command.
cmd_vel bridge has 500ms deadman: stops robot if /cmd_vel goes silent.
ESP32 BALANCE AUTONOMOUS mode gate (md=2) in cmd_vel bridge robot stays still
until ESP32 BALANCE firmware is in AUTONOMOUS mode regardless of /cmd_vel.
=======
ESP32-S3 bridge must be up before cmd_vel bridge sends any command. ESP32-S3 bridge must be up before cmd_vel bridge sends any command.
cmd_vel bridge has 500ms deadman: stops robot if /cmd_vel goes silent. cmd_vel bridge has 500ms deadman: stops robot if /cmd_vel goes silent.
ESP32-S3 AUTONOMOUS mode gate (md=2) in cmd_vel bridge robot stays still ESP32-S3 AUTONOMOUS mode gate (md=2) in cmd_vel bridge robot stays still
until ESP32-S3 firmware is in AUTONOMOUS mode regardless of /cmd_vel. until ESP32-S3 firmware is in AUTONOMOUS mode regardless of /cmd_vel. follow_enabled:=false disables person follower without stopping the node.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
follow_enabled:=false disables person follower without stopping the node.
To e-stop at runtime: ros2 topic pub /saltybot/estop std_msgs/Bool '{data: true}' To e-stop at runtime: ros2 topic pub /saltybot/estop std_msgs/Bool '{data: true}'
Topics published by this stack Topics published by this stack
@ -107,12 +88,7 @@ Topics published by this stack
/person/target PoseStamped (camera position, base_link) /person/target PoseStamped (camera position, base_link)
/person/detections Detection2DArray /person/detections Detection2DArray
/cmd_vel Twist (from follower or Nav2) /cmd_vel Twist (from follower or Nav2)
<<<<<<< HEAD /saltybot/cmd String (to ESP32-S3) /saltybot/imu Imu
/saltybot/cmd String (to ESP32 BALANCE)
=======
/saltybot/cmd String (to ESP32-S3)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
/saltybot/imu Imu
/saltybot/balance_state String /saltybot/balance_state String
""" """
@ -229,12 +205,7 @@ def generate_launch_description():
enable_bridge_arg = DeclareLaunchArgument( enable_bridge_arg = DeclareLaunchArgument(
"enable_bridge", "enable_bridge",
default_value="true", default_value="true",
<<<<<<< HEAD description="Launch ESP32-S3 serial bridge + cmd_vel bridge (disable for sim/rosbag)", )
description="Launch ESP32 serial bridge + cmd_vel bridge (disable for sim/rosbag)",
=======
description="Launch ESP32-S3 serial bridge + cmd_vel bridge (disable for sim/rosbag)",
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
)
enable_rosbridge_arg = DeclareLaunchArgument( enable_rosbridge_arg = DeclareLaunchArgument(
"enable_rosbridge", "enable_rosbridge",
@ -294,12 +265,7 @@ enable_mission_logging_arg = DeclareLaunchArgument(
esp32_port_arg = DeclareLaunchArgument( esp32_port_arg = DeclareLaunchArgument(
"esp32_port", "esp32_port",
default_value="/dev/esp32-bridge", default_value="/dev/esp32-bridge",
<<<<<<< HEAD description="ESP32-S3 USB CDC serial port", )
description="ESP32 USB CDC serial port",
=======
description="ESP32-S3 USB CDC serial port",
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
)
# ── Shared substitution handles ─────────────────────────────────────────── # ── Shared substitution handles ───────────────────────────────────────────
# Profile argument for parameter override (Issue #506) # Profile argument for parameter override (Issue #506)
@ -318,12 +284,7 @@ enable_mission_logging_arg = DeclareLaunchArgument(
launch_arguments={"use_sim_time": use_sim_time}.items(), launch_arguments={"use_sim_time": use_sim_time}.items(),
) )
<<<<<<< HEAD # ── t=0s ESP32-S3 bidirectional serial bridge ──────────────────────────────── esp32_bridge = GroupAction(
# ── t=0s ESP32 bidirectional serial bridge ────────────────────────────────
=======
# ── t=0s ESP32-S3 bidirectional serial bridge ────────────────────────────────
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
esp32_bridge = GroupAction(
condition=IfCondition(LaunchConfiguration("enable_bridge")), condition=IfCondition(LaunchConfiguration("enable_bridge")),
actions=[ actions=[
IncludeLaunchDescription( IncludeLaunchDescription(
@ -352,12 +313,7 @@ enable_mission_logging_arg = DeclareLaunchArgument(
], ],
) )
<<<<<<< HEAD # ── t=2s cmd_vel safety bridge (depends on ESP32-S3 bridge) ──────────────── cmd_vel_bridge = TimerAction(
# ── t=2s cmd_vel safety bridge (depends on ESP32 bridge) ────────────────
=======
# ── t=2s cmd_vel safety bridge (depends on ESP32-S3 bridge) ────────────────
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
cmd_vel_bridge = TimerAction(
period=2.0, period=2.0,
actions=[ actions=[
GroupAction( GroupAction(

View File

@ -19,12 +19,7 @@ Usage
Startup sequence Startup sequence
<<<<<<< HEAD GROUP A Drivers t= 0 s ESP32-S3 bridge, RealSense+RPLIDAR, motor daemon, IMU health gate t= 8 s (full/debug)
GROUP A Drivers t= 0 s ESP32 bridge, RealSense+RPLIDAR, motor daemon, IMU
=======
GROUP A Drivers t= 0 s ESP32-S3 bridge, RealSense+RPLIDAR, motor daemon, IMU
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
health gate t= 8 s (full/debug)
GROUP B Perception t= 8 s UWB, person detection, object detection, depth costmap, gimbal GROUP B Perception t= 8 s UWB, person detection, object detection, depth costmap, gimbal
health gate t=16 s (full/debug) health gate t=16 s (full/debug)
GROUP C Navigation t=16 s SLAM, Nav2, lidar avoidance, follower, docking GROUP C Navigation t=16 s SLAM, Nav2, lidar avoidance, follower, docking
@ -127,12 +122,7 @@ def generate_launch_description() -> LaunchDescription: # noqa: C901
esp32_port_arg = DeclareLaunchArgument( esp32_port_arg = DeclareLaunchArgument(
"esp32_port", "esp32_port",
default_value="/dev/esp32-bridge", default_value="/dev/esp32-bridge",
<<<<<<< HEAD description="ESP32-S3 USART bridge serial device", )
description="ESP32 UART bridge serial device",
=======
description="ESP32-S3 USART bridge serial device",
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
)
uwb_port_a_arg = DeclareLaunchArgument( uwb_port_a_arg = DeclareLaunchArgument(
"uwb_port_a", "uwb_port_a",
@ -206,12 +196,7 @@ def generate_launch_description() -> LaunchDescription: # noqa: C901
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# GROUP A — DRIVERS (t = 0 s, all profiles) # GROUP A — DRIVERS (t = 0 s, all profiles)
<<<<<<< HEAD # Dependency order: ESP32-S3 bridge first, then sensors, then motor daemon. # Health gate: subsequent groups delayed until t_perception (8 s full/debug).
# Dependency order: ESP32 bridge first, then sensors, then motor daemon.
=======
# Dependency order: ESP32-S3 bridge first, then sensors, then motor daemon.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
# Health gate: subsequent groups delayed until t_perception (8 s full/debug).
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
group_a_banner = LogInfo( group_a_banner = LogInfo(
@ -224,12 +209,7 @@ def generate_launch_description() -> LaunchDescription: # noqa: C901
launch_arguments={"use_sim_time": use_sim_time}.items(), launch_arguments={"use_sim_time": use_sim_time}.items(),
) )
<<<<<<< HEAD # ESP32-S3 bidirectional bridge (JLINK USART1) esp32_bridge = IncludeLaunchDescription(
# ESP32 BALANCE bridge
=======
# ESP32-S3 bidirectional bridge (JLINK USART1)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
esp32_bridge = IncludeLaunchDescription(
_launch("saltybot_bridge", "launch", "bridge.launch.py"), _launch("saltybot_bridge", "launch", "bridge.launch.py"),
launch_arguments={ launch_arguments={
"mode": "bidirectional", "mode": "bidirectional",
@ -248,12 +228,7 @@ def generate_launch_description() -> LaunchDescription: # noqa: C901
], ],
) )
<<<<<<< HEAD # Motor daemon: /cmd_vel → ESP32-S3 DRIVE frames (depends on bridge at t=0) motor_daemon = TimerAction(
# Motor daemon: /cmd_vel → ESP32 BALANCE DRIVE frames (depends on bridge at t=0)
=======
# Motor daemon: /cmd_vel → ESP32-S3 DRIVE frames (depends on bridge at t=0)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
motor_daemon = TimerAction(
period=2.5, period=2.5,
actions=[ actions=[
LogInfo(msg="[saltybot_bringup] t=2.5s Starting motor daemon"), LogInfo(msg="[saltybot_bringup] t=2.5s Starting motor daemon"),

View File

@ -20,12 +20,7 @@ theta is kept in (−π, π] after every step.
Int32 rollover Int32 rollover
-------------- --------------
<<<<<<< HEAD ESP32-S3 encoder counters are int32 and wrap at ±2^31. `unwrap_delta` handlesthis by detecting jumps larger than half the int32 range and adjusting by the
ESP32 BALANCE encoder counters are int32 and wrap at ±2^31. `unwrap_delta` handles
=======
ESP32-S3 encoder counters are int32 and wrap at ±2^31. `unwrap_delta` handles
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
this by detecting jumps larger than half the int32 range and adjusting by the
full range: full range:
if delta > 2^30 : delta -= 2^31 if delta > 2^30 : delta -= 2^31

View File

@ -69,12 +69,7 @@ class Profile:
t_ui: float = 22.0 # Group D (nav2 needs ~4 s to load costmaps) t_ui: float = 22.0 # Group D (nav2 needs ~4 s to load costmaps)
# ── Safety ──────────────────────────────────────────────────────────── # ── Safety ────────────────────────────────────────────────────────────
<<<<<<< HEAD watchdog_timeout_s: float = 5.0 # max silence from ESP32-S3 bridge (s) cmd_vel_deadman_s: float = 0.5 # cmd_vel watchdog in bridge
watchdog_timeout_s: float = 5.0 # max silence from ESP32 bridge (s)
=======
watchdog_timeout_s: float = 5.0 # max silence from ESP32-S3 bridge (s)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
cmd_vel_deadman_s: float = 0.5 # cmd_vel watchdog in bridge
max_linear_vel: float = 0.5 # m/s cap passed to bridge + follower max_linear_vel: float = 0.5 # m/s cap passed to bridge + follower
follow_distance_m: float = 1.5 # target follow distance (m) follow_distance_m: float = 1.5 # target follow distance (m)
@ -94,12 +89,7 @@ class Profile:
# ── Profile factory ──────────────────────────────────────────────────────────── # ── Profile factory ────────────────────────────────────────────────────────────
def _minimal() -> Profile: def _minimal() -> Profile:
<<<<<<< HEAD
"""Minimal: ESP32 bridge + sensors + motor daemon.
=======
"""Minimal: ESP32-S3 bridge + sensors + motor daemon. """Minimal: ESP32-S3 bridge + sensors + motor daemon.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Safe drive control only. No AI, no nav, no social. Safe drive control only. No AI, no nav, no social.
Boot time ~4 s. RAM ~400 MB. Boot time ~4 s. RAM ~400 MB.
""" """

View File

@ -1,12 +1,7 @@
""" """
wheel_odom_node.py Differential drive wheel encoder odometry (Issue #184). wheel_odom_node.py Differential drive wheel encoder odometry (Issue #184).
<<<<<<< HEAD Subscribes to raw encoder tick counts from the ESP32-S3 bridge, integratesdifferential drive kinematics, and publishes nav_msgs/Odometry at 50 Hz.
Subscribes to raw encoder tick counts from the ESP32 bridge, integrates
=======
Subscribes to raw encoder tick counts from the ESP32-S3 bridge, integrates
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
differential drive kinematics, and publishes nav_msgs/Odometry at 50 Hz.
Optionally broadcasts the odom base_link TF transform. Optionally broadcasts the odom base_link TF transform.
Subscribes: Subscribes:

View File

@ -61,12 +61,7 @@ kill %1
### Core System Components ### Core System Components
- Robot Description (URDF/TF tree) - Robot Description (URDF/TF tree)
<<<<<<< HEAD - ESP32-S3 Serial Bridge- cmd_vel Bridge
- ESP32 Serial Bridge
=======
- ESP32-S3 Serial Bridge
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
- cmd_vel Bridge
- Rosbridge WebSocket - Rosbridge WebSocket
### Sensors ### Sensors
@ -129,12 +124,7 @@ free -h
### cmd_vel bridge not responding ### cmd_vel bridge not responding
```bash ```bash
<<<<<<< HEAD # Verify ESP32-S3 bridge is running firstros2 node list | grep bridge
# Verify ESP32 bridge is running first
=======
# Verify ESP32-S3 bridge is running first
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
ros2 node list | grep bridge
# Check serial port # Check serial port
ls -l /dev/esp32-bridge ls -l /dev/esp32-bridge

View File

@ -3,5 +3,5 @@ can_bridge_node:
can_interface: slcan0 can_interface: slcan0
left_vesc_can_id: 56 left_vesc_can_id: 56
right_vesc_can_id: 68 right_vesc_can_id: 68
mamba_can_id: 1 balance_can_id: 1
command_timeout_s: 0.5 command_timeout_s: 0.5

View File

@ -6,13 +6,13 @@ and VESC telemetry.
CAN message layout CAN message layout
------------------ ------------------
Command frames (Orin ESP32-S3 BALANCE / VESC): Command frames (Orin ESP32-S3 BALANCE / VESC):
MAMBA_CMD_VELOCITY 0x100 8 bytes left_speed (f32, m/s) | right_speed (f32, m/s) BALANCE_CMD_VELOCITY 0x100 8 bytes left_speed (f32, m/s) | right_speed (f32, m/s)
MAMBA_CMD_MODE 0x101 1 byte mode (0=idle, 1=drive, 2=estop) BALANCE_CMD_MODE 0x101 1 byte mode (0=idle, 1=drive, 2=estop)
MAMBA_CMD_ESTOP 0x102 1 byte 0x01 = stop BALANCE_CMD_ESTOP 0x102 1 byte 0x01 = stop
Telemetry frames (ESP32-S3 BALANCE Orin): Telemetry frames (ESP32-S3 BALANCE Orin):
MAMBA_TELEM_IMU 0x200 24 bytes accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z (f32 each) BALANCE_TELEM_IMU 0x200 24 bytes accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z (f32 each)
MAMBA_TELEM_BATTERY 0x201 8 bytes voltage (f32, V) | current (f32, A) BALANCE_TELEM_BATTERY 0x201 8 bytes voltage (f32, V) | current (f32, A)
VESC telemetry frame (VESC Orin): VESC telemetry frame (VESC Orin):
VESC_TELEM_STATE 0x300 16 bytes erpm (f32) | duty (f32) | voltage (f32) | current (f32) VESC_TELEM_STATE 0x300 16 bytes erpm (f32) | duty (f32) | voltage (f32) | current (f32)
@ -30,12 +30,12 @@ from typing import Tuple
# CAN message IDs # CAN message IDs
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
MAMBA_CMD_VELOCITY: int = 0x100 BALANCE_CMD_VELOCITY: int = 0x100
MAMBA_CMD_MODE: int = 0x101 BALANCE_CMD_MODE: int = 0x101
MAMBA_CMD_ESTOP: int = 0x102 BALANCE_CMD_ESTOP: int = 0x102
MAMBA_TELEM_IMU: int = 0x200 BALANCE_TELEM_IMU: int = 0x200
MAMBA_TELEM_BATTERY: int = 0x201 BALANCE_TELEM_BATTERY: int = 0x201
VESC_TELEM_STATE: int = 0x300 VESC_TELEM_STATE: int = 0x300
ORIN_CAN_ID_PID_SET: int = 0x305 ORIN_CAN_ID_PID_SET: int = 0x305
@ -56,7 +56,7 @@ MODE_ESTOP: int = 2
@dataclass @dataclass
class ImuTelemetry: class ImuTelemetry:
"""Decoded IMU telemetry from ESP32-S3 BALANCE (MAMBA_TELEM_IMU).""" """Decoded IMU telemetry from ESP32-S3 BALANCE (BALANCE_TELEM_IMU)."""
accel_x: float = 0.0 # m/s² accel_x: float = 0.0 # m/s²
accel_y: float = 0.0 accel_y: float = 0.0
@ -68,7 +68,7 @@ class ImuTelemetry:
@dataclass @dataclass
class BatteryTelemetry: class BatteryTelemetry:
"""Decoded battery telemetry from ESP32-S3 BALANCE (MAMBA_TELEM_BATTERY).""" """Decoded battery telemetry from ESP32-S3 BALANCE (BALANCE_TELEM_BATTERY)."""
voltage: float = 0.0 # V voltage: float = 0.0 # V
current: float = 0.0 # A current: float = 0.0 # A
@ -106,7 +106,7 @@ _FMT_VESC = ">ffff" # 4 × float32
def encode_velocity_cmd(left_mps: float, right_mps: float) -> bytes: def encode_velocity_cmd(left_mps: float, right_mps: float) -> bytes:
""" """
Encode a MAMBA_CMD_VELOCITY payload. Encode a BALANCE_CMD_VELOCITY payload.
Parameters Parameters
---------- ----------
@ -122,7 +122,7 @@ def encode_velocity_cmd(left_mps: float, right_mps: float) -> bytes:
def encode_mode_cmd(mode: int) -> bytes: def encode_mode_cmd(mode: int) -> bytes:
""" """
Encode a MAMBA_CMD_MODE payload. Encode a BALANCE_CMD_MODE payload.
Parameters Parameters
---------- ----------
@ -139,7 +139,7 @@ def encode_mode_cmd(mode: int) -> bytes:
def encode_estop_cmd(stop: bool = True) -> bytes: def encode_estop_cmd(stop: bool = True) -> bytes:
""" """
Encode a MAMBA_CMD_ESTOP payload. Encode a BALANCE_CMD_ESTOP payload.
Parameters Parameters
---------- ----------
@ -165,7 +165,7 @@ def encode_pid_set_cmd(kp: float, ki: float, kd: float) -> bytes:
def decode_imu_telem(data: bytes) -> ImuTelemetry: def decode_imu_telem(data: bytes) -> ImuTelemetry:
""" """
Decode a MAMBA_TELEM_IMU payload. Decode a BALANCE_TELEM_IMU payload.
Parameters Parameters
---------- ----------
@ -188,7 +188,7 @@ def decode_imu_telem(data: bytes) -> ImuTelemetry:
def decode_battery_telem(data: bytes) -> BatteryTelemetry: def decode_battery_telem(data: bytes) -> BatteryTelemetry:
""" """
Decode a MAMBA_TELEM_BATTERY payload. Decode a BALANCE_TELEM_BATTERY payload.
Parameters Parameters
---------- ----------

View File

@ -39,22 +39,22 @@ from sensor_msgs.msg import BatteryState
from std_msgs.msg import Bool, Float32MultiArray, String from std_msgs.msg import Bool, Float32MultiArray, String
from saltybot_can_bridge.balance_protocol import ( from saltybot_can_bridge.balance_protocol import (
MAMBA_CMD_ESTOP, BALANCE_CMD_ESTOP,
MAMBA_CMD_MODE, BALANCE_CMD_MODE,
MAMBA_CMD_VELOCITY, BALANCE_CMD_VELOCITY,
MAMBA_TELEM_BATTERY, BALANCE_TELEM_BATTERY,
MAMBA_TELEM_IMU, BALANCE_TELEM_IMU,
VESC_TELEM_STATE, VESC_TELEM_STATE,
ORIN_CAN_ID_FC_PID_ACK, ORIN_CAN_ID_FC_PID_ACK,
ORIN_CAN_ID_PID_SET, ORIN_CAN_ID_PID_SET,
MODE_DRIVE, MODE_DRIVE,
MODE_IDLE, MODE_IDLE,
encode_drive_cmd, encode_velocity_cmd,
encode_arm_cmd, encode_mode_cmd,
encode_estop_cmd, encode_estop_cmd,
decode_attitude, decode_imu_telem,
decode_battery, decode_battery_telem,
decode_vesc_status1, decode_vesc_state,
) )
# Reconnect attempt interval when CAN bus is lost # Reconnect attempt interval when CAN bus is lost
@ -179,10 +179,10 @@ class CanBridgeNode(Node):
right_mps = linear + angular right_mps = linear + angular
payload = encode_velocity_cmd(left_mps, right_mps) payload = encode_velocity_cmd(left_mps, right_mps)
self._send_can(MAMBA_CMD_VELOCITY, payload, "cmd_vel") self._send_can(BALANCE_CMD_VELOCITY, payload, "cmd_vel")
# Keep ESP32-S3 BALANCE in DRIVE mode while receiving commands # Keep ESP32-S3 BALANCE in DRIVE mode while receiving commands
self._send_can(MAMBA_CMD_MODE, encode_mode_cmd(MODE_DRIVE), "cmd_vel mode") self._send_can(BALANCE_CMD_MODE, encode_mode_cmd(MODE_DRIVE), "cmd_vel mode")
def _estop_cb(self, msg: Bool) -> None: def _estop_cb(self, msg: Bool) -> None:
"""Forward /estop to ESP32-S3 BALANCE over CAN.""" """Forward /estop to ESP32-S3 BALANCE over CAN."""
@ -190,7 +190,7 @@ class CanBridgeNode(Node):
return return
if msg.data: if msg.data:
self._send_can( self._send_can(
MAMBA_CMD_MODE, encode_mode_cmd(MODE_ESTOP), "estop mode" BALANCE_CMD_MODE, encode_mode_cmd(MODE_ESTOP), "estop mode"
) )
self.get_logger().warning("E-stop asserted — sent ESTOP to ESP32-S3 BALANCE") self.get_logger().warning("E-stop asserted — sent ESTOP to ESP32-S3 BALANCE")
@ -201,7 +201,8 @@ class CanBridgeNode(Node):
if not self._connected: if not self._connected:
return return
if time.monotonic() - self._last_cmd_time > self._cmd_timeout: if time.monotonic() - self._last_cmd_time > self._cmd_timeout:
self._send_can(ORIN_CMD_DRIVE, encode_drive_cmd(0, 0, MODE_IDLE), "watchdog") self._send_can(BALANCE_CMD_VELOCITY, encode_velocity_cmd(0.0, 0.0), "watchdog")
self._send_can(BALANCE_CMD_MODE, encode_mode_cmd(MODE_IDLE), "watchdog mode")
# ── CAN send helper ─────────────────────────────────────────────────── # ── CAN send helper ───────────────────────────────────────────────────
@ -236,25 +237,28 @@ class CanBridgeNode(Node):
continue continue
self._dispatch_frame(frame) self._dispatch_frame(frame)
# VESC STATUS packet type = 9 (upper byte of extended arb_id)
_VESC_STATUS_PKT: int = 9
def _dispatch_frame(self, frame: can.Message) -> None: def _dispatch_frame(self, frame: can.Message) -> None:
arb_id = frame.arbitration_id arb_id = frame.arbitration_id
data = bytes(frame.data) data = bytes(frame.data)
vesc_l = (VESC_STATUS_1 << 8) | self._left_vesc_id vesc_l = (self._VESC_STATUS_PKT << 8) | self._left_vesc_id
vesc_r = (VESC_STATUS_1 << 8) | self._right_vesc_id vesc_r = (self._VESC_STATUS_PKT << 8) | self._right_vesc_id
try: try:
if arb_id == ESP32_TELEM_ATTITUDE: if arb_id == BALANCE_TELEM_IMU:
self._handle_attitude(data) self._handle_imu(data)
elif arb_id == ESP32_TELEM_BATTERY: elif arb_id == BALANCE_TELEM_BATTERY:
self._handle_battery(data) self._handle_battery(data)
elif arb_id == vesc_l: elif arb_id == vesc_l:
t = decode_vesc_status1(self._left_vesc_id, data) t = decode_vesc_state(data)
m = Float32MultiArray() m = Float32MultiArray()
m.data = [t.erpm, t.duty, 0.0, t.current] m.data = [t.erpm, t.duty, t.voltage, t.current]
self._pub_vesc_left.publish(m) self._pub_vesc_left.publish(m)
elif arb_id == vesc_r: elif arb_id == vesc_r:
t = decode_vesc_status1(self._right_vesc_id, data) t = decode_vesc_state(data)
m = Float32MultiArray() m = Float32MultiArray()
m.data = [t.erpm, t.duty, 0.0, t.current] m.data = [t.erpm, t.duty, t.voltage, t.current]
self._pub_vesc_right.publish(m) self._pub_vesc_right.publish(m)
except Exception as exc: except Exception as exc:
self.get_logger().warning( self.get_logger().warning(
@ -263,19 +267,17 @@ class CanBridgeNode(Node):
# ── Frame handlers ──────────────────────────────────────────────────── # ── Frame handlers ────────────────────────────────────────────────────
_STATE_LABEL = {0: "IDLE", 1: "RUNNING", 2: "FAULT"} def _handle_imu(self, data: bytes) -> None:
"""BALANCE_TELEM_IMU (0x200): accel + gyro → /saltybot/attitude."""
def _handle_attitude(self, data: bytes) -> None: t = decode_imu_telem(data)
"""ATTITUDE (0x400): pitch, speed, yaw_rate, state, flags → /saltybot/attitude."""
t = decode_attitude(data)
now = self.get_clock().now().to_msg() now = self.get_clock().now().to_msg()
payload = { payload = {
"pitch_deg": round(t.pitch_deg, 2), "accel_x": round(t.accel_x, 4),
"speed_mps": round(t.speed, 3), "accel_y": round(t.accel_y, 4),
"yaw_rate": round(t.yaw_rate, 3), "accel_z": round(t.accel_z, 4),
"state": t.state, "gyro_x": round(t.gyro_x, 4),
"state_label": self._STATE_LABEL.get(t.state, f"UNKNOWN({t.state})"), "gyro_y": round(t.gyro_y, 4),
"flags": t.flags, "gyro_z": round(t.gyro_z, 4),
"ts": f"{now.sec}.{now.nanosec:09d}", "ts": f"{now.sec}.{now.nanosec:09d}",
} }
msg = String() msg = String()
@ -284,11 +286,12 @@ class CanBridgeNode(Node):
self._pub_balance.publish(msg) # keep /saltybot/balance_state alive self._pub_balance.publish(msg) # keep /saltybot/balance_state alive
def _handle_battery(self, data: bytes) -> None: def _handle_battery(self, data: bytes) -> None:
"""BATTERY (0x401): vbat_mv, fault_code, rssi → /can/battery.""" """BALANCE_TELEM_BATTERY (0x201): voltage + current → /can/battery."""
t = decode_battery(data) t = decode_battery_telem(data)
msg = BatteryState() msg = BatteryState()
msg.header.stamp = self.get_clock().now().to_msg() msg.header.stamp = self.get_clock().now().to_msg()
msg.voltage = t.vbat_mv / 1000.0 msg.voltage = t.voltage
msg.current = t.current
msg.present = True msg.present = True
msg.power_supply_status = BatteryState.POWER_SUPPLY_STATUS_DISCHARGING msg.power_supply_status = BatteryState.POWER_SUPPLY_STATUS_DISCHARGING
self._pub_battery.publish(msg) self._pub_battery.publish(msg)
@ -305,8 +308,9 @@ class CanBridgeNode(Node):
def destroy_node(self) -> None: def destroy_node(self) -> None:
if self._connected and self._bus is not None: if self._connected and self._bus is not None:
try: try:
self._send_can(ORIN_CMD_DRIVE, encode_drive_cmd(0, 0, MODE_IDLE), "shutdown") # Send zero velocity and idle mode on shutdown
self._send_can(ORIN_CMD_ARM, encode_arm_cmd(False), "shutdown") self._send_can(BALANCE_CMD_VELOCITY, encode_velocity_cmd(0.0, 0.0), "shutdown")
self._send_can(BALANCE_CMD_MODE, encode_mode_cmd(MODE_IDLE), "shutdown")
except Exception: except Exception:
pass pass
try: try:

View File

@ -15,12 +15,7 @@ setup(
zip_safe=True, zip_safe=True,
maintainer="sl-controls", maintainer="sl-controls",
maintainer_email="sl-controls@saltylab.local", maintainer_email="sl-controls@saltylab.local",
<<<<<<< HEAD description="CAN bus bridge for ESP32-S3 BALANCE controller and VESC telemetry", license="MIT",
description="CAN bus bridge for ESP32 IO motor controller and VESC telemetry",
=======
description="CAN bus bridge for ESP32-S3 BALANCE controller and VESC telemetry",
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
license="MIT",
tests_require=["pytest"], tests_require=["pytest"],
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [

View File

@ -12,11 +12,11 @@ import struct
import unittest import unittest
from saltybot_can_bridge.balance_protocol import ( from saltybot_can_bridge.balance_protocol import (
MAMBA_CMD_ESTOP, BALANCE_CMD_ESTOP,
MAMBA_CMD_MODE, BALANCE_CMD_MODE,
MAMBA_CMD_VELOCITY, BALANCE_CMD_VELOCITY,
MAMBA_TELEM_BATTERY, BALANCE_TELEM_BATTERY,
MAMBA_TELEM_IMU, BALANCE_TELEM_IMU,
VESC_TELEM_STATE, VESC_TELEM_STATE,
MODE_DRIVE, MODE_DRIVE,
MODE_ESTOP, MODE_ESTOP,
@ -37,13 +37,13 @@ class TestMessageIDs(unittest.TestCase):
"""Verify the CAN message ID constants are correct.""" """Verify the CAN message ID constants are correct."""
def test_command_ids(self): def test_command_ids(self):
self.assertEqual(MAMBA_CMD_VELOCITY, 0x100) self.assertEqual(BALANCE_CMD_VELOCITY, 0x100)
self.assertEqual(MAMBA_CMD_MODE, 0x101) self.assertEqual(BALANCE_CMD_MODE, 0x101)
self.assertEqual(MAMBA_CMD_ESTOP, 0x102) self.assertEqual(BALANCE_CMD_ESTOP, 0x102)
def test_telemetry_ids(self): def test_telemetry_ids(self):
self.assertEqual(MAMBA_TELEM_IMU, 0x200) self.assertEqual(BALANCE_TELEM_IMU, 0x200)
self.assertEqual(MAMBA_TELEM_BATTERY, 0x201) self.assertEqual(BALANCE_TELEM_BATTERY, 0x201)
self.assertEqual(VESC_TELEM_STATE, 0x300) self.assertEqual(VESC_TELEM_STATE, 0x300)

View File

@ -4,42 +4,39 @@ protocol_defs.py — CAN message ID constants and frame builders/parsers for the
OrinESP32-S3 BALANCEVESC integration test suite. OrinESP32-S3 BALANCEVESC integration test suite.
All IDs and payload formats are derived from: All IDs and payload formats are derived from:
include/orin_can.h OrinFC (ESP32-S3 BALANCE) protocol include/orin_can.h OrinBALANCE (ESP32-S3 BALANCE) protocol
include/vesc_can.h VESC CAN protocol include/vesc_can.h VESC CAN protocol
saltybot_can_bridge/balance_protocol.py existing bridge constants saltybot_can_bridge/balance_protocol.py existing bridge constants
CAN IDs used in tests CAN IDs used in tests
--------------------- ---------------------
Orin FC (ESP32-S3 BALANCE) commands (standard 11-bit, matching orin_can.h): Orin BALANCE (ESP32-S3 BALANCE) commands (standard 11-bit, matching orin_can.h):
ORIN_CMD_HEARTBEAT 0x300 ORIN_CMD_HEARTBEAT 0x300
ORIN_CMD_DRIVE 0x301 int16 speed (1000..+1000), int16 steer (1000..+1000) ORIN_CMD_DRIVE 0x301 int16 speed (-1000..+1000), int16 steer (-1000..+1000)
ORIN_CMD_MODE 0x302 uint8 mode byte ORIN_CMD_MODE 0x302 uint8 mode byte
ORIN_CMD_ESTOP 0x303 uint8 action (1=ESTOP, 0=CLEAR) ORIN_CMD_ESTOP 0x303 uint8 action (1=ESTOP, 0=CLEAR)
FC (ESP32-S3 BALANCE) Orin telemetry (standard 11-bit, matching orin_can.h): BALANCE (ESP32-S3 BALANCE) -> Orin telemetry (standard 11-bit, matching orin_can.h):
FC_STATUS 0x400 8 bytes (see orin_can_fc_status_t) BALANCE_STATUS 0x400 8 bytes (see orin_can_balance_status_t)
FC_VESC 0x401 8 bytes (see orin_can_fc_vesc_t) BALANCE_VESC 0x401 8 bytes (see orin_can_balance_vesc_t)
FC_IMU 0x402 8 bytes BALANCE_IMU 0x402 8 bytes
FC_BARO 0x403 8 bytes BALANCE_BARO 0x403 8 bytes
Mamba VESC internal commands (matching balance_protocol.py): ESP32-S3 BALANCE <-> VESC internal commands (matching balance_protocol.py):
======= BALANCE_CMD_VELOCITY 0x100 8 bytes left_mps (f32) | right_mps (f32) big-endian
ESP32-S3 BALANCE VESC internal commands (matching balance_protocol.py): BALANCE_CMD_MODE 0x101 1 byte mode (0=idle,1=drive,2=estop)
>>>>>>> 9aed963 (fix: scrub remaining Mamba references in can_bridge and e2e test protocol files) BALANCE_CMD_ESTOP 0x102 1 byte 0x01=stop
MAMBA_CMD_VELOCITY 0x100 8 bytes left_mps (f32) | right_mps (f32) big-endian
MAMBA_CMD_MODE 0x101 1 byte mode (0=idle,1=drive,2=estop)
MAMBA_CMD_ESTOP 0x102 1 byte 0x01=stop
VESC STATUS (extended 29-bit, matching vesc_can.h): VESC STATUS (extended 29-bit, matching vesc_can.h):
arb_id = (VESC_PKT_STATUS << 8) | vesc_node_id = (9 << 8) | node_id arb_id = (VESC_PKT_STATUS << 8) | vesc_node_id = (9 << 8) | node_id
Payload: int32 RPM (BE), int16 current×10 (BE), int16 duty×1000 (BE) Payload: int32 RPM (BE), int16 current x10 (BE), int16 duty x1000 (BE)
""" """
import struct import struct
from typing import Tuple from typing import Tuple
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Orin → FC (ESP32-S3 BALANCE) command IDs (from orin_can.h) # Orin -> BALANCE (ESP32-S3 BALANCE) command IDs (from orin_can.h)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
ORIN_CMD_HEARTBEAT: int = 0x300 ORIN_CMD_HEARTBEAT: int = 0x300
@ -48,27 +45,24 @@ ORIN_CMD_MODE: int = 0x302
ORIN_CMD_ESTOP: int = 0x303 ORIN_CMD_ESTOP: int = 0x303
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# FC (ESP32-S3 BALANCE) → Orin telemetry IDs (from orin_can.h) # BALANCE (ESP32-S3 BALANCE) -> Orin telemetry IDs (from orin_can.h)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
FC_STATUS: int = 0x400 BALANCE_STATUS: int = 0x400
FC_VESC: int = 0x401 BALANCE_VESC: int = 0x401
FC_IMU: int = 0x402 BALANCE_IMU: int = 0x402
FC_BARO: int = 0x403 BALANCE_BARO: int = 0x403
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Mamba → VESC internal command IDs (from balance_protocol.py) # ESP32-S3 BALANCE -> VESC internal command IDs (from balance_protocol.py)
=======
# ESP32-S3 BALANCE → VESC internal command IDs (from balance_protocol.py)
>>>>>>> 9aed963 (fix: scrub remaining Mamba references in can_bridge and e2e test protocol files)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
MAMBA_CMD_VELOCITY: int = 0x100 BALANCE_CMD_VELOCITY: int = 0x100
MAMBA_CMD_MODE: int = 0x101 BALANCE_CMD_MODE: int = 0x101
MAMBA_CMD_ESTOP: int = 0x102 BALANCE_CMD_ESTOP: int = 0x102
MAMBA_TELEM_IMU: int = 0x200 BALANCE_TELEM_IMU: int = 0x200
MAMBA_TELEM_BATTERY: int = 0x201 BALANCE_TELEM_BATTERY: int = 0x201
VESC_TELEM_STATE: int = 0x300 VESC_TELEM_STATE: int = 0x300
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -111,7 +105,7 @@ def VESC_SET_RPM_ID(vesc_node_id: int) -> int:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Frame builders — Orin → FC # Frame builders — Orin -> BALANCE
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def build_heartbeat(seq: int = 0) -> bytes: def build_heartbeat(seq: int = 0) -> bytes:
@ -125,8 +119,8 @@ def build_drive_cmd(speed: int, steer: int) -> bytes:
Parameters Parameters
---------- ----------
speed: int, 1000..+1000 (mapped directly to int16) speed: int, -1000..+1000 (mapped directly to int16)
steer: int, 1000..+1000 steer: int, -1000..+1000
""" """
return struct.pack(">hh", int(speed), int(steer)) return struct.pack(">hh", int(speed), int(steer))
@ -137,20 +131,17 @@ def build_mode_cmd(mode: int) -> bytes:
def build_estop_cmd(action: int = 1) -> bytes: def build_estop_cmd(action: int = 1) -> bytes:
"""Build an ORIN_CMD_ESTOP payload. action=1 → ESTOP, 0 → CLEAR.""" """Build an ORIN_CMD_ESTOP payload. action=1 -> ESTOP, 0 -> CLEAR."""
return struct.pack(">B", action & 0xFF) return struct.pack(">B", action & 0xFF)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Frame builders — Mamba velocity commands (balance_protocol.py encoding)
=======
# Frame builders — ESP32-S3 BALANCE velocity commands (balance_protocol.py encoding) # Frame builders — ESP32-S3 BALANCE velocity commands (balance_protocol.py encoding)
>>>>>>> 9aed963 (fix: scrub remaining Mamba references in can_bridge and e2e test protocol files)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def build_velocity_cmd(left_mps: float, right_mps: float) -> bytes: def build_velocity_cmd(left_mps: float, right_mps: float) -> bytes:
""" """
Build a MAMBA_CMD_VELOCITY payload (8 bytes, 2 × float32 big-endian). Build a BALANCE_CMD_VELOCITY payload (8 bytes, 2 x float32 big-endian).
Matches encode_velocity_cmd() in balance_protocol.py. Matches encode_velocity_cmd() in balance_protocol.py.
""" """
@ -158,10 +149,10 @@ def build_velocity_cmd(left_mps: float, right_mps: float) -> bytes:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Frame builders — FC → Orin telemetry # Frame builders — BALANCE -> Orin telemetry
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def build_fc_status( def build_balance_status(
pitch_x10: int = 0, pitch_x10: int = 0,
motor_cmd: int = 0, motor_cmd: int = 0,
vbat_mv: int = 24000, vbat_mv: int = 24000,
@ -169,9 +160,9 @@ def build_fc_status(
flags: int = 0, flags: int = 0,
) -> bytes: ) -> bytes:
""" """
Build an FC_STATUS (0x400) payload. Build a BALANCE_STATUS (0x400) payload.
Layout (orin_can_fc_status_t, 8 bytes, big-endian): Layout (orin_can_balance_status_t, 8 bytes, big-endian):
int16 pitch_x10 int16 pitch_x10
int16 motor_cmd int16 motor_cmd
uint16 vbat_mv uint16 vbat_mv
@ -188,23 +179,23 @@ def build_fc_status(
) )
def build_fc_vesc( def build_balance_vesc(
left_rpm_x10: int = 0, left_rpm_x10: int = 0,
right_rpm_x10: int = 0, right_rpm_x10: int = 0,
left_current_x10: int = 0, left_current_x10: int = 0,
right_current_x10: int = 0, right_current_x10: int = 0,
) -> bytes: ) -> bytes:
""" """
Build an FC_VESC (0x401) payload. Build a BALANCE_VESC (0x401) payload.
Layout (orin_can_fc_vesc_t, 8 bytes, big-endian): Layout (orin_can_balance_vesc_t, 8 bytes, big-endian):
int16 left_rpm_x10 int16 left_rpm_x10
int16 right_rpm_x10 int16 right_rpm_x10
int16 left_current_x10 int16 left_current_x10
int16 right_current_x10 int16 right_current_x10
RPM values are RPM / 10 (e.g. 3000 RPM 300). RPM values are RPM / 10 (e.g. 3000 RPM -> 300).
Current values are A × 10 (e.g. 5.5 A 55). Current values are A x 10 (e.g. 5.5 A -> 55).
""" """
return struct.pack( return struct.pack(
">hhhh", ">hhhh",
@ -225,8 +216,8 @@ def build_vesc_status(
Layout (from vesc_can.h / VESC FW 6.x, big-endian): Layout (from vesc_can.h / VESC FW 6.x, big-endian):
int32 rpm int32 rpm
int16 current × 10 int16 current x 10
int16 duty × 1000 int16 duty x 1000
Total: 8 bytes. Total: 8 bytes.
""" """
return struct.pack( return struct.pack(
@ -241,9 +232,9 @@ def build_vesc_status(
# Frame parsers # Frame parsers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def parse_fc_status(data: bytes): def parse_balance_status(data: bytes):
""" """
Parse an FC_STATUS (0x400) payload. Parse a BALANCE_STATUS (0x400) payload.
Returns Returns
------- -------
@ -251,7 +242,7 @@ def parse_fc_status(data: bytes):
estop_active (bool), armed (bool) estop_active (bool), armed (bool)
""" """
if len(data) < 8: if len(data) < 8:
raise ValueError(f"FC_STATUS needs 8 bytes, got {len(data)}") raise ValueError(f"BALANCE_STATUS needs 8 bytes, got {len(data)}")
pitch_x10, motor_cmd, vbat_mv, balance_state, flags = struct.unpack( pitch_x10, motor_cmd, vbat_mv, balance_state, flags = struct.unpack(
">hhHBB", data[:8] ">hhHBB", data[:8]
) )
@ -266,9 +257,9 @@ def parse_fc_status(data: bytes):
} }
def parse_fc_vesc(data: bytes): def parse_balance_vesc(data: bytes):
""" """
Parse an FC_VESC (0x401) payload. Parse a BALANCE_VESC (0x401) payload.
Returns Returns
------- -------
@ -276,7 +267,7 @@ def parse_fc_vesc(data: bytes):
right_current_x10, left_rpm (float), right_rpm (float) right_current_x10, left_rpm (float), right_rpm (float)
""" """
if len(data) < 8: if len(data) < 8:
raise ValueError(f"FC_VESC needs 8 bytes, got {len(data)}") raise ValueError(f"BALANCE_VESC needs 8 bytes, got {len(data)}")
left_rpm_x10, right_rpm_x10, left_cur_x10, right_cur_x10 = struct.unpack( left_rpm_x10, right_rpm_x10, left_cur_x10, right_cur_x10 = struct.unpack(
">hhhh", data[:8] ">hhhh", data[:8]
) )
@ -312,12 +303,12 @@ def parse_vesc_status(data: bytes):
def parse_velocity_cmd(data: bytes) -> Tuple[float, float]: def parse_velocity_cmd(data: bytes) -> Tuple[float, float]:
""" """
Parse a MAMBA_CMD_VELOCITY payload (8 bytes, 2 × float32 big-endian). Parse a BALANCE_CMD_VELOCITY payload (8 bytes, 2 x float32 big-endian).
Returns Returns
------- -------
(left_mps, right_mps) (left_mps, right_mps)
""" """
if len(data) < 8: if len(data) < 8:
raise ValueError(f"MAMBA_CMD_VELOCITY needs 8 bytes, got {len(data)}") raise ValueError(f"BALANCE_CMD_VELOCITY needs 8 bytes, got {len(data)}")
return struct.unpack(">ff", data[:8]) return struct.unpack(">ff", data[:8])

View File

@ -4,7 +4,7 @@ test_drive_command.py — Integration tests for the drive command path.
Tests verify: Tests verify:
DRIVE cmd ESP32-S3 BALANCE receives velocity command frame mock VESC status response DRIVE cmd ESP32-S3 BALANCE receives velocity command frame mock VESC status response
FC_VESC broadcast contains correct RPMs. BALANCE_VESC broadcast contains correct RPMs.
All tests run without real hardware or a running ROS2 system. All tests run without real hardware or a running ROS2 system.
Run with: python -m pytest test/test_drive_command.py -v Run with: python -m pytest test/test_drive_command.py -v
@ -14,19 +14,19 @@ import struct
import pytest import pytest
from saltybot_can_e2e_test.protocol_defs import ( from saltybot_can_e2e_test.protocol_defs import (
MAMBA_CMD_VELOCITY, BALANCE_CMD_VELOCITY,
MAMBA_CMD_MODE, BALANCE_CMD_MODE,
FC_VESC, BALANCE_VESC,
MODE_DRIVE, MODE_DRIVE,
MODE_IDLE, MODE_IDLE,
VESC_CAN_ID_LEFT, VESC_CAN_ID_LEFT,
VESC_CAN_ID_RIGHT, VESC_CAN_ID_RIGHT,
VESC_STATUS_ID, VESC_STATUS_ID,
build_velocity_cmd, build_velocity_cmd,
build_fc_vesc, build_balance_vesc,
build_vesc_status, build_vesc_status,
parse_velocity_cmd, parse_velocity_cmd,
parse_fc_vesc, parse_balance_vesc,
) )
from saltybot_can_bridge.balance_protocol import ( from saltybot_can_bridge.balance_protocol import (
encode_velocity_cmd, encode_velocity_cmd,
@ -50,8 +50,8 @@ def _send_drive(bus, left_mps: float, right_mps: float) -> None:
self.data = bytearray(data) self.data = bytearray(data)
self.is_extended_id = False self.is_extended_id = False
bus.send(_Msg(MAMBA_CMD_VELOCITY, payload)) bus.send(_Msg(BALANCE_CMD_VELOCITY, payload))
bus.send(_Msg(MAMBA_CMD_MODE, encode_mode_cmd(MODE_DRIVE))) bus.send(_Msg(BALANCE_CMD_MODE, encode_mode_cmd(MODE_DRIVE)))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -62,11 +62,11 @@ class TestDriveForward:
def test_drive_forward_velocity_frame_sent(self, mock_can_bus): def test_drive_forward_velocity_frame_sent(self, mock_can_bus):
""" """
Inject DRIVE cmd (1.0 m/s, 1.0 m/s) verify ESP32-S3 BALANCE receives Inject DRIVE cmd (1.0 m/s, 1.0 m/s) verify ESP32-S3 BALANCE receives
a MAMBA_CMD_VELOCITY frame with correct payload. a BALANCE_CMD_VELOCITY frame with correct payload.
""" """
_send_drive(mock_can_bus, 1.0, 1.0) _send_drive(mock_can_bus, 1.0, 1.0)
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) == 1, "Expected exactly one velocity command frame" assert len(vel_frames) == 1, "Expected exactly one velocity command frame"
left, right = parse_velocity_cmd(bytes(vel_frames[0].data)) left, right = parse_velocity_cmd(bytes(vel_frames[0].data))
@ -77,26 +77,26 @@ class TestDriveForward:
"""After a drive command, a MODE=drive frame must accompany it.""" """After a drive command, a MODE=drive frame must accompany it."""
_send_drive(mock_can_bus, 1.0, 1.0) _send_drive(mock_can_bus, 1.0, 1.0)
mode_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_MODE) mode_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_MODE)
assert len(mode_frames) >= 1, "Expected at least one MODE frame" assert len(mode_frames) >= 1, "Expected at least one MODE frame"
assert bytes(mode_frames[0].data) == bytes([MODE_DRIVE]) assert bytes(mode_frames[0].data) == bytes([MODE_DRIVE])
def test_drive_forward_fc_vesc_broadcast(self, mock_can_bus): def test_drive_forward_fc_vesc_broadcast(self, mock_can_bus):
""" """
Simulate FC_VESC broadcast arriving after drive cmd; verify parse is correct. Simulate BALANCE_VESC broadcast arriving after drive cmd; verify parse is correct.
(In the real loop ESP32-S3 BALANCE computes RPM from m/s and broadcasts FC_VESC.) (In the real loop ESP32-S3 BALANCE computes RPM from m/s and broadcasts BALANCE_VESC.)
This test checks the FC_VESC frame format and parser. This test checks the BALANCE_VESC frame format and parser.
""" """
# Simulate: 1.0 m/s → ~300 RPM × 10 = 3000 (representative, not physics) # Simulate: 1.0 m/s → ~300 RPM × 10 = 3000 (representative, not physics)
fc_payload = build_fc_vesc( fc_payload = build_balance_vesc(
left_rpm_x10=300, right_rpm_x10=300, left_rpm_x10=300, right_rpm_x10=300,
left_current_x10=50, right_current_x10=50, left_current_x10=50, right_current_x10=50,
) )
mock_can_bus.inject(FC_VESC, fc_payload) mock_can_bus.inject(BALANCE_VESC, fc_payload)
frame = mock_can_bus.recv(timeout=0.1) frame = mock_can_bus.recv(timeout=0.1)
assert frame is not None, "FC_VESC frame not received" assert frame is not None, "BALANCE_VESC frame not received"
parsed = parse_fc_vesc(bytes(frame.data)) parsed = parse_balance_vesc(bytes(frame.data))
assert parsed["left_rpm_x10"] == 300 assert parsed["left_rpm_x10"] == 300
assert parsed["right_rpm_x10"] == 300 assert parsed["right_rpm_x10"] == 300
assert abs(parsed["left_rpm"] - 3000.0) < 0.1 assert abs(parsed["left_rpm"] - 3000.0) < 0.1
@ -109,7 +109,7 @@ class TestDriveTurn:
""" """
_send_drive(mock_can_bus, 0.5, -0.5) _send_drive(mock_can_bus, 0.5, -0.5)
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) == 1 assert len(vel_frames) == 1
left, right = parse_velocity_cmd(bytes(vel_frames[0].data)) left, right = parse_velocity_cmd(bytes(vel_frames[0].data))
@ -119,14 +119,14 @@ class TestDriveTurn:
assert left > 0 and right < 0 assert left > 0 and right < 0
def test_drive_turn_fc_vesc_differential(self, mock_can_bus): def test_drive_turn_fc_vesc_differential(self, mock_can_bus):
"""Simulated FC_VESC for a turn has opposite-sign RPMs.""" """Simulated BALANCE_VESC for a turn has opposite-sign RPMs."""
fc_payload = build_fc_vesc( fc_payload = build_balance_vesc(
left_rpm_x10=150, right_rpm_x10=-150, left_rpm_x10=150, right_rpm_x10=-150,
left_current_x10=30, right_current_x10=30, left_current_x10=30, right_current_x10=30,
) )
mock_can_bus.inject(FC_VESC, fc_payload) mock_can_bus.inject(BALANCE_VESC, fc_payload)
frame = mock_can_bus.recv(timeout=0.1) frame = mock_can_bus.recv(timeout=0.1)
parsed = parse_fc_vesc(bytes(frame.data)) parsed = parse_balance_vesc(bytes(frame.data))
assert parsed["left_rpm_x10"] > 0 assert parsed["left_rpm_x10"] > 0
assert parsed["right_rpm_x10"] < 0 assert parsed["right_rpm_x10"] < 0
@ -142,7 +142,7 @@ class TestDriveZero:
_send_drive(mock_can_bus, 0.0, 0.0) _send_drive(mock_can_bus, 0.0, 0.0)
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) == 1 assert len(vel_frames) == 1
left, right = parse_velocity_cmd(bytes(vel_frames[0].data)) left, right = parse_velocity_cmd(bytes(vel_frames[0].data))
assert abs(left) < 1e-5, "Left motor not stopped" assert abs(left) < 1e-5, "Left motor not stopped"
@ -156,7 +156,7 @@ class TestDriveCmdTimeout:
zero velocity is sent. We test the encoding directly (without timers). zero velocity is sent. We test the encoding directly (without timers).
""" """
# The watchdog in CanBridgeNode calls encode_velocity_cmd(0.0, 0.0) and # The watchdog in CanBridgeNode calls encode_velocity_cmd(0.0, 0.0) and
# sends it on MAMBA_CMD_VELOCITY. Replicate that here. # sends it on BALANCE_CMD_VELOCITY. Replicate that here.
zero_payload = encode_velocity_cmd(0.0, 0.0) zero_payload = encode_velocity_cmd(0.0, 0.0)
class _Msg: class _Msg:
@ -165,16 +165,16 @@ class TestDriveCmdTimeout:
self.data = bytearray(data) self.data = bytearray(data)
self.is_extended_id = False self.is_extended_id = False
mock_can_bus.send(_Msg(MAMBA_CMD_VELOCITY, zero_payload)) mock_can_bus.send(_Msg(BALANCE_CMD_VELOCITY, zero_payload))
mock_can_bus.send(_Msg(MAMBA_CMD_MODE, encode_mode_cmd(MODE_IDLE))) mock_can_bus.send(_Msg(BALANCE_CMD_MODE, encode_mode_cmd(MODE_IDLE)))
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) == 1 assert len(vel_frames) == 1
left, right = parse_velocity_cmd(bytes(vel_frames[0].data)) left, right = parse_velocity_cmd(bytes(vel_frames[0].data))
assert abs(left) < 1e-5 assert abs(left) < 1e-5
assert abs(right) < 1e-5 assert abs(right) < 1e-5
mode_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_MODE) mode_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_MODE)
assert len(mode_frames) == 1 assert len(mode_frames) == 1
assert bytes(mode_frames[0].data) == bytes([MODE_IDLE]) assert bytes(mode_frames[0].data) == bytes([MODE_IDLE])

View File

@ -6,7 +6,7 @@ Covers:
- ESTOP command halts motors immediately - ESTOP command halts motors immediately
- ESTOP persists: DRIVE commands ignored while ESTOP is active - ESTOP persists: DRIVE commands ignored while ESTOP is active
- ESTOP clear restores normal drive operation - ESTOP clear restores normal drive operation
- Firmware-side estop via FC_STATUS flags is detected correctly - Firmware-side estop via BALANCE_STATUS flags is detected correctly
No ROS2 or real CAN hardware required. No ROS2 or real CAN hardware required.
Run with: python -m pytest test/test_estop.py -v Run with: python -m pytest test/test_estop.py -v
@ -17,20 +17,20 @@ import pytest
from saltybot_can_e2e_test.can_mock import MockCANBus from saltybot_can_e2e_test.can_mock import MockCANBus
from saltybot_can_e2e_test.protocol_defs import ( from saltybot_can_e2e_test.protocol_defs import (
MAMBA_CMD_VELOCITY, BALANCE_CMD_VELOCITY,
MAMBA_CMD_MODE, BALANCE_CMD_MODE,
MAMBA_CMD_ESTOP, BALANCE_CMD_ESTOP,
ORIN_CMD_ESTOP, ORIN_CMD_ESTOP,
FC_STATUS, BALANCE_STATUS,
MODE_IDLE, MODE_IDLE,
MODE_DRIVE, MODE_DRIVE,
MODE_ESTOP, MODE_ESTOP,
build_estop_cmd, build_estop_cmd,
build_mode_cmd, build_mode_cmd,
build_velocity_cmd, build_velocity_cmd,
build_fc_status, build_balance_status,
parse_velocity_cmd, parse_velocity_cmd,
parse_fc_status, parse_balance_status,
) )
from saltybot_can_bridge.balance_protocol import ( from saltybot_can_bridge.balance_protocol import (
encode_velocity_cmd, encode_velocity_cmd,
@ -68,16 +68,16 @@ class EstopStateMachine:
"""Send ESTOP and transition to estop mode.""" """Send ESTOP and transition to estop mode."""
self._estop_active = True self._estop_active = True
self._mode = MODE_ESTOP self._mode = MODE_ESTOP
self._bus.send(_Msg(MAMBA_CMD_VELOCITY, encode_velocity_cmd(0.0, 0.0))) self._bus.send(_Msg(BALANCE_CMD_VELOCITY, encode_velocity_cmd(0.0, 0.0)))
self._bus.send(_Msg(MAMBA_CMD_MODE, encode_mode_cmd(MODE_ESTOP))) self._bus.send(_Msg(BALANCE_CMD_MODE, encode_mode_cmd(MODE_ESTOP)))
self._bus.send(_Msg(MAMBA_CMD_ESTOP, encode_estop_cmd(True))) self._bus.send(_Msg(BALANCE_CMD_ESTOP, encode_estop_cmd(True)))
def clear_estop(self) -> None: def clear_estop(self) -> None:
"""Clear ESTOP and return to IDLE mode.""" """Clear ESTOP and return to IDLE mode."""
self._estop_active = False self._estop_active = False
self._mode = MODE_IDLE self._mode = MODE_IDLE
self._bus.send(_Msg(MAMBA_CMD_ESTOP, encode_estop_cmd(False))) self._bus.send(_Msg(BALANCE_CMD_ESTOP, encode_estop_cmd(False)))
self._bus.send(_Msg(MAMBA_CMD_MODE, encode_mode_cmd(MODE_IDLE))) self._bus.send(_Msg(BALANCE_CMD_MODE, encode_mode_cmd(MODE_IDLE)))
def send_drive(self, left_mps: float, right_mps: float) -> None: def send_drive(self, left_mps: float, right_mps: float) -> None:
"""Send velocity command only if ESTOP is not active.""" """Send velocity command only if ESTOP is not active."""
@ -85,8 +85,8 @@ class EstopStateMachine:
# Bridge silently drops commands while estopped # Bridge silently drops commands while estopped
return return
self._mode = MODE_DRIVE self._mode = MODE_DRIVE
self._bus.send(_Msg(MAMBA_CMD_VELOCITY, encode_velocity_cmd(left_mps, right_mps))) self._bus.send(_Msg(BALANCE_CMD_VELOCITY, encode_velocity_cmd(left_mps, right_mps)))
self._bus.send(_Msg(MAMBA_CMD_MODE, encode_mode_cmd(MODE_DRIVE))) self._bus.send(_Msg(BALANCE_CMD_MODE, encode_mode_cmd(MODE_DRIVE)))
@property @property
def estop_active(self) -> bool: def estop_active(self) -> bool:
@ -105,7 +105,7 @@ class TestEstopHaltsMotors:
sm = EstopStateMachine(mock_can_bus) sm = EstopStateMachine(mock_can_bus)
sm.assert_estop() sm.assert_estop()
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) >= 1, "No velocity frame after ESTOP" assert len(vel_frames) >= 1, "No velocity frame after ESTOP"
l, r = parse_velocity_cmd(bytes(vel_frames[-1].data)) l, r = parse_velocity_cmd(bytes(vel_frames[-1].data))
assert abs(l) < 1e-5, f"Left motor {l} not zero after ESTOP" assert abs(l) < 1e-5, f"Left motor {l} not zero after ESTOP"
@ -116,17 +116,17 @@ class TestEstopHaltsMotors:
sm = EstopStateMachine(mock_can_bus) sm = EstopStateMachine(mock_can_bus)
sm.assert_estop() sm.assert_estop()
mode_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_MODE) mode_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_MODE)
assert any( assert any(
bytes(f.data) == bytes([MODE_ESTOP]) for f in mode_frames bytes(f.data) == bytes([MODE_ESTOP]) for f in mode_frames
), "MODE=ESTOP not found in sent frames" ), "MODE=ESTOP not found in sent frames"
def test_estop_flag_byte_is_0x01(self, mock_can_bus): def test_estop_flag_byte_is_0x01(self, mock_can_bus):
"""MAMBA_CMD_ESTOP payload must be 0x01 when asserting e-stop.""" """BALANCE_CMD_ESTOP payload must be 0x01 when asserting e-stop."""
sm = EstopStateMachine(mock_can_bus) sm = EstopStateMachine(mock_can_bus)
sm.assert_estop() sm.assert_estop()
estop_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_ESTOP) estop_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_ESTOP)
assert len(estop_frames) >= 1 assert len(estop_frames) >= 1
assert bytes(estop_frames[-1].data) == b"\x01", \ assert bytes(estop_frames[-1].data) == b"\x01", \
f"ESTOP payload {estop_frames[-1].data!r} != 0x01" f"ESTOP payload {estop_frames[-1].data!r} != 0x01"
@ -143,7 +143,7 @@ class TestEstopPersists:
sm.send_drive(1.0, 1.0) # should be suppressed sm.send_drive(1.0, 1.0) # should be suppressed
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) == 0, \ assert len(vel_frames) == 0, \
"Velocity command was forwarded while ESTOP is active" "Velocity command was forwarded while ESTOP is active"
@ -158,7 +158,7 @@ class TestEstopPersists:
sm.send_drive(0.5, 0.5) sm.send_drive(0.5, 0.5)
# No mode frames should have been emitted (drive was suppressed) # No mode frames should have been emitted (drive was suppressed)
mode_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_MODE) mode_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_MODE)
assert all( assert all(
bytes(f.data) != bytes([MODE_DRIVE]) for f in mode_frames bytes(f.data) != bytes([MODE_DRIVE]) for f in mode_frames
), "MODE=DRIVE was set despite active ESTOP" ), "MODE=DRIVE was set despite active ESTOP"
@ -174,19 +174,19 @@ class TestEstopClear:
sm.send_drive(0.8, 0.8) sm.send_drive(0.8, 0.8)
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) == 1, "Velocity command not sent after ESTOP clear" assert len(vel_frames) == 1, "Velocity command not sent after ESTOP clear"
l, r = parse_velocity_cmd(bytes(vel_frames[0].data)) l, r = parse_velocity_cmd(bytes(vel_frames[0].data))
assert abs(l - 0.8) < 1e-4 assert abs(l - 0.8) < 1e-4
assert abs(r - 0.8) < 1e-4 assert abs(r - 0.8) < 1e-4
def test_estop_clear_flag_byte_is_0x00(self, mock_can_bus): def test_estop_clear_flag_byte_is_0x00(self, mock_can_bus):
"""MAMBA_CMD_ESTOP payload must be 0x00 when clearing e-stop.""" """BALANCE_CMD_ESTOP payload must be 0x00 when clearing e-stop."""
sm = EstopStateMachine(mock_can_bus) sm = EstopStateMachine(mock_can_bus)
sm.assert_estop() sm.assert_estop()
sm.clear_estop() sm.clear_estop()
estop_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_ESTOP) estop_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_ESTOP)
assert len(estop_frames) >= 2 assert len(estop_frames) >= 2
# Last ESTOP frame should be the clear # Last ESTOP frame should be the clear
assert bytes(estop_frames[-1].data) == b"\x00", \ assert bytes(estop_frames[-1].data) == b"\x00", \
@ -198,7 +198,7 @@ class TestEstopClear:
sm.assert_estop() sm.assert_estop()
sm.clear_estop() sm.clear_estop()
mode_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_MODE) mode_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_MODE)
last_mode = bytes(mode_frames[-1].data) last_mode = bytes(mode_frames[-1].data)
assert last_mode == bytes([MODE_IDLE]), \ assert last_mode == bytes([MODE_IDLE]), \
f"Mode after ESTOP clear is {last_mode!r}, expected MODE_IDLE" f"Mode after ESTOP clear is {last_mode!r}, expected MODE_IDLE"
@ -207,55 +207,55 @@ class TestEstopClear:
class TestFirmwareSideEstop: class TestFirmwareSideEstop:
def test_fc_status_estop_flag_detected(self, mock_can_bus): def test_fc_status_estop_flag_detected(self, mock_can_bus):
""" """
Simulate firmware sending estop via FC_STATUS flags (bit0=estop_active). Simulate firmware sending estop via BALANCE_STATUS flags (bit0=estop_active).
Verify the Orin bridge side correctly parses the flag. Verify the Orin bridge side correctly parses the flag.
""" """
# Build FC_STATUS with estop_active bit set (flags=0x01) # Build BALANCE_STATUS with estop_active bit set (flags=0x01)
payload = build_fc_status( payload = build_balance_status(
pitch_x10=0, pitch_x10=0,
motor_cmd=0, motor_cmd=0,
vbat_mv=24000, vbat_mv=24000,
balance_state=2, # TILT_FAULT balance_state=2, # TILT_FAULT
flags=0x01, # bit0 = estop_active flags=0x01, # bit0 = estop_active
) )
mock_can_bus.inject(FC_STATUS, payload) mock_can_bus.inject(BALANCE_STATUS, payload)
frame = mock_can_bus.recv(timeout=0.1) frame = mock_can_bus.recv(timeout=0.1)
assert frame is not None, "FC_STATUS frame not received" assert frame is not None, "BALANCE_STATUS frame not received"
parsed = parse_fc_status(bytes(frame.data)) parsed = parse_balance_status(bytes(frame.data))
assert parsed["estop_active"] is True, \ assert parsed["estop_active"] is True, \
"estop_active flag not set in FC_STATUS" "estop_active flag not set in BALANCE_STATUS"
assert parsed["balance_state"] == 2 assert parsed["balance_state"] == 2
def test_fc_status_no_estop_flag(self, mock_can_bus): def test_fc_status_no_estop_flag(self, mock_can_bus):
"""FC_STATUS with flags=0x00 must NOT set estop_active.""" """BALANCE_STATUS with flags=0x00 must NOT set estop_active."""
payload = build_fc_status(flags=0x00) payload = build_balance_status(flags=0x00)
mock_can_bus.inject(FC_STATUS, payload) mock_can_bus.inject(BALANCE_STATUS, payload)
frame = mock_can_bus.recv(timeout=0.1) frame = mock_can_bus.recv(timeout=0.1)
parsed = parse_fc_status(bytes(frame.data)) parsed = parse_balance_status(bytes(frame.data))
assert parsed["estop_active"] is False assert parsed["estop_active"] is False
def test_fc_status_armed_flag_detected(self, mock_can_bus): def test_fc_status_armed_flag_detected(self, mock_can_bus):
"""FC_STATUS flags bit1=armed must parse correctly.""" """BALANCE_STATUS flags bit1=armed must parse correctly."""
payload = build_fc_status(flags=0x02) # bit1 = armed payload = build_balance_status(flags=0x02) # bit1 = armed
mock_can_bus.inject(FC_STATUS, payload) mock_can_bus.inject(BALANCE_STATUS, payload)
frame = mock_can_bus.recv(timeout=0.1) frame = mock_can_bus.recv(timeout=0.1)
parsed = parse_fc_status(bytes(frame.data)) parsed = parse_balance_status(bytes(frame.data))
assert parsed["armed"] is True assert parsed["armed"] is True
assert parsed["estop_active"] is False assert parsed["estop_active"] is False
def test_fc_status_roundtrip(self, mock_can_bus): def test_fc_status_roundtrip(self, mock_can_bus):
"""build_fc_status → inject → recv → parse_fc_status must be identity.""" """build_balance_status → inject → recv → parse_balance_status must be identity."""
payload = build_fc_status( payload = build_balance_status(
pitch_x10=150, pitch_x10=150,
motor_cmd=-200, motor_cmd=-200,
vbat_mv=23800, vbat_mv=23800,
balance_state=1, balance_state=1,
flags=0x03, flags=0x03,
) )
mock_can_bus.inject(FC_STATUS, payload) mock_can_bus.inject(BALANCE_STATUS, payload)
frame = mock_can_bus.recv(timeout=0.1) frame = mock_can_bus.recv(timeout=0.1)
parsed = parse_fc_status(bytes(frame.data)) parsed = parse_balance_status(bytes(frame.data))
assert parsed["pitch_x10"] == 150 assert parsed["pitch_x10"] == 150
assert parsed["motor_cmd"] == -200 assert parsed["motor_cmd"] == -200
assert parsed["vbat_mv"] == 23800 assert parsed["vbat_mv"] == 23800

View File

@ -1,11 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
test_fc_vesc_broadcast.py FC_VESC broadcast and VESC STATUS integration tests. test_fc_vesc_broadcast.py BALANCE_VESC broadcast and VESC STATUS integration tests.
Covers: Covers:
- VESC STATUS extended frame for left VESC (ID 56) triggers FC_VESC broadcast - VESC STATUS extended frame for left VESC (ID 56) triggers BALANCE_VESC broadcast
- Both left (56) and right (68) VESC STATUS combined in FC_VESC - Both left (56) and right (68) VESC STATUS combined in BALANCE_VESC
- FC_VESC broadcast rate (~10 Hz) - BALANCE_VESC broadcast rate (~10 Hz)
- current_x10 scaling matches protocol spec - current_x10 scaling matches protocol spec
No ROS2 or real CAN hardware required. No ROS2 or real CAN hardware required.
@ -19,15 +19,15 @@ import pytest
from saltybot_can_e2e_test.can_mock import MockCANBus from saltybot_can_e2e_test.can_mock import MockCANBus
from saltybot_can_e2e_test.protocol_defs import ( from saltybot_can_e2e_test.protocol_defs import (
FC_VESC, BALANCE_VESC,
VESC_CAN_ID_LEFT, VESC_CAN_ID_LEFT,
VESC_CAN_ID_RIGHT, VESC_CAN_ID_RIGHT,
VESC_STATUS_ID, VESC_STATUS_ID,
VESC_SET_RPM_ID, VESC_SET_RPM_ID,
VESC_TELEM_STATE, VESC_TELEM_STATE,
build_vesc_status, build_vesc_status,
build_fc_vesc, build_balance_vesc,
parse_fc_vesc, parse_balance_vesc,
parse_vesc_status, parse_vesc_status,
) )
from saltybot_can_bridge.balance_protocol import ( from saltybot_can_bridge.balance_protocol import (
@ -44,8 +44,8 @@ class VescStatusAggregator:
""" """
Simulates the firmware logic that: Simulates the firmware logic that:
1. Receives VESC STATUS extended frames from left/right VESCs 1. Receives VESC STATUS extended frames from left/right VESCs
2. Builds an FC_VESC broadcast payload 2. Builds an BALANCE_VESC broadcast payload
3. Injects the FC_VESC frame onto the mock bus 3. Injects the BALANCE_VESC frame onto the mock bus
This represents the ESP32-S3 BALANCE Orin telemetry path. This represents the ESP32-S3 BALANCE Orin telemetry path.
""" """
@ -62,7 +62,7 @@ class VescStatusAggregator:
def process_vesc_status(self, arb_id: int, data: bytes) -> None: def process_vesc_status(self, arb_id: int, data: bytes) -> None:
""" """
Process an incoming VESC STATUS frame (extended 29-bit ID). Process an incoming VESC STATUS frame (extended 29-bit ID).
Updates internal state; broadcasts FC_VESC when at least one side is known. Updates internal state; broadcasts BALANCE_VESC when at least one side is known.
""" """
node_id = arb_id & 0xFF node_id = arb_id & 0xFF
parsed = parse_vesc_status(data) parsed = parse_vesc_status(data)
@ -77,17 +77,17 @@ class VescStatusAggregator:
self._right_current_x10 = parsed["current_x10"] self._right_current_x10 = parsed["current_x10"]
self._right_seen = True self._right_seen = True
# Broadcast FC_VESC whenever we receive any update # Broadcast BALANCE_VESC whenever we receive any update
self._broadcast_fc_vesc() self._broadcast_fc_vesc()
def _broadcast_fc_vesc(self) -> None: def _broadcast_fc_vesc(self) -> None:
payload = build_fc_vesc( payload = build_balance_vesc(
left_rpm_x10=self._left_rpm_x10, left_rpm_x10=self._left_rpm_x10,
right_rpm_x10=self._right_rpm_x10, right_rpm_x10=self._right_rpm_x10,
left_current_x10=self._left_current_x10, left_current_x10=self._left_current_x10,
right_current_x10=self._right_current_x10, right_current_x10=self._right_current_x10,
) )
self._bus.inject(FC_VESC, payload) self._bus.inject(BALANCE_VESC, payload)
def _inject_vesc_status(bus: MockCANBus, vesc_id: int, rpm: int, def _inject_vesc_status(bus: MockCANBus, vesc_id: int, rpm: int,
@ -105,7 +105,7 @@ def _inject_vesc_status(bus: MockCANBus, vesc_id: int, rpm: int,
class TestVescStatusToFcVesc: class TestVescStatusToFcVesc:
def test_left_vesc_status_triggers_broadcast(self, mock_can_bus): def test_left_vesc_status_triggers_broadcast(self, mock_can_bus):
""" """
Inject VESC STATUS for left VESC (ID 56) verify FC_VESC contains Inject VESC STATUS for left VESC (ID 56) verify BALANCE_VESC contains
the correct left RPM (rpm / 10). the correct left RPM (rpm / 10).
""" """
agg = VescStatusAggregator(mock_can_bus) agg = VescStatusAggregator(mock_can_bus)
@ -116,14 +116,14 @@ class TestVescStatusToFcVesc:
agg.process_vesc_status(arb_id, payload) agg.process_vesc_status(arb_id, payload)
frame = mock_can_bus.recv(timeout=0.1) frame = mock_can_bus.recv(timeout=0.1)
assert frame is not None, "No FC_VESC broadcast after left VESC STATUS" assert frame is not None, "No BALANCE_VESC broadcast after left VESC STATUS"
parsed = parse_fc_vesc(bytes(frame.data)) parsed = parse_balance_vesc(bytes(frame.data))
assert parsed["left_rpm_x10"] == 300, \ assert parsed["left_rpm_x10"] == 300, \
f"left_rpm_x10 {parsed['left_rpm_x10']} != 300" f"left_rpm_x10 {parsed['left_rpm_x10']} != 300"
assert abs(parsed["left_rpm"] - 3000.0) < 1.0 assert abs(parsed["left_rpm"] - 3000.0) < 1.0
def test_right_vesc_status_triggers_broadcast(self, mock_can_bus): def test_right_vesc_status_triggers_broadcast(self, mock_can_bus):
"""Inject VESC STATUS for right VESC (ID 68) → verify right RPM in FC_VESC.""" """Inject VESC STATUS for right VESC (ID 68) → verify right RPM in BALANCE_VESC."""
agg = VescStatusAggregator(mock_can_bus) agg = VescStatusAggregator(mock_can_bus)
arb_id = VESC_STATUS_ID(VESC_CAN_ID_RIGHT) arb_id = VESC_STATUS_ID(VESC_CAN_ID_RIGHT)
@ -132,7 +132,7 @@ class TestVescStatusToFcVesc:
frame = mock_can_bus.recv(timeout=0.1) frame = mock_can_bus.recv(timeout=0.1)
assert frame is not None assert frame is not None
parsed = parse_fc_vesc(bytes(frame.data)) parsed = parse_balance_vesc(bytes(frame.data))
assert parsed["right_rpm_x10"] == 200 assert parsed["right_rpm_x10"] == 200
def test_left_vesc_id_matches_constant(self): def test_left_vesc_id_matches_constant(self):
@ -150,7 +150,7 @@ class TestBothVescStatusCombined:
def test_both_vesc_status_combined_in_fc_vesc(self, mock_can_bus): def test_both_vesc_status_combined_in_fc_vesc(self, mock_can_bus):
""" """
Inject both left (56) and right (68) VESC STATUS frames. Inject both left (56) and right (68) VESC STATUS frames.
Final FC_VESC must contain both RPMs. Final BALANCE_VESC must contain both RPMs.
""" """
agg = VescStatusAggregator(mock_can_bus) agg = VescStatusAggregator(mock_can_bus)
@ -165,7 +165,7 @@ class TestBothVescStatusCombined:
build_vesc_status(rpm=-1500, current_x10=30), build_vesc_status(rpm=-1500, current_x10=30),
) )
# Drain two FC_VESC frames (one per update), check the latest # Drain two BALANCE_VESC frames (one per update), check the latest
frames = [] frames = []
while True: while True:
f = mock_can_bus.recv(timeout=0.05) f = mock_can_bus.recv(timeout=0.05)
@ -173,16 +173,16 @@ class TestBothVescStatusCombined:
break break
frames.append(f) frames.append(f)
assert len(frames) >= 2, "Expected at least 2 FC_VESC frames" assert len(frames) >= 2, "Expected at least 2 BALANCE_VESC frames"
# Last frame must have both sides # Last frame must have both sides
last = parse_fc_vesc(bytes(frames[-1].data)) last = parse_balance_vesc(bytes(frames[-1].data))
assert last["left_rpm_x10"] == 300, \ assert last["left_rpm_x10"] == 300, \
f"left_rpm_x10 {last['left_rpm_x10']} != 300" f"left_rpm_x10 {last['left_rpm_x10']} != 300"
assert last["right_rpm_x10"] == -150, \ assert last["right_rpm_x10"] == -150, \
f"right_rpm_x10 {last['right_rpm_x10']} != -150" f"right_rpm_x10 {last['right_rpm_x10']} != -150"
def test_both_vesc_currents_combined(self, mock_can_bus): def test_both_vesc_currents_combined(self, mock_can_bus):
"""Both current values must appear in FC_VESC after two STATUS frames.""" """Both current values must appear in BALANCE_VESC after two STATUS frames."""
agg = VescStatusAggregator(mock_can_bus) agg = VescStatusAggregator(mock_can_bus)
agg.process_vesc_status( agg.process_vesc_status(
VESC_STATUS_ID(VESC_CAN_ID_LEFT), VESC_STATUS_ID(VESC_CAN_ID_LEFT),
@ -198,7 +198,7 @@ class TestBothVescStatusCombined:
if f is None: if f is None:
break break
frames.append(f) frames.append(f)
last = parse_fc_vesc(bytes(frames[-1].data)) last = parse_balance_vesc(bytes(frames[-1].data))
assert last["left_current_x10"] == 55 assert last["left_current_x10"] == 55
assert last["right_current_x10"] == 42 assert last["right_current_x10"] == 42
@ -206,11 +206,11 @@ class TestBothVescStatusCombined:
class TestVescBroadcastRate: class TestVescBroadcastRate:
def test_fc_vesc_broadcast_at_10hz(self, mock_can_bus): def test_fc_vesc_broadcast_at_10hz(self, mock_can_bus):
""" """
Simulate FC_VESC broadcasts at ~10 Hz and verify the rate. Simulate BALANCE_VESC broadcasts at ~10 Hz and verify the rate.
We inject 12 frames over ~120 ms, then verify count and average interval. We inject 12 frames over ~120 ms, then verify count and average interval.
""" """
_FC_VESC_HZ = 10 _BALANCE_VESC_HZ = 10
_interval = 1.0 / _FC_VESC_HZ _interval = 1.0 / _BALANCE_VESC_HZ
timestamps = [] timestamps = []
stop_event = threading.Event() stop_event = threading.Event()
@ -219,8 +219,8 @@ class TestVescBroadcastRate:
while not stop_event.is_set(): while not stop_event.is_set():
t = time.monotonic() t = time.monotonic()
mock_can_bus.inject( mock_can_bus.inject(
FC_VESC, BALANCE_VESC,
build_fc_vesc(100, -100, 30, 30), build_balance_vesc(100, -100, 30, 30),
timestamp=t, timestamp=t,
) )
timestamps.append(t) timestamps.append(t)
@ -232,18 +232,18 @@ class TestVescBroadcastRate:
stop_event.set() stop_event.set()
t.join(timeout=0.2) t.join(timeout=0.2)
assert len(timestamps) >= 1, "No FC_VESC broadcasts in 150 ms window" assert len(timestamps) >= 1, "No BALANCE_VESC broadcasts in 150 ms window"
if len(timestamps) >= 2: if len(timestamps) >= 2:
intervals = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)] intervals = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)]
avg = sum(intervals) / len(intervals) avg = sum(intervals) / len(intervals)
# ±40 ms tolerance for OS scheduling # ±40 ms tolerance for OS scheduling
assert 0.06 <= avg <= 0.14, \ assert 0.06 <= avg <= 0.14, \
f"FC_VESC broadcast interval {avg*1000:.1f} ms not ~100 ms" f"BALANCE_VESC broadcast interval {avg*1000:.1f} ms not ~100 ms"
def test_fc_vesc_frame_is_8_bytes(self): def test_fc_vesc_frame_is_8_bytes(self):
"""FC_VESC payload must always be exactly 8 bytes.""" """BALANCE_VESC payload must always be exactly 8 bytes."""
payload = build_fc_vesc(300, -150, 55, 42) payload = build_balance_vesc(300, -150, 55, 42)
assert len(payload) == 8 assert len(payload) == 8
@ -267,16 +267,16 @@ class TestVescCurrentScaling:
assert abs(parsed["current"] - (-3.0)) < 0.01 assert abs(parsed["current"] - (-3.0)) < 0.01
def test_fc_vesc_current_x10_roundtrip(self, mock_can_bus): def test_fc_vesc_current_x10_roundtrip(self, mock_can_bus):
"""build_fc_vesc → inject → recv → parse must preserve current_x10.""" """build_balance_vesc → inject → recv → parse must preserve current_x10."""
payload = build_fc_vesc( payload = build_balance_vesc(
left_rpm_x10=200, left_rpm_x10=200,
right_rpm_x10=200, right_rpm_x10=200,
left_current_x10=55, left_current_x10=55,
right_current_x10=42, right_current_x10=42,
) )
mock_can_bus.inject(FC_VESC, payload) mock_can_bus.inject(BALANCE_VESC, payload)
frame = mock_can_bus.recv(timeout=0.1) frame = mock_can_bus.recv(timeout=0.1)
parsed = parse_fc_vesc(bytes(frame.data)) parsed = parse_balance_vesc(bytes(frame.data))
assert parsed["left_current_x10"] == 55 assert parsed["left_current_x10"] == 55
assert parsed["right_current_x10"] == 42 assert parsed["right_current_x10"] == 42
@ -306,7 +306,7 @@ class TestVescCurrentScaling:
) )
frame = mock_can_bus.recv(timeout=0.05) frame = mock_can_bus.recv(timeout=0.05)
assert frame is not None assert frame is not None
parsed = parse_fc_vesc(bytes(frame.data)) parsed = parse_balance_vesc(bytes(frame.data))
if vesc_id == VESC_CAN_ID_LEFT: if vesc_id == VESC_CAN_ID_LEFT:
assert parsed["left_rpm_x10"] == expected_rpm_x10, \ assert parsed["left_rpm_x10"] == expected_rpm_x10, \
f"left_rpm_x10={parsed['left_rpm_x10']} expected {expected_rpm_x10}" f"left_rpm_x10={parsed['left_rpm_x10']} expected {expected_rpm_x10}"

View File

@ -21,9 +21,9 @@ from saltybot_can_e2e_test.protocol_defs import (
ORIN_CMD_HEARTBEAT, ORIN_CMD_HEARTBEAT,
ORIN_CMD_ESTOP, ORIN_CMD_ESTOP,
ORIN_CMD_MODE, ORIN_CMD_MODE,
MAMBA_CMD_VELOCITY, BALANCE_CMD_VELOCITY,
MAMBA_CMD_MODE, BALANCE_CMD_MODE,
MAMBA_CMD_ESTOP, BALANCE_CMD_ESTOP,
MODE_IDLE, MODE_IDLE,
MODE_DRIVE, MODE_DRIVE,
MODE_ESTOP, MODE_ESTOP,
@ -100,9 +100,9 @@ def _simulate_estop_on_timeout(bus: MockCANBus) -> None:
self.data = bytearray(data) self.data = bytearray(data)
self.is_extended_id = False self.is_extended_id = False
bus.send(_Msg(MAMBA_CMD_VELOCITY, encode_velocity_cmd(0.0, 0.0))) bus.send(_Msg(BALANCE_CMD_VELOCITY, encode_velocity_cmd(0.0, 0.0)))
bus.send(_Msg(MAMBA_CMD_MODE, encode_mode_cmd(MODE_ESTOP))) bus.send(_Msg(BALANCE_CMD_MODE, encode_mode_cmd(MODE_ESTOP)))
bus.send(_Msg(MAMBA_CMD_ESTOP, encode_estop_cmd(True))) bus.send(_Msg(BALANCE_CMD_ESTOP, encode_estop_cmd(True)))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -121,25 +121,25 @@ class TestHeartbeatLoss:
# Simulate bridge detecting timeout and escalating # Simulate bridge detecting timeout and escalating
_simulate_estop_on_timeout(mock_can_bus) _simulate_estop_on_timeout(mock_can_bus)
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) >= 1, "Zero velocity not sent after timeout" assert len(vel_frames) >= 1, "Zero velocity not sent after timeout"
l, r = parse_velocity_cmd(bytes(vel_frames[-1].data)) l, r = parse_velocity_cmd(bytes(vel_frames[-1].data))
assert abs(l) < 1e-5, "Left not zero on timeout" assert abs(l) < 1e-5, "Left not zero on timeout"
assert abs(r) < 1e-5, "Right not zero on timeout" assert abs(r) < 1e-5, "Right not zero on timeout"
mode_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_MODE) mode_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_MODE)
assert any( assert any(
bytes(f.data) == bytes([MODE_ESTOP]) for f in mode_frames bytes(f.data) == bytes([MODE_ESTOP]) for f in mode_frames
), "ESTOP mode not asserted on heartbeat timeout" ), "ESTOP mode not asserted on heartbeat timeout"
estop_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_ESTOP) estop_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_ESTOP)
assert len(estop_frames) >= 1, "ESTOP command not sent" assert len(estop_frames) >= 1, "ESTOP command not sent"
assert bytes(estop_frames[0].data) == b"\x01" assert bytes(estop_frames[0].data) == b"\x01"
def test_heartbeat_loss_zero_velocity(self, mock_can_bus): def test_heartbeat_loss_zero_velocity(self, mock_can_bus):
"""Zero velocity frame must appear among sent frames after timeout.""" """Zero velocity frame must appear among sent frames after timeout."""
_simulate_estop_on_timeout(mock_can_bus) _simulate_estop_on_timeout(mock_can_bus)
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) >= 1 assert len(vel_frames) >= 1
for f in vel_frames: for f in vel_frames:
l, r = parse_velocity_cmd(bytes(f.data)) l, r = parse_velocity_cmd(bytes(f.data))
@ -165,20 +165,20 @@ class TestHeartbeatRecovery:
mock_can_bus.reset() mock_can_bus.reset()
# Phase 2: recovery — clear estop, restore drive mode # Phase 2: recovery — clear estop, restore drive mode
mock_can_bus.send(_Msg(MAMBA_CMD_ESTOP, encode_estop_cmd(False))) mock_can_bus.send(_Msg(BALANCE_CMD_ESTOP, encode_estop_cmd(False)))
mock_can_bus.send(_Msg(MAMBA_CMD_MODE, encode_mode_cmd(MODE_DRIVE))) mock_can_bus.send(_Msg(BALANCE_CMD_MODE, encode_mode_cmd(MODE_DRIVE)))
mock_can_bus.send(_Msg(MAMBA_CMD_VELOCITY, encode_velocity_cmd(0.5, 0.5))) mock_can_bus.send(_Msg(BALANCE_CMD_VELOCITY, encode_velocity_cmd(0.5, 0.5)))
estop_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_ESTOP) estop_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_ESTOP)
assert any(bytes(f.data) == b"\x00" for f in estop_frames), \ assert any(bytes(f.data) == b"\x00" for f in estop_frames), \
"ESTOP clear not sent on recovery" "ESTOP clear not sent on recovery"
mode_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_MODE) mode_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_MODE)
assert any( assert any(
bytes(f.data) == bytes([MODE_DRIVE]) for f in mode_frames bytes(f.data) == bytes([MODE_DRIVE]) for f in mode_frames
), "DRIVE mode not restored after recovery" ), "DRIVE mode not restored after recovery"
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) >= 1 assert len(vel_frames) >= 1
l, r = parse_velocity_cmd(bytes(vel_frames[-1].data)) l, r = parse_velocity_cmd(bytes(vel_frames[-1].data))
assert abs(l - 0.5) < 1e-4 assert abs(l - 0.5) < 1e-4

View File

@ -17,9 +17,9 @@ import pytest
from saltybot_can_e2e_test.can_mock import MockCANBus from saltybot_can_e2e_test.can_mock import MockCANBus
from saltybot_can_e2e_test.protocol_defs import ( from saltybot_can_e2e_test.protocol_defs import (
MAMBA_CMD_VELOCITY, BALANCE_CMD_VELOCITY,
MAMBA_CMD_MODE, BALANCE_CMD_MODE,
MAMBA_CMD_ESTOP, BALANCE_CMD_ESTOP,
MODE_IDLE, MODE_IDLE,
MODE_DRIVE, MODE_DRIVE,
MODE_ESTOP, MODE_ESTOP,
@ -64,12 +64,12 @@ class ModeStateMachine:
prev_mode = self._mode prev_mode = self._mode
self._mode = mode self._mode = mode
self._bus.send(_Msg(MAMBA_CMD_MODE, encode_mode_cmd(mode))) self._bus.send(_Msg(BALANCE_CMD_MODE, encode_mode_cmd(mode)))
# Side-effects of entering ESTOP from DRIVE # Side-effects of entering ESTOP from DRIVE
if mode == MODE_ESTOP and prev_mode == MODE_DRIVE: if mode == MODE_ESTOP and prev_mode == MODE_DRIVE:
self._bus.send(_Msg(MAMBA_CMD_VELOCITY, encode_velocity_cmd(0.0, 0.0))) self._bus.send(_Msg(BALANCE_CMD_VELOCITY, encode_velocity_cmd(0.0, 0.0)))
self._bus.send(_Msg(MAMBA_CMD_ESTOP, encode_estop_cmd(True))) self._bus.send(_Msg(BALANCE_CMD_ESTOP, encode_estop_cmd(True)))
return True return True
@ -79,7 +79,7 @@ class ModeStateMachine:
""" """
if self._mode != MODE_DRIVE: if self._mode != MODE_DRIVE:
return False return False
self._bus.send(_Msg(MAMBA_CMD_VELOCITY, encode_velocity_cmd(left_mps, right_mps))) self._bus.send(_Msg(BALANCE_CMD_VELOCITY, encode_velocity_cmd(left_mps, right_mps)))
return True return True
@property @property
@ -97,7 +97,7 @@ class TestIdleToDrive:
sm = ModeStateMachine(mock_can_bus) sm = ModeStateMachine(mock_can_bus)
sm.set_mode(MODE_DRIVE) sm.set_mode(MODE_DRIVE)
mode_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_MODE) mode_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_MODE)
assert len(mode_frames) == 1 assert len(mode_frames) == 1
assert bytes(mode_frames[0].data) == bytes([MODE_DRIVE]) assert bytes(mode_frames[0].data) == bytes([MODE_DRIVE])
@ -108,7 +108,7 @@ class TestIdleToDrive:
forwarded = sm.send_drive(1.0, 1.0) forwarded = sm.send_drive(1.0, 1.0)
assert forwarded is False, "Drive cmd should be blocked in IDLE mode" assert forwarded is False, "Drive cmd should be blocked in IDLE mode"
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) == 0 assert len(vel_frames) == 0
def test_drive_mode_allows_commands(self, mock_can_bus): def test_drive_mode_allows_commands(self, mock_can_bus):
@ -120,7 +120,7 @@ class TestIdleToDrive:
forwarded = sm.send_drive(0.5, 0.5) forwarded = sm.send_drive(0.5, 0.5)
assert forwarded is True assert forwarded is True
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) == 1 assert len(vel_frames) == 1
l, r = parse_velocity_cmd(bytes(vel_frames[0].data)) l, r = parse_velocity_cmd(bytes(vel_frames[0].data))
assert abs(l - 0.5) < 1e-4 assert abs(l - 0.5) < 1e-4
@ -137,7 +137,7 @@ class TestDriveToEstop:
sm.set_mode(MODE_ESTOP) sm.set_mode(MODE_ESTOP)
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) >= 1, "No velocity frame on DRIVE→ESTOP transition" assert len(vel_frames) >= 1, "No velocity frame on DRIVE→ESTOP transition"
l, r = parse_velocity_cmd(bytes(vel_frames[-1].data)) l, r = parse_velocity_cmd(bytes(vel_frames[-1].data))
assert abs(l) < 1e-5, f"Left motor {l} not zero after ESTOP" assert abs(l) < 1e-5, f"Left motor {l} not zero after ESTOP"
@ -149,7 +149,7 @@ class TestDriveToEstop:
sm.set_mode(MODE_DRIVE) sm.set_mode(MODE_DRIVE)
sm.set_mode(MODE_ESTOP) sm.set_mode(MODE_ESTOP)
mode_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_MODE) mode_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_MODE)
assert any(bytes(f.data) == bytes([MODE_ESTOP]) for f in mode_frames) assert any(bytes(f.data) == bytes([MODE_ESTOP]) for f in mode_frames)
def test_estop_blocks_subsequent_drive(self, mock_can_bus): def test_estop_blocks_subsequent_drive(self, mock_can_bus):
@ -162,7 +162,7 @@ class TestDriveToEstop:
forwarded = sm.send_drive(1.0, 1.0) forwarded = sm.send_drive(1.0, 1.0)
assert forwarded is False assert forwarded is False
vel_frames = mock_can_bus.get_sent_frames_by_id(MAMBA_CMD_VELOCITY) vel_frames = mock_can_bus.get_sent_frames_by_id(BALANCE_CMD_VELOCITY)
assert len(vel_frames) == 0 assert len(vel_frames) == 0

View File

@ -27,12 +27,7 @@ robot:
stem_od: 0.0381 # m STEM_OD = 38.1mm stem_od: 0.0381 # m STEM_OD = 38.1mm
stem_height: 1.050 # m nominal cut length stem_height: 1.050 # m nominal cut length
<<<<<<< HEAD # ── FC / IMU (ESP32-S3 BALANCE) ────────────────────────────────────────────────── # fc_x = -50mm in SCAD (front = -X SCAD = +X ROS REP-105)
# ── FC / IMU (ESP32 BALANCE) ──────────────────────────────────────────────────
=======
# ── FC / IMU (ESP32-S3 BALANCE) ──────────────────────────────────────────────────
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# fc_x = -50mm in SCAD (front = -X SCAD = +X ROS REP-105)
# z = deck_thickness/2 + mounting_pad(3mm) + standoff(6mm) = 12mm # z = deck_thickness/2 + mounting_pad(3mm) + standoff(6mm) = 12mm
imu_x: 0.050 # m forward of base_link center imu_x: 0.050 # m forward of base_link center
imu_y: 0.000 # m imu_y: 0.000 # m

View File

@ -5,12 +5,7 @@ Comprehensive hardware diagnostics and health monitoring for SaltyBot.
## Features ## Features
### Startup Checks ### Startup Checks
<<<<<<< HEAD - RPLIDAR, RealSense, VESC, Jabra mic, ESP32-S3, servos- WiFi, GPS, disk space, RAM
- RPLIDAR, RealSense, VESC, Jabra mic, ESP32 BALANCE, servos
=======
- RPLIDAR, RealSense, VESC, Jabra mic, ESP32-S3, servos
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
- WiFi, GPS, disk space, RAM
- Boot result TTS + face animation - Boot result TTS + face animation
- JSON logging - JSON logging

View File

@ -138,12 +138,7 @@ class DiagnosticsNode(Node):
self.hardware_checks["jabra"] = ("WARN", "Audio check failed", {}) self.hardware_checks["jabra"] = ("WARN", "Audio check failed", {})
def _check_stm32(self): def _check_stm32(self):
<<<<<<< HEAD
self.hardware_checks["stm32"] = ("OK", "ESP32 bridge online", {})
=======
self.hardware_checks["stm32"] = ("OK", "ESP32-S3 bridge online", {}) self.hardware_checks["stm32"] = ("OK", "ESP32-S3 bridge online", {})
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
def _check_servos(self): def _check_servos(self):
try: try:
result = subprocess.run(["i2cdetect", "-y", "1"], capture_output=True, text=True, timeout=2) result = subprocess.run(["i2cdetect", "-y", "1"], capture_output=True, text=True, timeout=2)

View File

@ -7,12 +7,7 @@
# ros2 launch saltybot_follower person_follower.launch.py follow_distance:=1.2 # ros2 launch saltybot_follower person_follower.launch.py follow_distance:=1.2
# #
# IMPORTANT: This node publishes raw /cmd_vel. The cmd_vel_bridge_node (PR #46) # IMPORTANT: This node publishes raw /cmd_vel. The cmd_vel_bridge_node (PR #46)
<<<<<<< HEAD # applies the ESC ramp, deadman switch, and ESP32-S3 AUTONOMOUS mode gate.# Do not run this node without the cmd_vel bridge running on the same robot.
# applies the ESC ramp, deadman switch, and ESP32 BALANCE AUTONOMOUS mode gate.
=======
# applies the ESC ramp, deadman switch, and ESP32-S3 AUTONOMOUS mode gate.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# Do not run this node without the cmd_vel bridge running on the same robot.
# ── Follow geometry ──────────────────────────────────────────────────────────── # ── Follow geometry ────────────────────────────────────────────────────────────
# The target distance to maintain behind the person (metres). # The target distance to maintain behind the person (metres).
@ -74,9 +69,4 @@ control_rate: 20.0 # Hz — lower than cmd_vel bridge (50Hz) by desig
# ── Mode integration ────────────────────────────────────────────────────────── # ── Mode integration ──────────────────────────────────────────────────────────
# Master enable for the follow controller. When false, node publishes zero cmd_vel. # Master enable for the follow controller. When false, node publishes zero cmd_vel.
# Toggle at runtime: ros2 param set /person_follower follow_enabled false # Toggle at runtime: ros2 param set /person_follower follow_enabled false
<<<<<<< HEAD # The cmd_vel bridge independently gates on ESP32-S3 AUTONOMOUS mode (md=2).follow_enabled: true
# The cmd_vel bridge independently gates on ESP32 BALANCE AUTONOMOUS mode (md=2).
=======
# The cmd_vel bridge independently gates on ESP32-S3 AUTONOMOUS mode (md=2).
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
follow_enabled: true

View File

@ -28,12 +28,7 @@ State machine
Safety wiring Safety wiring
------------- -------------
<<<<<<< HEAD * cmd_vel bridge (PR #46) applies ramp + deadman + ESP32-S3 AUTONOMOUS mode gate -- this node publishes raw /cmd_vel, the bridge handles hardware safety.
* cmd_vel bridge (PR #46) applies ramp + deadman + ESP32 BALANCE AUTONOMOUS mode gate --
=======
* cmd_vel bridge (PR #46) applies ramp + deadman + ESP32-S3 AUTONOMOUS mode gate --
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
this node publishes raw /cmd_vel, the bridge handles hardware safety.
* follow_enabled param (default True) lets the operator disable the controller * follow_enabled param (default True) lets the operator disable the controller
at runtime: ros2 param set /person_follower follow_enabled false at runtime: ros2 param set /person_follower follow_enabled false
* Obstacle override: if Nav2 local costmap shows a lethal cell in the forward * Obstacle override: if Nav2 local costmap shows a lethal cell in the forward

View File

@ -1,11 +1,6 @@
gimbal_node: gimbal_node:
ros__parameters: ros__parameters:
<<<<<<< HEAD # Serial port connecting to ESP32-S3 over JLINK protocol serial_port: "/dev/ttyTHS1"
# Serial port connecting to ESP32 BALANCE over JLINK protocol
=======
# Serial port connecting to ESP32-S3 over JLINK protocol
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
serial_port: "/dev/ttyTHS1"
baud_rate: 921600 baud_rate: 921600
# Soft angle limits (degrees, ± from center) # Soft angle limits (degrees, ± from center)

View File

@ -14,12 +14,7 @@ def generate_launch_description() -> LaunchDescription:
serial_port_arg = DeclareLaunchArgument( serial_port_arg = DeclareLaunchArgument(
"serial_port", "serial_port",
default_value="/dev/ttyTHS1", default_value="/dev/ttyTHS1",
<<<<<<< HEAD description="JLINK serial port to ESP32-S3", )
description="JLINK serial port to ESP32 BALANCE",
=======
description="JLINK serial port to ESP32-S3",
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
)
pan_limit_arg = DeclareLaunchArgument( pan_limit_arg = DeclareLaunchArgument(
"pan_limit_deg", "pan_limit_deg",
default_value="150.0", default_value="150.0",

View File

@ -1,12 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""gimbal_node.py — ROS2 gimbal control node for SaltyBot pan/tilt camera head (Issue #548). """gimbal_node.py — ROS2 gimbal control node for SaltyBot pan/tilt camera head (Issue #548).
<<<<<<< HEAD Controls pan/tilt gimbal via JLINK binary protocol over serial to ESP32-S3.Implements smooth trapezoidal motion profiles with configurable axis limits.
Controls pan/tilt gimbal via JLINK binary protocol over serial to ESP32 BALANCE.
=======
Controls pan/tilt gimbal via JLINK binary protocol over serial to ESP32-S3.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Implements smooth trapezoidal motion profiles with configurable axis limits.
Subscribed topics: Subscribed topics:
/saltybot/gimbal/cmd (geometry_msgs/Vector3) x=pan_deg, y=tilt_deg, z=speed_deg_s /saltybot/gimbal/cmd (geometry_msgs/Vector3) x=pan_deg, y=tilt_deg, z=speed_deg_s

View File

@ -1,25 +1,13 @@
"""jlink_gimbal.py — JLINK binary frame codec for gimbal commands (Issue #548). """jlink_gimbal.py — JLINK binary frame codec for gimbal commands (Issue #548).
<<<<<<< HEAD
Matches the JLINK protocol defined in include/jlink.h (Issue #547 ESP32 side).
Command type (Jetson ESP32 BALANCE):
=======
Matches the JLINK protocol defined in include/jlink.h (Issue #547 ESP32-S3 side). Matches the JLINK protocol defined in include/jlink.h (Issue #547 ESP32-S3 side).
Command type (Jetson ESP32-S3): Command type (Jetson ESP32-S3): 0x0B GIMBAL_POS int16 pan_x10 + int16 tilt_x10 + uint16 speed (6 bytes)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
0x0B GIMBAL_POS int16 pan_x10 + int16 tilt_x10 + uint16 speed (6 bytes)
pan_x10 = pan_deg * 10 (±1500 for ±150°) pan_x10 = pan_deg * 10 (±1500 for ±150°)
tilt_x10 = tilt_deg * 10 (±450 for ±45°) tilt_x10 = tilt_deg * 10 (±450 for ±45°)
speed = servo speed register 04095 (0 = max) speed = servo speed register 04095 (0 = max)
<<<<<<< HEAD Telemetry type (ESP32-S3 Jetson): 0x84 GIMBAL_STATE int16 pan_x10 + int16 tilt_x10 +
Telemetry type (ESP32 BALANCE Jetson):
=======
Telemetry type (ESP32-S3 Jetson):
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
0x84 GIMBAL_STATE int16 pan_x10 + int16 tilt_x10 +
uint16 pan_speed_raw + uint16 tilt_speed_raw + uint16 pan_speed_raw + uint16 tilt_speed_raw +
uint8 torque_en + uint8 rx_err_pct (10 bytes) uint8 torque_en + uint8 rx_err_pct (10 bytes)
@ -41,14 +29,8 @@ ETX = 0x03
# ── Command / telemetry type codes ───────────────────────────────────────────── # ── Command / telemetry type codes ─────────────────────────────────────────────
<<<<<<< HEAD
CMD_GIMBAL_POS = 0x0B # Jetson → ESP32 BALANCE: set pan/tilt target
TLM_GIMBAL_STATE = 0x84 # ESP32 BALANCE → Jetson: measured state
=======
CMD_GIMBAL_POS = 0x0B # Jetson → ESP32-S3: set pan/tilt target CMD_GIMBAL_POS = 0x0B # Jetson → ESP32-S3: set pan/tilt target
TLM_GIMBAL_STATE = 0x84 # ESP32-S3 → Jetson: measured state TLM_GIMBAL_STATE = 0x84 # ESP32-S3 → Jetson: measured state
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
# Speed register: 0 = maximum servo speed; 4095 = slowest non-zero speed. # Speed register: 0 = maximum servo speed; 4095 = slowest non-zero speed.
# Map deg/s to this register: speed_reg = max(0, 4095 - int(deg_s * 4095 / 360)) # Map deg/s to this register: speed_reg = max(0, 4095 - int(deg_s * 4095 / 360))
_MAX_SPEED_DEGS = 360.0 # degrees/sec at speed_reg=0 _MAX_SPEED_DEGS = 360.0 # degrees/sec at speed_reg=0

View File

@ -5,12 +5,7 @@
# #
# Topic wiring: # Topic wiring:
# /rc/joy → mode_switch_node (CRSF channels) # /rc/joy → mode_switch_node (CRSF channels)
<<<<<<< HEAD # /saltybot/balance_state → mode_switch_node (ESP32-S3 state)# /slam_toolbox/pose_with_covariance_stamped → mode_switch_node (SLAM fix)
# /saltybot/balance_state → mode_switch_node (ESP32 BALANCE state)
=======
# /saltybot/balance_state → mode_switch_node (ESP32-S3 state)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# /slam_toolbox/pose_with_covariance_stamped → mode_switch_node (SLAM fix)
# /saltybot/control_mode ← mode_switch_node (JSON mode + alpha) # /saltybot/control_mode ← mode_switch_node (JSON mode + alpha)
# /saltybot/led_pattern ← mode_switch_node (LED name) # /saltybot/led_pattern ← mode_switch_node (LED name)
# #

View File

@ -13,12 +13,7 @@ Topic graph
In RC mode (blend_alpha 0) the node publishes Twist(0,0) so the bridge In RC mode (blend_alpha 0) the node publishes Twist(0,0) so the bridge
receives zeros this is harmless because the bridge's mode gate already receives zeros this is harmless because the bridge's mode gate already
<<<<<<< HEAD
prevents autonomous commands when the ESP32 BALANCE is in RC_MANUAL.
=======
prevents autonomous commands when the ESP32-S3 is in RC_MANUAL. prevents autonomous commands when the ESP32-S3 is in RC_MANUAL.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
The bridge's existing ESC ramp handles hardware-level smoothing; The bridge's existing ESC ramp handles hardware-level smoothing;
the blend_alpha here provides the higher-level cmd_vel policy ramp. the blend_alpha here provides the higher-level cmd_vel policy ramp.

View File

@ -6,16 +6,9 @@ state machine can be exercised in unit tests without a ROS2 runtime.
Mode vocabulary Mode vocabulary
--------------- ---------------
<<<<<<< HEAD
"RC" ESP32 BALANCE executing RC pilot commands; Jetson cmd_vel blocked.
"RAMP_TO_AUTO" Transitioning RCAUTO; blend_alpha 0.01.0 over ramp_s.
"AUTO" ESP32 BALANCE executing Jetson cmd_vel; RC sticks idle.
=======
"RC" ESP32-S3 executing RC pilot commands; Jetson cmd_vel blocked. "RC" ESP32-S3 executing RC pilot commands; Jetson cmd_vel blocked.
"RAMP_TO_AUTO" Transitioning RCAUTO; blend_alpha 0.01.0 over ramp_s. "RAMP_TO_AUTO" Transitioning RCAUTO; blend_alpha 0.01.0 over ramp_s.
"AUTO" ESP32-S3 executing Jetson cmd_vel; RC sticks idle. "AUTO" ESP32-S3 executing Jetson cmd_vel; RC sticks idle. "RAMP_TO_RC" Transitioning AUTORC; blend_alpha 1.00.0 over ramp_s.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
"RAMP_TO_RC" Transitioning AUTORC; blend_alpha 1.00.0 over ramp_s.
Blend alpha Blend alpha
----------- -----------

View File

@ -9,12 +9,7 @@ Inputs
axes[stick_axes...] Roll/Pitch/Throttle/Yaw override detection axes[stick_axes...] Roll/Pitch/Throttle/Yaw override detection
/saltybot/balance_state (std_msgs/String JSON) /saltybot/balance_state (std_msgs/String JSON)
<<<<<<< HEAD
Parsed for RC link health (field "rc_link") and ESP32 BALANCE mode.
=======
Parsed for RC link health (field "rc_link") and ESP32-S3 mode. Parsed for RC link health (field "rc_link") and ESP32-S3 mode.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
<slam_fix_topic> (geometry_msgs/PoseWithCovarianceStamped) <slam_fix_topic> (geometry_msgs/PoseWithCovarianceStamped)
Any message received within slam_fix_timeout_s SLAM fix valid. Any message received within slam_fix_timeout_s SLAM fix valid.

View File

@ -1,14 +1,8 @@
vesc_can_odometry: vesc_can_odometry:
ros__parameters: ros__parameters:
# ── CAN motor IDs (used for CAN addressing) ─────────────────────────────── # ── CAN motor IDs (used for CAN addressing) ───────────────────────────────
<<<<<<< HEAD
left_can_id: 56 # left motor VESC CAN ID (ESP32 BALANCE)
right_can_id: 68 # right motor VESC CAN ID (ESP32 BALANCE)
=======
left_can_id: 56 # left motor VESC CAN ID (ESP32-S3 BALANCE) left_can_id: 56 # left motor VESC CAN ID (ESP32-S3 BALANCE)
right_can_id: 68 # right motor VESC CAN ID (ESP32-S3 BALANCE) right_can_id: 68 # right motor VESC CAN ID (ESP32-S3 BALANCE)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# ── State topic names (must match VESC telemetry publisher) ────────────── # ── State topic names (must match VESC telemetry publisher) ──────────────
left_state_topic: /vesc/left/state left_state_topic: /vesc/left/state
right_state_topic: /vesc/right/state right_state_topic: /vesc/right/state

View File

@ -12,12 +12,7 @@
# Hardware: # Hardware:
# IMU: RealSense D435i BMI055 → /imu/data # IMU: RealSense D435i BMI055 → /imu/data
# GPS: SIM7600X cellular → /gps/fix (±2.5 m CEP) # GPS: SIM7600X cellular → /gps/fix (±2.5 m CEP)
<<<<<<< HEAD # Odom: ESP32-S3 wheel encoders → /odom# RTK: ZED-F9P (optional) → /gps/fix (±2 cm CEP when use_rtk: true)
# Odom: ESP32 BALANCE wheel encoders → /odom
=======
# Odom: ESP32-S3 wheel encoders → /odom
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# RTK: ZED-F9P (optional) → /gps/fix (±2 cm CEP when use_rtk: true)
# ── Local EKF: fuses wheel odometry + IMU in odom frame ────────────────────── # ── Local EKF: fuses wheel odometry + IMU in odom frame ──────────────────────
ekf_filter_node_odom: ekf_filter_node_odom:

View File

@ -71,12 +71,7 @@ class ParameterServer(Node):
defs = { defs = {
'hardware': { 'hardware': {
'serial_port': ParamInfo('serial_port', '/dev/esp32-bridge', 'string', 'serial_port': ParamInfo('serial_port', '/dev/esp32-bridge', 'string',
<<<<<<< HEAD 'hardware', description='ESP32-S3 bridge serial port'), 'baud_rate': ParamInfo('baud_rate', 921600, 'int', 'hardware',
'hardware', description='ESP32 bridge serial port'),
=======
'hardware', description='ESP32-S3 bridge serial port'),
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
'baud_rate': ParamInfo('baud_rate', 921600, 'int', 'hardware',
min_val=9600, max_val=3000000, min_val=9600, max_val=3000000,
description='Serial baud rate'), description='Serial baud rate'),
'timeout': ParamInfo('timeout', 0.05, 'float', 'hardware', 'timeout': ParamInfo('timeout', 0.05, 'float', 'hardware',

View File

@ -370,12 +370,7 @@ class PIDAutotuneNode(Node):
ser.write(frame_set) ser.write(frame_set)
time.sleep(0.05) # allow FC to process PID_SET time.sleep(0.05) # allow FC to process PID_SET
ser.write(frame_save) ser.write(frame_save)
<<<<<<< HEAD # Flash erase takes ~1s on ESP32-S3; wait for it time.sleep(1.5)
# Flash erase takes ~1s on ESP32; wait for it
=======
# Flash erase takes ~1s on ESP32-S3; wait for it
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
time.sleep(1.5)
self.get_logger().info( self.get_logger().info(
f"Sent PID_SET+PID_SAVE to FC via {self._jlink_port}: " f"Sent PID_SET+PID_SAVE to FC via {self._jlink_port}: "

View File

@ -9,12 +9,7 @@
# #
# GPS source: SIM7600X → /gps/fix (NavSatFix, ±2.5m CEP) — PR #65 # GPS source: SIM7600X → /gps/fix (NavSatFix, ±2.5m CEP) — PR #65
# Heading: D435i IMU → /imu/data, converted yaw → route waypoint heading_deg # Heading: D435i IMU → /imu/data, converted yaw → route waypoint heading_deg
<<<<<<< HEAD # Odometry: ESP32-S3 wheel encoders → /odom# UWB: /uwb/target (follow-me reference, logged for context)
# Odometry: ESP32 BALANCE wheel encoders → /odom
=======
# Odometry: ESP32-S3 wheel encoders → /odom
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# UWB: /uwb/target (follow-me reference, logged for context)
route_recorder: route_recorder:
ros__parameters: ros__parameters:

View File

@ -10,12 +10,7 @@ Depends on:
saltybot-nav2 container (Nav2 action server /navigate_through_poses) saltybot-nav2 container (Nav2 action server /navigate_through_poses)
saltybot_cellular (/gps/fix from SIM7600X GPS PR #65) saltybot_cellular (/gps/fix from SIM7600X GPS PR #65)
saltybot_uwb (/uwb/target PR #66, used for context during recording) saltybot_uwb (/uwb/target PR #66, used for context during recording)
<<<<<<< HEAD ESP32-S3 bridge (/odom from wheel encoders) D435i (/imu/data for heading)
ESP32 bridge (/odom from wheel encoders)
=======
ESP32-S3 bridge (/odom from wheel encoders)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
D435i (/imu/data for heading)
Usage record a route: Usage record a route:
# Set name, start recording, ride with Tee, stop and save: # Set name, start recording, ride with Tee, stop and save:

View File

@ -5,12 +5,7 @@ Hardware
SaltyRover: 4-wheel ground robot with individual brushless ESCs. SaltyRover: 4-wheel ground robot with individual brushless ESCs.
ESCs controlled via PWM (servo-style 10002000 µs pulses). ESCs controlled via PWM (servo-style 10002000 µs pulses).
<<<<<<< HEAD
Communication: USB CDC serial to ESP32 BALANCE or Raspberry Pi Pico GPIO PWM bridge.
=======
Communication: USB CDC serial to ESP32-S3 or Raspberry Pi Pico GPIO PWM bridge. Communication: USB CDC serial to ESP32-S3 or Raspberry Pi Pico GPIO PWM bridge.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
ESC channel assignments (configurable): ESC channel assignments (configurable):
CH1 = left-front CH1 = left-front
CH2 = left-rear CH2 = left-rear

View File

@ -39,12 +39,6 @@ safety_zone:
# ── cmd_vel topics ─────────────────────────────────────────────────────── # ── cmd_vel topics ───────────────────────────────────────────────────────
# Safety zone node intercepts cmd_vel from upstream, overrides to zero on estop. # Safety zone node intercepts cmd_vel from upstream, overrides to zero on estop.
# Typical chain: # Typical chain:
<<<<<<< HEAD
# cmd_vel_mux → /cmd_vel_safe → [safety_zone: cmd_vel_input] → /cmd_vel → ESP32 BALANCE
cmd_vel_input_topic: /cmd_vel_input # upstream velocity (remap as needed)
cmd_vel_output_topic: /cmd_vel # downstream (to ESP32 bridge)
=======
# cmd_vel_mux → /cmd_vel_safe → [safety_zone: cmd_vel_input] → /cmd_vel → ESP32-S3 # cmd_vel_mux → /cmd_vel_safe → [safety_zone: cmd_vel_input] → /cmd_vel → ESP32-S3
cmd_vel_input_topic: /cmd_vel_input # upstream velocity (remap as needed) cmd_vel_input_topic: /cmd_vel_input # upstream velocity (remap as needed)
cmd_vel_output_topic: /cmd_vel # downstream (to ESP32-S3 bridge) cmd_vel_output_topic: /cmd_vel # downstream (to ESP32-S3 bridge)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)

View File

@ -143,14 +143,7 @@ class SocialEnrollmentNode(Node):
self.create_timer(0.5, self._enrollment_timeout_check) self.create_timer(0.5, self._enrollment_timeout_check)
self.get_logger().info( self.get_logger().info(
<<<<<<< HEAD f'Social enrollment node initialized. Queue: {self.queue_dir}' )
f'Social enrollment node initialized. '
f'Queue: {self.queue_dir}, '
f'Speakers: {self.speaker_embeddings_path}'
=======
f'Social enrollment node initialized. Queue: {self.queue_dir}'
>>>>>>> origin/sl-firmware/issue-400-encounter-enrollment
)
def _on_orchestrator_state(self, msg: String) -> None: def _on_orchestrator_state(self, msg: String) -> None:
"""Handle orchestrator state transitions.""" """Handle orchestrator state transitions."""
@ -170,12 +163,6 @@ class SocialEnrollmentNode(Node):
context=context, context=context,
timestamp=time.time() timestamp=time.time()
) )
<<<<<<< HEAD
self._face_embedding_timestamp = 0.0
self._voice_embedding_timestamp = 0.0
self._image_timestamp = 0.0
=======
>>>>>>> origin/sl-firmware/issue-400-encounter-enrollment
self.get_logger().info( self.get_logger().info(
f'Enrollment triggered: {name} (ID: {person_id})' f'Enrollment triggered: {name} (ID: {person_id})'
@ -193,24 +180,13 @@ class SocialEnrollmentNode(Node):
if self._enrollment_request is None: if self._enrollment_request is None:
return return
<<<<<<< HEAD
# Take first detected face embedding
=======
>>>>>>> origin/sl-firmware/issue-400-encounter-enrollment
face_emb = msg.embeddings[0] face_emb = msg.embeddings[0]
emb_array = np.frombuffer(face_emb.embedding, dtype=np.float32) emb_array = np.frombuffer(face_emb.embedding, dtype=np.float32)
if len(emb_array) == self.face_emb_dim: if len(emb_array) == self.face_emb_dim:
self._latest_face_embedding = emb_array.copy() self._latest_face_embedding = emb_array.copy()
self._face_embedding_timestamp = time.time() self._face_embedding_timestamp = time.time()
<<<<<<< HEAD
self.get_logger().debug(
f'Face embedding captured: {face_emb.track_id}'
)
=======
self.get_logger().debug(f'Face embedding captured') self.get_logger().debug(f'Face embedding captured')
>>>>>>> origin/sl-firmware/issue-400-encounter-enrollment
def _on_speaker_embedding(self, msg: String) -> None: def _on_speaker_embedding(self, msg: String) -> None:
"""Capture voice speaker embedding from ECAPA-TDNN.""" """Capture voice speaker embedding from ECAPA-TDNN."""
try: try:
@ -226,14 +202,7 @@ class SocialEnrollmentNode(Node):
if len(emb_array) == self.voice_emb_dim: if len(emb_array) == self.voice_emb_dim:
self._latest_voice_embedding = emb_array.copy() self._latest_voice_embedding = emb_array.copy()
self._voice_embedding_timestamp = time.time() self._voice_embedding_timestamp = time.time()
<<<<<<< HEAD
self.get_logger().debug(
f'Voice embedding captured: {len(emb_array)} dims'
)
=======
self.get_logger().debug(f'Voice embedding captured') self.get_logger().debug(f'Voice embedding captured')
>>>>>>> origin/sl-firmware/issue-400-encounter-enrollment
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
self.get_logger().error(f'Invalid speaker embedding JSON: {e}') self.get_logger().error(f'Invalid speaker embedding JSON: {e}')
@ -244,10 +213,6 @@ class SocialEnrollmentNode(Node):
if self._enrollment_request is None: if self._enrollment_request is None:
return return
<<<<<<< HEAD
# Store latest image
=======
>>>>>>> origin/sl-firmware/issue-400-encounter-enrollment
self._latest_image = msg self._latest_image = msg
self._image_timestamp = time.time() self._image_timestamp = time.time()
@ -261,22 +226,12 @@ class SocialEnrollmentNode(Node):
return return
now = time.time() now = time.time()
<<<<<<< HEAD
timeout = 10.0 # 10 seconds to collect embeddings
=======
timeout = 10.0 timeout = 10.0
>>>>>>> origin/sl-firmware/issue-400-encounter-enrollment
# Check if all data collected # Check if all data collected
has_face = self._latest_face_embedding is not None and \ has_face = self._latest_face_embedding is not None and \
(now - self._face_embedding_timestamp < 5.0) (now - self._face_embedding_timestamp < 5.0)
has_voice = self._latest_voice_embedding is not None and \ has_voice = self._latest_voice_embedding is not None and \
(now - self._voice_embedding_timestamp < 5.0) (now - self._voice_embedding_timestamp < 5.0)
<<<<<<< HEAD
has_image = self._latest_image is not None and \
(now - self._image_timestamp < 5.0)
=======
>>>>>>> origin/sl-firmware/issue-400-encounter-enrollment
# If we have face + voice, proceed with enrollment # If we have face + voice, proceed with enrollment
if has_face and has_voice: if has_face and has_voice:
@ -303,18 +258,7 @@ class SocialEnrollmentNode(Node):
'context': request.context, 'context': request.context,
'timestamp': request.timestamp, 'timestamp': request.timestamp,
'datetime': datetime.fromtimestamp(request.timestamp).isoformat(), 'datetime': datetime.fromtimestamp(request.timestamp).isoformat(),
<<<<<<< HEAD
'face_embedding_shape': list(self._latest_face_embedding.shape)
if self._latest_face_embedding is not None else None,
'voice_embedding_shape': list(self._latest_voice_embedding.shape)
if self._latest_voice_embedding is not None else None,
} }
# Save queue JSON
=======
}
>>>>>>> origin/sl-firmware/issue-400-encounter-enrollment
queue_file = self.queue_dir / f"enrollment_{request.person_id}_{int(request.timestamp)}.json" queue_file = self.queue_dir / f"enrollment_{request.person_id}_{int(request.timestamp)}.json"
with open(queue_file, 'w') as f: with open(queue_file, 'w') as f:
json.dump(enroll_data, f, indent=2) json.dump(enroll_data, f, indent=2)
@ -418,10 +362,6 @@ class SocialEnrollmentNode(Node):
) )
return return
<<<<<<< HEAD
# Call EnrollPerson service
=======
>>>>>>> origin/sl-firmware/issue-400-encounter-enrollment
req = EnrollPerson.Request() req = EnrollPerson.Request()
req.name = request.name req.name = request.name
req.mode = 'face' req.mode = 'face'

View File

@ -10,12 +10,7 @@
# ros2 launch saltybot_bridge cmd_vel_bridge.launch.py max_linear_vel:=8.0 # ros2 launch saltybot_bridge cmd_vel_bridge.launch.py max_linear_vel:=8.0
# #
# Data flow: # Data flow:
<<<<<<< HEAD
# person_follower → /cmd_vel_raw → [speed_controller] → /cmd_vel → cmd_vel_bridge → ESP32 BALANCE
=======
# person_follower → /cmd_vel_raw → [speed_controller] → /cmd_vel → cmd_vel_bridge → ESP32-S3 # person_follower → /cmd_vel_raw → [speed_controller] → /cmd_vel → cmd_vel_bridge → ESP32-S3
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# ── Controller ───────────────────────────────────────────────────────────────── # ── Controller ─────────────────────────────────────────────────────────────────
control_rate: 50.0 # Hz — 50ms tick, same as cmd_vel_bridge control_rate: 50.0 # Hz — 50ms tick, same as cmd_vel_bridge
input_topic: /cmd_vel_raw # Upstream cmd_vel source input_topic: /cmd_vel_raw # Upstream cmd_vel source
@ -87,20 +82,11 @@ ride:
target_vel_max: 15.0 # m/s — cap; EUC max ~30 km/h = 8.3 m/s typical target_vel_max: 15.0 # m/s — cap; EUC max ~30 km/h = 8.3 m/s typical
# ── Notes ───────────────────────────────────────────────────────────────────── # ── Notes ─────────────────────────────────────────────────────────────────────
<<<<<<< HEAD
# 1. To enable ride profile, the Jetson → ESP32 BALANCE cmd_vel_bridge must also be
# reconfigured: max_linear_vel=8.0, ramp_rate=500 → consider ramp_rate=150
# at ride speed (slower ramp = smoother balance).
#
# 2. The ESP32 BALANCE balance PID gains likely need retuning for ride speed. Expect
=======
# 1. To enable ride profile, the Jetson → ESP32-S3 cmd_vel_bridge must also be # 1. To enable ride profile, the Jetson → ESP32-S3 cmd_vel_bridge must also be
# reconfigured: max_linear_vel=8.0, ramp_rate=500 → consider ramp_rate=150 # reconfigured: max_linear_vel=8.0, ramp_rate=500 → consider ramp_rate=150
# at ride speed (slower ramp = smoother balance). # at ride speed (slower ramp = smoother balance).
# #
# 2. The ESP32-S3 balance PID gains likely need retuning for ride speed. Expect # 2. The ESP32-S3 balance PID gains likely need retuning for ride speed. Expect# increased sensitivity to pitch angle errors at 8 m/s vs 0.5 m/s.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# increased sensitivity to pitch angle errors at 8 m/s vs 0.5 m/s.
# #
# 3. Test sequence recommendation: # 3. Test sequence recommendation:
# - Validate walk profile on flat indoor surface first # - Validate walk profile on flat indoor surface first

View File

@ -10,12 +10,7 @@ cmd_vel_bridge with matching limits:
ros2 launch saltybot_bridge cmd_vel_bridge.launch.py max_linear_vel:=8.0 ros2 launch saltybot_bridge cmd_vel_bridge.launch.py max_linear_vel:=8.0
Prerequisite node pipeline: Prerequisite node pipeline:
<<<<<<< HEAD
person_follower /cmd_vel_raw [speed_controller] /cmd_vel cmd_vel_bridge ESP32 BALANCE
=======
person_follower /cmd_vel_raw [speed_controller] /cmd_vel cmd_vel_bridge ESP32-S3 person_follower /cmd_vel_raw [speed_controller] /cmd_vel cmd_vel_bridge ESP32-S3
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Usage: Usage:
# Defaults (walk profile initially, adapts via UWB + GPS): # Defaults (walk profile initially, adapts via UWB + GPS):
ros2 launch saltybot_speed_controller outdoor_speed.launch.py ros2 launch saltybot_speed_controller outdoor_speed.launch.py

View File

@ -5,12 +5,7 @@ Hardware
SaltyTank: tracked robot with left/right independent brushless ESCs. SaltyTank: tracked robot with left/right independent brushless ESCs.
ESCs controlled via PWM (servo-style 10002000 µs pulses). ESCs controlled via PWM (servo-style 10002000 µs pulses).
<<<<<<< HEAD
Communication: USB CDC serial to ESP32 BALANCE or Raspberry Pi Pico GPIO PWM bridge.
=======
Communication: USB CDC serial to ESP32-S3 or Raspberry Pi Pico GPIO PWM bridge. Communication: USB CDC serial to ESP32-S3 or Raspberry Pi Pico GPIO PWM bridge.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
ESC channel assignments (configurable): ESC channel assignments (configurable):
CH1 = left-front (or left-track in 2WD/tracked mode) CH1 = left-front (or left-track in 2WD/tracked mode)
CH2 = left-rear (mirrored in 2WD/tracked mode) CH2 = left-rear (mirrored in 2WD/tracked mode)

View File

@ -298,12 +298,7 @@ class TestBatteryMonitoring(unittest.TestCase):
rclpy.spin_once(self.node, timeout_sec=0.1) rclpy.spin_once(self.node, timeout_sec=0.1)
def test_01_battery_topic_advertised(self): def test_01_battery_topic_advertised(self):
<<<<<<< HEAD """Battery topic must be advertised (from ESP32-S3 bridge).""" self._spin(5.0)
"""Battery topic must be advertised (from ESP32 bridge)."""
=======
"""Battery topic must be advertised (from ESP32-S3 bridge)."""
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
self._spin(5.0)
all_topics = {name for name, _ in self.node.get_topic_names_and_types()} all_topics = {name for name, _ in self.node.get_topic_names_and_types()}
battery_topics = [ battery_topics = [
@ -331,12 +326,7 @@ class TestBatteryMonitoring(unittest.TestCase):
self.node.destroy_subscription(sub) self.node.destroy_subscription(sub)
if not received: if not received:
<<<<<<< HEAD
pytest.skip("Battery data not publishing (ESP32 bridge may be disabled in test mode)")
=======
pytest.skip("Battery data not publishing (ESP32-S3 bridge may be disabled in test mode)") pytest.skip("Battery data not publishing (ESP32-S3 bridge may be disabled in test mode)")
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
class TestDockingServices(unittest.TestCase): class TestDockingServices(unittest.TestCase):
"""Verify autonomous docking services are available.""" """Verify autonomous docking services are available."""

View File

@ -1,10 +1,5 @@
# VESC CAN Telemetry Node — SaltyBot dual FSESC 6.7 Pro (FW 6.6) # VESC CAN Telemetry Node — SaltyBot dual FSESC 6.7 Pro (FW 6.6)
<<<<<<< HEAD
# SocketCAN interface: can0 (SN65HVD230 transceiver on ESP32 BALANCE CAN2)
=======
# SocketCAN interface: can0 (SN65HVD230 transceiver on ESP32-S3 BALANCE CAN2) # SocketCAN interface: can0 (SN65HVD230 transceiver on ESP32-S3 BALANCE CAN2)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
vesc_telemetry: vesc_telemetry:
ros__parameters: ros__parameters:
# SocketCAN interface name # SocketCAN interface name

View File

@ -16,12 +16,7 @@
| Depth Cam | Intel RealSense D435i — 848×480 @ 90fps, BMI055 IMU | | Depth Cam | Intel RealSense D435i — 848×480 @ 90fps, BMI055 IMU |
| LIDAR | RPLIDAR A1M8 — 360° 2D, 12m range, ~5.5 Hz | | LIDAR | RPLIDAR A1M8 — 360° 2D, 12m range, ~5.5 Hz |
| Wide Cams | 4× IMX219 160° CSI — front/right/rear/left 90° intervals *(arriving)* | | Wide Cams | 4× IMX219 160° CSI — front/right/rear/left 90° intervals *(arriving)* |
<<<<<<< HEAD
| FC | ESP32 — UART bridge `/dev/ttyACM0` @ 921600 |
=======
| FC | ESP32-S3 — UART bridge `/dev/ttyACM0` @ 921600 | | FC | ESP32-S3 — UART bridge `/dev/ttyACM0` @ 921600 |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
--- ---
## 1. OS & ROS2 ## 1. OS & ROS2
@ -80,12 +75,7 @@ Jetson Orin Nano Super (Ubuntu 22.04 / JetPack 6 / CUDA 12.x)
Nav2 stack (Phase 2b) Nav2 stack (Phase 2b)
20Hz costmap 20Hz costmap
<<<<<<< HEAD
/cmd_vel → ESP32 BALANCE
=======
/cmd_vel → ESP32-S3 /cmd_vel → ESP32-S3
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
4× IMX219 CSI (Phase 2c — pending hardware) 4× IMX219 CSI (Phase 2c — pending hardware)
front/right/rear/left 160° front/right/rear/left 160°
→ panoramic stitch, person tracking → panoramic stitch, person tracking

View File

@ -4,12 +4,7 @@ test_bno055_data.py — Issue #135: BNO055 driver unit tests.
Tests data scaling, byte parsing, calibration status extraction, Tests data scaling, byte parsing, calibration status extraction,
calibration offset packing/unpacking, and temperature handling. calibration offset packing/unpacking, and temperature handling.
<<<<<<< HEAD No HAL or ESP32-S3 hardware required pure Python logic."""
No HAL or STM32/ESP32 hardware required pure Python logic.
=======
No HAL or ESP32-S3 hardware required pure Python logic.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
"""
import struct import struct
import pytest import pytest

View File

@ -4,12 +4,7 @@ test_jlink_frames.py — Issue #120: JLink binary protocol unit tests.
Tests CRC16-XModem, frame building, frame parsing (state machine), Tests CRC16-XModem, frame building, frame parsing (state machine),
command payload encoding, and telemetry frame layout. command payload encoding, and telemetry frame layout.
<<<<<<< HEAD No HAL or ESP32-S3 hardware required pure Python logic."""
No HAL or STM32/ESP32 hardware required pure Python logic.
=======
No HAL or ESP32-S3 hardware required pure Python logic.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
"""
import struct import struct
import pytest import pytest

View File

@ -3,14 +3,12 @@ test_ota.py — OTA firmware update utilities (Issue #124)
Tests: Tests:
- CRC-32/ISO-HDLC (crc32_file / binascii.crc32) - CRC-32/ISO-HDLC (crc32_file / binascii.crc32)
<<<<<<< HEAD - CRC-32/MPEG-2 (stm32_crc32 legacy CRC utility, archived to legacy/stm32/)
- CRC-32/MPEG-2 (stm32_crc32 matches ESP32 hardware CRC unit)
=======
- CRC-32/MPEG-2 (stm32_crc32 matches ESP32-S3 hardware CRC unit)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
- CRC-16/XMODEM (_crc16_xmodem JLink frame integrity) - CRC-16/XMODEM (_crc16_xmodem JLink frame integrity)
- DFU_ENTER frame (JLINK_CMD_DFU_ENTER = 0x06, no payload) - DFU_ENTER frame (JLINK_CMD_DFU_ENTER = 0x06, no payload)
- Safety constants (BKP index, flash region, magic value) - Safety constants (BKP index, flash region, magic value)
NOTE: flash_firmware.py has been archived to legacy/stm32/scripts/flash_firmware.py.
""" """
import binascii import binascii
@ -21,8 +19,8 @@ import tempfile
import pytest import pytest
# Add scripts directory to path so we can import flash_firmware.py # Add legacy scripts directory to path so we can import flash_firmware.py
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'legacy', 'stm32', 'scripts'))
from flash_firmware import ( from flash_firmware import (
crc32_file, crc32_file,
stm32_crc32, stm32_crc32,
@ -237,14 +235,8 @@ class TestOtaConstants:
BNO055_BKP_RANGE = range(0, 7) BNO055_BKP_RANGE = range(0, 7)
assert OTA_DFU_BKP_IDX not in BNO055_BKP_RANGE assert OTA_DFU_BKP_IDX not in BNO055_BKP_RANGE
<<<<<<< HEAD
def test_bkp_idx_valid_esp32(self):
"""ESP32 has 32 backup registers (BKP0RBKP31R)."""
=======
def test_bkp_idx_valid_esp32s3(self): def test_bkp_idx_valid_esp32s3(self):
"""ESP32-S3 has 32 backup registers (BKP0RBKP31R).""" """ESP32-S3 has 32 backup registers (BKP0RBKP31R).""" OTA_DFU_BKP_IDX = 15
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
OTA_DFU_BKP_IDX = 15
assert 0 <= OTA_DFU_BKP_IDX <= 31 assert 0 <= OTA_DFU_BKP_IDX <= 31
def test_flash_base(self): def test_flash_base(self):
@ -261,12 +253,7 @@ class TestOtaConstants:
assert DFU_VID == 0x0483 assert DFU_VID == 0x0483
def test_dfu_pid_dfu_mode(self): def test_dfu_pid_dfu_mode(self):
<<<<<<< HEAD """Default PID = 0xDF11 (ESP32-S3 DFU mode).""" assert DFU_PID == 0xDF11
"""Default PID = 0xDF11 (ESP32 DFU mode)."""
=======
"""Default PID = 0xDF11 (ESP32-S3 DFU mode)."""
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
assert DFU_PID == 0xDF11
def test_bkp_idx_not_zero(self): def test_bkp_idx_not_zero(self):
"""BKP15 ≠ 0 — the old request_bootloader() used BKP0R.""" """BKP15 ≠ 0 — the old request_bootloader() used BKP0R."""