feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only #715

Merged
sl-jetson merged 2 commits from sl-firmware/cleanup-legacy-hw into main 2026-04-04 09:00:56 -04:00
245 changed files with 1691 additions and 335 deletions

View File

@ -7,7 +7,11 @@ 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 CDC to the ESP32 BALANCE firmware - 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 - 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
@ -42,7 +46,11 @@ The robot can now be armed and operated autonomously from the Jetson without req
## Command Protocol ## Command Protocol
<<<<<<< HEAD
### From Jetson to ESP32 BALANCE (USB CDC) ### 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)
@ -52,7 +60,11 @@ 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 BALANCE to Jetson (USB CDC) ### 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`: 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,6 +1,7 @@
# SaltyLab Firmware — Agent Playbook # SaltyLab Firmware — Agent Playbook
## Project ## Project
<<<<<<< HEAD
**SAUL-TEE** — 4-wheel wagon (870×510×550 mm, 23 kg). **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` Two ESP32-S3 boards + Jetson Orin via CAN. Full spec: `docs/SAUL-TEE-SYSTEM-REFERENCE.md`
@ -11,16 +12,25 @@ Two ESP32-S3 boards + Jetson Orin via CAN. Full spec: `docs/SAUL-TEE-SYSTEM-REFE
| **Jetson Orin** | AI/SLAM, CANable2 USB→CAN, cmds 0x3000x303, telemetry 0x4000x401 | | **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/`. > **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.
>>>>>>> 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-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-controls** | Control Systems Engineer | PID tuning, IMU fusion, balance loop, safety |
| **sl-perception** | Perception / SLAM Engineer | Jetson Orin, RealSense D435i, RPLIDAR, ROS2, Nav2 | | **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-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 |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Status ## Status
USB CDC 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).
## Repo Structure ## Repo Structure
- `projects/saltybot/SALTYLAB.md` — Design doc - `projects/saltybot/SALTYLAB.md` — Design doc
@ -38,11 +48,11 @@ USB CDC TX bug resolved (PR #10 — DCache MPU non-cacheable region + IWDG order
| `saltyrover-dev` | Integration — rover variant | | `saltyrover-dev` | Integration — rover variant |
| `saltytank` | Stable — tracked tank variant | | `saltytank` | Stable — tracked tank variant |
| `saltytank-dev` | Integration — tank variant | | `saltytank-dev` | Integration — tank variant |
| `main` | Shared code only (IMU drivers, USB CDC, balance core, safety) | | `main` | Shared code only (IMU drivers, USB Serial (CH343), balance core, safety) |
### Rules ### Rules
- Agents branch FROM `<variant>-dev` and PR back TO `<variant>-dev` - Agents branch FROM `<variant>-dev` and PR back TO `<variant>-dev`
- Shared/infrastructure code (IMU drivers, USB CDC, balance core, safety) goes in `main` - Shared/infrastructure code (IMU drivers, USB Serial (CH343), balance core, safety) goes in `main`
- Variant-specific code (motor topology, kinematics, config) goes in variant branches - Variant-specific code (motor topology, kinematics, config) goes in variant branches
- Stable branches get promoted from `-dev` after review and hardware testing - Stable branches get promoted from `-dev` after review and hardware testing
- **Current SaltyLab team** works against `saltylab-dev` - **Current SaltyLab team** works against `saltylab-dev`

35
TEAM.md
View File

@ -1,6 +1,7 @@
# SaltyLab — Ideal Team # SaltyLab — Ideal Team
## Project ## Project
<<<<<<< HEAD
**SAUL-TEE** — 4-wheel wagon (870×510×550 mm, 23 kg). **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`. Two ESP32-S3 boards (BALANCE + IO) + Jetson Orin. See `docs/SAUL-TEE-SYSTEM-REFERENCE.md`.
@ -8,6 +9,14 @@ Two ESP32-S3 boards (BALANCE + IO) + Jetson Orin. See `docs/SAUL-TEE-SYSTEM-REFE
- **Hardware:** ESP32-S3 BALANCE (Waveshare Touch LCD 1.28, CH343 USB) + ESP32-S3 IO (bare devkit, JTAG USB) - **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 - **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) - **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.
## Current Status
- **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
- **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)
--- ---
@ -15,18 +24,30 @@ Two ESP32-S3 boards (BALANCE + IO) + Jetson Orin. See `docs/SAUL-TEE-SYSTEM-REFE
### 1. Embedded Firmware Engineer (Lead) ### 1. Embedded Firmware Engineer (Lead)
**Must-have:** **Must-have:**
<<<<<<< HEAD
- Deep ESP32 (Arduino/ESP-IDF) or STM32 HAL experience - Deep ESP32 (Arduino/ESP-IDF) or STM32 HAL experience
- USB OTG FS / CDC ACM debugging (TxState, endpoint management, DMA conflicts) - USB OTG FS / CDC ACM debugging (TxState, endpoint management, DMA conflicts)
- SPI + UART + USB coexistence on ESP32 - SPI + UART + USB coexistence on ESP32
- PlatformIO or bare-metal ESP32 toolchain - PlatformIO or bare-metal ESP32 toolchain
- DFU bootloader implementation - DFU bootloader implementation
=======
- Deep ESP-IDF experience (ESP32-S3 specifically)
- USB Serial (CH343) / UART debugging on ESP32-S3
- SPI + UART + USB coexistence on ESP32-S3
- ESP-IDF / Arduino-ESP32 toolchain
- OTA firmware update implementation
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
**Nice-to-have:** **Nice-to-have:**
- Betaflight/iNav/ArduPilot codebase familiarity - 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. 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.
>>>>>>> 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:**
@ -44,7 +65,7 @@ Two ESP32-S3 boards (BALANCE + IO) + Jetson Orin. See `docs/SAUL-TEE-SYSTEM-REFE
### 3. Perception / SLAM Engineer (Phase 2) ### 3. Perception / SLAM Engineer (Phase 2)
**Must-have:** **Must-have:**
- Jetson Nano / NVIDIA Jetson platform - Jetson Orin Nano Super / NVIDIA Jetson platform
- Intel RealSense D435i depth camera - Intel RealSense D435i depth camera
- RPLIDAR integration - RPLIDAR integration
- SLAM (ORB-SLAM3, RTAB-Map, or similar) - SLAM (ORB-SLAM3, RTAB-Map, or similar)
@ -55,19 +76,23 @@ Two ESP32-S3 boards (BALANCE + IO) + Jetson Orin. See `docs/SAUL-TEE-SYSTEM-REFE
- Obstacle avoidance - Obstacle avoidance
- Nav2 stack - Nav2 stack
**Why:** Phase 2 goal is autonomous navigation. Jetson Nano with RealSense + RPLIDAR for indoor mapping and person following. **Why:** Phase 2 goal is autonomous navigation. Jetson Orin Nano Super with RealSense + RPLIDAR for indoor mapping and person following.
--- ---
## Hardware Reference ## Hardware Reference
| Component | Details | | Component | Details |
|-----------|---------| |-----------|---------|
<<<<<<< HEAD
| FC | ESP32 BALANCE (ESP32RET6, MPU6000) | | 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 | | 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 |
| AI Brain | Jetson Nano + Noctua fan | | AI Brain | Jetson Orin Nano Super + Noctua fan |
| Depth | Intel RealSense D435i | | Depth | Intel RealSense D435i |
| LIDAR | RPLIDAR A1M8 | | LIDAR | RPLIDAR A1M8 |
| Spare IMUs | BNO055, MPU6050 | | Spare IMUs | BNO055, MPU6050 |
@ -75,4 +100,4 @@ Two ESP32-S3 boards (BALANCE + IO) + Jetson Orin. See `docs/SAUL-TEE-SYSTEM-REFE
## Repo ## Repo
- Gitea: https://gitea.vayrette.com/seb/saltylab-firmware - Gitea: https://gitea.vayrette.com/seb/saltylab-firmware
- Design doc: `projects/saltybot/SALTYLAB.md` - Design doc: `projects/saltybot/SALTYLAB.md`
- Bug doc: `USB_CDC_BUG.md` - Bug doc: `legacy/stm32/USB_CDC_BUG.md` (archived — STM32 era)

View File

@ -60,7 +60,7 @@ color("Purple", 0.9)
translate([0, 0, h_fc]) translate([0, 0, h_fc])
cube([36, 36, 5], center=true); cube([36, 36, 5], center=true);
// Jetson Nano // Jetson Orin Nano Super
color("LimeGreen", 0.7) color("LimeGreen", 0.7)
translate([0, 0, h_jetson]) translate([0, 0, h_jetson])
cube([100, 80, 29], center=true); cube([100, 80, 29], center=true);

View File

@ -20,7 +20,7 @@ fc_hole_dia = 3.2; // M3 clearance
fc_board_size = 36; // Typical FC PCB fc_board_size = 36; // Typical FC PCB
fc_standoff_h = 5; // Rubber standoff height fc_standoff_h = 5; // Rubber standoff height
// --- Jetson Nano --- // --- Jetson Orin Nano Super ---
jetson_w = 100; jetson_w = 100;
jetson_d = 80; jetson_d = 80;
jetson_h = 29; // With heatsink jetson_h = 29; // With heatsink

View File

@ -1,7 +1,7 @@
// ============================================ // ============================================
// SaltyLab Jetson Nano Shelf // SaltyLab Jetson Orin Nano Super Shelf
// 120×100×15mm PETG // 120×100×15mm PETG
// Mounts Jetson Nano to 2020 extrusion // Mounts Jetson Orin Nano Super to 2020 extrusion
// ============================================ // ============================================
include <dimensions.scad> include <dimensions.scad>

View File

@ -56,20 +56,24 @@
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 MCU mount (ESP32 BALANCE + ESP32 IO) ### 7 MCU mount (ESP32 BALANCE + ESP32 IO)
> ⚠️ **ARCHITECTURE CHANGE (2026-04-03):** ESP32 BALANCE retired. Two ESP32 boards replace it. > ⚠️ **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. > 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. 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.
### 8 Jetson Nano mount plate ### 8 Jetson Orin Nano Super mount plate
1. Press or thread M3 nylon standoffs (8mm) into plate holes. 1. Press or thread M3 nylon standoffs (8mm) into plate holes.
2. Bolt plate to deck: 4× M3×10 SHCS at deck corners. 2. Bolt plate to deck: 4× M3×10 SHCS at deck corners.
3. Set Jetson Nano B01 carrier onto plate standoffs; fasten M3×6 BHCS. 3. Set Jetson Orin Nano Super B01 carrier onto plate standoffs; fasten M3×6 BHCS.
### 9 Bumper brackets ### 9 Bumper brackets
1. Slide 22mm EMT conduit through saddle clamp openings. 1. Slide 22mm EMT conduit through saddle clamp openings.

View File

@ -41,7 +41,11 @@ 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 | MCU standoff M3×6mm nylon | 4 | Nylon | — | ESP32 BALANCE / IO board isolation (dimensions TBD) | | 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 | | 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
@ -70,7 +74,7 @@ PR #7 (`chassis_frame.scad`) used placeholder values. The table below records th
| 10 | Motor fork bracket (R) | 1 | 8mm 6061 aluminium | Mirror of item 9 | | 10 | Motor fork bracket (R) | 1 | 8mm 6061 aluminium | Mirror of item 9 |
| 11 | Battery tray | 1 | 3mm PETG FDM or 3mm aluminium fold | `chassis_frame.scad``battery_tray()` module | | 11 | Battery tray | 1 | 3mm PETG FDM or 3mm aluminium fold | `chassis_frame.scad``battery_tray()` module |
| 12 | FC mount plate / standoffs | 1 set | PETG or nylon FDM | Includes 4× M3 nylon standoffs, 6mm height | | 12 | FC mount plate / standoffs | 1 set | PETG or nylon FDM | Includes 4× M3 nylon standoffs, 6mm height |
| 13 | Jetson Nano mount plate | 1 | 4mm 5052 aluminium or 4mm PETG FDM | B01 58×58mm hole pattern | | 13 | Jetson Orin Nano Super mount plate | 1 | 4mm 5052 aluminium or 4mm PETG FDM | B01 58×58mm hole pattern |
| 14 | Front bumper bracket | 1 | 5mm PETG FDM | Saddle clamps for 22mm EMT conduit | | 14 | Front bumper bracket | 1 | 5mm PETG FDM | Saddle clamps for 22mm EMT conduit |
| 15 | Rear bumper bracket | 1 | 5mm PETG FDM | Mirror of item 14 | | 15 | Rear bumper bracket | 1 | 5mm PETG FDM | Mirror of item 14 |
@ -93,11 +97,18 @@ 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 | | 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 | | 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 | | 14 | Nylon M3 standoff 6mm | 4 | F/F nylon | ESP32 board isolation |
| 15 | Anti-vibration grommet M3 | 4 | Ø6mm silicone | Under ESP32 mount pads | | 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 | | 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 |
| 14 | Nylon M3 standoff 6mm | 4 | F/F nylon | FC vibration isolation |
| 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 |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| 17 | Nylon M3 standoff 8mm | 4 | F/F nylon | Jetson board standoffs | | 17 | Nylon M3 standoff 8mm | 4 | F/F nylon | Jetson board standoffs |
--- ---

View File

@ -8,9 +8,9 @@
// Requirements: // Requirements:
// - 600mm wheelbase // - 600mm wheelbase
// - 2x hoverboard hub motors (170mm OD) // - 2x hoverboard hub motors (170mm OD)
// - STM32 MAMBA F722S FC mount (30.5x30.5mm pattern) // - ESP32-S3 ESP32-S3 BALANCE FC mount (30.5x30.5mm pattern)
// - Battery tray (24V 4Ah ~180x70x50mm pack) // - Battery tray (24V 4Ah ~180x70x50mm pack)
// - Jetson Nano B01 mount plate (100x80mm, M3 holes) // - Jetson Orin Nano Super B01 mount plate (100x80mm, M3 holes)
// - Front/rear bumper brackets // - Front/rear bumper brackets
// ============================================================================= // =============================================================================
@ -37,7 +37,7 @@ MOTOR_FORK_H = 80; // mm, total height of motor fork bracket
MOTOR_FORK_T = 8; // mm, fork plate thickness MOTOR_FORK_T = 8; // mm, fork plate thickness
AXLE_HEIGHT = 310; // mm, axle CL above ground (motor radius + clearance) AXLE_HEIGHT = 310; // mm, axle CL above ground (motor radius + clearance)
// FC mount (MAMBA F722S 30.5 × 30.5 mm M3 pattern) // FC mount (ESP32-S3 BALANCE 30.5 × 30.5 mm M3 pattern)
FC_MOUNT_SPACING = 30.5; // mm, hole pattern pitch FC_MOUNT_SPACING = 30.5; // mm, hole pattern pitch
FC_MOUNT_HOLE_D = 3.2; // mm, M3 clearance FC_MOUNT_HOLE_D = 3.2; // mm, M3 clearance
FC_STANDOFF_H = 6; // mm, standoff height FC_STANDOFF_H = 6; // mm, standoff height
@ -52,7 +52,7 @@ BATT_FLOOR = 4; // mm, tray floor thickness
BATT_STRAP_W = 20; // mm, Velcro strap slot width BATT_STRAP_W = 20; // mm, Velcro strap slot width
BATT_STRAP_T = 2; // mm, strap slot depth BATT_STRAP_T = 2; // mm, strap slot depth
// Jetson Nano B01 mount plate // Jetson Orin Nano Super B01 mount plate
// B01 carrier board hole pattern: 58 x 58 mm M3 (inner) + corner pass-throughs // B01 carrier board hole pattern: 58 x 58 mm M3 (inner) + corner pass-throughs
JETSON_HOLE_PITCH = 58; // mm, M3 mounting hole pattern JETSON_HOLE_PITCH = 58; // mm, M3 mounting hole pattern
JETSON_HOLE_D = 3.2; // mm JETSON_HOLE_D = 3.2; // mm
@ -210,7 +210,7 @@ module battery_tray() {
// FC mount holes helper // FC mount holes helper
module fc_mount_holes(z_offset=0, depth=10) { module fc_mount_holes(z_offset=0, depth=10) {
// MAMBA F722S: 30.5×30.5 mm M3 pattern, centred at origin // ESP32-S3 BALANCE: 30.5×30.5 mm M3 pattern, centred at origin
for (x = [-FC_MOUNT_SPACING/2, FC_MOUNT_SPACING/2]) for (x = [-FC_MOUNT_SPACING/2, FC_MOUNT_SPACING/2])
for (y = [-FC_MOUNT_SPACING/2, FC_MOUNT_SPACING/2]) for (y = [-FC_MOUNT_SPACING/2, FC_MOUNT_SPACING/2])
translate([x, y, z_offset]) translate([x, y, z_offset])
@ -247,7 +247,7 @@ module fc_mount_plate() {
} }
} }
// Jetson Nano B01 mount plate // Jetson Orin Nano Super B01 mount plate
// Positioned rear of deck, elevated on standoffs // Positioned rear of deck, elevated on standoffs
module jetson_mount_plate() { module jetson_mount_plate() {
jet_x = 60; // offset toward rear jet_x = 60; // offset toward rear

View File

@ -104,7 +104,11 @@ 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 BALANCE) | Passive; FC has own EMI shield | 85 °C | <60 °C ambient OK | | 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 | | 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 | — |

View File

@ -65,7 +65,7 @@ CLAMP_ALIGN_D = 4.1; // Ø4 pin
// D-cut bore clearance // D-cut bore clearance
DCUT_CL = 0.3; DCUT_CL = 0.3;
// FC mount MAMBA F722S 30.5 × 30.5 mm M3 // FC mount ESP32-S3 BALANCE 30.5 × 30.5 mm M3
FC_PITCH = 30.5; FC_PITCH = 30.5;
FC_HOLE_D = 3.2; FC_HOLE_D = 3.2;
// FC is offset toward front of plate (away from stem) // FC is offset toward front of plate (away from stem)
@ -202,7 +202,7 @@ module base_plate() {
translate([STEM_FLANGE_BC/2, 0, -1]) translate([STEM_FLANGE_BC/2, 0, -1])
cylinder(d=M5, h=PLATE_THICK + 2); cylinder(d=M5, h=PLATE_THICK + 2);
// FC mount (MAMBA F722S 30.5 × 30.5 M3) // FC mount (ESP32-S3 BALANCE 30.5 × 30.5 M3)
for (x = [FC_X_OFFSET - FC_PITCH/2, FC_X_OFFSET + FC_PITCH/2]) for (x = [FC_X_OFFSET - FC_PITCH/2, FC_X_OFFSET + FC_PITCH/2])
for (y = [-FC_PITCH/2, FC_PITCH/2]) for (y = [-FC_PITCH/2, FC_PITCH/2])
translate([x, y, -1]) translate([x, y, -1])

View File

@ -11,7 +11,7 @@
// Ventilation slots all 4 walls + lid // Ventilation slots all 4 walls + lid
// //
// Shared mounting patterns (swappable with SaltyLab): // Shared mounting patterns (swappable with SaltyLab):
// FC : 30.5 × 30.5 mm M3 (MAMBA F722S / Pixhawk) // FC : 30.5 × 30.5 mm M3 (ESP32-S3 BALANCE / Pixhawk)
// Jetson: 58 × 49 mm M3 (Orin NX / Nano Devkit carrier) // Jetson: 58 × 49 mm M3 (Orin NX / Nano Devkit carrier)
// //
// Coordinate: bay centred at origin; Z=0 = deck top face. // Coordinate: bay centred at origin; Z=0 = deck top face.

View File

@ -17,7 +17,7 @@
// Weight target: <2 kg frame (excl. motors/electronics) // Weight target: <2 kg frame (excl. motors/electronics)
// //
// Shared SaltyLab patterns (swappable electronics): // Shared SaltyLab patterns (swappable electronics):
// FC : 30.5 × 30.5 mm M3 (MAMBA F722S / Pixhawk) // FC : 30.5 × 30.5 mm M3 (ESP32-S3 BALANCE / Pixhawk)
// Jetson: 58 × 49 mm M3 (Orin NX / Nano carrier board) // Jetson: 58 × 49 mm M3 (Orin NX / Nano carrier board)
// Stem : Ø25 mm bore (sensor head unchanged) // Stem : Ø25 mm bore (sensor head unchanged)
// //
@ -87,7 +87,7 @@ STEM_COLLAR_OD = 50.0;
STEM_COLLAR_H = 20.0; // raised boss height above deck top STEM_COLLAR_H = 20.0; // raised boss height above deck top
STEM_FLANGE_BC = 40.0; // 4× M4 bolt circle for stem adapter STEM_FLANGE_BC = 40.0; // 4× M4 bolt circle for stem adapter
// FC mount MAMBA F722S / Pixhawk (30.5 × 30.5 mm M3) // FC mount ESP32-S3 BALANCE / Pixhawk (30.5 × 30.5 mm M3)
// Shared with SaltyLab swappable electronics // Shared with SaltyLab swappable electronics
FC_PITCH = 30.5; FC_PITCH = 30.5;
FC_HOLE_D = 3.2; FC_HOLE_D = 3.2;

View File

@ -4,6 +4,7 @@ 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.** Full hardware spec: `docs/SAUL-TEE-SYSTEM-REFERENCE.md` — **read it before writing firmware.**
| Board | Role | | Board | Role |
@ -20,6 +21,20 @@ Jetson Orin ──CANable2──► CAN 500kbps ◄─────────
│ CAN 500kbps │ CAN 500kbps
┌─────────┴──────────┐ ┌─────────┴──────────┐
VESC Left (ID 68) VESC Right (ID 56) VESC Left (ID 68) VESC Right (ID 56)
=======
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.
2. **Jetson Orin Nano Super** — AI brain. ROS2, SLAM, person tracking. Sends velocity commands to FC via UART. Not safety-critical — FC operates independently.
```
Jetson (speed+steer via UART1) ←→ ELRS RC (UART3, kill switch)
ESP32-S3 BALANCE (MPU6000 IMU, PID balance)
▼ UART2
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]`
@ -42,10 +57,14 @@ This is not a toy. 8" hub motors + 36V battery can crush fingers, break toes, an
## Repository Layout ## Repository Layout
``` ```
<<<<<<< HEAD
firmware/ # Legacy ESP32/STM32 HAL firmware (PlatformIO, archived) 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/ ├── src/
│ ├── main.c # Entry point, clock config, main loop │ ├── main.c # Entry point, clock config, main loop
│ ├── icm42688.c # ICM-42688-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)
│ └── status.c # LED + buzzer status patterns │ └── status.c # LED + buzzer status patterns
├── include/ ├── include/
@ -56,7 +75,7 @@ firmware/ # Legacy ESP32/STM32 HAL firmware (PlatformIO, archived)
│ ├── crsf.h # ELRS CRSF protocol │ ├── crsf.h # ELRS CRSF protocol
│ ├── bmp280.h │ ├── bmp280.h
│ └── status.h │ └── status.h
├── lib/USB_CDC/ # USB CDC stack (serial over USB) ├── lib/USB_CDC/ # USB Serial (CH343) stack (serial over USB)
│ ├── src/ # CDC implementation, USB descriptors, PCD config │ ├── src/ # CDC implementation, USB descriptors, PCD config
│ └── include/ │ └── include/
└── platformio.ini # Build config └── platformio.ini # Build config
@ -89,16 +108,24 @@ PLATFORM.md # Hardware platform reference
## Hardware Quick Reference ## Hardware Quick Reference
<<<<<<< HEAD
### ESP32 BALANCE Flight Controller ### ESP32 BALANCE Flight Controller
| Spec | Value | | Spec | Value |
|------|-------| |------|-------|
| MCU | ESP32RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) | | MCU | ESP32RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) |
=======
### ESP32-S3 BALANCE Flight Controller
| Spec | Value |
|------|-------|
| MCU | ESP32-S3RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| Primary IMU | MPU6000 (WHO_AM_I = 0x68) | | 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 | ICM-42688-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 | | Betaflight Target | DIAT-MAMBAF722_2022B |
| 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 |
@ -111,7 +138,7 @@ PLATFORM.md # Hardware platform reference
| UART | Pins | Connected To | Baud | | UART | Pins | Connected To | Baud |
|------|------|-------------|------| |------|------|-------------|------|
| USART1 | PA9/PA10 | Jetson Nano | 115200 | | USART1 | PA9/PA10 | Jetson Orin Nano Super | 115200 |
| USART2 | PA2/PA3 | Hoverboard ESC | 115200 | | USART2 | PA2/PA3 | Hoverboard ESC | 115200 |
| USART3 | PB10/PB11 | ELRS Receiver | 420000 (CRSF) | | USART3 | PB10/PB11 | ELRS Receiver | 420000 (CRSF) |
| UART4 | — | Spare | — | | UART4 | — | Spare | — |
@ -132,7 +159,7 @@ PLATFORM.md # Hardware platform reference
| FC board size | ~36mm square | | FC board size | ~36mm square |
| Hub motor body | Ø200mm (~8") | | Hub motor body | Ø200mm (~8") |
| Motor axle | Ø12mm, 45mm long | | Motor axle | Ø12mm, 45mm long |
| Jetson Nano | 100×80×29mm, M2.5 holes at 86×58mm | | Jetson Orin Nano Super | 100×80×29mm, M2.5 holes at 86×58mm |
| RealSense D435i | 90×25×25mm, 1/4-20 tripod mount | | RealSense D435i | 90×25×25mm, 1/4-20 tripod mount |
| RPLIDAR A1 | Ø70×41mm, 4× M2.5 on Ø67mm circle | | RPLIDAR A1 | Ø70×41mm, 4× M2.5 on Ø67mm circle |
| Kill switch hole | Ø22mm panel mount | | Kill switch hole | Ø22mm panel mount |
@ -167,19 +194,27 @@ 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** — disable DCache or use cache-aligned DMA buffers with clean/invalidate. We disable it. 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. 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 CDC 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.
### DFU Reboot (Betaflight Method) ### DFU Reboot (Betaflight Method)
The firmware supports reboot-to-DFU via USB command: The firmware supports reboot-to-DFU via USB command:
1. Send `R` byte over USB CDC 1. Send `R` byte over USB Serial (CH343)
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 BALANCE bootloader at `0x1FF00000` 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 6. Board appears as DFU device, ready for `dfu-util` flash
### Build & Flash ### Build & Flash
@ -205,14 +240,14 @@ Fallback: HSI 16MHz if HSE fails (PLL M=16)
## Current Status & Known Issues ## Current Status & Known Issues
### Working ### Working
- USB CDC serial streaming (50Hz JSON: `{"ax":...,"ay":...,"az":...,"gx":...,"gy":...,"gz":...}`) - USB Serial (CH343) serial streaming (50Hz JSON: `{"ax":...,"ay":...,"az":...,"gx":...,"gy":...,"gz":...}`)
- Clock config with HSE + HSI fallback - Clock config with HSE + HSI fallback
- Reboot-to-DFU via USB 'R' command - Reboot-to-DFU via USB 'R' command
- LED status patterns (status.c) - LED status patterns (status.c)
- Web UI with WebSerial + Three.js 3D visualization - Web UI with WebSerial + Three.js 3D visualization
### Broken / In Progress ### Broken / In Progress
- **ICM-42688-P SPI reads return all zeros** — was the original IMU target, but SPI communication completely non-functional despite correct pin config. May be dead silicon. Switched to MPU6000 as primary. - **QMI8658-P SPI reads return all zeros** — was the original IMU target, but SPI communication completely non-functional despite correct pin config. May be dead silicon. Switched to MPU6000 as primary.
- **MPU6000 driver** — header exists but implementation needs completion - **MPU6000 driver** — header exists but implementation needs completion
- **PID balance loop** — not yet implemented - **PID balance loop** — not yet implemented
- **Hoverboard ESC UART** — protocol defined, driver not written - **Hoverboard ESC UART** — protocol defined, driver not written
@ -250,7 +285,7 @@ T:12.3,P:45,L:100,R:-80,S:3\n
// T=tilt°, P=PID output, L/R=motor commands, S=state (0-3) // T=tilt°, P=PID output, L/R=motor commands, S=state (0-3)
``` ```
### FC → USB CDC (50Hz JSON) ### FC → USB Serial (CH343) (50Hz JSON)
```json ```json
{"ax":123,"ay":-456,"az":16384,"gx":10,"gy":-5,"gz":3,"t":250,"p":0,"bt":0} {"ax":123,"ay":-456,"az":16384,"gx":10,"gy":-5,"gz":3,"t":250,"p":0,"bt":0}
// Raw IMU values (int16), t=temp×10, p=pressure, bt=baro temp // Raw IMU values (int16), t=temp×10, p=pressure, bt=baro temp

View File

@ -1,6 +1,10 @@
# 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 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
@ -82,7 +86,11 @@ STATUS → Echo current emotion + idle state
- Colors: Monochrome (1-bit) or RGB565 - Colors: Monochrome (1-bit) or RGB565
### Microcontroller ### Microcontroller
<<<<<<< HEAD
- ESP32xx (ESP32 BALANCE) - 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) - Available UART: USART3 (PB10=TX, PB11=RX)
- Clock: 216 MHz - Clock: 216 MHz

View File

@ -81,7 +81,7 @@
│ │ │ │
│ [RealSense D435i] │ ← Front-facing, angled down ~10° │ [RealSense D435i] │ ← Front-facing, angled down ~10°
│ │ Height: ~400mm from ground │ │ Height: ~400mm from ground
│ [Jetson Nano] │ ← Center, in ventilated enclosure │ [Jetson Orin Nano Super] │ ← Center, in ventilated enclosure
│ [WiFi/4G module] │ Noctua fan draws air through │ [WiFi/4G module] │ Noctua fan draws air through
│ │ │ │
│ [Speaker] [LEDs] │ ← Rear: audio feedback + status │ [Speaker] [LEDs] │ ← Rear: audio feedback + status
@ -173,7 +173,7 @@ PACK1 ═╤═ PACK2 (parallel, XT60)
│ │ │ │
│ └── UART TX/RX ──→ Jetson GPIO │ └── UART TX/RX ──→ Jetson GPIO
├──→ DC-DC 36V→5V ──→ Jetson Nano (barrel jack 5V/4A) ├──→ DC-DC 36V→5V ──→ Jetson Orin Nano Super (barrel jack 5V/4A)
│ ──→ USB hub (sensors) │ ──→ USB hub (sensors)
├──→ DC-DC 36V→12V ──→ LED strips ├──→ DC-DC 36V→12V ──→ LED strips

View File

@ -33,7 +33,7 @@ Self-balancing two-wheeled indoor robot with AI brain.
| Component | Voltage | Current | Power (W) | Notes | | Component | Voltage | Current | Power (W) | Notes |
|-----------|---------|---------|-----------|-------| |-----------|---------|---------|-----------|-------|
| Jetson Nano | 5V | 2-4A | 10-20W | AI inference mode: ~15W avg | | Jetson Orin Nano Super | 5V | 2-4A | 10-20W | AI inference mode: ~15W avg |
| RealSense D435i | 5V (USB) | 0.7A | 3.5W | Depth + RGB streaming | | RealSense D435i | 5V (USB) | 0.7A | 3.5W | Depth + RGB streaming |
| RPLIDAR A1M8 | 5V | 0.5A | 2.5W | Spinning at 5.5Hz | | RPLIDAR A1M8 | 5V | 0.5A | 2.5W | Spinning at 5.5Hz |
| BNO055 IMU | 3.3V | 0.01A | 0.04W | Negligible | | BNO055 IMU | 3.3V | 0.01A | 0.04W | Negligible |
@ -80,7 +80,7 @@ Self-balancing two-wheeled indoor robot with AI brain.
| Battery pack (1x) | 2500 | Estimated, weigh to verify | | Battery pack (1x) | 2500 | Estimated, weigh to verify |
| 2x 8" hub motors | 2400 | ~1200g each with tire | | 2x 8" hub motors | 2400 | ~1200g each with tire |
| ESC board | 150 | Single board | | ESC board | 150 | Single board |
| Jetson Nano + heatsink | 280 | With Noctua fan | | Jetson Orin Nano Super + heatsink | 280 | With Noctua fan |
| RealSense D435i | 72 | Very light | | RealSense D435i | 72 | Very light |
| RPLIDAR A1M8 | 170 | With motor | | RPLIDAR A1M8 | 170 | With motor |
| BNO055 breakout | 5 | Tiny | | BNO055 breakout | 5 | Tiny |
@ -233,7 +233,7 @@ Self-balancing two-wheeled indoor robot with AI brain.
0mm — Base plate 0mm — Base plate
30mm — Battery shelf (holds pack on its side) 30mm — Battery shelf (holds pack on its side)
150mm — ESC + DC-DC shelf 150mm — ESC + DC-DC shelf
250mm — Jetson Nano shelf 250mm — Jetson Orin Nano Super shelf
300mm — BNO055 (attached to spine directly) 300mm — BNO055 (attached to spine directly)
370mm — RealSense bracket (front-facing arm) 370mm — RealSense bracket (front-facing arm)
420mm — LIDAR standoff begins 420mm — LIDAR standoff begins
@ -325,7 +325,7 @@ Self-balancing two-wheeled indoor robot with AI brain.
- [ ] Assemble spine onto base plate - [ ] Assemble spine onto base plate
- [ ] Mount battery to lowest shelf (velcro straps) - [ ] Mount battery to lowest shelf (velcro straps)
- [ ] Mount ESC + DC-DC converters - [ ] Mount ESC + DC-DC converters
- [ ] Mount Jetson Nano on shelf, connect 5V power - [ ] Mount Jetson Orin Nano Super on shelf, connect 5V power
- [ ] Wire Jetson UART → ESC UART - [ ] Wire Jetson UART → ESC UART
- [ ] Install JetPack 4.6 on Jetson (if not already) - [ ] Install JetPack 4.6 on Jetson (if not already)
- [ ] Write serial bridge: Jetson Python → ESC UART commands - [ ] Write serial bridge: Jetson Python → ESC UART commands

View File

@ -32,10 +32,8 @@ Four-wheel wagon (870×510×550 mm, 23 kg). Full spec: `docs/SAUL-TEE-SYSTEM-REF
|------|--------| |------|--------|
| 2x 8" pneumatic hub motors (36 PSI) | ✅ Have | | 2x 8" pneumatic hub motors (36 PSI) | ✅ Have |
| 1x hoverboard ESC (FOC firmware) | ✅ Have | | 1x hoverboard ESC (FOC firmware) | ✅ Have |
| ~~1x Drone FC (ESP3245 + MPU-6000)~~ | ❌ RETIRED — replaced by ESP32 BALANCE | | 1x Drone FC (ESP32-S3 + QMI8658) | ✅ Have — balance brain |
| 1x ESP32 BALANCE (PID loop) | ⬜ TBD — spec from max | | 1x Jetson Orin Nano Super + Noctua fan | ✅ Have |
| 1x ESP32 IO (motors/sensors/comms) | ⬜ TBD — spec from max |
| 1x Jetson Orin + Noctua fan | ✅ Have |
| 1x RealSense D435i | ✅ Have | | 1x RealSense D435i | ✅ Have |
| 1x RPLIDAR A1M8 | ✅ Have | | 1x RPLIDAR A1M8 | ✅ Have |
| 1x battery pack (36V) | ✅ Have | | 1x battery pack (36V) | ✅ Have |
@ -51,20 +49,19 @@ Four-wheel wagon (870×510×550 mm, 23 kg). Full spec: `docs/SAUL-TEE-SYSTEM-REF
| 1x BetaFPV ELRS 2.4GHz 1W TX module | ✅ Have — RC control + kill switch | | 1x BetaFPV ELRS 2.4GHz 1W TX module | ✅ Have — RC control + kill switch |
| 1x ELRS receiver (matching) | ✅ Have — mounts on FC UART | | 1x ELRS receiver (matching) | ✅ Have — mounts on FC UART |
### Drone FC Details — GEPRC GEP-F7 AIO ### ESP32-S3 BALANCE Board Details — Waveshare ESP32-S3 Touch LCD 1.28
- **MCU:** ESP32RET6 (216MHz Cortex-M7, 512KB flash, 256KB RAM) - **MCU:** ESP32-S3RET6 (Xtensa LX7 dual-core, 240MHz, 8MB Flash, 512KB SRAM)
- **IMU:** TDK ICM-42688-P (6-axis, 32kHz gyro, ultra-low noise, SPI) ← the good one! - **IMU:** QMI8658 (6-axis, 32kHz gyro, ultra-low noise, SPI) ← the good one!
- **Flash:** 8MB Winbond W25Q64 (blackbox, unused) - **Display:** 1.28" round LCD (GC9A01 driver, 240x240)
- **OSD:** AT7456E (unused) - **DFU mode:** Hold BOOT button while plugging USB
- **4-in-1 ESC:** Built into AIO board (unused — we use hoverboard ESC) - **Firmware:** Custom balance firmware (ESP-IDF / Arduino-ESP32)
- **DFU mode:** Hold yellow BOOT button while plugging USB - **USB:** USB Serial via CH343 chip
- **Firmware:** Custom balance firmware (PlatformIO + STM32 HAL) — LEGACY, see ESP32 BALANCE - **UART assignments:**
- **UART pads (confirmed from silkscreen):** - UART0 → USB Serial (CH343) → debug/flash
- T1/R1 (bottom) → USART1 (PA9/PA10) → Jetson - UART1 → Jetson Orin Nano Super
- T2/R2 (right top) → USART2 (PA2/PA3) → Hoverboard ESC - UART2 → Hoverboard ESC
- T3/R3 (bottom) → USART3 (PB10/PB11) → ELRS receiver - UART3 → ELRS receiver
- T4/R4 (bottom) → UART4 → spare - UART4/5 → spare
- T5/R5 (right bottom) → UART5 → spare
## Architecture ## Architecture
@ -76,7 +73,7 @@ Four-wheel wagon (870×510×550 mm, 23 kg). Full spec: `docs/SAUL-TEE-SYSTEM-REF
│ RealSense │ ← Forward-facing depth+RGB │ RealSense │ ← Forward-facing depth+RGB
│ D435i │ │ D435i │
├──────────────┤ ├──────────────┤
│ Jetson Nano │ ← AI brain: navigation, person tracking │ Jetson Orin Nano Super │ ← AI brain: navigation, person tracking
│ │ Sends velocity commands via UART │ │ Sends velocity commands via UART
├──────────────┤ ├──────────────┤
│ Drone FC │ ← Balance brain: IMU + PID @ 8kHz │ Drone FC │ ← Balance brain: IMU + PID @ 8kHz
@ -94,145 +91,22 @@ Four-wheel wagon (870×510×550 mm, 23 kg). Full spec: `docs/SAUL-TEE-SYSTEM-REF
└─────┘ └─────┘ └─────┘ └─────┘
``` ```
## Self-Balancing Control — Custom Firmware on Drone FC ## Self-Balancing Control — ESP32-S3 BALANCE Board
### Why a Drone FC? > For full system architecture, firmware details, and protocol specs, see
The F745 board was a premium STM32 dev board (legacy; now replaced by ESP32 BALANCE) with a high-quality IMU (MPU-6000) already soldered on, proper voltage regulation, and multiple UARTs broken out. We write a lean custom balance firmware (~50 lines of C). > **docs/SAUL-TEE-SYSTEM-REFERENCE.md**
### Architecture The balance controller runs on the Waveshare ESP32-S3 Touch LCD 1.28 board
``` (ESP32-S3 BALANCE). It reads the onboard QMI8658 IMU at 8kHz, runs a PID
Jetson (speed+steer via UART1) balance loop, and drives the hoverboard ESC via UART. Jetson Orin Nano Super
sends velocity commands over UART1. ELRS receiver on UART3 provides RC
override and kill-switch capability.
Drone FC (F745 + MPU-6000)
│ - Reads IMU @ 8kHz (SPI)
│ - Runs PID balance loop
│ - Mixes balance correction + Jetson commands
│ - Outputs speed+steer via UART2
Hoverboard ESC (FOC firmware)
│ - Receives UART commands
│ - Drives hub motors
Left + Right wheels
```
- **No motor outputs used** — FC talks UART directly to hoverboard ESC The legacy STM32 firmware (Mamba F722S era) has been archived to
- **Custom firmware only** — no third-party flight software =======
- **Dead motor output irrelevant** — not using any PWM channels The legacy STM32 firmware (STM32 era) has been archived to
`legacy/stm32/` and is no longer built or deployed.
### Wiring >>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
```
Jetson UART1 Drone FC (UART1)
──────────── ────────────────
TX (Pin 8) ──→ RX
RX (Pin 10) ──→ TX
GND ──→ GND
Drone FC (UART2) Hoverboard ESC
──────────────── ──────────────
TX ──→ RX (serial input)
GND ──→ GND
5V (BEC) ←── ESC 5V out (powers FC)
ELRS Receiver Drone FC (UART3)
───────────── ────────────────
TX ──→ RX
RX ←── TX (for telemetry/binding)
GND ──→ GND
5V ←── 5V
```
### Custom Firmware (Legacy STM32 C — archived)
```c
// Core balance loop — runs in timer interrupt @ 1-8kHz
void balance_loop(void) {
// 1. Read pitch angle from MPU-6000 (complementary filter)
float pitch = get_pitch_angle(); // SPI read + filter
// 2. Get velocity command from Jetson (updated async via UART1 RX)
float target_speed = jetson_cmd.speed; // -1000 to 1000
float target_steer = jetson_cmd.steer; // -1000 to 1000
// 3. PID on pitch error
// Target angle shifts with speed command (lean forward = go forward)
float target_angle = target_speed * SPEED_TO_ANGLE_FACTOR;
float error = target_angle - pitch;
integral += error * dt;
integral = clamp(integral, -MAX_I, MAX_I); // anti-windup
float derivative = (error - prev_error) / dt;
prev_error = error;
float output = Kp * error + Ki * integral + Kd * derivative;
// 4. Mix balance + steering → hoverboard ESC UART command
int16_t left = clamp(output + target_steer, -1000, 1000);
int16_t right = clamp(output - target_steer, -1000, 1000);
// 5. Send to hoverboard ESC via UART2
send_hoverboard_cmd(left, right);
// 6. Safety: kill motors if tipped beyond recovery
if (fabs(pitch) > MAX_TILT_DEG) {
send_hoverboard_cmd(0, 0);
disarm();
}
// 7. Safety: RC kill switch (ELRS channel, checked every loop)
if (rc_channels.arm_switch == DISARMED) {
send_hoverboard_cmd(0, 0);
disarm();
}
// 8. Safety: kill if Jetson UART heartbeat lost
if (millis() - jetson_last_rx > JETSON_TIMEOUT_MS) {
send_hoverboard_cmd(0, 0);
disarm();
}
// 8. Safety: clamp output to max allowed speed
left = clamp(left, -max_speed_limit, max_speed_limit);
right = clamp(right, -max_speed_limit, max_speed_limit);
}
```
### Hoverboard ESC UART Protocol
```c
typedef struct {
uint16_t start; // 0xABCD
int16_t speed; // -1000 to 1000 (left)
int16_t steer; // -1000 to 1000 (right)
uint16_t checksum; // XOR of all bytes
} HoverboardCmd;
// 115200 baud, send at loop rate
```
### Jetson → FC Protocol (simple custom)
```c
typedef struct {
uint8_t header; // 0xAA
int16_t speed; // -1000 to 1000
int16_t steer; // -1000 to 1000
uint8_t mode; // 0=idle, 1=balance, 2=follow, 3=RC
uint8_t checksum;
} JetsonCmd;
// 115200 baud, ~50Hz from Jetson is plenty
```
### PID Tuning
| Param | Starting Value | Notes |
|-------|---------------|-------|
| Kp | 30-50 | Main balance response |
| Ki | 0.5-2 | Drift correction |
| Kd | 0.5-2 | Damping oscillation |
| Loop rate | 1-8 kHz | Start at 1kHz, increase if needed |
| Max tilt | ±25° | Beyond this = cut motors, require re-arm |
| JETSON_TIMEOUT_MS | 200 | Kill motors if Jetson stops talking |
| max_speed_limit | 100 | Start at 10% (100/1000), increase gradually |
| SPEED_TO_ANGLE_FACTOR | 0.01-0.05 | How much lean per speed unit |
## LED Subsystem (ESP32-C3) ## LED Subsystem (ESP32-C3)
@ -282,8 +156,8 @@ GND ──→ Common ground
``` ```
### Dev Tools ### Dev Tools
- **Flashing:** STM32CubeProgrammer via USB (DFU mode) or SWD (legacy) - **Flashing:** ESP32-S3CubeProgrammer via USB (DFU mode) or SWD
- **IDE:** PlatformIO + ESP-IDF (new) or STM32 HAL/STM32CubeIDE (legacy) - **IDE:** PlatformIO + ESP-IDF, or ESP32-S3CubeIDE
- **Debug:** SWD via ST-Link (or use FC's USB as virtual COM for printf debug) - **Debug:** SWD via ST-Link (or use FC's USB as virtual COM for printf debug)
## Physical Design ## Physical Design
@ -350,7 +224,7 @@ GND ──→ Common ground
## Software Stack ## Software Stack
### Jetson Nano ### Jetson Orin Nano Super
- **OS:** JetPack 4.6.1 (Ubuntu 18.04) - **OS:** JetPack 4.6.1 (Ubuntu 18.04)
- **ROS2 Humble** (or Foxy) for: - **ROS2 Humble** (or Foxy) for:
- `nav2` — navigation stack - `nav2` — navigation stack
@ -377,8 +251,8 @@ GND ──→ Common ground
- [ ] Install hardware kill switch inline with 36V battery (NC — press to kill) - [ ] Install hardware kill switch inline with 36V battery (NC — press to kill)
- [ ] Set up ceiling tether point above test area (rated for >15kg) - [ ] Set up ceiling tether point above test area (rated for >15kg)
- [ ] Clear test area: 3m radius, no loose items, shoes on - [ ] Clear test area: 3m radius, no loose items, shoes on
- [ ] Set up PlatformIO project for ESP32 BALANCE (ESP-IDF) - [ ] Set up PlatformIO project for ESP32-S3 (ESP-IDF)
- [ ] Write MPU-6000 SPI driver (read gyro+accel, complementary filter) - [ ] Write QMI8658 SPI driver (read gyro+accel, complementary filter)
- [ ] Write PID balance loop with ALL safety checks: - [ ] Write PID balance loop with ALL safety checks:
- ±25° tilt cutoff → disarm, require manual re-arm - ±25° tilt cutoff → disarm, require manual re-arm
- Watchdog timer (50ms hardware WDT) - Watchdog timer (50ms hardware WDT)

View File

@ -112,8 +112,13 @@ h1 { color: #e94560; margin-bottom: 5px; font-size: 1.4em; }
</style> </style>
</head> </head>
<body> <body>
<<<<<<< HEAD
<h1>🤖 GEPRC GEP-F722-45A AIO — SaltyLab Pinout (Legacy / Archived)</h1> <h1>🤖 GEPRC GEP-F722-45A AIO — SaltyLab Pinout (Legacy / Archived)</h1>
<p class="subtitle">ESP32RET6 + ICM-42688-P | Betaflight target: GEPR-GEPRC_F722_AIO</p> <p class="subtitle">ESP32RET6 + ICM-42688-P | Betaflight target: GEPR-GEPRC_F722_AIO</p>
=======
<h1>🤖 GEPRC GEP-F722-45A AIO — SaltyLab Pinout</h1>
<p class="subtitle">ESP32-S3RET6 + ICM-42688-P | Betaflight target: GEPR-GEPRC_F722_AIO</p>
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
<div class="container"> <div class="container">
<div class="board-wrap"> <div class="board-wrap">
@ -125,7 +130,11 @@ h1 { color: #e94560; margin-bottom: 5px; font-size: 1.4em; }
<div class="mount br"></div> <div class="mount br"></div>
<!-- MCU --> <!-- MCU -->
<<<<<<< HEAD
<div class="mcu"><div class="dot"></div>ESP32<br>(legacy:<br>F722RET6)</div> <div class="mcu"><div class="dot"></div>ESP32<br>(legacy:<br>F722RET6)</div>
=======
<div class="mcu"><div class="dot"></div>ESP32-S3<br>F722RET6<br>216MHz</div>
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
<!-- IMU --> <!-- IMU -->
<div class="imu">ICM<br>42688</div> <div class="imu">ICM<br>42688</div>
@ -206,7 +215,7 @@ h1 { color: #e94560; margin-bottom: 5px; font-size: 1.4em; }
<h2>🔌 UART Assignments</h2> <h2>🔌 UART Assignments</h2>
<div class="legend-item"> <div class="legend-item">
<div class="swatch" style="background:#2196F3"></div> <div class="swatch" style="background:#2196F3"></div>
<span><b>USART1</b> T1/R1 → Jetson Nano</span> <span><b>USART1</b> T1/R1 → Jetson Orin Nano Super</span>
</div> </div>
<div class="legend-item"> <div class="legend-item">
<div class="swatch" style="background:#FF9800"></div> <div class="swatch" style="background:#FF9800"></div>

View File

@ -14,8 +14,12 @@
│ ORIN NANO SUPER │ │ ORIN NANO SUPER │
│ (Top Plate — 25W) │ │ (Top Plate — 25W) │
│ │ │ │
<<<<<<< HEAD
│ USB-A ──── CANable2 USB-CAN adapter (slcan0, 500 kbps) │ │ USB-A ──── CANable2 USB-CAN adapter (slcan0, 500 kbps) │
│ USB-A ──── ESP32-S3 IO (/dev/esp32-io, 460800 baud) │ │ 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-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) │
@ -34,8 +38,13 @@
│ 500 kbps │ │ 500 kbps │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────┐
<<<<<<< HEAD
│ ESP32-S3 BALANCE │ │ ESP32-S3 BALANCE │
│ (Waveshare Touch LCD 1.28, Middle Plate) │ │ (Waveshare Touch LCD 1.28, Middle Plate) │
=======
│ ESP32-S3 BALANCE (FC) │
│ (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) │
@ -68,7 +77,11 @@
## Wire-by-Wire Connections ## Wire-by-Wire Connections
<<<<<<< HEAD
### 1. Orin <-> ESP32-S3 BALANCE (Primary: CAN Bus via CANable2) ### 1. Orin <-> ESP32-S3 BALANCE (Primary: CAN Bus via CANable2)
=======
### 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 |
|------|----|------|-------| |------|----|------|-------|
@ -76,10 +89,16 @@
| 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 - Interface: SocketCAN `slcan0`, 500 kbps
- Device node: `/dev/canable2` (via udev, symlink to ttyUSBx) - Device node: `/dev/canable2` (via udev, symlink to ttyUSBx)
- Protocol: CAN frames --- ORIN_CMD_DRIVE (0x300), ORIN_CMD_MODE (0x301), ORIN_CMD_ESTOP (0x302) - 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) - Telemetry: BALANCE_STATUS (0x400), BALANCE_VESC (0x401), BALANCE_IMU (0x402), BALANCE_BATTERY (0x403)
=======
- Device: `/dev/ttyACM0` → symlink `/dev/esp32-bridge`
- Baud: 921600, 8N1
- 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)
@ -145,6 +164,7 @@ 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) ## FC UART Summary (MAMBA F722S — OBSOLETE)
| Interface | Pins | Baud/Rate | Assignment | Notes | | Interface | Pins | Baud/Rate | Assignment | Notes |
@ -171,6 +191,19 @@ BATTERY (36V) ──┬── VESC Left (36V direct -> BLDC left motor)
| 0x910+ID | VESC Right -> | 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). VESC Left CAN ID = 56 (0x38), VESC Right CAN ID = 68 (0x44).
=======
## FC UART Summary (ESP32-S3 BALANCE)
| UART | Pins | Baud | Assignment | Notes |
|------|------|------|------------|-------|
| USART1 | PB6=TX, PB7=RX | — | SmartAudio/VTX | Unused in SaltyLab |
| USART2 | PA2=TX, PA3=RX | 26400 | Hoverboard ESC | Binary motor commands |
| USART3 | PB10=TX, PB11=RX | — | Available | Was SBUS default |
| UART4 | PA0=TX, PA1=RX | 420000 | ELRS RX (CRSF) | RC control |
| UART5 | PC12=TX, PD2=RX | 115200 | Debug serial | Optional |
| USART6 | PC6=TX, PC7=RX | 921600 | Jetson UART | Fallback link |
| 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)
@ -230,9 +263,13 @@ VESC Left CAN ID = 56 (0x38), VESC Right CAN ID = 68 (0x44).
| Device | Interface | Power Draw | | Device | Interface | Power Draw |
|--------|-----------|------------| |--------|-----------|------------|
<<<<<<< HEAD
| CANable2 USB-CAN | USB-A | ~0.5W | | CANable2 USB-CAN | USB-A | ~0.5W |
| ESP32-S3 BALANCE | USB-C | ~0.8W (WiFi off) | | ESP32-S3 BALANCE | USB-C | ~0.8W (WiFi off) |
| ESP32-S3 IO | USB-C | ~0.5W | | 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) | | 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 |
@ -257,15 +294,24 @@ 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) │ │ (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 CAN 500kbps─┘ └───── CAN bus / UART fallback
=======
USART2 ─────┘ └───── USB Serial (CH343) / USART6
26400 baud 921600 baud
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ │ │ │
┌────┴────────────┐ ▼ ┌────┴────────────┐ ▼
│ CAN bus (500k) │ ┌───────────────────┐ │ CAN bus (500k) │ ┌───────────────────┐

View File

@ -2,7 +2,7 @@
# Base: JetPack 6 (L4T R36.2.0) + CUDA 12.x / Ubuntu 22.04 # Base: JetPack 6 (L4T R36.2.0) + CUDA 12.x / Ubuntu 22.04
# #
# Hardware: Jetson Orin Nano Super 8GB (67 TOPS, 1024-core Ampere) # Hardware: Jetson Orin Nano Super 8GB (67 TOPS, 1024-core Ampere)
# Previous: Jetson Nano 4GB (JetPack 4.6 / L4T R32.6.1) — see git history # Previous: Jetson Orin Nano Super 4GB (JetPack 4.6 / L4T R32.6.1) — see git history
FROM nvcr.io/nvidia/l4t-jetpack:r36.2.0 FROM nvcr.io/nvidia/l4t-jetpack:r36.2.0

View File

@ -1,12 +1,12 @@
# Jetson Nano — AI/SLAM Platform Setup # Jetson Orin Nano Super — AI/SLAM Platform Setup
Self-balancing robot: Jetson Nano dev environment for ROS2 Humble + SLAM stack. Self-balancing robot: Jetson Orin Nano Super dev environment for ROS2 Humble + SLAM stack.
## Stack ## Stack
| Component | Version / Part | | Component | Version / Part |
|-----------|---------------| |-----------|---------------|
| Platform | Jetson Nano 4GB | | Platform | Jetson Orin Nano Super 4GB |
| JetPack | 4.6 (L4T R32.6.1, CUDA 10.2) | | JetPack | 4.6 (L4T R32.6.1, CUDA 10.2) |
| ROS2 | Humble Hawksbill | | ROS2 | Humble Hawksbill |
| DDS | CycloneDDS | | DDS | CycloneDDS |
@ -14,7 +14,11 @@ Self-balancing robot: Jetson Nano dev environment for ROS2 Humble + SLAM stack.
| 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 (USB CDC @ 921600) |
=======
| MCU bridge | ESP32-S3 (USB Serial (CH343) @ 921600) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Quick Start ## Quick Start
@ -42,7 +46,11 @@ 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 BALANCE) ├── 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 ├── README.md # This file
├── docs/ ├── docs/
│ ├── pinout.md # GPIO/I2C/UART pinout reference │ ├── pinout.md # GPIO/I2C/UART pinout reference

View File

@ -34,7 +34,11 @@ 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 BALANCE 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

View File

@ -12,7 +12,11 @@
# /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 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:

View File

@ -97,7 +97,11 @@ services:
rgb_camera.profile:=640x480x30 rgb_camera.profile:=640x480x30
" "
<<<<<<< HEAD
# ── ESP32 bridge node (bidirectional serial<->ROS2) ──────────────────────── # ── 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: esp32-bridge:
image: saltybot/ros2-humble:jetson-orin image: saltybot/ros2-humble:jetson-orin
build: build:
@ -208,8 +212,13 @@ services:
" "
<<<<<<< HEAD
# -- Remote e-stop bridge (MQTT over 4G -> ESP32 CDC) ---------------------- # -- Remote e-stop bridge (MQTT over 4G -> ESP32 CDC) ----------------------
# Subscribes to saltybot/estop MQTT topic. {"kill":true} -> 'E\r\n' to ESP32 BALANCE. # Subscribes to saltybot/estop MQTT topic. {"kill":true} -> 'E\r\n' to ESP32 BALANCE.
=======
# -- Remote e-stop bridge (MQTT over 4G -> ESP32-S3 CDC) ----------------------
# Subscribes to saltybot/estop MQTT topic. {"kill":true} -> 'E\r\n' to ESP32-S3.
>>>>>>> 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). # 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

View File

@ -1,5 +1,9 @@
# 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 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)
@ -43,21 +47,37 @@ i2cdetect -l
--- ---
<<<<<<< HEAD
## 1. ESP32 Bridge (USB CDC — Primary) ## 1. ESP32 Bridge (USB CDC — Primary)
The ESP32 BALANCE acts as a real-time motor + IMU controller. Communication is via **USB CDC serial**. 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)
### USB CDC Connection 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
| Connection | Detail | | Connection | Detail |
|-----------|--------| |-----------|--------|
<<<<<<< HEAD
| Interface | USB on ESP32 BALANCE board → USB-A on Jetson | | Interface | USB on ESP32 BALANCE 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 BALANCE firmware) | | Baud rate | 921600 (configured in ESP32 BALANCE firmware) |
=======
| Interface | USB Micro-B on ESP32-S3 dev board → USB-A on Jetson |
| Device node | `/dev/ttyACM0` → symlink `/dev/esp32-bridge` (via udev) |
| Baud rate | 921600 (configured in ESP32-S3 firmware) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| Protocol | JSON telemetry RX + ASCII command TX (see bridge docs) | | 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 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 |
@ -65,7 +85,11 @@ The ESP32 BALANCE acts as a real-time motor + IMU controller. Communication is v
**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 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
@ -75,6 +99,7 @@ sudo usermod -aG dialout $USER
picocom -b 921600 /dev/ttyTHS0 picocom -b 921600 /dev/ttyTHS0
``` ```
<<<<<<< HEAD
**ROS2 topics (ESP32 bridge node):** **ROS2 topics (ESP32 bridge node):**
| ROS2 Topic | Direction | Content | | ROS2 Topic | Direction | Content |
|-----------|-----------|--------- |-----------|-----------|---------
@ -82,6 +107,15 @@ picocom -b 921600 /dev/ttyTHS0
| `/saltybot/balance_state` | ESP32 BALANCE→Jetson | Motor cmd, pitch, state | | `/saltybot/balance_state` | ESP32 BALANCE→Jetson | Motor cmd, pitch, state |
| `/cmd_vel` | Jetson→ESP32 BALANCE | Velocity commands → `C<spd>,<str>\n` | | `/cmd_vel` | Jetson→ESP32 BALANCE | Velocity commands → `C<spd>,<str>\n` |
| `/saltybot/estop` | Jetson→ESP32 BALANCE | Emergency stop | | `/saltybot/estop` | Jetson→ESP32 BALANCE | Emergency stop |
=======
**ROS2 topics (ESP32-S3 bridge node):**
| ROS2 Topic | Direction | Content |
|-----------|-----------|---------
| `/saltybot/imu` | ESP32-S3→Jetson | IMU data (accel, gyro) at 50Hz |
| `/saltybot/balance_state` | ESP32-S3→Jetson | Motor cmd, pitch, state |
| `/cmd_vel` | Jetson→ESP32-S3 | Velocity commands → `C<spd>,<str>\n` |
| `/saltybot/estop` | Jetson→ESP32-S3 | Emergency stop |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
--- ---
@ -266,7 +300,11 @@ 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 CDC or host flash | | 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 | | Micro-USB | Debug/flash | JetPack flash only |
--- ---
@ -277,10 +315,17 @@ 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) | | 8 | TXD0 | 3.3V | UART TX → ESP32 BALANCE (fallback) |
| 10 | RXD0 | 3.3V | UART RX ← ESP32 BALANCE (fallback) | | 10 | RXD0 | 3.3V | UART RX ← ESP32 BALANCE (fallback) |
| USB-A ×2 | — | 5V | D435i, RPLIDAR | | USB-A ×2 | — | 5V | D435i, RPLIDAR |
| USB-C | — | 5V | ESP32 CDC | | USB-C | — | 5V | ESP32 CDC |
=======
| 8 | TXD0 | 3.3V | UART TX → ESP32-S3 (fallback) |
| 10 | RXD0 | 3.3V | UART RX ← ESP32-S3 (fallback) |
| USB-A ×2 | — | 5V | D435i, RPLIDAR |
| USB-C | — | 5V | ESP32-S3 CDC |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| CSI-A (J5) | MIPI CSI-2 | — | Cameras front + left | | 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 |
@ -298,7 +343,11 @@ 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 USB CDC (STMicroelectronics) # 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", \ KERNEL=="ttyACM*", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", \
SYMLINK+="esp32-bridge", MODE="0666" SYMLINK+="esp32-bridge", MODE="0666"

View File

@ -56,7 +56,11 @@ 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 bridge | 0.0 | 0.0 | 0.0 | USB CDC | Self-powered from robot 5V | | 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 | | 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** | | |
@ -72,7 +76,7 @@ sudo jtop
## Budget Analysis vs Previous Platform ## Budget Analysis vs Previous Platform
| Metric | Jetson Nano | Jetson Orin Nano Super | | Metric | Jetson Orin Nano Super | Jetson Orin Nano Super |
|--------|------------|------------------------| |--------|------------|------------------------|
| TDP | 10W | 25W | | TDP | 10W | 25W |
| CPU | 4× Cortex-A57 @ 1.43GHz | 6× A78AE @ 1.5GHz | | CPU | 4× Cortex-A57 @ 1.43GHz | 6× A78AE @ 1.5GHz |
@ -151,7 +155,11 @@ 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 + 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,7 +11,11 @@ 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 BALANCE reverts steer to 0 after JETSON_HB_TIMEOUT_MS (500ms) without heartbeat. # 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) heartbeat_period: 0.2 # seconds (= 200ms)
# Twist → ESC command scaling # Twist → ESC command scaling

View File

@ -1,5 +1,9 @@
# cmd_vel_bridge_params.yaml # cmd_vel_bridge_params.yaml
<<<<<<< HEAD
# Configuration for cmd_vel_bridge_node — Nav2 /cmd_vel → ESP32 BALANCE 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
@ -14,7 +18,11 @@ 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 BALANCE jetson_cmd module reverts steer to 0 after JETSON_HB_TIMEOUT_MS (500ms). # 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. # Keep heartbeat well below that threshold.
heartbeat_period: 0.2 # seconds (200ms) heartbeat_period: 0.2 # seconds (200ms)
@ -50,5 +58,9 @@ 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 BALANCE jetson heartbeat timeout for consistency. # 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 cmd_vel_timeout: 0.5 # seconds

View File

@ -0,0 +1,49 @@
<<<<<<< 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)
# Binary-framed Jetson↔ESP32-S3 bridge at 921600 baud.
# ── Serial port ────────────────────────────────────────────────────────────────
# Use /dev/esp32-bridge if the udev rule is applied:
# SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740",
# SYMLINK+="esp32-bridge", MODE="0660", GROUP="dialout"
serial_port: /dev/ttyACM0
baud_rate: 921600
reconnect_delay: 2.0 # seconds between USB reconnect attempts
# ── Heartbeat ─────────────────────────────────────────────────────────────────
# HEARTBEAT frame sent every heartbeat_period seconds.
# ESP32-S3 fires watchdog and reverts to safe state if no frame received for 500ms.
heartbeat_period: 0.2 # 200ms → well within 500ms ESP32-S3 watchdog
# ── Watchdog (Jetson-side) ────────────────────────────────────────────────────
# If no /cmd_vel message received for watchdog_timeout seconds,
# send SPEED_STEER(0,0) to stop the robot.
watchdog_timeout: 0.5 # 500ms
# ── Twist velocity scaling ────────────────────────────────────────────────────
# speed = clamp(linear.x * speed_scale, -1000, 1000) (m/s → ESC units)
# steer = clamp(angular.z * steer_scale, -1000, 1000) (rad/s → ESC units)
#
# Default: 1 m/s → 1000 ESC units, ±2 rad/s → ±1000 steer.
# Negative steer_scale flips ROS2 CCW+ convention to match ESC steer direction.
# Tune speed_scale to set the physical top speed.
speed_scale: 1000.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

@ -1,16 +0,0 @@
# 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

View File

@ -6,7 +6,11 @@ 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 BALANCE 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
@ -65,7 +69,11 @@ 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 USB CDC device node"), 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("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)"),

View File

@ -1,10 +1,18 @@
""" """
<<<<<<< HEAD
cmd_vel_bridge.launch.py Nav2 cmd_vel ESP32 BALANCE autonomous drive bridge. 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.
>>>>>>> 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 BALANCE is in AUTONOMOUS mode, md=2) - 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 - Telemetry RX /saltybot/imu, /saltybot/balance_state, /diagnostics
- /saltybot/cmd publisher (observability) - /saltybot/cmd publisher (observability)
@ -72,12 +80,20 @@ 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 USB CDC device node"), 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( 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 BALANCE HB timeout (0.5s)"), 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( 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)"),

View File

@ -1,3 +1,4 @@
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/launch/stm32_cmd.launch.py
"""stm32_cmd.launch.py — Launch the ESP32-S3 IO auxiliary bridge node. """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). Connects to ESP32-S3 IO board via USB-CDC @ 460800 baud (inter-board protocol).
@ -9,6 +10,19 @@ Spec: docs/SAUL-TEE-SYSTEM-REFERENCE.md §5
Usage: Usage:
ros2 launch saltybot_bridge stm32_cmd.launch.py ros2 launch saltybot_bridge stm32_cmd.launch.py
ros2 launch saltybot_bridge stm32_cmd.launch.py serial_port:=/dev/ttyACM0 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).
Usage:
# Default (binary protocol, bidirectional):
ros2 launch saltybot_bridge esp32_cmd.launch.py
# Override serial port:
ros2 launch saltybot_bridge esp32_cmd.launch.py serial_port:=/dev/ttyACM1
# Custom velocity scales:
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
@ -21,7 +35,7 @@ from launch_ros.actions import Node
def generate_launch_description() -> LaunchDescription: def generate_launch_description() -> LaunchDescription:
pkg = get_package_share_directory("saltybot_bridge") pkg = get_package_share_directory("saltybot_bridge")
params_file = os.path.join(pkg, "config", "stm32_cmd_params.yaml") params_file = os.path.join(pkg, "config", "esp32_cmd_params.yaml")
return LaunchDescription([ return LaunchDescription([
DeclareLaunchArgument("serial_port", default_value="/dev/esp32-io"), DeclareLaunchArgument("serial_port", default_value="/dev/esp32-io"),
@ -30,8 +44,8 @@ def generate_launch_description() -> LaunchDescription:
Node( Node(
package="saltybot_bridge", package="saltybot_bridge",
executable="stm32_cmd_node", executable="esp32_cmd_node",
name="stm32_cmd_node", name="esp32_cmd_node",
output="screen", output="screen",
emulate_tty=True, emulate_tty=True,
parameters=[ parameters=[

View File

@ -2,7 +2,11 @@
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) 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
@ -20,7 +24,11 @@ Usage:
Prerequisites: Prerequisites:
- Flight Controller connected to /dev/ttyTHS1 @ 921600 baud - Flight Controller connected to /dev/ttyTHS1 @ 921600 baud
<<<<<<< HEAD
- ESP32 BALANCE firmware transmitting JSON telemetry frames (50 Hz) - 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) - ROS2 environment sourced (source install/setup.bash)
Note: Note:

View File

@ -4,9 +4,9 @@
<name>saltybot_bridge</name> <name>saltybot_bridge</name>
<version>0.1.0</version> <version>0.1.0</version>
<description> <description>
STM32F722 USB CDC serial bridge for saltybot. ESP32-S3 USB CDC serial bridge for saltybot.
serial_bridge_node: JSON telemetry RX → sensor_msgs/Imu + diagnostics. serial_bridge_node: JSON telemetry RX → sensor_msgs/Imu + diagnostics.
stm32_cmd_node (Issue #119): binary-framed protocol — STX/TYPE/LEN/CRC16/ETX, esp32_cmd_node (Issue #119): binary-framed protocol — STX/TYPE/LEN/CRC16/ETX,
commands: HEARTBEAT, SPEED_STEER, ARM, SET_MODE, PID_UPDATE; commands: HEARTBEAT, SPEED_STEER, ARM, SET_MODE, PID_UPDATE;
telemetry: IMU, BATTERY, MOTOR_RPM, ARM_STATE, ERROR; watchdog 500ms. telemetry: IMU, BATTERY, MOTOR_RPM, ARM_STATE, ERROR; watchdog 500ms.
battery_node (Issue #125): SoC tracking, threshold alerts, SQLite history. battery_node (Issue #125): SoC tracking, threshold alerts, SQLite history.

View File

@ -1,6 +1,6 @@
"""battery_node.py — Battery management for saltybot (Issue #125). """battery_node.py — Battery management for saltybot (Issue #125).
Subscribes to /saltybot/telemetry/battery (JSON from stm32_cmd_node) and: Subscribes to /saltybot/telemetry/battery (JSON from esp32_cmd_node) and:
- Publishes sensor_msgs/BatteryState on /saltybot/battery - Publishes sensor_msgs/BatteryState on /saltybot/battery
- Publishes JSON alerts on /saltybot/battery/alert at threshold crossings - Publishes JSON alerts on /saltybot/battery/alert at threshold crossings
- Reduces speed limit at low SoC via /saltybot/speed_limit (std_msgs/Float32) - Reduces speed limit at low SoC via /saltybot/speed_limit (std_msgs/Float32)
@ -14,7 +14,11 @@ 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 BATTERY telemetry (fuel gauge or lookup on ESP32 BALANCE) 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 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):
@ -320,7 +324,11 @@ 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 BALANCE.""" """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") self.get_logger().fatal("EMERGENCY: publishing zero /cmd_vel and disarming")
# Publish zero velocity # Publish zero velocity
zero_twist = Twist() zero_twist = Twist()

View File

@ -1,5 +1,9 @@
""" """
<<<<<<< HEAD
cmd_vel_bridge_node Nav2 /cmd_vel ESP32 BALANCE drive command bridge. cmd_vel_bridge_node Nav2 /cmd_vel ESP32 BALANCE 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:
@ -12,7 +16,11 @@ 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 BALANCE reports 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, 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
@ -20,9 +28,15 @@ autonomous operation on a self-balancing robot:
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. H\\n heartbeat. ESP32 BALANCE reverts steer to 0 after 500ms silence.
Telemetry (50 Hz from ESP32 BALANCE): Telemetry (50 Hz from ESP32 BALANCE):
=======
H\\n heartbeat. ESP32-S3 reverts steer to 0 after 500ms silence.
Telemetry (50 Hz from ESP32-S3):
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Same RX/publish pipeline as saltybot_cmd_node. 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.
@ -150,7 +164,11 @@ class CmdVelBridgeNode(Node):
self._open_serial() self._open_serial()
# ── Timers ──────────────────────────────────────────────────────────── # ── Timers ────────────────────────────────────────────────────────────
<<<<<<< HEAD
# Telemetry read at 100 Hz (ESP32 BALANCE sends at 50 Hz) # 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) 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)
@ -238,7 +256,11 @@ class CmdVelBridgeNode(Node):
speed = self._current_speed speed = self._current_speed
steer = self._current_steer steer = self._current_steer
<<<<<<< HEAD
# Send to ESP32 BALANCE # 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") 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(
@ -256,7 +278,11 @@ class CmdVelBridgeNode(Node):
# ── Heartbeat TX ────────────────────────────────────────────────────────── # ── Heartbeat TX ──────────────────────────────────────────────────────────
def _heartbeat_cb(self): def _heartbeat_cb(self):
<<<<<<< HEAD
"""H\\n keeps ESP32 BALANCE jetson_cmd heartbeat alive regardless of mode.""" """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") self._write(b"H\n")
# ── Telemetry RX ────────────────────────────────────────────────────────── # ── Telemetry RX ──────────────────────────────────────────────────────────
@ -378,7 +404,11 @@ 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 = "esp32" 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.message = f"{state_label} [{mode_label}]"
status.level = ( status.level = (
DiagnosticStatus.OK if state == 1 else DiagnosticStatus.OK if state == 1 else
@ -406,11 +436,19 @@ 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.hardware_id = "esp32"
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 BALANCE IMU fault: errno={errno}") self.get_logger().error(f"ESP32 BALANCE IMU fault: errno={errno}")
=======
status.hardware_id = "esp32s322"
status.message = f"IMU fault errno={errno}"
diag.status.append(status)
self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32-S3 IMU fault: errno={errno}")
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
# ── Lifecycle ───────────────────────────────────────────────────────────── # ── Lifecycle ─────────────────────────────────────────────────────────────

View File

@ -1,27 +1,61 @@
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
"""stm32_cmd_node.py — Orin ↔ ESP32-S3 IO auxiliary bridge node. """stm32_cmd_node.py — Orin ↔ ESP32-S3 IO auxiliary bridge node.
=======
"""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). 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. It handles auxiliary I/O: RC monitoring, sensor data, LED/output control.
=======
TX commands (Jetson ESP32-S3):
SPEED_STEER 50 Hz from /cmd_vel subscription
HEARTBEAT 200 ms timer (ESP32-S3 watchdog fires at 500 ms)
ARM via /saltybot/arm service
SET_MODE via /saltybot/set_mode service
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: RX from ESP32 IO:
RC_CHANNELS (0x01) /saltybot/rc_channels (std_msgs/String JSON) RC_CHANNELS (0x01) /saltybot/rc_channels (std_msgs/String JSON)
SENSORS (0x02) /saltybot/sensors (std_msgs/String JSON) SENSORS (0x02) /saltybot/sensors (std_msgs/String JSON)
=======
RX telemetry (ESP32-S3 Jetson):
IMU /saltybot/imu (sensor_msgs/Imu)
BATTERY /saltybot/telemetry/battery (std_msgs/String JSON)
MOTOR_RPM /saltybot/telemetry/motor_rpm (std_msgs/String JSON)
ARM_STATE /saltybot/arm_state (std_msgs/String JSON)
ERROR /saltybot/error (std_msgs/String JSON)
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): Parameters (config/stm32_cmd_params.yaml):
serial_port /dev/esp32-io serial_port /dev/esp32-io
baud_rate 460800 baud_rate 460800
reconnect_delay 2.0 reconnect_delay 2.0
heartbeat_period 0.2 (ESP32 IO watchdog fires at ~500 ms) heartbeat_period 0.2 (ESP32 IO watchdog fires at ~500 ms)
=======
Parameters (config/esp32_cmd_params.yaml):
serial_port /dev/ttyACM0
baud_rate 921600
reconnect_delay 2.0 (seconds)
heartbeat_period 0.2 (seconds)
watchdog_timeout 0.5 (seconds no /cmd_vel send zero-speed)
speed_scale 1000.0 (linear.x m/s ESC units)
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
@ -39,8 +73,12 @@ 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 .stm32_protocol import ( from .stm32_protocol import (
BAUD_RATE, 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, FrameParser,
RcChannels, RcChannels,
SensorData, SensorData,
@ -51,10 +89,14 @@ from .stm32_protocol import (
class Stm32CmdNode(Node): class Stm32CmdNode(Node):
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
"""Orin ↔ ESP32-S3 IO auxiliary bridge node.""" """Orin ↔ ESP32-S3 IO auxiliary 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__("stm32_cmd_node") super().__init__("esp32_cmd_node")
# ── Parameters ──────────────────────────────────────────────────── # ── Parameters ────────────────────────────────────────────────────
self.declare_parameter("serial_port", "/dev/esp32-io") self.declare_parameter("serial_port", "/dev/esp32-io")
@ -96,7 +138,12 @@ 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"stm32_cmd_node started — {self._port_name} @ {self._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"
>>>>>>> 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 ─────────────────────────────────────────────────
@ -198,7 +245,120 @@ 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 ──────────────────────────────────────────────────────────────── # ── TX ────────────────────────────────────────────────────────────────
=======
elif isinstance(frame, ArmStateFrame):
self._publish_arm_state(frame, now)
elif isinstance(frame, ErrorFrame):
self._publish_error(frame, now)
elif isinstance(frame, tuple):
type_code, payload = frame
self.get_logger().debug(
f"Unknown telemetry type 0x{type_code:02X} len={len(payload)}"
)
# ── Telemetry publishers ──────────────────────────────────────────────────
def _publish_imu(self, frame: ImuFrame, stamp) -> None:
msg = Imu()
msg.header.stamp = stamp
msg.header.frame_id = IMU_FRAME_ID
# orientation: unknown — signal with -1 in first covariance
msg.orientation_covariance[0] = -1.0
msg.angular_velocity.x = math.radians(frame.pitch_deg)
msg.angular_velocity.y = math.radians(frame.roll_deg)
msg.angular_velocity.z = math.radians(frame.yaw_deg)
cov = math.radians(0.3) ** 2 # ±0.3° noise estimate from ESP32-S3 BMI088
msg.angular_velocity_covariance[0] = cov
msg.angular_velocity_covariance[4] = cov
msg.angular_velocity_covariance[8] = cov
msg.linear_acceleration.x = frame.accel_x
msg.linear_acceleration.y = frame.accel_y
msg.linear_acceleration.z = frame.accel_z
acov = 0.05 ** 2 # ±0.05 m/s² noise
msg.linear_acceleration_covariance[0] = acov
msg.linear_acceleration_covariance[4] = acov
msg.linear_acceleration_covariance[8] = acov
self._imu_pub.publish(msg)
def _publish_battery(self, frame: BatteryFrame, stamp) -> None:
payload = {
"voltage_v": round(frame.voltage_mv / 1000.0, 3),
"voltage_mv": frame.voltage_mv,
"current_ma": frame.current_ma,
"soc_pct": frame.soc_pct,
"charging": frame.current_ma < -100,
"ts": f"{stamp.sec}.{stamp.nanosec:09d}",
}
self._last_battery_mv = frame.voltage_mv
msg = String()
msg.data = json.dumps(payload)
self._battery_pub.publish(msg)
def _publish_motor_rpm(self, frame: MotorRpmFrame, stamp) -> None:
payload = {
"left_rpm": frame.left_rpm,
"right_rpm": frame.right_rpm,
"ts": f"{stamp.sec}.{stamp.nanosec:09d}",
}
msg = String()
msg.data = json.dumps(payload)
self._rpm_pub.publish(msg)
def _publish_arm_state(self, frame: ArmStateFrame, stamp) -> None:
label = _ARM_LABEL.get(frame.state, f"UNKNOWN({frame.state})")
if frame.state != self._last_arm_state:
self.get_logger().info(f"Arm state → {label} (flags=0x{frame.error_flags:02X})")
self._last_arm_state = frame.state
payload = {
"state": frame.state,
"state_label": label,
"error_flags": frame.error_flags,
"ts": f"{stamp.sec}.{stamp.nanosec:09d}",
}
msg = String()
msg.data = json.dumps(payload)
self._arm_pub.publish(msg)
def _publish_error(self, frame: ErrorFrame, stamp) -> None:
self.get_logger().error(
f"ESP32-S3 error code=0x{frame.error_code:02X} sub=0x{frame.subcode:02X}"
)
payload = {
"error_code": frame.error_code,
"subcode": frame.subcode,
"ts": f"{stamp.sec}.{stamp.nanosec:09d}",
}
msg = String()
msg.data = json.dumps(payload)
self._error_pub.publish(msg)
# ── TX — command send ─────────────────────────────────────────────────────
def _on_cmd_vel(self, msg: Twist) -> None:
"""Convert /cmd_vel Twist to SPEED_STEER frame at up to 50 Hz."""
speed = int(_clamp(msg.linear.x * self._speed_scale, -1000.0, 1000.0))
steer = int(_clamp(msg.angular.z * self._steer_scale, -1000.0, 1000.0))
self._last_speed = speed
self._last_steer = steer
self._last_cmd_t = time.monotonic()
self._watchdog_sent = False
frame = encode_speed_steer(speed, steer)
if not self._write(frame):
self.get_logger().warn(
"SPEED_STEER dropped — serial not open",
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())
@ -239,8 +399,14 @@ 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.name = "saltybot/esp32_io_bridge"
status.hardware_id = "esp32-s3-io" status.hardware_id = "esp32-s3-io"
=======
status.name = "saltybot/esp32_cmd_node"
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}"

View File

@ -0,0 +1,332 @@
"""esp32_protocol.py — Binary frame codec for Jetson↔ESP32-S3 communication.
Issue #119: defines the binary serial protocol between the Jetson Orin Nano Super and the
ESP32-S3 ESP32-S3 BALANCE over USB CDC @ 921600 baud.
Frame layout (all multi-byte fields are big-endian):
STX TYPE LEN PAYLOAD CRC16 ETX
0x02 1B 1B LEN bytes 2B BE 0x03
CRC16 covers: TYPE + LEN + PAYLOAD (not STX, ETX, or CRC bytes themselves).
CRC algorithm: CCITT-16, polynomial=0x1021, init=0xFFFF, no final XOR.
Command types (Jetson ESP32-S3):
0x01 HEARTBEAT no payload (len=0)
0x02 SPEED_STEER int16 speed + int16 steer (len=4) range: -1000..+1000
0x03 ARM uint8 (0=disarm, 1=arm) (len=1)
0x04 SET_MODE uint8 mode (len=1)
0x05 PID_UPDATE float32 kp + ki + kd (len=12)
Telemetry types (ESP32-S3 Jetson):
0x10 IMU int16×6: pitch,roll,yaw (×100 deg), ax,ay,az (×100 m/) (len=12)
0x11 BATTERY uint16 voltage_mv + int16 current_ma + uint8 soc_pct (len=5)
0x12 MOTOR_RPM int16 left_rpm + int16 right_rpm (len=4)
0x13 ARM_STATE uint8 state + uint8 error_flags (len=2)
0x14 ERROR uint8 error_code + uint8 subcode (len=2)
Usage:
# Encoding (Jetson → ESP32-S3)
frame = encode_speed_steer(300, -150)
ser.write(frame)
# Decoding (ESP32-S3 → Jetson), one byte at a time
parser = FrameParser()
for byte in incoming_bytes:
result = parser.feed(byte)
if result is not None:
handle_frame(result)
"""
from __future__ import annotations
import struct
from dataclasses import dataclass
from enum import IntEnum
from typing import Optional
# ── Frame constants ───────────────────────────────────────────────────────────
STX = 0x02
ETX = 0x03
MAX_PAYLOAD_LEN = 64 # hard limit; any frame larger is corrupt
# ── Command / telemetry type codes ────────────────────────────────────────────
class CmdType(IntEnum):
HEARTBEAT = 0x01
SPEED_STEER = 0x02
ARM = 0x03
SET_MODE = 0x04
PID_UPDATE = 0x05
class TelType(IntEnum):
IMU = 0x10
BATTERY = 0x11
MOTOR_RPM = 0x12
ARM_STATE = 0x13
ERROR = 0x14
# ── Parsed telemetry objects ──────────────────────────────────────────────────
@dataclass
class ImuFrame:
pitch_deg: float # degrees (positive = forward tilt)
roll_deg: float
yaw_deg: float
accel_x: float # m/s²
accel_y: float
accel_z: float
@dataclass
class BatteryFrame:
voltage_mv: int # millivolts (e.g. 11100 = 11.1 V)
current_ma: int # milliamps (negative = charging)
soc_pct: int # state of charge 0100 (from ESP32-S3 fuel gauge or lookup)
@dataclass
class MotorRpmFrame:
left_rpm: int
right_rpm: int
@dataclass
class ArmStateFrame:
state: int # 0=DISARMED 1=ARMED 2=TILT_FAULT
error_flags: int # bitmask
@dataclass
class ErrorFrame:
error_code: int
subcode: int
# Union type for decoded results
TelemetryFrame = ImuFrame | BatteryFrame | MotorRpmFrame | ArmStateFrame | ErrorFrame
# ── CRC16 CCITT ───────────────────────────────────────────────────────────────
def _crc16_ccitt(data: bytes, init: int = 0xFFFF) -> int:
"""CRC16-CCITT: polynomial 0x1021, init 0xFFFF, no final XOR."""
crc = init
for byte in data:
crc ^= byte << 8
for _ in range(8):
if crc & 0x8000:
crc = (crc << 1) ^ 0x1021
else:
crc <<= 1
crc &= 0xFFFF
return crc
# ── Frame encoder ─────────────────────────────────────────────────────────────
def _build_frame(cmd_type: int, payload: bytes) -> bytes:
"""Assemble a complete binary frame with CRC16."""
assert len(payload) <= MAX_PAYLOAD_LEN, "Payload too large"
length = len(payload)
header = bytes([cmd_type, length])
crc = _crc16_ccitt(header + payload)
return bytes([STX, cmd_type, length]) + payload + struct.pack(">H", crc) + bytes([ETX])
def encode_heartbeat() -> bytes:
"""HEARTBEAT frame — no payload."""
return _build_frame(CmdType.HEARTBEAT, b"")
def encode_speed_steer(speed: int, steer: int) -> bytes:
"""SPEED_STEER frame — int16 speed + int16 steer, both in -1000..+1000."""
speed = max(-1000, min(1000, int(speed)))
steer = max(-1000, min(1000, int(steer)))
return _build_frame(CmdType.SPEED_STEER, struct.pack(">hh", speed, steer))
def encode_arm(arm: bool) -> bytes:
"""ARM frame — 0=disarm, 1=arm."""
return _build_frame(CmdType.ARM, struct.pack("B", 1 if arm else 0))
def encode_set_mode(mode: int) -> bytes:
"""SET_MODE frame — mode byte."""
return _build_frame(CmdType.SET_MODE, struct.pack("B", mode & 0xFF))
def encode_pid_update(kp: float, ki: float, kd: float) -> bytes:
"""PID_UPDATE frame — three float32 values."""
return _build_frame(CmdType.PID_UPDATE, struct.pack(">fff", kp, ki, kd))
# ── Frame decoder (state-machine parser) ─────────────────────────────────────
class ParserState(IntEnum):
WAIT_STX = 0
WAIT_TYPE = 1
WAIT_LEN = 2
PAYLOAD = 3
CRC_HI = 4
CRC_LO = 5
WAIT_ETX = 6
class ParseError(Exception):
pass
class FrameParser:
"""Byte-by-byte streaming parser for ESP32-S3 telemetry frames.
Feed individual bytes via feed(); returns a decoded TelemetryFrame (or raw
bytes tuple) when a complete valid frame is received.
Thread-safety: single-threaded wrap in a lock if shared across threads.
Usage::
parser = FrameParser()
for b in incoming:
result = parser.feed(b)
if result is not None:
process(result)
"""
def __init__(self) -> None:
self._state = ParserState.WAIT_STX
self._type = 0
self._length = 0
self._payload = bytearray()
self._crc_rcvd = 0
self.frames_ok = 0
self.frames_error = 0
def reset(self) -> None:
"""Reset parser to initial state (call after error or port reconnect)."""
self._state = ParserState.WAIT_STX
self._payload = bytearray()
def feed(self, byte: int) -> Optional[TelemetryFrame | tuple]:
"""Process one byte. Returns decoded frame on success, None otherwise.
On CRC error, increments frames_error and resets. The return value on
success is a dataclass (ImuFrame, BatteryFrame, etc.) or a
(type_code, raw_payload) tuple for unknown type codes.
"""
s = self._state
if s == ParserState.WAIT_STX:
if byte == STX:
self._state = ParserState.WAIT_TYPE
return None
if s == ParserState.WAIT_TYPE:
self._type = byte
self._state = ParserState.WAIT_LEN
return None
if s == ParserState.WAIT_LEN:
self._length = byte
self._payload = bytearray()
if self._length > MAX_PAYLOAD_LEN:
# Corrupt frame — too big; reset
self.frames_error += 1
self.reset()
return None
if self._length == 0:
self._state = ParserState.CRC_HI
else:
self._state = ParserState.PAYLOAD
return None
if s == ParserState.PAYLOAD:
self._payload.append(byte)
if len(self._payload) == self._length:
self._state = ParserState.CRC_HI
return None
if s == ParserState.CRC_HI:
self._crc_rcvd = byte << 8
self._state = ParserState.CRC_LO
return None
if s == ParserState.CRC_LO:
self._crc_rcvd |= byte
self._state = ParserState.WAIT_ETX
return None
if s == ParserState.WAIT_ETX:
self.reset() # always reset so we look for next STX
if byte != ETX:
self.frames_error += 1
return None
# Verify CRC
crc_data = bytes([self._type, self._length]) + self._payload
expected = _crc16_ccitt(crc_data)
if expected != self._crc_rcvd:
self.frames_error += 1
return None
# Decode
self.frames_ok += 1
return _decode_telemetry(self._type, bytes(self._payload))
# Should never reach here
self.reset()
return None
# ── Telemetry decoder ─────────────────────────────────────────────────────────
def _decode_telemetry(type_code: int, payload: bytes) -> Optional[TelemetryFrame | tuple]:
"""Decode a validated telemetry payload into a typed dataclass."""
try:
if type_code == TelType.IMU:
if len(payload) < 12:
return None
p, r, y, ax, ay, az = struct.unpack_from(">hhhhhh", payload)
return ImuFrame(
pitch_deg=p / 100.0,
roll_deg=r / 100.0,
yaw_deg=y / 100.0,
accel_x=ax / 100.0,
accel_y=ay / 100.0,
accel_z=az / 100.0,
)
if type_code == TelType.BATTERY:
if len(payload) < 5:
return None
v_mv, i_ma, soc = struct.unpack_from(">HhB", payload)
return BatteryFrame(voltage_mv=v_mv, current_ma=i_ma, soc_pct=soc)
if type_code == TelType.MOTOR_RPM:
if len(payload) < 4:
return None
left, right = struct.unpack_from(">hh", payload)
return MotorRpmFrame(left_rpm=left, right_rpm=right)
if type_code == TelType.ARM_STATE:
if len(payload) < 2:
return None
state, flags = struct.unpack_from("BB", payload)
return ArmStateFrame(state=state, error_flags=flags)
if type_code == TelType.ERROR:
if len(payload) < 2:
return None
code, sub = struct.unpack_from("BB", payload)
return ErrorFrame(error_code=code, subcode=sub)
except struct.error:
return None
# Unknown telemetry type — return raw
return (type_code, payload)

View File

@ -1,8 +1,15 @@
""" """
<<<<<<< HEAD
remote_estop_node.py -- Remote e-stop bridge: MQTT -> ESP32 USB CDC 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": 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) {"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
{"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)
>>>>>>> 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

@ -322,7 +322,11 @@ 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 = "esp32" st.hardware_id = "esp32"
=======
st.hardware_id = "esp32s322"
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
st.message = state_label 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

View File

@ -1,9 +1,14 @@
""" """
<<<<<<< HEAD
saltybot_cmd_node full bidirectional ESP32 BALANCEJetson bridge 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. 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): RX path (50Hz from ESP32 BALANCE):
JSON telemetry /saltybot/imu, /saltybot/balance_state, /diagnostics JSON telemetry /saltybot/imu, /saltybot/balance_state, /diagnostics
@ -15,6 +20,19 @@ Protocol:
H\\n heartbeat. ESP32 BALANCE reverts steer to 0 if gap > 500ms. H\\n heartbeat. ESP32 BALANCE 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 BALANCE heartbeat timer. C command also refreshes ESP32 BALANCE heartbeat timer.
=======
RX path (50Hz from ESP32-S3):
JSON telemetry /saltybot/imu, /saltybot/balance_state, /diagnostics
TX path:
/cmd_vel (geometry_msgs/Twist) C<speed>,<steer>\\n ESP32-S3
Heartbeat timer (200ms) H\\n ESP32-S3
Protocol:
H\\n heartbeat. ESP32-S3 reverts steer to 0 if gap > 500ms.
C<spd>,<str>\\n drive command. speed/steer: -1000..+1000 integers.
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)
@ -100,7 +118,11 @@ class SaltybotCmdNode(Node):
self._open_serial() self._open_serial()
# ── Timers ──────────────────────────────────────────────────────────── # ── Timers ────────────────────────────────────────────────────────────
<<<<<<< HEAD
# Telemetry read at 100Hz (ESP32 BALANCE sends at 50Hz) # 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) 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)
@ -266,7 +288,11 @@ 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 = "esp32" status.hardware_id = "esp32"
=======
status.hardware_id = "esp32s322"
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
status.message = state_label status.message = state_label
if state == 1: if state == 1:
status.level = DiagnosticStatus.OK status.level = DiagnosticStatus.OK
@ -294,11 +320,19 @@ 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.hardware_id = "esp32"
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 BALANCE IMU fault: errno={errno}") self.get_logger().error(f"ESP32 BALANCE IMU fault: errno={errno}")
=======
status.hardware_id = "esp32s322"
status.message = f"IMU fault errno={errno}"
diag.status.append(status)
self._diag_pub.publish(diag)
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 ─────────────────────────────────────────────────────
@ -316,7 +350,11 @@ class SaltybotCmdNode(Node):
) )
def _heartbeat_cb(self): def _heartbeat_cb(self):
<<<<<<< HEAD
"""Send H\\n heartbeat. ESP32 BALANCE reverts steer to 0 if gap > 500ms.""" """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") self._write(b"H\n")
# ── Lifecycle ───────────────────────────────────────────────────────────── # ── Lifecycle ─────────────────────────────────────────────────────────────

View File

@ -1,6 +1,10 @@
""" """
saltybot_bridge serial_bridge_node saltybot_bridge serial_bridge_node
<<<<<<< HEAD
ESP32 USB CDC ROS2 topic publisher ESP32 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>,
@ -29,7 +33,11 @@ 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 BALANCE balance_state_t enum # 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"} _STATE_LABEL = {0: "DISARMED", 1: "ARMED", 2: "TILT_FAULT"}
# Sensor frame_id published in Imu header # Sensor frame_id published in Imu header
@ -83,7 +91,11 @@ 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 BALANCE sends at 50 Hz, so we never miss a frame # 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._timer = self.create_timer(0.01, self._read_cb)
self.get_logger().info( self.get_logger().info(
@ -117,7 +129,11 @@ class SerialBridgeNode(Node):
def write_serial(self, data: bytes) -> bool: def write_serial(self, data: bytes) -> bool:
""" """
<<<<<<< HEAD
Send raw bytes to ESP32 BALANCE over the open serial port. 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). 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.
""" """
@ -206,7 +222,11 @@ class SerialBridgeNode(Node):
""" """
Publish sensor_msgs/Imu. Publish sensor_msgs/Imu.
<<<<<<< HEAD
The ESP32 BALANCE IMU gives Euler angles (pitch/roll from accelerometer+gyro 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 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.
@ -264,7 +284,11 @@ 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 = "esp32" status.hardware_id = "esp32"
=======
status.hardware_id = "esp32s322"
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
status.message = state_label status.message = state_label
if state == 1: # ARMED if state == 1: # ARMED
@ -293,11 +317,19 @@ 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.hardware_id = "esp32"
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 BALANCE reported IMU fault: errno={errno}") self.get_logger().error(f"ESP32 BALANCE reported IMU fault: errno={errno}")
=======
status.hardware_id = "esp32s322"
status.message = f"IMU fault errno={errno}"
diag.status.append(status)
self._diag_pub.publish(diag)
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()

View File

@ -13,7 +13,7 @@ setup(
"launch/bridge.launch.py", "launch/bridge.launch.py",
"launch/cmd_vel_bridge.launch.py", "launch/cmd_vel_bridge.launch.py",
"launch/remote_estop.launch.py", "launch/remote_estop.launch.py",
"launch/stm32_cmd.launch.py", "launch/esp32_cmd.launch.py",
"launch/battery.launch.py", "launch/battery.launch.py",
"launch/uart_bridge.launch.py", "launch/uart_bridge.launch.py",
]), ]),
@ -21,7 +21,7 @@ setup(
"config/bridge_params.yaml", "config/bridge_params.yaml",
"config/cmd_vel_bridge_params.yaml", "config/cmd_vel_bridge_params.yaml",
"config/estop_params.yaml", "config/estop_params.yaml",
"config/stm32_cmd_params.yaml", "config/esp32_cmd_params.yaml",
"config/battery_params.yaml", "config/battery_params.yaml",
]), ]),
], ],
@ -29,7 +29,11 @@ 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 USB CDC → ROS2 serial bridge for saltybot", 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", license="MIT",
tests_require=["pytest"], tests_require=["pytest"],
entry_points={ entry_points={
@ -41,8 +45,13 @@ 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) # Binary-framed ESP32 BALANCE command node (Issue #119)
"stm32_cmd_node = saltybot_bridge.stm32_cmd_node:main", "stm32_cmd_node = saltybot_bridge.stm32_cmd_node:main",
=======
# Binary-framed ESP32-S3 command node (Issue #119)
"esp32_cmd_node = saltybot_bridge.esp32_cmd_node:main",
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
# Battery management node (Issue #125) # 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)

View File

@ -1,5 +1,9 @@
""" """
<<<<<<< HEAD
Unit tests for JetsonESP32 BALANCE command serialization logic. 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. 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_stm32_cmd_node.py — Unit tests for Stm32CmdNode with mock serial port. """test_esp32_cmd_node.py — Unit tests for Stm32CmdNode with mock serial port.
Tests: Tests:
- Serial open/close lifecycle - Serial open/close lifecycle
@ -12,7 +12,7 @@ Tests:
- Zero-speed sent on node shutdown - Zero-speed sent on node shutdown
- CRC errors counted correctly - CRC errors counted correctly
Run with: pytest test/test_stm32_cmd_node.py -v Run with: pytest test/test_esp32_cmd_node.py -v
No ROS2 runtime required uses mock Node infrastructure. No ROS2 runtime required uses mock Node infrastructure.
""" """
@ -29,7 +29,7 @@ import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from saltybot_bridge.stm32_protocol import ( from saltybot_bridge.esp32_protocol import (
STX, ETX, CmdType, TelType, STX, ETX, CmdType, TelType,
encode_speed_steer, encode_heartbeat, encode_arm, encode_pid_update, encode_speed_steer, encode_heartbeat, encode_arm, encode_pid_update,
_build_frame, _crc16_ccitt, _build_frame, _crc16_ccitt,
@ -219,10 +219,10 @@ class TestMockSerialTX:
class TestMockSerialRX: class TestMockSerialRX:
"""Test RX parsing path using MockSerial with pre-loaded telemetry data.""" """Test RX parsing path using MockSerial with pre-loaded telemetry data."""
from saltybot_bridge.stm32_protocol import FrameParser from saltybot_bridge.esp32_protocol import FrameParser
def test_rx_imu_frame(self): def test_rx_imu_frame(self):
from saltybot_bridge.stm32_protocol import FrameParser, ImuFrame from saltybot_bridge.esp32_protocol import FrameParser, ImuFrame
raw = _imu_frame_bytes(pitch=500, roll=-200, yaw=100, ax=0, ay=0, az=981) raw = _imu_frame_bytes(pitch=500, roll=-200, yaw=100, ax=0, ay=0, az=981)
ms = MockSerial(rx_data=raw) ms = MockSerial(rx_data=raw)
parser = FrameParser() parser = FrameParser()
@ -241,7 +241,7 @@ class TestMockSerialRX:
assert f.accel_z == pytest.approx(9.81) assert f.accel_z == pytest.approx(9.81)
def test_rx_battery_frame(self): def test_rx_battery_frame(self):
from saltybot_bridge.stm32_protocol import FrameParser, BatteryFrame from saltybot_bridge.esp32_protocol import FrameParser, BatteryFrame
raw = _battery_frame_bytes(v_mv=10500, i_ma=1200, soc=45) raw = _battery_frame_bytes(v_mv=10500, i_ma=1200, soc=45)
ms = MockSerial(rx_data=raw) ms = MockSerial(rx_data=raw)
parser = FrameParser() parser = FrameParser()
@ -257,7 +257,7 @@ class TestMockSerialRX:
assert f.soc_pct == 45 assert f.soc_pct == 45
def test_rx_multiple_frames_in_one_read(self): def test_rx_multiple_frames_in_one_read(self):
from saltybot_bridge.stm32_protocol import FrameParser from saltybot_bridge.esp32_protocol import FrameParser
raw = (_imu_frame_bytes() + _arm_state_frame_bytes() + _battery_frame_bytes()) raw = (_imu_frame_bytes() + _arm_state_frame_bytes() + _battery_frame_bytes())
ms = MockSerial(rx_data=raw) ms = MockSerial(rx_data=raw)
parser = FrameParser() parser = FrameParser()
@ -271,7 +271,7 @@ class TestMockSerialRX:
assert parser.frames_error == 0 assert parser.frames_error == 0
def test_rx_bad_crc_counted_as_error(self): def test_rx_bad_crc_counted_as_error(self):
from saltybot_bridge.stm32_protocol import FrameParser from saltybot_bridge.esp32_protocol import FrameParser
raw = bytearray(_arm_state_frame_bytes(state=1)) raw = bytearray(_arm_state_frame_bytes(state=1))
raw[-3] ^= 0xFF # corrupt CRC raw[-3] ^= 0xFF # corrupt CRC
ms = MockSerial(rx_data=bytes(raw)) ms = MockSerial(rx_data=bytes(raw))
@ -282,7 +282,7 @@ class TestMockSerialRX:
assert parser.frames_error == 1 assert parser.frames_error == 1
def test_rx_resync_after_corrupt_byte(self): def test_rx_resync_after_corrupt_byte(self):
from saltybot_bridge.stm32_protocol import FrameParser, ArmStateFrame from saltybot_bridge.esp32_protocol import FrameParser, ArmStateFrame
garbage = b"\xDE\xAD\x00\x00" garbage = b"\xDE\xAD\x00\x00"
valid = _arm_state_frame_bytes(state=1) valid = _arm_state_frame_bytes(state=1)
ms = MockSerial(rx_data=garbage + valid) ms = MockSerial(rx_data=garbage + valid)

View File

@ -1,4 +1,4 @@
"""test_stm32_protocol.py — Unit tests for binary STM32 frame codec. """test_esp32_protocol.py — Unit tests for binary ESP32-S3 frame codec.
Tests: Tests:
- CRC16-CCITT correctness - CRC16-CCITT correctness
@ -12,7 +12,7 @@ Tests:
- Speed/steer clamping in encode_speed_steer - Speed/steer clamping in encode_speed_steer
- Round-trip encode decode for all known telemetry types - Round-trip encode decode for all known telemetry types
Run with: pytest test/test_stm32_protocol.py -v Run with: pytest test/test_esp32_protocol.py -v
""" """
from __future__ import annotations from __future__ import annotations
@ -25,7 +25,7 @@ import os
# ── Path setup (no ROS2 install needed) ────────────────────────────────────── # ── Path setup (no ROS2 install needed) ──────────────────────────────────────
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from saltybot_bridge.stm32_protocol import ( from saltybot_bridge.esp32_protocol import (
STX, ETX, STX, ETX,
CmdType, TelType, CmdType, TelType,
ImuFrame, BatteryFrame, MotorRpmFrame, ArmStateFrame, ErrorFrame, ImuFrame, BatteryFrame, MotorRpmFrame, ArmStateFrame, ErrorFrame,

View File

@ -1,5 +1,9 @@
""" """
<<<<<<< HEAD
Unit tests for ESP32 BALANCE telemetry parsing logic. 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 Run with: pytest jetson/ros2_ws/src/saltybot_bridge/test/test_parse.py
""" """

View File

@ -1,4 +1,4 @@
# WheelTicks.msg — cumulative wheel encoder tick counts from STM32 (Issue #184) # WheelTicks.msg — cumulative wheel encoder tick counts from ESP32-S3 (Issue #184)
# #
# left_ticks : cumulative left encoder count (int32, wraps at ±2^31) # left_ticks : cumulative left encoder count (int32, wraps at ±2^31)
# right_ticks : cumulative right encoder count (int32, wraps at ±2^31) # right_ticks : cumulative right encoder count (int32, wraps at ±2^31)

View File

@ -3,7 +3,7 @@
<package format="3"> <package format="3">
<name>saltybot_bridge_msgs</name> <name>saltybot_bridge_msgs</name>
<version>0.1.0</version> <version>0.1.0</version>
<description>STM32 bridge message definitions — wheel encoder ticks and low-level hardware telemetry (Issue #184)</description> <description>ESP32-S3 bridge message definitions — wheel encoder ticks and low-level hardware telemetry (Issue #184)</description>
<maintainer email="sl-perception@saltylab.local">sl-perception</maintainer> <maintainer email="sl-perception@saltylab.local">sl-perception</maintainer>
<license>MIT</license> <license>MIT</license>

View File

@ -19,7 +19,11 @@
# 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 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:

View File

@ -2,7 +2,11 @@
# Master configuration for full stack bringup # Master configuration for full stack bringup
# ──────────────────────────────────────────────────────────────────────────── # ────────────────────────────────────────────────────────────────────────────
<<<<<<< HEAD
# HARDWARE — ESP32 BALANCE 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:

View File

@ -39,7 +39,11 @@ 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 BALANCE (deadman + ramp + AUTONOMOUS gate) 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) rosbridge WebSocket (port 9090)
outdoor outdoor
@ -57,8 +61,13 @@ 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= 0s ESP32 bridge (serial port owner must be first)
t= 2s cmd_vel bridge (consumes /cmd_vel, needs ESP32 bridge up) t= 2s cmd_vel bridge (consumes /cmd_vel, needs ESP32 bridge up)
=======
t= 0s ESP32-S3 bridge (serial port owner must be first)
t= 2s cmd_vel bridge (consumes /cmd_vel, needs ESP32-S3 bridge up)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
t= 2s sensors (RPLIDAR + RealSense) 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)
@ -71,10 +80,17 @@ 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. 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. 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 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. 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.
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
until ESP32-S3 firmware is in AUTONOMOUS mode regardless of /cmd_vel.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
follow_enabled:=false disables person follower without stopping the node. 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}'
@ -91,7 +107,11 @@ 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 BALANCE) /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/imu Imu
/saltybot/balance_state String /saltybot/balance_state String
""" """
@ -209,7 +229,11 @@ 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 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(
@ -270,7 +294,11 @@ 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 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 ───────────────────────────────────────────
@ -290,7 +318,11 @@ 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 bidirectional serial bridge ──────────────────────────────── # ── 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( esp32_bridge = GroupAction(
condition=IfCondition(LaunchConfiguration("enable_bridge")), condition=IfCondition(LaunchConfiguration("enable_bridge")),
actions=[ actions=[
@ -320,7 +352,11 @@ enable_mission_logging_arg = DeclareLaunchArgument(
], ],
) )
<<<<<<< HEAD
# ── t=2s cmd_vel safety bridge (depends on ESP32 bridge) ──────────────── # ── 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( cmd_vel_bridge = TimerAction(
period=2.0, period=2.0,
actions=[ actions=[

View File

@ -1,7 +1,7 @@
""" """
realsense.launch.py Intel RealSense D435i driver (standalone) realsense.launch.py Intel RealSense D435i driver (standalone)
Launches realsense2_camera_node with Jetson Nano power-budget settings: Launches realsense2_camera_node with Jetson Orin Nano Super power-budget settings:
- 640×480 @ 15fps (depth + RGB) saves ~0.4W vs 30fps - 640×480 @ 15fps (depth + RGB) saves ~0.4W vs 30fps
- IMU enabled with linear interpolation (unified /camera/imu topic) - IMU enabled with linear interpolation (unified /camera/imu topic)
- Depth aligned to color frame - Depth aligned to color frame

View File

@ -19,7 +19,11 @@ Usage
Startup sequence Startup sequence
<<<<<<< HEAD
GROUP A Drivers t= 0 s ESP32 bridge, RealSense+RPLIDAR, motor daemon, IMU 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) 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)
@ -123,7 +127,11 @@ 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 UART 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(
@ -198,7 +206,11 @@ 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 bridge first, then sensors, then motor daemon. # 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). # Health gate: subsequent groups delayed until t_perception (8 s full/debug).
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@ -212,7 +224,11 @@ 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 BALANCE bridge # ESP32 BALANCE bridge
=======
# ESP32-S3 bidirectional bridge (JLINK USART1)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
esp32_bridge = IncludeLaunchDescription( esp32_bridge = IncludeLaunchDescription(
_launch("saltybot_bridge", "launch", "bridge.launch.py"), _launch("saltybot_bridge", "launch", "bridge.launch.py"),
launch_arguments={ launch_arguments={
@ -232,7 +248,11 @@ def generate_launch_description() -> LaunchDescription: # noqa: C901
], ],
) )
<<<<<<< HEAD
# Motor daemon: /cmd_vel → ESP32 BALANCE DRIVE frames (depends on bridge at t=0) # 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( motor_daemon = TimerAction(
period=2.5, period=2.5,
actions=[ actions=[

View File

@ -20,7 +20,11 @@ theta is kept in (−π, π] after every step.
Int32 rollover Int32 rollover
-------------- --------------
<<<<<<< HEAD
ESP32 BALANCE encoder counters are int32 and wrap at ±2^31. `unwrap_delta` handles 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 this by detecting jumps larger than half the int32 range and adjusting by the
full range: full range:

View File

@ -69,7 +69,11 @@ 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 bridge (s) 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 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)
@ -90,7 +94,11 @@ class Profile:
# ── Profile factory ──────────────────────────────────────────────────────────── # ── Profile factory ────────────────────────────────────────────────────────────
def _minimal() -> Profile: def _minimal() -> Profile:
<<<<<<< HEAD
"""Minimal: ESP32 bridge + sensors + motor daemon. """Minimal: ESP32 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,7 +1,11 @@
""" """
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 bridge, integrates 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. 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.

View File

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

View File

@ -10,7 +10,7 @@
- Sensors: - Sensors:
* RPLIDAR A1M8 (360° scanning LiDAR) * RPLIDAR A1M8 (360° scanning LiDAR)
* RealSense D435i (RGB-D camera + IMU) * RealSense D435i (RGB-D camera + IMU)
* BNO055 (9-DOF IMU, STM32 FC) * BNO055 (9-DOF IMU, ESP32-S3 FC)
- Actuators: - Actuators:
* 2x differential drive motors * 2x differential drive motors
* Pan/Tilt servos for camera * Pan/Tilt servos for camera
@ -120,7 +120,7 @@
<child link="right_wheel_link" /> <child link="right_wheel_link" />
</joint> </joint>
<!-- IMU Link (STM32 FC BNO055, mounted on main board) --> <!-- IMU Link (ESP32-S3 FC BNO055, mounted on main board) -->
<link name="imu_link"> <link name="imu_link">
<inertial> <inertial>
<mass value="0.01" /> <mass value="0.01" />

View File

@ -1 +1 @@
"""SaltyBot CAN bridge package — ESP32 IO motor controller and VESC telemetry via python-can.""" """SaltyBot CAN bridge package — ESP32-S3 BALANCE controller and VESC telemetry via python-can."""

View File

@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""
balance_protocol.py CAN message encoding/decoding for the ESP32-S3 BALANCE motor controller
and VESC telemetry.
CAN message layout
------------------
Command frames (Orin ESP32-S3 BALANCE / VESC):
MAMBA_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)
MAMBA_CMD_ESTOP 0x102 1 byte 0x01 = stop
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)
MAMBA_TELEM_BATTERY 0x201 8 bytes voltage (f32, V) | current (f32, A)
VESC telemetry frame (VESC Orin):
VESC_TELEM_STATE 0x300 16 bytes erpm (f32) | duty (f32) | voltage (f32) | current (f32)
All multi-byte fields are big-endian.
Issue: https://gitea.vayrette.com/seb/saltylab-firmware/issues/674
"""
import struct
from dataclasses import dataclass
from typing import Tuple
# ---------------------------------------------------------------------------
# CAN message IDs
# ---------------------------------------------------------------------------
MAMBA_CMD_VELOCITY: int = 0x100
MAMBA_CMD_MODE: int = 0x101
MAMBA_CMD_ESTOP: int = 0x102
MAMBA_TELEM_IMU: int = 0x200
MAMBA_TELEM_BATTERY: int = 0x201
VESC_TELEM_STATE: int = 0x300
ORIN_CAN_ID_PID_SET: int = 0x305
ORIN_CAN_ID_FC_PID_ACK: int = 0x405
# ---------------------------------------------------------------------------
# Mode constants
# ---------------------------------------------------------------------------
MODE_IDLE: int = 0
MODE_DRIVE: int = 1
MODE_ESTOP: int = 2
# ---------------------------------------------------------------------------
# Data classes for decoded telemetry
# ---------------------------------------------------------------------------
@dataclass
class ImuTelemetry:
"""Decoded IMU telemetry from ESP32-S3 BALANCE (MAMBA_TELEM_IMU)."""
accel_x: float = 0.0 # m/s²
accel_y: float = 0.0
accel_z: float = 0.0
gyro_x: float = 0.0 # rad/s
gyro_y: float = 0.0
gyro_z: float = 0.0
@dataclass
class BatteryTelemetry:
"""Decoded battery telemetry from ESP32-S3 BALANCE (MAMBA_TELEM_BATTERY)."""
voltage: float = 0.0 # V
current: float = 0.0 # A
@dataclass
class VescStateTelemetry:
"""Decoded VESC state telemetry (VESC_TELEM_STATE)."""
erpm: float = 0.0 # electrical RPM
duty: float = 0.0 # duty cycle [-1.0, 1.0]
voltage: float = 0.0 # bus voltage, V
current: float = 0.0 # phase current, A
@dataclass
class PidGains:
"""Balance PID gains (Issue #693)."""
kp: float = 0.0
ki: float = 0.0
kd: float = 0.0
# ---------------------------------------------------------------------------
# Encode helpers
# ---------------------------------------------------------------------------
_FMT_VEL = ">ff" # 2 × float32, big-endian
_FMT_MODE = ">B" # 1 × uint8
_FMT_ESTOP = ">B" # 1 × uint8
_FMT_IMU = ">ffffff" # 6 × float32
_FMT_BAT = ">ff" # 2 × float32
_FMT_VESC = ">ffff" # 4 × float32
def encode_velocity_cmd(left_mps: float, right_mps: float) -> bytes:
"""
Encode a MAMBA_CMD_VELOCITY payload.
Parameters
----------
left_mps: target left wheel speed in m/s (positive = forward)
right_mps: target right wheel speed in m/s (positive = forward)
Returns
-------
8-byte big-endian payload suitable for a CAN frame.
"""
return struct.pack(_FMT_VEL, float(left_mps), float(right_mps))
def encode_mode_cmd(mode: int) -> bytes:
"""
Encode a MAMBA_CMD_MODE payload.
Parameters
----------
mode: one of MODE_IDLE (0), MODE_DRIVE (1), MODE_ESTOP (2)
Returns
-------
1-byte payload.
"""
if mode not in (MODE_IDLE, MODE_DRIVE, MODE_ESTOP):
raise ValueError(f"Invalid mode {mode!r}; expected 0, 1, or 2")
return struct.pack(_FMT_MODE, mode)
def encode_estop_cmd(stop: bool = True) -> bytes:
"""
Encode a MAMBA_CMD_ESTOP payload.
Parameters
----------
stop: True to assert e-stop, False to clear.
Returns
-------
1-byte payload (0x01 = stop, 0x00 = clear).
"""
return struct.pack(_FMT_ESTOP, 0x01 if stop else 0x00)
def encode_pid_set_cmd(kp: float, ki: float, kd: float) -> bytes:
"""Encode ORIN_CAN_ID_PID_SET (6 bytes, uint16 BE x3). Issue #693."""
if kp < 0.0 or ki < 0.0 or kd < 0.0:
raise ValueError("PID gains must be non-negative")
return struct.pack(_FMT_PID, round(min(kp,_PID_KP_MAX)*100), round(min(ki,_PID_KI_MAX)*100), round(min(kd,_PID_KD_MAX)*100))
# ---------------------------------------------------------------------------
# Decode helpers
# ---------------------------------------------------------------------------
def decode_imu_telem(data: bytes) -> ImuTelemetry:
"""
Decode a MAMBA_TELEM_IMU payload.
Parameters
----------
data: exactly 24 bytes (6 × float32, big-endian).
Returns
-------
ImuTelemetry dataclass instance.
Raises
------
struct.error if data is the wrong length.
"""
ax, ay, az, gx, gy, gz = struct.unpack(_FMT_IMU, data)
return ImuTelemetry(
accel_x=ax, accel_y=ay, accel_z=az,
gyro_x=gx, gyro_y=gy, gyro_z=gz,
)
def decode_battery_telem(data: bytes) -> BatteryTelemetry:
"""
Decode a MAMBA_TELEM_BATTERY payload.
Parameters
----------
data: exactly 8 bytes (2 × float32, big-endian).
Returns
-------
BatteryTelemetry dataclass instance.
"""
voltage, current = struct.unpack(_FMT_BAT, data)
return BatteryTelemetry(voltage=voltage, current=current)
def decode_vesc_state(data: bytes) -> VescStateTelemetry:
"""
Decode a VESC_TELEM_STATE payload.
Parameters
----------
data: exactly 16 bytes (4 × float32, big-endian).
Returns
-------
VescStateTelemetry dataclass instance.
"""
erpm, duty, voltage, current = struct.unpack(_FMT_VESC, data)
return VescStateTelemetry(erpm=erpm, duty=duty, voltage=voltage, current=current)
def decode_pid_ack(data: bytes) -> PidGains:
"""Decode ORIN_CAN_ID_FC_PID_ACK (6 bytes). Issue #693."""
kp_x100, ki_x100, kd_x100 = struct.unpack(_FMT_PID, data)
return PidGains(kp=kp_x100/100.0, ki=ki_x100/100.0, kd=kd_x100/100.0)

View File

@ -1,23 +1,21 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
can_bridge_node.py ROS2 node bridging the Jetson Orin to the ESP32-S3 BALANCE can_bridge_node.py ROS2 node bridging the SaltyBot Orin to the ESP32-S3 BALANCE motor
board and VESC motor controllers over CAN bus (CANable 2.0 / slcan0, 500 kbps). controller and VESC motor controllers over CAN bus.
Spec: docs/SAUL-TEE-SYSTEM-REFERENCE.md §6 (2026-04-04) Spec: docs/SAUL-TEE-SYSTEM-REFERENCE.md §6 (2026-04-04)
Subscriptions Subscriptions
------------- -------------
/cmd_vel geometry_msgs/Twist ORIN_CMD_DRIVE (0x300) /cmd_vel geometry_msgs/Twist VESC speed commands (CAN)
/estop std_msgs/Bool ORIN_CMD_ESTOP (0x303) /estop std_msgs/Bool ESP32-S3 BALANCE e-stop (CAN)
/saltybot/arm std_msgs/Bool ORIN_CMD_ARM (0x301)
Publications Publications
------------ ------------
/saltybot/attitude std_msgs/String (JSON) pitch, speed, yaw_rate, state /can/imu sensor_msgs/Imu ESP32-S3 BALANCE IMU telemetry
/saltybot/balance_state std_msgs/String (JSON) alias of /saltybot/attitude /can/battery sensor_msgs/BatteryState ESP32-S3 BALANCE battery telemetry
/can/battery sensor_msgs/BatteryState vbat_mv, fault, rssi /can/vesc/left/state std_msgs/Float32MultiArray Left VESC state
/can/vesc/left/state std_msgs/Float32MultiArray VESC STATUS_1 left /can/vesc/right/state std_msgs/Float32MultiArray Right VESC state
/can/vesc/right/state std_msgs/Float32MultiArray VESC STATUS_1 right
/can/connection_status std_msgs/String "connected" | "disconnected" /can/connection_status std_msgs/String "connected" | "disconnected"
Parameters Parameters
@ -41,14 +39,14 @@ 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 (
ORIN_CMD_DRIVE, MAMBA_CMD_ESTOP,
ORIN_CMD_ARM, MAMBA_CMD_MODE,
ORIN_CMD_ESTOP, MAMBA_CMD_VELOCITY,
ESP32_TELEM_ATTITUDE, MAMBA_TELEM_BATTERY,
ESP32_TELEM_BATTERY, MAMBA_TELEM_IMU,
VESC_LEFT_ID, VESC_TELEM_STATE,
VESC_RIGHT_ID, ORIN_CAN_ID_FC_PID_ACK,
VESC_STATUS_1, ORIN_CAN_ID_PID_SET,
MODE_DRIVE, MODE_DRIVE,
MODE_IDLE, MODE_IDLE,
encode_drive_cmd, encode_drive_cmd,
@ -67,7 +65,7 @@ _WATCHDOG_HZ: float = 10.0
class CanBridgeNode(Node): class CanBridgeNode(Node):
"""CAN bus bridge between Orin ROS2 and ESP32 BALANCE / VESC controllers.""" """CAN bus bridge between Orin ROS2 and ESP32-S3 BALANCE / VESC controllers."""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("can_bridge_node") super().__init__("can_bridge_node")
@ -167,27 +165,34 @@ class CanBridgeNode(Node):
self._last_cmd_time = time.monotonic() self._last_cmd_time = time.monotonic()
if not self._connected: if not self._connected:
return return
speed = int(max(-1000.0, min(1000.0, msg.linear.x * self._speed_scale)))
steer = int(max(-1000.0, min(1000.0, msg.angular.z * self._steer_scale))) # Differential drive decomposition — individual wheel speeds in m/s.
self._send_can(ORIN_CMD_DRIVE, encode_drive_cmd(speed, steer, MODE_DRIVE), "cmd_vel") # The VESC nodes interpret linear velocity directly; angular is handled
# by the sign difference between left and right.
linear = msg.linear.x
angular = msg.angular.z
# Forward left = forward right for pure translation; for rotation
# left slows and right speeds up (positive angular = CCW = left turn).
# The ESP32-S3 BALANCE velocity command carries both wheels independently.
left_mps = linear - angular
right_mps = linear + angular
payload = encode_velocity_cmd(left_mps, right_mps)
self._send_can(MAMBA_CMD_VELOCITY, payload, "cmd_vel")
# Keep ESP32-S3 BALANCE in DRIVE mode while receiving commands
self._send_can(MAMBA_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 BALANCE over CAN.""" """Forward /estop to ESP32-S3 BALANCE over CAN."""
if not self._connected: if not self._connected:
return return
if msg.data: if msg.data:
self._send_can(ORIN_CMD_ESTOP, encode_estop_cmd(), "estop") self._send_can(
self.get_logger().warning("E-stop asserted — sent ESTOP to ESP32 BALANCE") MAMBA_CMD_MODE, encode_mode_cmd(MODE_ESTOP), "estop mode"
else: )
# Clear estop: send DISARM then re-ARM (let operator decide to re-arm) self.get_logger().warning("E-stop asserted — sent ESTOP to ESP32-S3 BALANCE")
self._send_can(ORIN_CMD_ARM, encode_arm_cmd(False), "estop_clear")
def _arm_cb(self, msg: Bool) -> None:
"""Forward /saltybot/arm to ORIN_CMD_ARM."""
if not self._connected:
return
self._send_can(ORIN_CMD_ARM, encode_arm_cmd(msg.data), "arm")
self.get_logger().info(f"ARM command: {'ARM' if msg.data else 'DISARM'}")
# ── Watchdog ────────────────────────────────────────────────────────── # ── Watchdog ──────────────────────────────────────────────────────────

View File

@ -15,7 +15,11 @@ 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 IO motor controller and VESC telemetry", 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", license="MIT",
tests_require=["pytest"], tests_require=["pytest"],
entry_points={ entry_points={

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Unit tests for saltybot_can_bridge.mamba_protocol. Unit tests for saltybot_can_bridge.balance_protocol.
No ROS2 or CAN hardware required tests exercise encode/decode round-trips No ROS2 or CAN hardware required tests exercise encode/decode round-trips
and boundary conditions entirely in Python. and boundary conditions entirely in Python.
@ -11,7 +11,7 @@ Run with: pytest test/test_can_bridge.py -v
import struct import struct
import unittest import unittest
from saltybot_can_bridge.mamba_protocol import ( from saltybot_can_bridge.balance_protocol import (
MAMBA_CMD_ESTOP, MAMBA_CMD_ESTOP,
MAMBA_CMD_MODE, MAMBA_CMD_MODE,
MAMBA_CMD_VELOCITY, MAMBA_CMD_VELOCITY,

View File

@ -17,7 +17,7 @@
<maintainer email="sl-jetson@saltylab.local">sl-jetson</maintainer> <maintainer email="sl-jetson@saltylab.local">sl-jetson</maintainer>
<license>MIT</license> <license>MIT</license>
<!-- Runtime dependency on saltybot_can_bridge for mamba_protocol --> <!-- Runtime dependency on saltybot_can_bridge for balance_protocol -->
<exec_depend>saltybot_can_bridge</exec_depend> <exec_depend>saltybot_can_bridge</exec_depend>
<buildtool_depend>ament_python</buildtool_depend> <buildtool_depend>ament_python</buildtool_depend>

View File

@ -1,28 +1,31 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
protocol_defs.py CAN message ID constants and frame builders/parsers for the protocol_defs.py CAN message ID constants and frame builders/parsers for the
OrinESP32 IOVESC 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 IO) protocol include/orin_can.h OrinFC (ESP32-S3 BALANCE) protocol
include/vesc_can.h VESC CAN protocol include/vesc_can.h VESC CAN protocol
saltybot_can_bridge/mamba_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 IO) commands (standard 11-bit, matching orin_can.h): Orin FC (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 IO) Orin telemetry (standard 11-bit, matching orin_can.h): FC (ESP32-S3 BALANCE) Orin telemetry (standard 11-bit, matching orin_can.h):
FC_STATUS 0x400 8 bytes (see orin_can_fc_status_t) FC_STATUS 0x400 8 bytes (see orin_can_fc_status_t)
FC_VESC 0x401 8 bytes (see orin_can_fc_vesc_t) FC_VESC 0x401 8 bytes (see orin_can_fc_vesc_t)
FC_IMU 0x402 8 bytes FC_IMU 0x402 8 bytes
FC_BARO 0x403 8 bytes FC_BARO 0x403 8 bytes
ESP32 IO VESC internal commands (matching mamba_protocol.py): Mamba VESC internal commands (matching balance_protocol.py):
=======
ESP32-S3 BALANCE VESC internal commands (matching balance_protocol.py):
>>>>>>> 9aed963 (fix: scrub remaining Mamba references in can_bridge and e2e test protocol files)
MAMBA_CMD_VELOCITY 0x100 8 bytes left_mps (f32) | right_mps (f32) big-endian 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_MODE 0x101 1 byte mode (0=idle,1=drive,2=estop)
MAMBA_CMD_ESTOP 0x102 1 byte 0x01=stop MAMBA_CMD_ESTOP 0x102 1 byte 0x01=stop
@ -36,7 +39,7 @@ import struct
from typing import Tuple from typing import Tuple
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Orin → FC (ESP32 IO) command IDs (from orin_can.h) # Orin → FC (ESP32-S3 BALANCE) command IDs (from orin_can.h)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
ORIN_CMD_HEARTBEAT: int = 0x300 ORIN_CMD_HEARTBEAT: int = 0x300
@ -45,7 +48,7 @@ ORIN_CMD_MODE: int = 0x302
ORIN_CMD_ESTOP: int = 0x303 ORIN_CMD_ESTOP: int = 0x303
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# FC (ESP32 IO) → Orin telemetry IDs (from orin_can.h) # FC (ESP32-S3 BALANCE) → Orin telemetry IDs (from orin_can.h)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
FC_STATUS: int = 0x400 FC_STATUS: int = 0x400
@ -54,7 +57,10 @@ FC_IMU: int = 0x402
FC_BARO: int = 0x403 FC_BARO: int = 0x403
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ESP32 IO → VESC internal command IDs (from mamba_protocol.py) # Mamba → 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 MAMBA_CMD_VELOCITY: int = 0x100
@ -136,14 +142,17 @@ def build_estop_cmd(action: int = 1) -> bytes:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Frame builders — ESP32 IO velocity commands (mamba_protocol.py encoding) # Frame builders — Mamba 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 MAMBA_CMD_VELOCITY payload (8 bytes, 2 × float32 big-endian).
Matches encode_velocity_cmd() in mamba_protocol.py. Matches encode_velocity_cmd() in balance_protocol.py.
""" """
return struct.pack(">ff", float(left_mps), float(right_mps)) return struct.pack(">ff", float(left_mps), float(right_mps))

View File

@ -14,7 +14,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",
description="End-to-end CAN integration tests for Orin↔ESP32 IO↔VESC full loop", description="End-to-end CAN integration tests for Orin↔ESP32-S3 BALANCE↔VESC full loop",
license="MIT", license="MIT",
tests_require=["pytest"], tests_require=["pytest"],
entry_points={ entry_points={

View File

@ -14,7 +14,7 @@ _pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _pkg_root not in sys.path: if _pkg_root not in sys.path:
sys.path.insert(0, _pkg_root) sys.path.insert(0, _pkg_root)
# Also add the saltybot_can_bridge package so we can import mamba_protocol. # Also add the saltybot_can_bridge package so we can import balance_protocol.
_bridge_pkg = os.path.join( _bridge_pkg = os.path.join(
os.path.dirname(_pkg_root), "saltybot_can_bridge" os.path.dirname(_pkg_root), "saltybot_can_bridge"
) )
@ -60,7 +60,7 @@ def loopback_can_bus():
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def bridge_components(): def bridge_components():
""" """
Return the mamba_protocol encode/decode callables and a fresh mock bus. Return the balance_protocol encode/decode callables and a fresh mock bus.
Yields a dict with keys: Yields a dict with keys:
bus MockCANBus instance bus MockCANBus instance
@ -69,7 +69,7 @@ def bridge_components():
encode_estop encode_estop_cmd(stop) bytes encode_estop encode_estop_cmd(stop) bytes
decode_vesc decode_vesc_state(data) VescStateTelemetry decode_vesc decode_vesc_state(data) VescStateTelemetry
""" """
from saltybot_can_bridge.mamba_protocol import ( from saltybot_can_bridge.balance_protocol import (
encode_velocity_cmd, encode_velocity_cmd,
encode_mode_cmd, encode_mode_cmd,
encode_estop_cmd, encode_estop_cmd,

View File

@ -3,7 +3,7 @@
test_drive_command.py Integration tests for the drive command path. test_drive_command.py Integration tests for the drive command path.
Tests verify: Tests verify:
DRIVE cmd ESP32 IO 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. FC_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.
@ -28,7 +28,7 @@ from saltybot_can_e2e_test.protocol_defs import (
parse_velocity_cmd, parse_velocity_cmd,
parse_fc_vesc, parse_fc_vesc,
) )
from saltybot_can_bridge.mamba_protocol import ( from saltybot_can_bridge.balance_protocol import (
encode_velocity_cmd, encode_velocity_cmd,
encode_mode_cmd, encode_mode_cmd,
) )
@ -61,7 +61,7 @@ def _send_drive(bus, left_mps: float, right_mps: float) -> None:
class TestDriveForward: 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 IO 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 MAMBA_CMD_VELOCITY frame with correct payload.
""" """
_send_drive(mock_can_bus, 1.0, 1.0) _send_drive(mock_can_bus, 1.0, 1.0)
@ -84,7 +84,7 @@ class TestDriveForward:
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 FC_VESC broadcast arriving after drive cmd; verify parse is correct.
(In the real loop ESP32 IO computes RPM from m/s and broadcasts FC_VESC.) (In the real loop ESP32-S3 BALANCE computes RPM from m/s and broadcasts FC_VESC.)
This test checks the FC_VESC frame format and parser. This test checks the FC_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)

View File

@ -32,7 +32,7 @@ from saltybot_can_e2e_test.protocol_defs import (
parse_velocity_cmd, parse_velocity_cmd,
parse_fc_status, parse_fc_status,
) )
from saltybot_can_bridge.mamba_protocol import ( from saltybot_can_bridge.balance_protocol import (
encode_velocity_cmd, encode_velocity_cmd,
encode_mode_cmd, encode_mode_cmd,
encode_estop_cmd, encode_estop_cmd,

View File

@ -30,7 +30,7 @@ from saltybot_can_e2e_test.protocol_defs import (
parse_fc_vesc, parse_fc_vesc,
parse_vesc_status, parse_vesc_status,
) )
from saltybot_can_bridge.mamba_protocol import ( from saltybot_can_bridge.balance_protocol import (
VESC_TELEM_STATE as BRIDGE_VESC_TELEM_STATE, VESC_TELEM_STATE as BRIDGE_VESC_TELEM_STATE,
decode_vesc_state, decode_vesc_state,
) )
@ -47,7 +47,7 @@ class VescStatusAggregator:
2. Builds an FC_VESC broadcast payload 2. Builds an FC_VESC broadcast payload
3. Injects the FC_VESC frame onto the mock bus 3. Injects the FC_VESC frame onto the mock bus
This represents the ESP32 IO Orin telemetry path. This represents the ESP32-S3 BALANCE Orin telemetry path.
""" """
def __init__(self, bus: MockCANBus): def __init__(self, bus: MockCANBus):

View File

@ -33,7 +33,7 @@ from saltybot_can_e2e_test.protocol_defs import (
build_velocity_cmd, build_velocity_cmd,
parse_velocity_cmd, parse_velocity_cmd,
) )
from saltybot_can_bridge.mamba_protocol import ( from saltybot_can_bridge.balance_protocol import (
encode_velocity_cmd, encode_velocity_cmd,
encode_mode_cmd, encode_mode_cmd,
encode_estop_cmd, encode_estop_cmd,
@ -90,7 +90,7 @@ class HeartbeatSimulator:
def _simulate_estop_on_timeout(bus: MockCANBus) -> None: def _simulate_estop_on_timeout(bus: MockCANBus) -> None:
""" """
Simulate the firmware-side logic: when heartbeat timeout expires, Simulate the firmware-side logic: when heartbeat timeout expires,
the FC sends an e-stop command by setting estop mode on the ESP32 IO bus. the FC sends an e-stop command by setting estop mode on the ESP32-S3 BALANCE bus.
We model this as the bridge sending zero velocity + ESTOP mode. We model this as the bridge sending zero velocity + ESTOP mode.
""" """

View File

@ -27,7 +27,7 @@ from saltybot_can_e2e_test.protocol_defs import (
build_velocity_cmd, build_velocity_cmd,
parse_velocity_cmd, parse_velocity_cmd,
) )
from saltybot_can_bridge.mamba_protocol import ( from saltybot_can_bridge.balance_protocol import (
encode_velocity_cmd, encode_velocity_cmd,
encode_mode_cmd, encode_mode_cmd,
encode_estop_cmd, encode_estop_cmd,
@ -189,7 +189,7 @@ class TestModeCommandEncoding:
"""build_mode_cmd in protocol_defs must produce identical bytes.""" """build_mode_cmd in protocol_defs must produce identical bytes."""
for mode in (MODE_IDLE, MODE_DRIVE, MODE_ESTOP): for mode in (MODE_IDLE, MODE_DRIVE, MODE_ESTOP):
assert build_mode_cmd(mode) == encode_mode_cmd(mode), \ assert build_mode_cmd(mode) == encode_mode_cmd(mode), \
f"protocol_defs.build_mode_cmd({mode}) != mamba_protocol.encode_mode_cmd({mode})" f"protocol_defs.build_mode_cmd({mode}) != balance_protocol.encode_mode_cmd({mode})"
class TestInvalidMode: class TestInvalidMode:
@ -218,8 +218,8 @@ class TestInvalidMode:
accepted = sm.set_mode(-1) accepted = sm.set_mode(-1)
assert accepted is False assert accepted is False
def test_mamba_protocol_invalid_mode_raises(self): def test_balance_protocol_invalid_mode_raises(self):
"""mamba_protocol.encode_mode_cmd must raise on invalid mode.""" """balance_protocol.encode_mode_cmd must raise on invalid mode."""
with pytest.raises(ValueError): with pytest.raises(ValueError):
encode_mode_cmd(99) encode_mode_cmd(99)
with pytest.raises(ValueError): with pytest.raises(ValueError):

View File

@ -27,7 +27,11 @@ 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 BALANCE) ────────────────────────────────────────────────── # ── 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) # 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

View File

@ -172,7 +172,7 @@
<xacro:wheel name="wheel_right_link" side="-1"/> <xacro:wheel name="wheel_right_link" side="-1"/>
<!-- ═══════════════════════════════════════════════════════════════════ <!-- ═══════════════════════════════════════════════════════════════════
imu_link — MPU-6000 on MAMBA F722S flight controller imu_link — MPU-6000 on ESP32-S3 BALANCE ESP32-S3 BALANCE
fc_x = -50mm SCAD = +x ROS; z = pad + standoff above deck = 12mm fc_x = -50mm SCAD = +x ROS; z = pad + standoff above deck = 12mm
═══════════════════════════════════════════════════════════════════ --> ═══════════════════════════════════════════════════════════════════ -->
<link name="imu_link"/> <link name="imu_link"/>

View File

@ -5,7 +5,11 @@ Comprehensive hardware diagnostics and health monitoring for SaltyBot.
## Features ## Features
### Startup Checks ### Startup Checks
<<<<<<< HEAD
- RPLIDAR, RealSense, VESC, Jabra mic, ESP32 BALANCE, servos - 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 - WiFi, GPS, disk space, RAM
- Boot result TTS + face animation - Boot result TTS + face animation
- JSON logging - JSON logging

View File

@ -138,7 +138,11 @@ 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 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:

View File

@ -7,7 +7,11 @@
# 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 BALANCE AUTONOMOUS mode gate. # 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. # Do not run this node without the cmd_vel bridge running on the same robot.
# ── Follow geometry ──────────────────────────────────────────────────────────── # ── Follow geometry ────────────────────────────────────────────────────────────
@ -70,5 +74,9 @@ 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 BALANCE AUTONOMOUS mode (md=2). # 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 follow_enabled: true

View File

@ -28,7 +28,11 @@ State machine
Safety wiring Safety wiring
------------- -------------
<<<<<<< HEAD
* cmd_vel bridge (PR #46) applies ramp + deadman + ESP32 BALANCE AUTONOMOUS mode gate -- * 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. 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

View File

@ -1,6 +1,10 @@
gimbal_node: gimbal_node:
ros__parameters: ros__parameters:
<<<<<<< HEAD
# Serial port connecting to ESP32 BALANCE over JLINK protocol # 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" serial_port: "/dev/ttyTHS1"
baud_rate: 921600 baud_rate: 921600

View File

@ -14,7 +14,11 @@ 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 BALANCE", 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",

View File

@ -3,7 +3,7 @@
<name>saltybot_gimbal</name> <name>saltybot_gimbal</name>
<version>1.0.0</version> <version>1.0.0</version>
<description> <description>
ROS2 gimbal control node: pan/tilt camera head via JLINK serial to STM32. ROS2 gimbal control node: pan/tilt camera head via JLINK serial to ESP32-S3.
Smooth trapezoidal motion profiles, configurable limits, look_at 3D projection. Smooth trapezoidal motion profiles, configurable limits, look_at 3D projection.
Issue #548. Issue #548.
</description> </description>

View File

@ -1,7 +1,11 @@
#!/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 BALANCE. 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. Implements smooth trapezoidal motion profiles with configurable axis limits.
Subscribed topics: Subscribed topics:

View File

@ -1,19 +1,29 @@
"""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). Matches the JLINK protocol defined in include/jlink.h (Issue #547 ESP32 side).
Command type (Jetson ESP32 BALANCE): Command type (Jetson ESP32 BALANCE):
=======
Matches the JLINK protocol defined in include/jlink.h (Issue #547 ESP32-S3 side).
Command type (Jetson ESP32-S3):
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
0x0B GIMBAL_POS int16 pan_x10 + int16 tilt_x10 + uint16 speed (6 bytes) 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 BALANCE Jetson): 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 + 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)
Frame format (shared with stm32_protocol.py): Frame format (shared with esp32_protocol.py):
[STX=0x02][CMD][LEN][PAYLOAD...][CRC16_hi][CRC16_lo][ETX=0x03] [STX=0x02][CMD][LEN][PAYLOAD...][CRC16_hi][CRC16_lo][ETX=0x03]
CRC16-CCITT: poly=0x1021, init=0xFFFF, covers CMD+LEN+PAYLOAD bytes. CRC16-CCITT: poly=0x1021, init=0xFFFF, covers CMD+LEN+PAYLOAD bytes.
""" """
@ -31,8 +41,13 @@ ETX = 0x03
# ── Command / telemetry type codes ───────────────────────────────────────────── # ── Command / telemetry type codes ─────────────────────────────────────────────
<<<<<<< HEAD
CMD_GIMBAL_POS = 0x0B # Jetson → ESP32 BALANCE: set pan/tilt target CMD_GIMBAL_POS = 0x0B # Jetson → ESP32 BALANCE: set pan/tilt target
TLM_GIMBAL_STATE = 0x84 # ESP32 BALANCE → Jetson: measured state TLM_GIMBAL_STATE = 0x84 # ESP32 BALANCE → Jetson: measured state
=======
CMD_GIMBAL_POS = 0x0B # Jetson → ESP32-S3: set pan/tilt target
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))

View File

@ -5,7 +5,11 @@
# #
# 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 BALANCE state) # /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) # /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,7 +13,11 @@ 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 BALANCE 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,9 +6,15 @@ 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. "RC" ESP32 BALANCE 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 BALANCE executing Jetson cmd_vel; RC sticks idle. "AUTO" ESP32 BALANCE executing Jetson cmd_vel; RC sticks idle.
=======
"RC" ESP32-S3 executing RC pilot commands; Jetson cmd_vel blocked.
"RAMP_TO_AUTO" Transitioning RCAUTO; blend_alpha 0.01.0 over ramp_s.
"AUTO" ESP32-S3 executing Jetson cmd_vel; RC sticks idle.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
"RAMP_TO_RC" Transitioning AUTORC; blend_alpha 1.00.0 over ramp_s. "RAMP_TO_RC" Transitioning AUTORC; blend_alpha 1.00.0 over ramp_s.
Blend alpha Blend alpha

View File

@ -9,7 +9,11 @@ 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 BALANCE 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,8 +1,13 @@
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) left_can_id: 56 # left motor VESC CAN ID (ESP32 BALANCE)
right_can_id: 68 # right 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)
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

View File

@ -12,7 +12,11 @@
# 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 BALANCE wheel encoders → /odom # 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) # 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 ──────────────────────

View File

@ -71,7 +71,11 @@ 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 bridge serial port'), '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', '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'),

View File

@ -370,7 +370,11 @@ 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; wait for it # 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) time.sleep(1.5)
self.get_logger().info( self.get_logger().info(

View File

@ -9,7 +9,11 @@
# #
# 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 BALANCE wheel encoders → /odom # 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) # UWB: /uwb/target (follow-me reference, logged for context)
route_recorder: route_recorder:

View File

@ -10,7 +10,11 @@ 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 bridge (/odom from wheel encoders) 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) D435i (/imu/data for heading)
Usage record a route: Usage record a route:

View File

@ -5,7 +5,11 @@ 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 BALANCE 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

View File

@ -39,6 +39,12 @@ 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_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_input_topic: /cmd_vel_input # upstream velocity (remap as needed)
cmd_vel_output_topic: /cmd_vel # downstream (to ESP32 bridge) 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_input_topic: /cmd_vel_input # upstream velocity (remap as needed)
cmd_vel_output_topic: /cmd_vel # downstream (to ESP32-S3 bridge)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)

View File

@ -10,7 +10,11 @@
# 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 BALANCE
=======
# 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
@ -83,11 +87,19 @@ 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 # 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 # 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 BALANCE balance PID gains likely need retuning for ride speed. Expect # 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
# reconfigured: max_linear_vel=8.0, ramp_rate=500 → consider ramp_rate=150
# at ride speed (slower ramp = smoother balance).
#
# 2. The ESP32-S3 balance PID gains likely need retuning for ride speed. Expect
>>>>>>> 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. # increased sensitivity to pitch angle errors at 8 m/s vs 0.5 m/s.
# #
# 3. Test sequence recommendation: # 3. Test sequence recommendation:

View File

@ -10,7 +10,11 @@ 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 BALANCE
=======
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):

Some files were not shown because too many files have changed in this diff Show More