Compare commits

..

11 Commits

Author SHA1 Message Date
a1233dbd04 fix: scrub remaining Mamba references in can_bridge and e2e test protocol files
- balance_protocol.py: Mamba→Orin / Mamba→VESC comments → ESP32-S3 BALANCE
- can_bridge_node.py: docstring and inline comments
- __init__.py: package description
- protocol_defs.py: all Mamba references in docstring and comments
- test_fc_vesc_broadcast.py, test_drive_command.py: test comments

Zero Mamba/STM32F722/BlackPill/stm32_protocol/mamba_protocol references
now exist outside legacy/stm32/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 09:00:44 -04:00
fa75c442a7 feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only
Archive STM32 firmware to legacy/stm32/:
- src/, include/, lib/USB_CDC/, platformio.ini, test stubs, flash_firmware.py
- test/test_battery_adc.c, test_hw_button.c, test_pid_schedule.c, test_vesc_can.c, test_can_watchdog.c
- USB_CDC_BUG.md

Rename: stm32_protocol → esp32_protocol, mamba_protocol → balance_protocol,
  stm32_cmd_node → esp32_cmd_node, stm32_cmd_params → esp32_cmd_params,
  stm32_cmd.launch.py → esp32_cmd.launch.py,
  test_stm32_protocol → test_esp32_protocol, test_stm32_cmd_node → test_esp32_cmd_node

Content cleanup across all files:
- Mamba F722S → ESP32-S3 BALANCE
- BlackPill → ESP32-S3 IO
- STM32F722/F7xx → ESP32-S3
- stm32Mode/Version/Port → esp32Mode/Version/Port
- STM32 State/Mode labels → ESP32 State/Mode
- Jetson Nano → Jetson Orin Nano Super
- /dev/stm32 → /dev/esp32
- stm32_bridge → esp32_bridge
- STM32 HAL → ESP-IDF

docs/SALTYLAB.md:
- Update "Drone FC Details" to describe ESP32-S3 BALANCE board (Waveshare ESP32-S3 Touch LCD 1.28)
- Replace verbose "Self-Balancing Control" STM32 section with brief note pointing to SAUL-TEE-SYSTEM-REFERENCE.md

TEAM.md: Update Embedded Firmware Engineer role to ESP32-S3 / ESP-IDF

No new functionality — cleanup only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 09:00:38 -04:00
fe84ff6039 Merge pull request 'feat(arch): CAN/UART bridge update for SAUL-TEE ESP32 architecture' (#714) from sl-controls/esp32-can-bridge-update into main 2026-04-04 09:00:10 -04:00
fda6ab99ff feat(arch): align CAN/UART bridges with SAUL-TEE-SYSTEM-REFERENCE.md spec
Update CAN and serial bridge code to match authoritative protocol spec
from docs/SAUL-TEE-SYSTEM-REFERENCE.md §5-6 (hal, 2026-04-04).

mamba_protocol.py (CAN, Orin ↔ ESP32 BALANCE):
- 0x300 DRIVE: [speed:i16][steer:i16][mode:u8][flags:u8][_:u16] — combined frame
- 0x301 ARM:   [arm:u8]
- 0x302 PID:   [kp:f16][ki:f16][kd:f16][_:u16]  — half-float gains
- 0x303 ESTOP: [0xE5] — magic byte cut
- 0x400 ATTITUDE: [pitch:f16][speed:f16][yaw_rate:f16][state:u8][flags:u8]
- 0x401 BATTERY:  [vbat_mv:u16][fault_code:u8][rssi:i8]
- Add VESC STATUS1/4/5 decode helpers; VESC IDs 56 (left) / 68 (right)

can_bridge_node.py:
- /cmd_vel → encode_drive_cmd (speed/steer int16, MODE_DRIVE)
- /estop → encode_estop_cmd (magic 0xE5); clear → DISARM
- /saltybot/arm → encode_arm_cmd (new subscription)
- Watchdog sends DRIVE(0,0,MODE_IDLE) when /cmd_vel silent
- ATTITUDE (0x400) → /saltybot/attitude + /saltybot/balance_state JSON
- BATTERY  (0x401) → /can/battery BatteryState
- VESC STATUS1 frames → /can/vesc/left|right/state

stm32_cmd_node.py — rewritten for inter-board protocol API:
- Imports from updated stm32_protocol (BAUD_RATE=460800, new frame types)
- RX: RcChannels → /saltybot/rc_channels, SensorData → /saltybot/sensors
- TX: encode_led_cmd, encode_output_cmd from /saltybot/leds + /saltybot/outputs
- HEARTBEAT (0x20) timer replaces old SPEED_STEER/ARM logic

stm32_cmd_params.yaml: serial_port=/dev/esp32-io, baud=460800
stm32_cmd.launch.py: updated defaults and description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:59:45 -04:00
308be74330 feat(arch): implement SAUL-TEE ESP32 protocol specs from hal reference doc
Spec source: docs/SAUL-TEE-SYSTEM-REFERENCE.md (hal, 2026-04-04)

stm32_protocol.py — rewritten for inter-board UART protocol (ESP32 BALANCE ↔ IO):
- Frame: [0xAA][LEN][TYPE][PAYLOAD][CRC8] @ 460800 baud (was STX/ETX/CRC16)
- CRC-8 poly 0x07 over LEN+TYPE+PAYLOAD
- New message types: RC_CHANNELS(0x01), SENSORS(0x02), LED_CMD(0x10),
  OUTPUT_CMD(0x11), MOTOR_CMD(0x12), HEARTBEAT(0x20)

mamba_protocol.py — updated CAN IDs and frame formats:
- Orin→BALANCE: DRIVE(0x300) f32×2 LE, MODE(0x301), ESTOP(0x302), LED(0x303)
- BALANCE→Orin: FC_STATUS(0x400) pitch/vbat/state, FC_VESC(0x401) rpm/current
- VESC node IDs: Left=56, Right=68 (authoritative per §8)
- VESC extended frames: STATUS1(cmd=9), STATUS4(cmd=16), STATUS5(cmd=27)
- Replaced old MAMBA_CMD_*/MAMBA_TELEM_* constants

can_bridge_node.py — updated to use new IDs:
- ORIN_CMD_DRIVE/MODE/ESTOP replace MAMBA_CMD_VELOCITY/MODE/ESTOP
- FC_STATUS handler: publishes pitch→/can/imu, vbat_mv→/can/battery
- FC_VESC handler: publishes rpm/cur→/can/vesc/left|right/state
- VESC STATUS1 extended frames decoded per node ID (56/68)
- Removed PID CAN command (not in new spec)

CLAUDE.md — updated with ESP32-S3 BALANCE/IO hardware summary + key protocols

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:59:28 -04:00
19be6bbe11 Merge pull request 'docs: SAUL-TEE full ESP32-S3 system reference (arch migration)' (#712) from sl-firmware/arch-esp32-migration into main 2026-04-04 08:57:11 -04:00
5ef1f7e365 docs: full SAUL-TEE ESP32-S3 spec — pins, CAN, UART, RC mapping
Complete hardware reference from hal@Orin spec (2026-04-04):
- docs/SAUL-TEE-SYSTEM-REFERENCE.md: authoritative pin/protocol/CAN reference
  ESP32-S3 BALANCE: QMI8658 SPI(IO38-42), GC9A01 LCD, SN65HVD230 CAN(IO43/44),
  inter-board UART(IO17/18)
  ESP32-S3 IO: Crossfire UART0(IO43/44), ELRS UART2(IO16/17), BTS7960(IO1-8),
  I2C(IO11/12), WS2812(IO13), buzzer/headlight/fan, arming btn, kill-sw, UART(IO18/21)
- Inter-board binary protocol: [0xAA][LEN][TYPE][PAYLOAD][CRC8] @ 460800 baud
- CAN: VESC L=68, R=56; Orin cmds 0x300-0x303; telemetry 0x400-0x401 @ 10Hz
- RC: CH5=ARM, CH6=ESTOP, CH7=speed-limit; CRSF loss >100ms = motors cut
- CLAUDE.md, TEAM.md, docs/AGENTS.md, docs/SALTYLAB.md updated with full spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:25:24 -04:00
bfca6d1d92 docs: Add SAUL-TEE system reference + update wiring diagram
- docs/SAUL-TEE-SYSTEM-REFERENCE.md: authoritative architecture doc for
  the new 4-wheel wagon. Covers ESP32-S3 BALANCE (Waveshare LCD 1.28,
  QMI8658, SN65HVD230 CAN), ESP32-S3 IO (TBS Crossfire, ELRS, BTS7960,
  NFC/baro/ToF, WS2812), inter-board UART protocol (460800 baud,
  [0xAA][len][type][payload][crc8]), CAN IDs (VESCs 68/56, Orin
  0x300-0x303 cmd / 0x400-0x401 telemetry), RC channel map, power
  architecture, safety systems, and firmware layout.

- docs/wiring-diagram.md: banner pointing to new reference doc;
  old Mamba F722S UART summary marked OBSOLETE.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:25:24 -04:00
f71dad5344 feat(arch): migrate all STM32/Mamba/BlackPill refs to ESP32 BALANCE/IO + fix roslib@1.4.0
Architecture change (2026-04-03): Mamba F722S (STM32F722) and BlackPill
replaced by ESP32 BALANCE (PID loop) and ESP32 IO (motors/sensors/comms).

- Update CLAUDE.md, docs, chassis BOM/ASSEMBLY, pinout, power-budget,
  wiring-diagram, TEAM.md, AUTONOMOUS_ARMING.md, docker-compose
- Update all ROS2 package comments, config labels, launch args
  (stm32_port→esp32_port, /dev/stm32-bridge→/dev/esp32-bridge)
- Update WebUI: stm32Mode→esp32Mode, stm32Version→esp32Version,
  "STM32 State/Mode" labels → "ESP32 State/Mode" (ControlMode, SettingsPanel)
- Add TODO(esp32-migration) markers on stm32_protocol.py and mamba_protocol.py
  binary frame layouts — pending ESP32 protocol spec from max
- Fix roslib CDN 1.3.0→1.4.0 in all 11 HTML panels (fixes ROS2 Humble
  rosbridge "Received a message without an op" incompatibility)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:25:24 -04:00
5e97676703 docs: Update chassis docs for ESP32 architecture (retire Mamba F722S)
Replace Mamba F722S / STM32F722 references in BOM.md and ASSEMBLY.md
with ESP32 BALANCE + ESP32 IO. Board dimensions marked TBD pending
spec from max.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:25:24 -04:00
30b0f245e1 docs: retire Mamba F722S/BlackPill, adopt ESP32 BALANCE + ESP32 IO architecture
Effective 2026-04-03: STM32F722 flight controller no longer used.
New architecture:
- ESP32 BALANCE: PID balance loop
- ESP32 IO: motors, sensors, comms

Updated: CLAUDE.md, TEAM.md, docs/AGENTS.md, docs/SALTYLAB.md
Legacy src/ STM32 firmware is archived — not extended.
Source code migration pending ESP32 hardware spec from max.

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

26
TEAM.md
View File

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

View File

@ -56,10 +56,19 @@
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)
> ⚠️ **ARCHITECTURE CHANGE (2026-04-03):** ESP32 BALANCE retired. Two ESP32 boards replace it.
> Board dimensions and hole patterns TBD — await spec from max before machining mount plate.
=======
### 7 FC mount (ESP32-S3 BALANCE) ### 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 FC onto standoffs; secure with M3×6 BHCS. Snug only — do not over-torque. 2. Lower ESP32 BALANCE board onto standoffs; secure with M3×6 BHCS. Snug only.
3. Orient USB-C port toward front of robot for cable access. 3. Mount ESP32 IO board adjacent — exact placement TBD pending board dimensions.
4. Orient USB connectors toward front of robot for cable access.
### 8 Jetson Orin Nano Super 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.
@ -86,7 +95,8 @@
| Wheelbase (axle C/L to C/L) | 600 mm | ±1 mm | | Wheelbase (axle C/L to C/L) | 600 mm | ±1 mm |
| Motor fork slot width | 24 mm | +0.5 / 0 | | Motor fork slot width | 24 mm | +0.5 / 0 |
| Motor fork dropout depth | 60 mm | ±0.5 mm | | Motor fork dropout depth | 60 mm | ±0.5 mm |
| FC hole pattern | 30.5 × 30.5 mm | ±0.2 mm | | ESP32 BALANCE hole pattern | TBD — await spec from max | ±0.2 mm |
| ESP32 IO hole pattern | TBD — await spec from max | ±0.2 mm |
| Jetson hole pattern | 58 × 58 mm | ±0.2 mm | | Jetson hole pattern | 58 × 58 mm | ±0.2 mm |
| Battery tray inner | 185 × 72 × 52 mm | +2 / 0 mm | | Battery tray inner | 185 × 72 × 52 mm | +2 / 0 mm |

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 | FC standoff M3×6mm nylon | 4 | Nylon | — | ESP32-S3 BALANCE vibration isolation | | 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
@ -88,12 +92,23 @@ PR #7 (`chassis_frame.scad`) used placeholder values. The table below records th
## Electronics Mounts ## Electronics Mounts
> ⚠️ **ARCHITECTURE CHANGE (2026-04-03):** ESP32 BALANCE (ESP32) is retired.
> Replaced by **ESP32 BALANCE** + **ESP32 IO**. Board dimensions and hole patterns TBD — await spec from max.
| # | Part | Qty | Spec | Notes | | # | Part | Qty | Spec | Notes |
|---|------|-----|------|-------| |---|------|-----|------|-------|
<<<<<<< HEAD
| 13 | ESP32 BALANCE board | 1 | TBD — mount pattern TBD | PID balance loop; replaces ESP32 BALANCE |
| 13b | ESP32 IO board | 1 | TBD — mount pattern TBD | Motor/sensor/comms I/O |
| 14 | Nylon M3 standoff 6mm | 4 | F/F nylon | ESP32 board isolation |
| 15 | Anti-vibration grommet M3 | 4 | Ø6mm silicone | Under ESP32 mount pads |
| 16 | Jetson Orin module | 1 | 69.6×45mm module + carrier | 58×58mm M3 carrier hole pattern |
=======
| 13 | ESP32-S3 ESP32-S3 BALANCE FC | 1 | 36×36mm PCB, 30.5×30.5mm M3 mount | Oriented USB-C port toward front | | 13 | ESP32-S3 ESP32-S3 BALANCE FC | 1 | 36×36mm PCB, 30.5×30.5mm M3 mount | Oriented USB-C port toward front |
| 14 | Nylon M3 standoff 6mm | 4 | F/F nylon | FC vibration isolation | | 14 | Nylon M3 standoff 6mm | 4 | F/F nylon | FC vibration isolation |
| 15 | Anti-vibration grommet M3 | 4 | Ø6mm silicone | Under FC mount pads | | 15 | Anti-vibration grommet M3 | 4 | Ø6mm silicone | Under FC mount pads |
| 16 | Jetson Orin Nano Super B01 module | 1 | 69.6×45mm module + carrier | 58×58mm M3 carrier hole pattern | | 16 | Jetson Orin Nano Super B01 module | 1 | 69.6×45mm module + carrier | 58×58mm M3 carrier hole pattern |
>>>>>>> 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 |
--- ---
@ -144,8 +159,8 @@ Slide entire carousel up/down the stem with M6 collar bolts loosened. Tighten at
| 26 | M6×60 SHCS | 4 | ISO 4762, SS | Collar clamping bolts | | 26 | M6×60 SHCS | 4 | ISO 4762, SS | Collar clamping bolts |
| 27 | M6 hex nut | 4 | ISO 4032, SS | Captured in collar pockets | | 27 | M6 hex nut | 4 | ISO 4032, SS | Captured in collar pockets |
| 28 | M6×12 set screw | 2 | ISO 4026, SS cup-point | Stem height lock (1 per collar half) | | 28 | M6×12 set screw | 2 | ISO 4026, SS cup-point | Stem height lock (1 per collar half) |
| 29 | M3×10 SHCS | 12 | ISO 4762, SS | FC mount + miscellaneous | | 29 | M3×10 SHCS | 12 | ISO 4762, SS | ESP32 mount + miscellaneous |
| 30 | M3×6 BHCS | 4 | ISO 4762, SS | FC board bolts | | 30 | M3×6 BHCS | 4 | ISO 4762, SS | ESP32 board bolts (qty TBD pending board spec) |
| 31 | Axle lock nut (match axle tip thread) | 4 | Flanged, confirm thread | 2 per motor | | 31 | Axle lock nut (match axle tip thread) | 4 | Flanged, confirm thread | 2 per motor |
| 32 | Flat washer M5 | 32 | SS | | | 32 | Flat washer M5 | 32 | SS | |
| 33 | Flat washer M4 | 32 | SS | | | 33 | Flat washer M4 | 32 | SS | |

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-S3 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

@ -2,8 +2,26 @@
You're working on **SaltyLab**, a self-balancing two-wheeled indoor robot. Read this entire file before touching anything. You're working on **SaltyLab**, a self-balancing two-wheeled indoor robot. Read this entire file before touching anything.
## Project Overview ## ⚠️ ARCHITECTURE — SAUL-TEE (finalised 2026-04-04)
<<<<<<< HEAD
Full hardware spec: `docs/SAUL-TEE-SYSTEM-REFERENCE.md` — **read it before writing firmware.**
| Board | Role |
|-------|------|
| **ESP32-S3 BALANCE** | Waveshare Touch LCD 1.28 (CH343 USB). QMI8658 IMU, PID loop, CAN→VESC L(68)/R(56), GC9A01 LCD |
| **ESP32-S3 IO** | Bare devkit (JTAG USB). TBS Crossfire RC (UART0), ELRS failover (UART2), BTS7960 motors, NFC/baro/ToF, WS2812, buzzer/horn/headlight/fan |
| **Jetson Orin** | CANable2 USB→CAN. Cmds on 0x3000x303, telemetry on 0x4000x401 |
```
Jetson Orin ──CANable2──► CAN 500kbps ◄───────────────────────┐
│ │
ESP32-S3 BALANCE ←─UART 460800─► ESP32-S3 IO
(QMI8658, PID loop) (BTS7960, RC, sensors)
│ CAN 500kbps
┌─────────┴──────────┐
VESC Left (ID 68) VESC Right (ID 56)
=======
A hoverboard-based balancing robot with two compute layers: A hoverboard-based balancing robot with two compute layers:
1. **ESP32-S3 BALANCE** — ESP32-S3 BALANCE (ESP32-S3RET6 + MPU6000 IMU). Runs a lean C balance loop at up to 8kHz. Talks UART to the hoverboard ESC. This is the safety-critical layer. 1. **ESP32-S3 BALANCE** — ESP32-S3 BALANCE (ESP32-S3RET6 + MPU6000 IMU). Runs a lean C balance loop at up to 8kHz. Talks UART to the hoverboard ESC. This is the safety-critical layer.
2. **Jetson Orin Nano Super** — AI brain. ROS2, SLAM, person tracking. Sends velocity commands to FC via UART. Not safety-critical — FC operates independently. 2. **Jetson Orin Nano Super** — AI brain. ROS2, SLAM, person tracking. Sends velocity commands to FC via UART. Not safety-critical — FC operates independently.
@ -16,8 +34,12 @@ Jetson (speed+steer via UART1) ←→ ELRS RC (UART3, kill switch)
▼ UART2 ▼ UART2
Hoverboard ESC (FOC) → 2× 8" hub motors Hoverboard ESC (FOC) → 2× 8" hub motors
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
``` ```
Frame: `[0xAA][LEN][TYPE][PAYLOAD][CRC8]`
Legacy `src/` STM32 HAL code is **archived — do not extend.**
## ⚠️ SAFETY — READ THIS OR PEOPLE GET HURT ## ⚠️ SAFETY — READ THIS OR PEOPLE GET HURT
This is not a toy. 8" hub motors + 36V battery can crush fingers, break toes, and launch the frame. Every firmware change must preserve these invariants: This is not a toy. 8" hub motors + 36V battery can crush fingers, break toes, and launch the frame. Every firmware change must preserve these invariants:
@ -35,7 +57,11 @@ 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/ # ESP-IDF firmware (PlatformIO) 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 # QMI8658-P SPI driver (backup IMU — currently broken) │ ├── icm42688.c # QMI8658-P SPI driver (backup IMU — currently broken)
@ -82,11 +108,19 @@ PLATFORM.md # Hardware platform reference
## Hardware Quick Reference ## Hardware Quick Reference
<<<<<<< HEAD
### ESP32 BALANCE Flight Controller
| Spec | Value |
|------|-------|
| MCU | ESP32RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) |
=======
### ESP32-S3 BALANCE Flight Controller ### ESP32-S3 BALANCE Flight Controller
| Spec | Value | | Spec | Value |
|------|-------| |------|-------|
| MCU | ESP32-S3RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) | | MCU | ESP32-S3RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) |
>>>>>>> 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) |
@ -160,7 +194,11 @@ 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-S3** — 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 Serial (CH343) needs ReceivePacket() primed in CDC_Init** — without it, the OUT endpoint never starts listening. No data reception. 5. **USB Serial (CH343) needs ReceivePacket() primed in CDC_Init** — without it, the OUT endpoint never starts listening. No data reception.
@ -172,7 +210,11 @@ The firmware supports reboot-to-DFU via USB command:
2. Firmware writes `0xDEADBEEF` to RTC backup register 0 2. Firmware writes `0xDEADBEEF` to RTC backup register 0
3. `NVIC_SystemReset()` — clean hardware reset 3. `NVIC_SystemReset()` — clean hardware reset
4. On boot, `checkForBootloader()` (called after `HAL_Init()`) reads the magic 4. On boot, `checkForBootloader()` (called after `HAL_Init()`) reads the magic
<<<<<<< HEAD
5. If magic found: clears it, remaps system memory, jumps to ESP32 BALANCE bootloader at `0x1FF00000`
=======
5. If magic found: clears it, remaps system memory, jumps to ESP32-S3 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

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-S3 LCD display with 5 core emotions and smooth transitions. Implements expressive face animations on an ESP32-S3 LCD display with 5 core emotions and smooth transitions.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Features ## Features
@ -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)
=======
- ESP32-S3xx (ESP32-S3 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

@ -1,6 +1,6 @@
# SaltyLab — Self-Balancing Indoor Bot 🔬 # SAUL-TEE — Self-Balancing Wagon Robot 🔬
Two-wheeled, self-balancing robot for indoor AI/SLAM experiments. Four-wheel wagon (870×510×550 mm, 23 kg). Full spec: `docs/SAUL-TEE-SYSTEM-REFERENCE.md`
## ⚠️ SAFETY — TOP PRIORITY ## ⚠️ SAFETY — TOP PRIORITY
@ -102,8 +102,11 @@ balance loop, and drives the hoverboard ESC via UART. Jetson Orin Nano Super
sends velocity commands over UART1. ELRS receiver on UART3 provides RC sends velocity commands over UART1. ELRS receiver on UART3 provides RC
override and kill-switch capability. override and kill-switch capability.
The legacy STM32 firmware (Mamba F722S era) has been archived to
=======
The legacy STM32 firmware (STM32 era) has been archived to The legacy STM32 firmware (STM32 era) has been archived to
`legacy/stm32/` and is no longer built or deployed. `legacy/stm32/` and is no longer built or deployed.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## LED Subsystem (ESP32-C3) ## LED Subsystem (ESP32-C3)

View File

@ -0,0 +1,222 @@
# SAUL-TEE System Reference — SaltyLab ESP32 Architecture
*Authoritative source of truth for hardware, pins, protocols, and CAN assignments.*
*Spec from hal@Orin, 2026-04-04.*
---
## Overview
| Board | Role | MCU | USB chip |
|-------|------|-----|----------|
| **ESP32-S3 BALANCE** | PID balance loop, CAN→VESCs, LCD display | ESP32-S3 | CH343 USB-serial |
| **ESP32-S3 IO** | RC input, motor drivers, sensors, LEDs, peripherals | ESP32-S3 | JTAG USB (native) |
**Robot form factor:** 4-wheel wagon — 870 × 510 × 550 mm, ~23 kg
**Power:** 36 V LiPo, DC-DC → 5 V and 12 V rails
**Orin connection:** CANable2 USB → 500 kbps CAN (same bus as VESCs)
---
## ESP32-S3 BALANCE
### Board
Waveshare ESP32-S3 Touch LCD 1.28
- GC9A01 round 240×240 LCD
- CST816S capacitive touch
- QMI8658 6-axis IMU (accel + gyro, SPI)
- CH343 USB-to-serial chip
### Pin Assignments
| Function | GPIO | Notes |
|----------|------|-------|
| **QMI8658 IMU (SPI)** | | |
| SCK | IO39 | |
| MOSI | IO38 | |
| MISO | IO40 | |
| CS | IO41 | |
| INT1 | IO42 | data-ready interrupt |
| **GC9A01 LCD (shares SPI bus)** | | |
| CS | IO12 | |
| DC | IO11 | |
| RST | IO10 | |
| BL | IO9 | PWM backlight |
| **CST816S Touch (I2C)** | | |
| SDA | IO4 | |
| SCL | IO5 | |
| INT | IO6 | |
| RST | IO7 | |
| **CAN — SN65HVD230 transceiver** | | 500 kbps |
| TX | IO43 | → SN65HVD230 TXD |
| RX | IO44 | ← SN65HVD230 RXD |
| **Inter-board UART (to IO board)** | | 460800 baud |
| TX | IO17 | |
| RX | IO18 | |
### Responsibilities
- Read QMI8658 @ 1 kHz (SPI, INT1-driven)
- Complementary filter → pitch angle
- PID balance loop (configurable Kp / Ki / Kd)
- Send VESC speed commands via CAN (ID 68 = left, ID 56 = right)
- Receive Orin velocity+mode commands via CAN (0x3000x303)
- Receive IO board status (arming, RC, faults) via UART protocol
- Drive GC9A01 LCD: pitch, speed, battery %, error state
- Enforce tilt cutoff at ±25°; IWDG 50 ms timeout
- Publish telemetry on CAN 0x4000x401 at 10 Hz
---
## ESP32-S3 IO
### Board
Bare ESP32-S3 devkit (JTAG USB)
### Pin Assignments
| Function | GPIO | Notes |
|----------|------|-------|
| **TBS Crossfire RC — UART0 (primary)** | | |
| RX | IO44 | CRSF frames from Crossfire RX |
| TX | IO43 | telemetry to Crossfire TX |
| **ELRS failover — UART2** | | active if CRSF absent >100 ms |
| RX | IO16 | |
| TX | IO17 | |
| **BTS7960 Motor Driver — Left** | | |
| RPWM | IO1 | forward PWM |
| LPWM | IO2 | reverse PWM |
| R_EN | IO3 | right enable |
| L_EN | IO4 | left enable |
| **BTS7960 Motor Driver — Right** | | |
| RPWM | IO5 | |
| LPWM | IO6 | |
| R_EN | IO7 | |
| L_EN | IO8 | |
| **I2C bus** | | |
| SDA | IO11 | |
| SCL | IO12 | |
| NFC (PN532 or similar) | I2C | |
| Barometer (BMP280/BMP388) | I2C | |
| ToF (VL53L0X/VL53L1X) | I2C | |
| **WS2812B LEDs** | | |
| Data | IO13 | |
| **Outputs** | | |
| Horn / buzzer | IO14 | PWM tone |
| Headlight | IO15 | PWM or digital |
| Fan | IO16 | (if ELRS not fitted on UART2) |
| **Inputs** | | |
| Arming button | IO9 | active-low, hold 3 s to arm |
| Kill switch sense | IO10 | hardware estop detect |
| **Inter-board UART (to BALANCE board)** | | 460800 baud |
| TX | IO18 | |
| RX | IO21 | |
### Responsibilities
- Parse CRSF frames (TBS Crossfire, primary)
- Parse ELRS frames (failover, activates if no CRSF for >100 ms)
- Drive BTS7960 left/right PWM motor drivers
- Read NFC, barometer, ToF via I2C
- Drive WS2812B LEDs (armed/fault/idle patterns)
- Control horn, headlight, fan, buzzer
- Manage arming: hold button 3 s while upright → send ARM to BALANCE
- Monitor kill switch input → immediate motor off + FAULT frame
- Forward RC + sensor data to BALANCE via binary UART protocol
- Report faults and RC-loss upstream
---
## Inter-Board Binary Protocol (UART @ 460800 baud)
```
[0xAA][LEN][TYPE][PAYLOAD × LEN bytes][CRC8]
```
- `0xAA` — start byte
- `LEN` — payload length in bytes (uint8)
- `TYPE` — message type (uint8)
- `CRC8` — CRC-8/MAXIM over TYPE + PAYLOAD bytes
### IO → BALANCE Messages
| TYPE | Name | Payload | Description |
|------|------|---------|-------------|
| 0x01 | RC_CMD | int16 throttle, int16 steer, uint8 flags | flags: bit0=armed, bit1=kill |
| 0x02 | SENSOR | uint16 tof_mm, int16 baro_delta_pa, uint8 nfc_present | |
| 0x03 | FAULT | uint8 fault_flags | bit0=rc_loss, bit1=motor_fault, bit2=estop |
### BALANCE → IO Messages
| TYPE | Name | Payload | Description |
|------|------|---------|-------------|
| 0x10 | STATE | int16 pitch_x100, int16 pid_out, uint8 error_state | |
| 0x11 | LED_CMD | uint8 pattern, uint8 r, uint8 g, uint8 b | |
| 0x12 | BUZZER | uint8 tone_id, uint16 duration_ms | |
---
## CAN Bus — 500 kbps
### Node Assignments
| Node | CAN ID | Role |
|------|--------|------|
| VESC Left motor | **68** | Receives speed/duty via VESC CAN protocol |
| VESC Right motor | **56** | Receives speed/duty via VESC CAN protocol |
| ESP32-S3 BALANCE | — | Sends VESC commands; publishes telemetry |
| Jetson Orin (CANable2) | — | Sends velocity commands; receives telemetry |
### Frame Table
| CAN ID | Direction | Description | Rate |
|--------|-----------|-------------|------|
| 0x300 | Orin → BALANCE | Velocity cmd: int16 speed_mmps, int16 steer_mrad | 20 Hz |
| 0x301 | Orin → BALANCE | PID tuning: float Kp, float Ki, float Kd (3×4B IEEE-754) | on demand |
| 0x302 | Orin → BALANCE | Mode: uint8 (0=off, 1=balance, 2=manual, 3=estop) | on demand |
| 0x303 | Orin → BALANCE | Config: uint16 tilt_limit_x100, uint16 max_speed_mmps | on demand |
| 0x400 | BALANCE → Orin | Telemetry A: int16 pitch_x100, int16 pid_out, int16 speed_mmps, uint8 state | 10 Hz |
| 0x401 | BALANCE → Orin | Telemetry B: int16 vesc_l_rpm, int16 vesc_r_rpm, uint16 battery_mv, uint8 faults | 10 Hz |
---
## RC Channel Mapping (TBS Crossfire / ELRS CRSF)
| CH | Function | Range (µs) | Notes |
|----|----------|------------|-------|
| 1 | Steer (Roll) | 9882012 | ±100% → ±max steer |
| 2 | Throttle (Pitch) | 9882012 | forward / back speed |
| 3 | Spare | 9882012 | |
| 4 | Spare | 9882012 | |
| 5 | ARM switch | <1500=disarm, >1500=arm | SB on TX |
| 6 | **ESTOP** | <1500=normal, >1500=kill | SC on TX — checked first every loop |
| 7 | Speed limit | 9882012 | maps to 10100% speed cap |
| 8 | Spare | | |
**RC loss:** No valid CRSF frame >100 ms → IO sends FAULT(rc_loss) → BALANCE cuts motors.
---
## Safety Invariants
1. **Motors NEVER spin on power-on** — 3 s button hold required while upright
2. **Tilt cutoff ±25°** — immediate motor zero, manual re-arm required
3. **IWDG 50 ms** — firmware hang → motors cut
4. **ESTOP RC channel** checked first in every loop iteration
5. **Orin CAN timeout 500 ms** → revert to RC-only mode
6. **Speed hard cap** — start at 10%, increase in 10% increments only after stable tethered testing
7. **Never untethered** until stable for 5+ continuous minutes tethered
---
## USB Debug Commands (both boards, serial console)
```
help list commands
status print pitch, PID state, CAN stats, UART stats
pid <Kp> <Ki> <Kd> set PID gains
arm arm (if upright and safe)
disarm disarm immediately
estop emergency stop (requires re-arm)
tilt_limit <deg> set tilt cutoff angle (default 25)
speed_limit <pct> set speed cap percentage (default 10)
can_stats CAN bus counters (tx/rx/errors/busoff)
uart_stats inter-board UART frame counters
reboot soft reboot
```

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>GEPRC GEP-F722-45A AIO — Board Layout</title> <title>GEPRC GEP-F722-45A AIO — Board Layout (Legacy / Archived)</title>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a2e; color: #eee; font-family: 'Courier New', monospace; display: flex; flex-direction: column; align-items: center; padding: 20px; } body { background: #1a1a2e; color: #eee; font-family: 'Courier New', monospace; display: flex; flex-direction: column; align-items: center; padding: 20px; }
@ -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>
<p class="subtitle">ESP32RET6 + ICM-42688-P | Betaflight target: GEPR-GEPRC_F722_AIO</p>
=======
<h1>🤖 GEPRC GEP-F722-45A AIO — SaltyLab Pinout</h1> <h1>🤖 GEPRC GEP-F722-45A AIO — SaltyLab Pinout</h1>
<p class="subtitle">ESP32-S3RET6 + ICM-42688-P | Betaflight target: GEPR-GEPRC_F722_AIO</p> <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-S3<br>F722RET6<br>216MHz</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>

View File

@ -1,131 +1,155 @@
# SaltyLab Wiring Diagram # SaltyLab / SAUL-TEE Wiring Reference
## System Overview > ⚠️ **ARCHITECTURE CHANGE (2026-04-03):** Mamba F722S / STM32 retired.
> New stack: **ESP32-S3 BALANCE** + **ESP32-S3 IO** + VESCs on 500 kbps CAN.
> **Authoritative reference:** [`docs/SAUL-TEE-SYSTEM-REFERENCE.md`](SAUL-TEE-SYSTEM-REFERENCE.md)
> Historical STM32/Mamba wiring below is **obsolete** — retained for reference only.
---
## ~~System Overview~~ (OBSOLETE — see SAUL-TEE-SYSTEM-REFERENCE.md)
``` ```
┌─────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────┐
│ ORIN NANO SUPER │ │ ORIN NANO SUPER │
│ (Top Plate — 25W) │ │ (Top Plate — 25W) │
│ │ │ │
<<<<<<< HEAD
│ USB-A ──── CANable2 USB-CAN adapter (slcan0, 500 kbps) │
│ USB-A ──── ESP32-S3 IO (/dev/esp32-io, 460800 baud) │
=======
│ USB-C ──── ESP32-S3 CDC (/dev/esp32-bridge, 921600 baud) │ │ 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) │
│ USB ─────── Leap Motion Controller (hand/gesture tracking) │ │ USB ─────── Leap Motion Controller (hand/gesture tracking) │
│ CSI-A ──── ArduCam adapter → 2× IMX219 (front + left) │ │ CSI-A ──── ArduCam adapter → 2x IMX219 (front + left) │
│ CSI-B ──── ArduCam adapter → 2× IMX219 (rear + right) │ │ CSI-B ──── ArduCam adapter → 2x IMX219 (rear + right) │
│ M.2 ───── 1TB NVMe SSD │ │ M.2 ───── 1TB NVMe SSD │
│ 40-pin ─── ReSpeaker 2-Mic HAT (I2S + I2C, WM8960 codec) │ │ 40-pin ─── ReSpeaker 2-Mic HAT (I2S + I2C, WM8960 codec) │
│ Pin 8 ──┐ │ │ Pin 8 ──┐ │
│ Pin 10 ─┤ UART fallback to FC (ttyTHS0, 921600) │ Pin 10 ─┤ UART fallback to ESP32-S3 BALANCE (ttyTHS0, 460800)
│ Pin 6 ──┘ GND │ │ Pin 6 ──┘ GND │
│ │ │ │
└─────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────┘
│ USB-C (data only) │ UART fallback (3 wires) │ USB-A (CANable2) │ UART fallback (3 wires)
│ 921600 baud │ 921600 baud, 3.3V │ SocketCAN slcan0 │ 460800 baud, 3.3V
│ 500 kbps │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────┐
<<<<<<< HEAD
│ ESP32-S3 BALANCE │
│ (Waveshare Touch LCD 1.28, Middle Plate) │
=======
│ ESP32-S3 BALANCE (FC) │ │ ESP32-S3 BALANCE (FC) │
│ (Middle Plate — foam mounted) │ │ (Middle Plate — foam mounted) │
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ │ │ │
│ USB-C ──── Orin (CDC serial, primary link) │ │ CAN bus ──── CANable2 → Orin (primary link, ISO 11898) │
│ │ │ UART0 ──── Orin UART fallback (460800 baud, 3.3V) │
│ USART2 (PA2=TX, PA3=RX) ──── Hoverboard ESC (26400 baud) │ │ UART1 ──── VESC Left (CAN ID 56) via UART/CAN bridge │
│ UART4 (PA0=TX, PA1=RX) ──── ELRS RX (CRSF, 420000 baud) │ │ UART2 ──── VESC Right (CAN ID 68) via UART/CAN bridge │
│ USART6 (PC6=TX, PC7=RX) ──── Orin UART fallback │ │ I2C ──── QMI8658 IMU (onboard, 6-DOF accel+gyro) │
│ UART5 (PC12=TX, PD2=RX) ─── Debug (optional) │ │ SPI ──── GC9A01 LCD (onboard, 240x240 round display) │
│ │ │ GPIO ──── WS2812B LED strip │
│ SPI1 ─── MPU6000 IMU (on-board, CW270) │ │ GPIO ──── Buzzer │
│ I2C1 ─── BMP280 baro (on-board, disabled) │ │ ADC ──── Battery voltage divider │
│ ADC ──── Battery voltage (PC1) + Current (PC3) │
│ PB3 ──── WS2812B LED strip │
│ PB2 ──── Buzzer │
│ │ │ │
└─────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────┘
│ USART2 │ UART4 │ CAN bus (ISO 11898) │ UART (460800 baud)
│ PA2=TX → ESC RX │ PA0=TX → ELRS TX │ 500 kbps │
│ PA3=RX ← ESC TX │ PA1=RX ← ELRS RX
│ GND ─── GND │ GND ─── GND
▼ ▼ ▼ ▼
┌────────────────────────┐ ┌──────────────────────────┐ ┌────────────────────────┐ ┌──────────────────────────┐
│ HOVERBOARD ESC │ │ ELRS 2.4GHz RX │ │ VESC Left (ID 56) │ │ VESC Right (ID 68) │
│ (Bottom Plate) │ │ (beside FC) │ │ (Bottom Plate) │ │ (Bottom Plate) │
│ │ │ │
│ BLDC hub motor │ │ BLDC hub motor │
│ CAN 500 kbps │ │ CAN 500 kbps │
│ FOC current control │ │ FOC current control │
│ VESC Status 1 (0x900) │ │ VESC Status 1 (0x910) │
│ │ │ │ │ │ │ │
│ 2× BLDC hub motors │ │ CRSF protocol │
│ 26400 baud UART │ │ 420000 baud │
│ Frame: [0xABCD] │ │ BetaFPV 1W TX → RX │
│ [steer][speed][csum] │ │ CH3=speed CH4=steer │
│ │ │ CH5=arm CH6=mode │
└────────────────────────┘ └──────────────────────────┘ └────────────────────────┘ └──────────────────────────┘
│ │
┌────┴────┐ LEFT MOTOR RIGHT MOTOR
▼ ▼ ```
🛞 LEFT RIGHT 🛞
MOTOR MOTOR
## Wire-by-Wire Connections ## Wire-by-Wire Connections
<<<<<<< HEAD
### 1. Orin <-> ESP32-S3 BALANCE (Primary: CAN Bus via CANable2)
=======
### 1. Orin ↔ FC (Primary: USB Serial (CH343)) ### 1. Orin ↔ FC (Primary: USB Serial (CH343))
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| From | To | Wire Color | Notes | | From | To | Wire | Notes |
|------|----|-----------|-------| |------|----|------|-------|
| Orin USB-C port | FC USB-C port | USB cable | Data only, FC powered from 5V bus | | Orin USB-A | CANable2 USB | USB cable | SocketCAN slcan0 @ 500 kbps |
| CANable2 CAN-H | ESP32-S3 BALANCE CAN-H | twisted pair | ISO 11898 differential |
| CANable2 CAN-L | ESP32-S3 BALANCE CAN-L | twisted pair | ISO 11898 differential |
<<<<<<< HEAD
- Interface: SocketCAN `slcan0`, 500 kbps
- Device node: `/dev/canable2` (via udev, symlink to ttyUSBx)
- Protocol: CAN frames --- ORIN_CMD_DRIVE (0x300), ORIN_CMD_MODE (0x301), ORIN_CMD_ESTOP (0x302)
- Telemetry: BALANCE_STATUS (0x400), BALANCE_VESC (0x401), BALANCE_IMU (0x402), BALANCE_BATTERY (0x403)
=======
- Device: `/dev/ttyACM0` → symlink `/dev/esp32-bridge` - Device: `/dev/ttyACM0` → symlink `/dev/esp32-bridge`
- Baud: 921600, 8N1 - Baud: 921600, 8N1
- Protocol: JSON telemetry (FC→Orin), ASCII commands (Orin→FC) - Protocol: JSON telemetry (FC→Orin), ASCII commands (Orin→FC)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
### 2. Orin ↔ FC (Fallback: Hardware UART) ### 2. Orin <-> ESP32-S3 BALANCE (Fallback: Hardware UART)
| Orin Pin | Signal | FC Pin | FC Signal | | Orin Pin | Signal | ESP32-S3 Pin | Notes |
|----------|--------|--------|-----------| |----------|--------|--------------|-------|
| Pin 8 | TXD0 | PC7 | USART6 RX | | Pin 8 | TXD0 | GPIO17 (UART0 RX) | Orin TX -> BALANCE RX |
| Pin 10 | RXD0 | PC6 | USART6 TX | | Pin 10 | RXD0 | GPIO18 (UART0 TX) | Orin RX <- BALANCE TX |
| Pin 6 | GND | GND | GND | | Pin 6 | GND | GND | Common ground |
- Jetson device: `/dev/ttyTHS0` - Jetson device: `/dev/ttyTHS0`
- Baud: 921600, 8N1 - Baud: 460800, 8N1
- Voltage: 3.3V both sides (no level shifter needed) - Voltage: 3.3V both sides (no level shifter needed)
- **Cross-connect:** Orin TX → FC RX, Orin RX ← FC TX - Cross-connect: Orin TX -> BALANCE RX, Orin RX <- BALANCE TX
### 3. FC ↔ Hoverboard ESC ### 3. Orin <-> ESP32-S3 IO (USB Serial)
| FC Pin | Signal | ESC Pin | Notes | | From | To | Notes |
|--------|--------|---------|-------| |------|----|-------|
| PA2 | USART2 TX | RX | FC sends speed/steer commands | | Orin USB-A | ESP32-S3 IO USB-C | USB cable, /dev/esp32-io |
| PA3 | USART2 RX | TX | ESC sends feedback (optional) |
- Device node: `/dev/esp32-io` (udev symlink)
- Baud: 460800, 8N1
- Protocol: Binary frames `[0xAA][LEN][TYPE][PAYLOAD][CRC8]`
- Use: IO expansion, GPIO control, sensor polling
### 4. ESP32-S3 BALANCE <-> VESC Motors (CAN Bus)
| BALANCE Pin | Signal | VESC Pin | Notes |
|-------------|--------|----------|-------|
| GPIO21 | CAN-H | CAN-H | ISO 11898 differential pair |
| GPIO22 | CAN-L | CAN-L | ISO 11898 differential pair |
| GND | GND | GND | Common ground | | GND | GND | GND | Common ground |
- Baud: 26400, 8N1 - Baud: 500 kbps CAN
- Protocol: Binary frame — `[0xABCD][steer:int16][speed:int16][checksum:uint16]` - VESC Left: CAN ID 56, VESC Right: CAN ID 68
- Speed range: -1000 to +1000 - Commands: COMM_SET_RPM, COMM_SET_CURRENT, COMM_SET_DUTY
- **Keep wires short and twisted** (EMI from ESC) - Telemetry: VESC Status 1 at 50 Hz (RPM, current, duty)
### 4. FC ↔ ELRS Receiver
| FC Pin | Signal | ELRS Pin | Notes |
|--------|--------|----------|-------|
| PA0 | UART4 TX | RX | Telemetry to TX (optional) |
| PA1 | UART4 RX | TX | CRSF frames from RX |
| GND | GND | GND | Common ground |
| 5V | — | VCC | Power ELRS from 5V bus |
- Baud: 420000 (CRSF protocol)
- Failsafe: disarm after 300ms without frame
### 5. Power Distribution ### 5. Power Distribution
``` ```
BATTERY (36V) ──┬── Hoverboard ESC (36V direct) BATTERY (36V) ──┬── VESC Left (36V direct -> BLDC left motor)
├── VESC Right (36V direct -> BLDC right motor)
├── 5V BEC/regulator ──┬── Orin (USB-C PD or barrel jack) ├── 5V BEC/regulator ──┬── Orin (USB-C PD or barrel jack)
│ ├── FC (via USB or 5V pad) │ ├── ESP32-S3 BALANCE (5V via USB-C)
│ ├── ELRS RX (5V) │ ├── ESP32-S3 IO (5V via USB-C)
│ ├── WS2812B LEDs (5V) │ ├── WS2812B LEDs (5V)
│ └── RPLIDAR (5V via USB) │ └── RPLIDAR (5V via USB)
└── Battery monitor ──── FC ADC (PC1=voltage, PC3=current) └── Battery monitor ──── ESP32-S3 BALANCE ADC (voltage divider)
``` ```
### 6. Sensors on Orin (USB/CSI) ### 6. Sensors on Orin (USB/CSI)
@ -136,9 +160,38 @@ BATTERY (36V) ──┬── Hoverboard ESC (36V direct)
| RPLIDAR A1M8 | USB-UART | USB-A | `/dev/rplidar` | | RPLIDAR A1M8 | USB-UART | USB-A | `/dev/rplidar` |
| IMX219 front+left | MIPI CSI-2 | CSI-A (J5) | `/dev/video0,2` | | IMX219 front+left | MIPI CSI-2 | CSI-A (J5) | `/dev/video0,2` |
| IMX219 rear+right | MIPI CSI-2 | CSI-B (J8) | `/dev/video4,6` | | IMX219 rear+right | MIPI CSI-2 | CSI-B (J8) | `/dev/video4,6` |
| 1TB NVMe | PCIe Gen3 ×4 | M.2 Key M | `/dev/nvme0n1` | | 1TB NVMe | PCIe Gen3 x4 | M.2 Key M | `/dev/nvme0n1` |
| CANable2 | USB-CAN | USB-A | `/dev/canable2` -> `slcan0` |
<<<<<<< HEAD
## FC UART Summary (MAMBA F722S — OBSOLETE)
| Interface | Pins | Baud/Rate | Assignment | Notes |
|-----------|------|-----------|------------|-------|
| UART0 | GPIO17=RX, GPIO18=TX | 460800 | Orin UART fallback | 3.3V, cross-connect |
| UART1 | GPIO19=RX, GPIO20=TX | 115200 | Debug serial | Optional |
| CAN (TWAI) | GPIO21=H, GPIO22=L | 500 kbps | CAN bus (VESCs + Orin) | SN65HVD230 transceiver |
| I2C | GPIO4=SDA, GPIO5=SCL | 400 kHz | QMI8658 IMU (addr 0x6B) | Onboard |
| SPI | GPIO36=MOSI, GPIO37=SCLK, GPIO35=CS | 40 MHz | GC9A01 LCD (onboard) | 240x240 round |
| USB CDC | USB-C | 460800 | Orin USB fallback | /dev/esp32-balance |
## CAN Frame ID Map
| CAN ID | Direction | Name | Contents |
|--------|-----------|------|----------|
| 0x300 | Orin -> BALANCE | ORIN_CMD_DRIVE | left_rpm_f32, right_rpm_f32 (8 bytes LE) |
| 0x301 | Orin -> BALANCE | ORIN_CMD_MODE | mode byte (0=IDLE, 1=DRIVE, 2=ESTOP) |
| 0x302 | Orin -> BALANCE | ORIN_CMD_ESTOP | flags byte (bit0=stop, bit1=clear) |
| 0x400 | BALANCE -> Orin | BALANCE_STATUS | pitch x10:i16, motor_cmd:u16, vbat_mv:u16, state:u8, flags:u8 |
| 0x401 | BALANCE -> Orin | BALANCE_VESC | l_rpm x10:i16, r_rpm x10:i16, l_cur x10:i16, r_cur x10:i16 |
| 0x402 | BALANCE -> Orin | BALANCE_IMU | pitch x100:i16, roll x100:i16, yaw x100:i16, ax x100:i16, ay x100:i16, az x100:i16 |
| 0x403 | BALANCE -> Orin | BALANCE_BATTERY | vbat_mv:u16, current_ma:i16, soc_pct:u8 |
| 0x900+ID | VESC Left -> | VESC_STATUS_1 | erpm:i32, current x10:i16, duty x1000:i16 |
| 0x910+ID | VESC Right -> | VESC_STATUS_1 | erpm:i32, current x10:i16, duty x1000:i16 |
VESC Left CAN ID = 56 (0x38), VESC Right CAN ID = 68 (0x44).
=======
## FC UART Summary (ESP32-S3 BALANCE) ## FC UART Summary (ESP32-S3 BALANCE)
| UART | Pins | Baud | Assignment | Notes | | UART | Pins | Baud | Assignment | Notes |
@ -150,6 +203,7 @@ BATTERY (36V) ──┬── Hoverboard ESC (36V direct)
| UART5 | PC12=TX, PD2=RX | 115200 | Debug serial | Optional | | UART5 | PC12=TX, PD2=RX | 115200 | Debug serial | Optional |
| USART6 | PC6=TX, PC7=RX | 921600 | Jetson UART | Fallback link | | USART6 | PC6=TX, PC7=RX | 921600 | Jetson UART | Fallback link |
| USB Serial (CH343) | USB-C | 921600 | Jetson primary | `/dev/esp32-bridge` | | USB Serial (CH343) | USB-C | 921600 | Jetson primary | `/dev/esp32-bridge` |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
### 7. ReSpeaker 2-Mic HAT (on Orin 40-pin header) ### 7. ReSpeaker 2-Mic HAT (on Orin 40-pin header)
@ -167,57 +221,63 @@ BATTERY (36V) ──┬── Hoverboard ESC (36V direct)
| Pin 2, 4 | 5V | Power | | Pin 2, 4 | 5V | Power |
| Pin 6, 9 | GND | Ground | | Pin 6, 9 | GND | Ground |
- **Codec:** Wolfson WM8960 (I2C addr 0x1A) - Codec: Wolfson WM8960 (I2C addr 0x1A)
- **Mics:** 2× MEMS (left + right) — basic stereo / sound localization - Mics: 2x MEMS (left + right) --- basic stereo / sound localization
- **Speaker:** 3W class-D amp output (JST connector) - Speaker: 3W class-D amp output (JST connector)
- **Headset:** 3.5mm TRRS jack - Headset: 3.5mm TRRS jack
- **Requires:** WM8960 device tree overlay for Jetson (community port) - Requires: WM8960 device tree overlay for Jetson (community port)
- **Use:** Voice commands (faster-whisper), wake word (openWakeWord), audio feedback, status announcements - Use: Voice commands (faster-whisper), wake word (openWakeWord), audio feedback, status announcements
### 8. SIM7600A 4G/LTE HAT (via USB) ### 8. SIM7600A 4G/LTE HAT (via USB)
| Connection | Detail | | Connection | Detail |
|-----------|--------| |-----------|--------|
| Interface | USB (micro-B on HAT USB-A/C on Orin) | | Interface | USB (micro-B on HAT -> USB-A/C on Orin) |
| Device nodes | `/dev/ttyUSB0` (AT), `/dev/ttyUSB1` (PPP/data), `/dev/ttyUSB2` (GPS NMEA) | | Device nodes | `/dev/ttyUSB0` (AT), `/dev/ttyUSB1` (PPP/data), `/dev/ttyUSB2` (GPS NMEA) |
| Power | 5V from USB or separate 5V supply (peak 2A during TX) | | Power | 5V from USB or separate 5V supply (peak 2A during TX) |
| SIM | Nano-SIM slot on HAT | | SIM | Nano-SIM slot on HAT |
| Antenna | 4G LTE + GPS/GNSS (external SMA antennas mount high on chassis) | | Antenna | 4G LTE + GPS/GNSS (external SMA antennas --- mount high on chassis) |
- **Data:** PPP or QMI for internet connectivity - Data: PPP or QMI for internet connectivity
- **GPS/GNSS:** Built-in receiver, NMEA sentences on ttyUSB2 — outdoor positioning - GPS/GNSS: Built-in receiver, NMEA sentences on ttyUSB2 --- outdoor positioning
- **AT commands:** `AT+CGPS=1` (enable GPS), `AT+CGPSINFO` (get fix) - AT commands: `AT+CGPS=1` (enable GPS), `AT+CGPSINFO` (get fix)
- **Connected via USB** (not 40-pin) — avoids UART conflict with FC fallback, flexible antenna placement - Connected via USB (not 40-pin) --- avoids UART conflict with BALANCE fallback, flexible antenna placement
- **Use:** Remote telemetry, 4G connectivity outdoors, GPS positioning, remote SSH/control - Use: Remote telemetry, 4G connectivity outdoors, GPS positioning, remote SSH/control
### 10. Leap Motion Controller (USB) ### 9. Leap Motion Controller (USB)
| Connection | Detail | | Connection | Detail |
|-----------|--------| |-----------|--------|
| Interface | USB 3.0 (micro-B on controller USB-A on Orin) | | Interface | USB 3.0 (micro-B on controller -> USB-A on Orin) |
| Power | ~0.5W | | Power | ~0.5W |
| Range | ~80cm, 150° FOV | | Range | ~80cm, 150 deg FOV |
| SDK | Ultraleap Gemini V5+ (Linux ARM64 support) | | SDK | Ultraleap Gemini V5+ (Linux ARM64 support) |
| ROS2 | `leap_motion_ros2` wrapper available | | ROS2 | `leap_motion_ros2` wrapper available |
- **2× IR cameras + 3× IR LEDs** tracks all 10 fingers in 3D, sub-mm precision - 2x IR cameras + 3x IR LEDs --- tracks all 10 fingers in 3D, sub-mm precision
- **Mount:** Forward-facing on sensor tower or upward on Orin plate - Mount: Forward-facing on sensor tower or upward on Orin plate
- **Use:** Gesture control (palm=stop, point=go, fist=arm), hand-following mode, demos - Use: Gesture control (palm=stop, point=go, fist=arm), hand-following mode, demos
- **Combined with ReSpeaker:** Voice + gesture control with zero hardware in hand - Combined with ReSpeaker: Voice + gesture control with zero hardware in hand
### 11. Power Budget (USB) ### 10. Power Budget (USB)
| Device | Interface | Power Draw | | Device | Interface | Power Draw |
|--------|-----------|------------| |--------|-----------|------------|
<<<<<<< HEAD
| CANable2 USB-CAN | USB-A | ~0.5W |
| ESP32-S3 BALANCE | USB-C | ~0.8W (WiFi off) |
| ESP32-S3 IO | USB-C | ~0.5W |
=======
| ESP32-S3 FC (CDC) | USB-C | ~0.5W (data only, FC on 5V bus) | | 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 |
| Leap Motion | USB | ~0.5W | | Leap Motion | USB-A | ~0.5W |
| ReSpeaker HAT | 40-pin | ~0.5W | | ReSpeaker HAT | 40-pin | ~0.5W |
| **Total USB** | | **~6.5W typical, ~10.5W peak** | | **Total USB** | | **~7.9W typical, ~11W peak** |
Orin Nano Super delivers up to 25W USB peripherals are well within budget. Orin Nano Super delivers up to 25W --- USB peripherals are well within budget.
--- ---
@ -225,38 +285,46 @@ Orin Nano Super delivers up to 25W — USB peripherals are well within budget.
``` ```
┌──────────────┐ ┌──────────────┐
ELRS TX │ (in your hand) RC TX │ (in your hand)
│ (2.4GHz) │ │ (2.4GHz) │
└──────┬───────┘ └──────┬───────┘
│ radio │ radio
┌──────▼───────┐ ┌──────▼───────┐
ELRS RX │ CRSF 420kbaud RC RX │ CRSF 420kbaud (future)
└──────┬───────┘ └──────┬───────┘
│ UART4 │ UART
┌────────────▼────────────┐ ┌────────────▼────────────┐
<<<<<<< HEAD
│ ESP32-S3 BALANCE │ │ ESP32-S3 BALANCE │
│ (Waveshare LCD 1.28) │
=======
│ ESP32-S3 BALANCE │
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ │ │ │
│ MPU6000 → Balance PID │ QMI8658 -> Balance PID
│ CRSF → Mode Manager │ RC -> Mode Manager
│ Safety Monitor │ │ Safety Monitor │
│ │ │ │
└──┬──────────┬───────────┘ └──┬──────────┬───────────┘
<<<<<<< HEAD
CAN 500kbps─┘ └───── CAN bus / UART fallback
=======
USART2 ─────┘ └───── USB Serial (CH343) / USART6 USART2 ─────┘ └───── USB Serial (CH343) / USART6
26400 baud 921600 baud 26400 baud 921600 baud
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
│ │ │ │
┌────┴────────────┐
┌────────────────┐ ┌───────────────────┐ │ CAN bus (500k) │ ┌───────────────────┐
│ Hoverboard ESC │ │ Orin Nano Super │ ├─ VESC Left 56 │ │ Orin Nano Super │
│ │ │ │ └─ VESC Right 68 │ │ │
│ L motor R motor│ │ SLAM / Nav2 / AI │ │ │ │ SLAM / Nav2 / AI │
│ 🛞 🛞 │ │ Person following │ ▼ ▼ │ Person following │
└────────────────┘ │ Voice commands │ LEFT RIGHT │ Voice commands │
│ 4G telemetry │ MOTOR MOTOR │ 4G telemetry │
└──┬──────────┬───────┘ └──┬──────────┬───────┘
│ │ │ │
┌──────────▼─┐ ┌────▼──────────┐ ┌──────────▼─┐ ┌────▼──────────┐
│ ReSpeaker │ │ SIM7600A │ │ ReSpeaker │ │ SIM7600A │
│ 2-Mic HAT │ │ 4G/LTE + GPS │ │ 2-Mic HAT │ │ 4G/LTE + GPS │
│ 🎤 🔊 │ │ 📡 🛰️ │
└────────────┘ └───────────────┘ └────────────┘ └───────────────┘
``` ```

View File

@ -14,7 +14,11 @@ Self-balancing robot: Jetson Orin Nano Super dev environment for ROS2 Humble + S
| Nav | Nav2 | | Nav | Nav2 |
| Depth camera | Intel RealSense D435i | | Depth camera | Intel RealSense D435i |
| LiDAR | RPLIDAR A1M8 | | LiDAR | RPLIDAR A1M8 |
<<<<<<< HEAD
| MCU bridge | ESP32 (USB CDC @ 921600) |
=======
| MCU bridge | ESP32-S3 (USB Serial (CH343) @ 921600) | | MCU bridge | ESP32-S3 (USB Serial (CH343) @ 921600) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Quick Start ## Quick Start
@ -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-S3) ├── 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-S3 firmware. Recovery behaviors cannot interfere with E-stop because the emergency system operates at the motor driver level on the ESP32-S3 firmware.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Behavior Tree Sequence ## Behavior Tree Sequence

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-S3 bridge consumes this topic. # Output: /cmd_vel (Twist) — ESP32-S3 bridge consumes this topic.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
bt_navigator: bt_navigator:
ros__parameters: ros__parameters:

View File

@ -97,7 +97,11 @@ services:
rgb_camera.profile:=640x480x30 rgb_camera.profile:=640x480x30
" "
<<<<<<< HEAD
# ── ESP32 bridge node (bidirectional serial<->ROS2) ────────────────────────
=======
# ── ESP32-S3 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) ----------------------
# Subscribes to saltybot/estop MQTT topic. {"kill":true} -> 'E\r\n' to ESP32 BALANCE.
=======
# -- Remote e-stop bridge (MQTT over 4G -> ESP32-S3 CDC) ---------------------- # -- Remote e-stop bridge (MQTT over 4G -> ESP32-S3 CDC) ----------------------
# Subscribes to saltybot/estop MQTT topic. {"kill":true} -> 'E\r\n' to ESP32-S3. # Subscribes to saltybot/estop MQTT topic. {"kill":true} -> 'E\r\n' to ESP32-S3.
>>>>>>> 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-S3 Bridge + RealSense D435i + RPLIDAR A1M8 + 4× IMX219 ## Self-Balancing Robot: ESP32-S3 Bridge + RealSense D435i + RPLIDAR A1M8 + 4× IMX219
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
Last updated: 2026-02-28 Last updated: 2026-02-28
JetPack version: 6.x (L4T R36.x / Ubuntu 22.04) JetPack version: 6.x (L4T R36.x / Ubuntu 22.04)
@ -43,21 +47,37 @@ i2cdetect -l
--- ---
<<<<<<< HEAD
## 1. ESP32 Bridge (USB CDC — Primary)
The ESP32 BALANCE acts as a real-time motor + IMU controller. Communication is via **USB CDC serial**.
=======
## 1. ESP32-S3 Bridge (USB Serial (CH343) — Primary) ## 1. ESP32-S3 Bridge (USB Serial (CH343) — Primary)
The ESP32-S3 acts as a real-time motor + IMU controller. Communication is via **USB Serial (CH343) serial**. The ESP32-S3 acts as a real-time motor + IMU controller. Communication is via **USB Serial (CH343) serial**.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
### USB Serial (CH343) Connection ### USB Serial (CH343) Connection
| Connection | Detail | | Connection | Detail |
|-----------|--------| |-----------|--------|
<<<<<<< HEAD
| Interface | USB on ESP32 BALANCE board → USB-A on Jetson |
| Device node | `/dev/ttyACM0` → symlink `/dev/esp32-bridge` (via udev) |
| Baud rate | 921600 (configured in ESP32 BALANCE firmware) |
=======
| Interface | USB Micro-B on ESP32-S3 dev board → USB-A on Jetson | | Interface | USB Micro-B on ESP32-S3 dev board → USB-A on Jetson |
| Device node | `/dev/ttyACM0` → symlink `/dev/esp32-bridge` (via udev) | | Device node | `/dev/ttyACM0` → symlink `/dev/esp32-bridge` (via udev) |
| Baud rate | 921600 (configured in ESP32-S3 firmware) | | Baud rate | 921600 (configured in ESP32-S3 firmware) |
>>>>>>> 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-S3 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-S3 acts as a real-time motor + IMU controller. Communication is via **
**Jetson device node:** `/dev/ttyTHS0` **Jetson device node:** `/dev/ttyTHS0`
**Baud rate:** 921600, 8N1 **Baud rate:** 921600, 8N1
<<<<<<< HEAD
**Voltage level:** 3.3V — both Jetson Orin and ESP32 are 3.3V GPIO
=======
**Voltage level:** 3.3V — both Jetson Orin and ESP32-S3 are 3.3V GPIO **Voltage level:** 3.3V — both Jetson Orin and ESP32-S3 are 3.3V GPIO
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
```bash ```bash
# Verify UART # Verify UART
@ -75,6 +99,15 @@ sudo usermod -aG dialout $USER
picocom -b 921600 /dev/ttyTHS0 picocom -b 921600 /dev/ttyTHS0
``` ```
<<<<<<< HEAD
**ROS2 topics (ESP32 bridge node):**
| ROS2 Topic | Direction | Content |
|-----------|-----------|---------
| `/saltybot/imu` | ESP32 BALANCE→Jetson | IMU data (accel, gyro) at 50Hz |
| `/saltybot/balance_state` | ESP32 BALANCE→Jetson | Motor cmd, pitch, state |
| `/cmd_vel` | Jetson→ESP32 BALANCE | Velocity commands → `C<spd>,<str>\n` |
| `/saltybot/estop` | Jetson→ESP32 BALANCE | Emergency stop |
=======
**ROS2 topics (ESP32-S3 bridge node):** **ROS2 topics (ESP32-S3 bridge node):**
| ROS2 Topic | Direction | Content | | ROS2 Topic | Direction | Content |
|-----------|-----------|--------- |-----------|-----------|---------
@ -82,6 +115,7 @@ picocom -b 921600 /dev/ttyTHS0
| `/saltybot/balance_state` | ESP32-S3→Jetson | Motor cmd, pitch, state | | `/saltybot/balance_state` | ESP32-S3→Jetson | Motor cmd, pitch, state |
| `/cmd_vel` | Jetson→ESP32-S3 | Velocity commands → `C<spd>,<str>\n` | | `/cmd_vel` | Jetson→ESP32-S3 | Velocity commands → `C<spd>,<str>\n` |
| `/saltybot/estop` | Jetson→ESP32-S3 | Emergency stop | | `/saltybot/estop` | Jetson→ESP32-S3 | Emergency stop |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
--- ---
@ -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-S3 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) |
| 10 | RXD0 | 3.3V | UART RX ← ESP32 BALANCE (fallback) |
| USB-A ×2 | — | 5V | D435i, RPLIDAR |
| USB-C | — | 5V | ESP32 CDC |
=======
| 8 | TXD0 | 3.3V | UART TX → ESP32-S3 (fallback) | | 8 | TXD0 | 3.3V | UART TX → ESP32-S3 (fallback) |
| 10 | RXD0 | 3.3V | UART RX ← ESP32-S3 (fallback) | | 10 | RXD0 | 3.3V | UART RX ← ESP32-S3 (fallback) |
| USB-A ×2 | — | 5V | D435i, RPLIDAR | | USB-A ×2 | — | 5V | D435i, RPLIDAR |
| USB-C | — | 5V | ESP32-S3 CDC | | USB-C | — | 5V | ESP32-S3 CDC |
>>>>>>> 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-S3 USB Serial (CH343) (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-S3 bridge | 0.0 | 0.0 | 0.0 | USB Serial (CH343) | 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** | | |
@ -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-S3 + 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-S3 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-S3 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-S3 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-S3 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

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

View File

@ -6,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-S3 commands + heartbeat. and TX /cmd_vel ESP32-S3 commands + heartbeat.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
2. RX-only (telemetry monitor, no drive commands): 2. RX-only (telemetry monitor, no drive commands):
ros2 launch saltybot_bridge bridge.launch.py mode:=rx_only ros2 launch saltybot_bridge bridge.launch.py mode:=rx_only
@ -40,7 +44,7 @@ def _launch_nodes(context, *args, **kwargs):
return [Node( return [Node(
package="saltybot_bridge", package="saltybot_bridge",
executable="serial_bridge_node", executable="serial_bridge_node",
name="stm32_serial_bridge", name="esp32_serial_bridge",
output="screen", output="screen",
parameters=[params], parameters=[params],
)] )]
@ -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-S3 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-S3 autonomous drive bridge. cmd_vel_bridge.launch.py Nav2 cmd_vel ESP32-S3 autonomous drive bridge.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Starts cmd_vel_bridge_node, which owns the serial port exclusively and provides: Starts cmd_vel_bridge_node, which owns the serial port exclusively and provides:
- /cmd_vel subscription with velocity limits + smooth ramp - /cmd_vel subscription with velocity limits + smooth ramp
- Deadman switch (zero speed if /cmd_vel silent > cmd_vel_timeout) - Deadman switch (zero speed if /cmd_vel silent > cmd_vel_timeout)
<<<<<<< HEAD
- Mode gate (drives only when ESP32 BALANCE is in AUTONOMOUS mode, md=2)
=======
- Mode gate (drives only when ESP32-S3 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-S3 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-S3 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,16 @@
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/launch/stm32_cmd.launch.py
"""stm32_cmd.launch.py — Launch the ESP32-S3 IO auxiliary bridge node.
Connects to ESP32-S3 IO board via USB-CDC @ 460800 baud (inter-board protocol).
Handles RC monitoring, sensor data, LED/output commands.
Primary drive path uses CAN (can_bridge_node / saltybot_can_node), not this node.
Spec: docs/SAUL-TEE-SYSTEM-REFERENCE.md §5
Usage:
ros2 launch saltybot_bridge stm32_cmd.launch.py
ros2 launch saltybot_bridge stm32_cmd.launch.py serial_port:=/dev/ttyACM0
=======
"""esp32_cmd.launch.py — Launch the binary-framed ESP32-S3 command node (Issue #119). """esp32_cmd.launch.py — Launch the binary-framed ESP32-S3 command node (Issue #119).
Usage: Usage:
@ -9,6 +22,7 @@ Usage:
# Custom velocity scales: # Custom velocity scales:
ros2 launch saltybot_bridge esp32_cmd.launch.py speed_scale:=800.0 steer_scale:=-400.0 ros2 launch saltybot_bridge esp32_cmd.launch.py speed_scale:=800.0 steer_scale:=-400.0
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/launch/esp32_cmd.launch.py
""" """
import os import os
@ -24,11 +38,8 @@ def generate_launch_description() -> LaunchDescription:
params_file = os.path.join(pkg, "config", "esp32_cmd_params.yaml") params_file = os.path.join(pkg, "config", "esp32_cmd_params.yaml")
return LaunchDescription([ return LaunchDescription([
DeclareLaunchArgument("serial_port", default_value="/dev/ttyACM0"), DeclareLaunchArgument("serial_port", default_value="/dev/esp32-io"),
DeclareLaunchArgument("baud_rate", default_value="921600"), DeclareLaunchArgument("baud_rate", default_value="460800"),
DeclareLaunchArgument("speed_scale", default_value="1000.0"),
DeclareLaunchArgument("steer_scale", default_value="-500.0"),
DeclareLaunchArgument("watchdog_timeout", default_value="0.5"),
DeclareLaunchArgument("heartbeat_period", default_value="0.2"), DeclareLaunchArgument("heartbeat_period", default_value="0.2"),
Node( Node(
@ -42,9 +53,6 @@ def generate_launch_description() -> LaunchDescription:
{ {
"serial_port": LaunchConfiguration("serial_port"), "serial_port": LaunchConfiguration("serial_port"),
"baud_rate": LaunchConfiguration("baud_rate"), "baud_rate": LaunchConfiguration("baud_rate"),
"speed_scale": LaunchConfiguration("speed_scale"),
"steer_scale": LaunchConfiguration("steer_scale"),
"watchdog_timeout": LaunchConfiguration("watchdog_timeout"),
"heartbeat_period": LaunchConfiguration("heartbeat_period"), "heartbeat_period": LaunchConfiguration("heartbeat_period"),
}, },
], ],

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-S3) telemetry from /dev/ttyTHS1 into ROS2. Bridges Flight Controller (ESP32-S3) telemetry from /dev/ttyTHS1 into ROS2.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Published topics (same as USB CDC bridge): Published topics (same as USB CDC bridge):
/saltybot/imu sensor_msgs/Imu pitch/roll/yaw as angular velocity /saltybot/imu sensor_msgs/Imu pitch/roll/yaw as angular velocity
@ -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-S3 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

@ -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-S3 BATTERY telemetry (fuel gauge or lookup on ESP32-S3) 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-S3.""" """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-S3 drive command bridge. cmd_vel_bridge_node Nav2 /cmd_vel ESP32-S3 drive command bridge.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Extends the basic saltybot_cmd_node with four additions required for safe Extends the basic saltybot_cmd_node with four additions required for safe
autonomous operation on a self-balancing robot: autonomous operation on a self-balancing robot:
@ -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-S3 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.
Telemetry (50 Hz from ESP32 BALANCE):
=======
H\\n heartbeat. ESP32-S3 reverts steer to 0 after 500ms silence. H\\n heartbeat. ESP32-S3 reverts steer to 0 after 500ms silence.
Telemetry (50 Hz from ESP32-S3): Telemetry (50 Hz from ESP32-S3):
>>>>>>> 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.
@ -134,7 +148,7 @@ class CmdVelBridgeNode(Node):
self._current_speed = 0 # ramped output actually sent self._current_speed = 0 # ramped output actually sent
self._current_steer = 0 self._current_steer = 0
self._last_cmd_vel = 0.0 # wall clock (seconds) of last /cmd_vel msg self._last_cmd_vel = 0.0 # wall clock (seconds) of last /cmd_vel msg
self._stm32_mode = 0 # parsed "md" field: 0=MANUAL,1=ASSISTED,2=AUTO self._esp32_mode = 0 # parsed "md" field: 0=MANUAL,1=ASSISTED,2=AUTO
self._last_state = -1 self._last_state = -1
self._frame_count = 0 self._frame_count = 0
self._error_count = 0 self._error_count = 0
@ -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-S3 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)
@ -225,7 +243,7 @@ class CmdVelBridgeNode(Node):
# Mode gate: in non-AUTONOMOUS mode, zero and reset ramp state so # Mode gate: in non-AUTONOMOUS mode, zero and reset ramp state so
# re-entry always accelerates smoothly from 0. # re-entry always accelerates smoothly from 0.
if self._stm32_mode != MODE_AUTONOMOUS: if self._esp32_mode != MODE_AUTONOMOUS:
self._current_speed = 0 self._current_speed = 0
self._current_steer = 0 self._current_steer = 0
speed, steer = 0, 0 speed, steer = 0, 0
@ -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-S3 # 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-S3 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 ──────────────────────────────────────────────────────────
@ -319,7 +345,7 @@ class CmdVelBridgeNode(Node):
state = int(data["s"]) state = int(data["s"])
mode = int(data.get("md", 0)) # 0=MANUAL if not present mode = int(data.get("md", 0)) # 0=MANUAL if not present
self._stm32_mode = mode self._esp32_mode = mode
self._frame_count += 1 self._frame_count += 1
self._publish_imu(pitch_deg, roll_deg, yaw_deg, now) self._publish_imu(pitch_deg, roll_deg, yaw_deg, now)
@ -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 = "esp32s322" 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.message = f"IMU fault errno={errno}"
diag.status.append(status)
self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32 BALANCE IMU fault: errno={errno}")
=======
status.hardware_id = "esp32s322" status.hardware_id = "esp32s322"
status.message = f"IMU fault errno={errno}" status.message = f"IMU fault errno={errno}"
diag.status.append(status) diag.status.append(status)
self._diag_pub.publish(diag) self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32-S3 IMU fault: errno={errno}") self.get_logger().error(f"ESP32-S3 IMU fault: errno={errno}")
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
# ── Lifecycle ───────────────────────────────────────────────────────────── # ── Lifecycle ─────────────────────────────────────────────────────────────

View File

@ -1,17 +1,31 @@
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
"""stm32_cmd_node.py — Orin ↔ ESP32-S3 IO auxiliary bridge node.
=======
"""esp32_cmd_node.py — Full bidirectional binary-framed ESP32-S3↔Jetson bridge. """esp32_cmd_node.py — Full bidirectional binary-framed ESP32-S3↔Jetson bridge.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
Issue #119: replaces the ASCII-protocol saltybot_cmd_node with a robust binary Connects to the ESP32-S3 IO board via USB-CDC (/dev/esp32-io) using the
framing protocol (STX/TYPE/LEN/PAYLOAD/CRC16/ETX) at 921600 baud. inter-board binary protocol (docs/SAUL-TEE-SYSTEM-REFERENCE.md §5).
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
This node is NOT the primary drive path (that is CAN via can_bridge_node).
It handles auxiliary I/O: RC monitoring, sensor data, LED/output control.
=======
TX commands (Jetson ESP32-S3): TX commands (Jetson ESP32-S3):
SPEED_STEER 50 Hz from /cmd_vel subscription SPEED_STEER 50 Hz from /cmd_vel subscription
HEARTBEAT 200 ms timer (ESP32-S3 watchdog fires at 500 ms) HEARTBEAT 200 ms timer (ESP32-S3 watchdog fires at 500 ms)
ARM via /saltybot/arm service ARM via /saltybot/arm service
SET_MODE via /saltybot/set_mode service SET_MODE via /saltybot/set_mode service
PID_UPDATE via /saltybot/pid_update topic PID_UPDATE via /saltybot/pid_update topic
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
Watchdog: if /cmd_vel is silent for 500 ms, send SPEED_STEER(0,0) and log warning. Frame format: [0xAA][LEN][TYPE][PAYLOAD][CRC8] @ 460800 baud
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
RX from ESP32 IO:
RC_CHANNELS (0x01) /saltybot/rc_channels (std_msgs/String JSON)
SENSORS (0x02) /saltybot/sensors (std_msgs/String JSON)
=======
RX telemetry (ESP32-S3 Jetson): RX telemetry (ESP32-S3 Jetson):
IMU /saltybot/imu (sensor_msgs/Imu) IMU /saltybot/imu (sensor_msgs/Imu)
BATTERY /saltybot/telemetry/battery (std_msgs/String JSON) BATTERY /saltybot/telemetry/battery (std_msgs/String JSON)
@ -19,13 +33,20 @@ RX telemetry (ESP32-S3 → Jetson):
ARM_STATE /saltybot/arm_state (std_msgs/String JSON) ARM_STATE /saltybot/arm_state (std_msgs/String JSON)
ERROR /saltybot/error (std_msgs/String JSON) ERROR /saltybot/error (std_msgs/String JSON)
All frames /diagnostics (diagnostic_msgs/DiagnosticArray) All frames /diagnostics (diagnostic_msgs/DiagnosticArray)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
Auto-reconnect: USB disconnect is detected when serial.read() raises; node TX to ESP32 IO:
continuously retries at reconnect_delay interval. LED_CMD (0x10) /saltybot/leds (std_msgs/String JSON)
OUTPUT_CMD (0x11) /saltybot/outputs (std_msgs/String JSON)
This node owns /dev/ttyACM0 exclusively do NOT run alongside HEARTBEAT (0x20) sent every heartbeat_period (keep IO watchdog alive)
serial_bridge_node or saltybot_cmd_node on the same port.
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
Parameters (config/stm32_cmd_params.yaml):
serial_port /dev/esp32-io
baud_rate 460800
reconnect_delay 2.0
heartbeat_period 0.2 (ESP32 IO watchdog fires at ~500 ms)
=======
Parameters (config/esp32_cmd_params.yaml): Parameters (config/esp32_cmd_params.yaml):
serial_port /dev/ttyACM0 serial_port /dev/ttyACM0
baud_rate 921600 baud_rate 921600
@ -34,12 +55,12 @@ Parameters (config/esp32_cmd_params.yaml):
watchdog_timeout 0.5 (seconds no /cmd_vel send zero-speed) watchdog_timeout 0.5 (seconds no /cmd_vel send zero-speed)
speed_scale 1000.0 (linear.x m/s ESC units) speed_scale 1000.0 (linear.x m/s ESC units)
steer_scale -500.0 (angular.z rad/s ESC units, neg to flip convention) steer_scale -500.0 (angular.z rad/s ESC units, neg to flip convention)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
""" """
from __future__ import annotations from __future__ import annotations
import json import json
import math
import threading import threading
import time import time
@ -50,119 +71,82 @@ from rclpy.qos import HistoryPolicy, QoSProfile, ReliabilityPolicy
import serial import serial
from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue
from geometry_msgs.msg import Twist
from sensor_msgs.msg import Imu
from std_msgs.msg import String from std_msgs.msg import String
from std_srvs.srv import SetBool, Trigger
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
from .stm32_protocol import (
BAUD_RATE,
=======
from .esp32_protocol import ( 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,
ImuFrame, BatteryFrame, MotorRpmFrame, ArmStateFrame, ErrorFrame, RcChannels,
encode_heartbeat, encode_speed_steer, encode_arm, encode_set_mode, SensorData,
encode_pid_update, encode_heartbeat,
encode_led_cmd,
encode_output_cmd,
) )
# ── Constants ─────────────────────────────────────────────────────────────────
IMU_FRAME_ID = "imu_link"
_ARM_LABEL = {0: "DISARMED", 1: "ARMED", 2: "TILT_FAULT"}
def _clamp(v: float, lo: float, hi: float) -> float:
return max(lo, min(hi, v))
# ── Node ──────────────────────────────────────────────────────────────────────
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."""
=======
"""Binary-framed Jetson↔ESP32-S3 bridge node.""" """Binary-framed Jetson↔ESP32-S3 bridge node."""
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("esp32_cmd_node") super().__init__("esp32_cmd_node")
# ── Parameters ──────────────────────────────────────────────────────── # ── Parameters ────────────────────────────────────────────────────
self.declare_parameter("serial_port", "/dev/ttyACM0") self.declare_parameter("serial_port", "/dev/esp32-io")
self.declare_parameter("baud_rate", 921600) self.declare_parameter("baud_rate", BAUD_RATE)
self.declare_parameter("reconnect_delay", 2.0) self.declare_parameter("reconnect_delay", 2.0)
self.declare_parameter("heartbeat_period", 0.2) self.declare_parameter("heartbeat_period", 0.2)
self.declare_parameter("watchdog_timeout", 0.5)
self.declare_parameter("speed_scale", 1000.0)
self.declare_parameter("steer_scale", -500.0)
port = self.get_parameter("serial_port").value self._port_name = self.get_parameter("serial_port").value
baud = self.get_parameter("baud_rate").value self._baud = self.get_parameter("baud_rate").value
self._reconnect_delay = self.get_parameter("reconnect_delay").value self._reconnect_delay = self.get_parameter("reconnect_delay").value
self._hb_period = self.get_parameter("heartbeat_period").value self._hb_period = self.get_parameter("heartbeat_period").value
self._wd_timeout = self.get_parameter("watchdog_timeout").value
self._speed_scale = self.get_parameter("speed_scale").value
self._steer_scale = self.get_parameter("steer_scale").value
# ── QoS ─────────────────────────────────────────────────────────────── # ── QoS ───────────────────────────────────────────────────────────
sensor_qos = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST, depth=10,
)
rel_qos = QoSProfile( rel_qos = QoSProfile(
reliability=ReliabilityPolicy.RELIABLE, reliability=ReliabilityPolicy.RELIABLE,
history=HistoryPolicy.KEEP_LAST, depth=10, history=HistoryPolicy.KEEP_LAST, depth=10,
) )
# ── Publishers ──────────────────────────────────────────────────────── # ── Publishers ────────────────────────────────────────────────────
self._imu_pub = self.create_publisher(Imu, "/saltybot/imu", sensor_qos) self._rc_pub = self.create_publisher(String, "/saltybot/rc_channels", rel_qos)
self._arm_pub = self.create_publisher(String, "/saltybot/arm_state", rel_qos) self._sens_pub = self.create_publisher(String, "/saltybot/sensors", rel_qos)
self._error_pub = self.create_publisher(String, "/saltybot/error", rel_qos)
self._battery_pub = self.create_publisher(String, "/saltybot/telemetry/battery", rel_qos)
self._rpm_pub = self.create_publisher(String, "/saltybot/telemetry/motor_rpm", rel_qos)
self._diag_pub = self.create_publisher(DiagnosticArray, "/diagnostics", rel_qos) self._diag_pub = self.create_publisher(DiagnosticArray, "/diagnostics", rel_qos)
# ── Subscribers ─────────────────────────────────────────────────────── # ── Subscriptions ─────────────────────────────────────────────────
self._cmd_vel_sub = self.create_subscription( self.create_subscription(String, "/saltybot/leds", self._on_leds, rel_qos)
Twist, "/cmd_vel", self._on_cmd_vel, rel_qos, self.create_subscription(String, "/saltybot/outputs", self._on_outputs, rel_qos)
)
self._pid_sub = self.create_subscription(
String, "/saltybot/pid_update", self._on_pid_update, rel_qos,
)
# ── Services ────────────────────────────────────────────────────────── # ── Serial state ──────────────────────────────────────────────────
self._arm_srv = self.create_service(SetBool, "/saltybot/arm", self._svc_arm)
self._mode_srv = self.create_service(SetBool, "/saltybot/set_mode", self._svc_set_mode)
# ── Serial state ──────────────────────────────────────────────────────
self._port_name = port
self._baud = baud
self._ser: serial.Serial | None = None self._ser: serial.Serial | None = None
self._ser_lock = threading.Lock() self._ser_lock = threading.Lock()
self._parser = FrameParser() self._parser = FrameParser()
self._rx_count = 0
# ── TX state ────────────────────────────────────────────────────────── # ── Open serial and start timers ──────────────────────────────────
self._last_speed = 0
self._last_steer = 0
self._last_cmd_t = time.monotonic()
self._watchdog_sent = False # tracks whether we already sent zero
# ── Diagnostics state ──────────────────────────────────────────────────
self._last_arm_state = -1
self._last_battery_mv = 0
self._rx_frame_count = 0
# ── Open serial and start timers ──────────────────────────────────────
self._open_serial() self._open_serial()
# Read at 200 Hz (serial RX thread is better, but timer keeps ROS2 integration clean)
self._read_timer = self.create_timer(0.005, self._read_cb) self._read_timer = self.create_timer(0.005, self._read_cb)
# Heartbeat TX
self._hb_timer = self.create_timer(self._hb_period, self._heartbeat_cb) self._hb_timer = self.create_timer(self._hb_period, self._heartbeat_cb)
# Watchdog check (fires at 2× watchdog_timeout for quick detection)
self._wd_timer = self.create_timer(self._wd_timeout / 2, self._watchdog_cb)
# Periodic diagnostics
self._diag_timer = self.create_timer(1.0, self._publish_diagnostics) self._diag_timer = self.create_timer(1.0, self._publish_diagnostics)
self.get_logger().info( self.get_logger().info(
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
f"stm32_cmd_node started — {self._port_name} @ {self._baud} baud"
=======
f"esp32_cmd_node started — {port} @ {baud} baud | " f"esp32_cmd_node started — {port} @ {baud} baud | "
f"HB {int(self._hb_period * 1000)}ms | WD {int(self._wd_timeout * 1000)}ms" f"HB {int(self._hb_period * 1000)}ms | WD {int(self._wd_timeout * 1000)}ms"
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
) )
# ── Serial management ───────────────────────────────────────────────────── # ── Serial management ─────────────────────────────────────────────────
def _open_serial(self) -> bool: def _open_serial(self) -> bool:
with self._ser_lock: with self._ser_lock:
@ -170,7 +154,7 @@ class Stm32CmdNode(Node):
self._ser = serial.Serial( self._ser = serial.Serial(
port=self._port_name, port=self._port_name,
baudrate=self._baud, baudrate=self._baud,
timeout=0.005, # non-blocking reads timeout=0.005,
write_timeout=0.1, write_timeout=0.1,
) )
self._ser.reset_input_buffer() self._ser.reset_input_buffer()
@ -185,17 +169,7 @@ class Stm32CmdNode(Node):
self._ser = None self._ser = None
return False return False
def _close_serial(self) -> None:
with self._ser_lock:
if self._ser and self._ser.is_open:
try:
self._ser.close()
except Exception:
pass
self._ser = None
def _write(self, data: bytes) -> bool: def _write(self, data: bytes) -> bool:
"""Thread-safe serial write. Returns False if port is not open."""
with self._ser_lock: with self._ser_lock:
if self._ser is None or not self._ser.is_open: if self._ser is None or not self._ser.is_open:
return False return False
@ -207,16 +181,15 @@ class Stm32CmdNode(Node):
self._ser = None self._ser = None
return False return False
# ── RX — read callback ──────────────────────────────────────────────────── # ── RX ────────────────────────────────────────────────────────────────
def _read_cb(self) -> None: def _read_cb(self) -> None:
"""Read bytes from serial and feed them to the frame parser."""
raw: bytes | None = None raw: bytes | None = None
reconnect_needed = False reconnect = False
with self._ser_lock: with self._ser_lock:
if self._ser is None or not self._ser.is_open: if self._ser is None or not self._ser.is_open:
reconnect_needed = True reconnect = True
else: else:
try: try:
n = self._ser.in_waiting n = self._ser.in_waiting
@ -225,9 +198,9 @@ class Stm32CmdNode(Node):
except serial.SerialException as exc: except serial.SerialException as exc:
self.get_logger().error(f"Serial read error: {exc}") self.get_logger().error(f"Serial read error: {exc}")
self._ser = None self._ser = None
reconnect_needed = True reconnect = True
if reconnect_needed: if reconnect:
self.get_logger().warn( self.get_logger().warn(
"Serial disconnected — will retry", "Serial disconnected — will retry",
throttle_duration_sec=self._reconnect_delay, throttle_duration_sec=self._reconnect_delay,
@ -240,24 +213,41 @@ class Stm32CmdNode(Node):
return return
for byte in raw: for byte in raw:
frame = self._parser.feed(byte) msg = self._parser.feed(byte)
if frame is not None: if msg is not None:
self._rx_frame_count += 1 self._rx_count += 1
self._dispatch_frame(frame) self._dispatch(msg)
def _dispatch_frame(self, frame) -> None: def _dispatch(self, msg) -> None:
"""Route a decoded frame to the appropriate publisher."""
now = self.get_clock().now().to_msg() now = self.get_clock().now().to_msg()
ts = f"{now.sec}.{now.nanosec:09d}"
if isinstance(frame, ImuFrame): if isinstance(msg, RcChannels):
self._publish_imu(frame, now) out = String()
out.data = json.dumps({
"channels": msg.channels,
"source": msg.source,
"ts": ts,
})
self._rc_pub.publish(out)
elif isinstance(frame, BatteryFrame): elif isinstance(msg, SensorData):
self._publish_battery(frame, now) out = String()
out.data = json.dumps({
"pressure_pa": msg.pressure_pa,
"temperature_c": msg.temperature_c,
"tof_mm": msg.tof_mm,
"ts": ts,
})
self._sens_pub.publish(out)
elif isinstance(frame, MotorRpmFrame): elif isinstance(msg, tuple):
self._publish_motor_rpm(frame, now) type_code, _ = msg
self.get_logger().debug(f"Unknown inter-board type 0x{type_code:02X}")
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
# ── TX ────────────────────────────────────────────────────────────────
=======
elif isinstance(frame, ArmStateFrame): elif isinstance(frame, ArmStateFrame):
self._publish_arm_state(frame, now) self._publish_arm_state(frame, now)
@ -368,102 +358,79 @@ class Stm32CmdNode(Node):
"SPEED_STEER dropped — serial not open", "SPEED_STEER dropped — serial not open",
throttle_duration_sec=2.0, throttle_duration_sec=2.0,
) )
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
def _heartbeat_cb(self) -> None: def _heartbeat_cb(self) -> None:
"""Send HEARTBEAT every heartbeat_period (default 200ms)."""
self._write(encode_heartbeat()) self._write(encode_heartbeat())
def _watchdog_cb(self) -> None: def _on_leds(self, msg: String) -> None:
"""Send zero-speed if /cmd_vel silent for watchdog_timeout seconds.""" """Parse JSON {"pattern":N,"r":R,"g":G,"b":B} and send LED_CMD."""
if time.monotonic() - self._last_cmd_t >= self._wd_timeout:
if not self._watchdog_sent:
self.get_logger().warn(
f"No /cmd_vel for {self._wd_timeout:.1f}s — sending zero-speed"
)
self._watchdog_sent = True
self._last_speed = 0
self._last_steer = 0
self._write(encode_speed_steer(0, 0))
def _on_pid_update(self, msg: String) -> None:
"""Parse JSON /saltybot/pid_update and send PID_UPDATE frame."""
try: try:
data = json.loads(msg.data) d = json.loads(msg.data)
kp = float(data["kp"]) frame = encode_led_cmd(
ki = float(data["ki"]) int(d.get("pattern", 0)),
kd = float(data["kd"]) int(d.get("r", 0)),
except (ValueError, KeyError, json.JSONDecodeError) as exc: int(d.get("g", 0)),
self.get_logger().error(f"Bad PID update JSON: {exc}") int(d.get("b", 0)),
return
frame = encode_pid_update(kp, ki, kd)
if self._write(frame):
self.get_logger().info(f"PID update: kp={kp}, ki={ki}, kd={kd}")
else:
self.get_logger().warn("PID_UPDATE dropped — serial not open")
# ── Services ──────────────────────────────────────────────────────────────
def _svc_arm(self, request: SetBool.Request, response: SetBool.Response):
"""SetBool(True) = arm, SetBool(False) = disarm."""
arm = request.data
frame = encode_arm(arm)
ok = self._write(frame)
response.success = ok
response.message = ("ARMED" if arm else "DISARMED") if ok else "serial not open"
self.get_logger().info(
f"ARM service: {'arm' if arm else 'disarm'}{'sent' if ok else 'FAILED'}"
) )
return response except (ValueError, KeyError, json.JSONDecodeError) as exc:
self.get_logger().error(f"Bad /saltybot/leds JSON: {exc}")
return
self._write(frame)
def _svc_set_mode(self, request: SetBool.Request, response: SetBool.Response): def _on_outputs(self, msg: String) -> None:
"""SetBool: data maps to mode byte (True=1, False=0).""" """Parse JSON {"horn":bool,"buzzer":bool,"headlight":0-255,"fan":0-255}."""
mode = 1 if request.data else 0 try:
frame = encode_set_mode(mode) d = json.loads(msg.data)
ok = self._write(frame) frame = encode_output_cmd(
response.success = ok bool(d.get("horn", False)),
response.message = f"mode={mode}" if ok else "serial not open" bool(d.get("buzzer", False)),
return response int(d.get("headlight", 0)),
int(d.get("fan", 0)),
)
except (ValueError, KeyError, json.JSONDecodeError) as exc:
self.get_logger().error(f"Bad /saltybot/outputs JSON: {exc}")
return
self._write(frame)
# ── Diagnostics ─────────────────────────────────────────────────────────── # ── Diagnostics ───────────────────────────────────────────────────────
def _publish_diagnostics(self) -> None: def _publish_diagnostics(self) -> None:
diag = DiagnosticArray() diag = DiagnosticArray()
diag.header.stamp = self.get_clock().now().to_msg() diag.header.stamp = self.get_clock().now().to_msg()
status = DiagnosticStatus() status = DiagnosticStatus()
<<<<<<< HEAD:jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py
status.name = "saltybot/esp32_io_bridge"
status.hardware_id = "esp32-s3-io"
=======
status.name = "saltybot/esp32_cmd_node" status.name = "saltybot/esp32_cmd_node"
status.hardware_id = "esp32s322" status.hardware_id = "esp32s322"
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only):jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_cmd_node.py
port_ok = self._ser is not None and self._ser.is_open port_ok = self._ser is not None and self._ser.is_open
if port_ok: status.level = DiagnosticStatus.OK if port_ok else DiagnosticStatus.ERROR
status.level = DiagnosticStatus.OK status.message = "Serial OK" if port_ok else f"Disconnected: {self._port_name}"
status.message = "Serial OK"
else:
status.level = DiagnosticStatus.ERROR
status.message = f"Serial disconnected: {self._port_name}"
wd_age = time.monotonic() - self._last_cmd_t
status.values = [ status.values = [
KeyValue(key="serial_port", value=self._port_name), KeyValue(key="serial_port", value=self._port_name),
KeyValue(key="baud_rate", value=str(self._baud)),
KeyValue(key="port_open", value=str(port_ok)), KeyValue(key="port_open", value=str(port_ok)),
KeyValue(key="rx_frames", value=str(self._rx_frame_count)), KeyValue(key="rx_frames", value=str(self._rx_count)),
KeyValue(key="rx_errors", value=str(self._parser.frames_error)), KeyValue(key="rx_errors", value=str(self._parser.frames_error)),
KeyValue(key="last_speed", value=str(self._last_speed)),
KeyValue(key="last_steer", value=str(self._last_steer)),
KeyValue(key="cmd_vel_age_s", value=f"{wd_age:.2f}"),
KeyValue(key="battery_mv", value=str(self._last_battery_mv)),
KeyValue(key="arm_state", value=_ARM_LABEL.get(self._last_arm_state, "?")),
] ]
diag.status.append(status) diag.status.append(status)
self._diag_pub.publish(diag) self._diag_pub.publish(diag)
# ── Lifecycle ───────────────────────────────────────────────────────────── # ── Lifecycle ─────────────────────────────────────────────────────────
def destroy_node(self) -> None: def destroy_node(self) -> None:
# Send zero-speed + disarm on shutdown self._write(encode_heartbeat(state=0))
self._write(encode_speed_steer(0, 0)) with self._ser_lock:
self._write(encode_arm(False)) if self._ser and self._ser.is_open:
self._close_serial() try:
self._ser.close()
except Exception:
pass
self._ser = None
super().destroy_node() super().destroy_node()

View File

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

View File

@ -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 = "esp32s322" 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,26 @@
""" """
<<<<<<< HEAD
saltybot_cmd_node full bidirectional ESP32 BALANCEJetson bridge
=======
saltybot_cmd_node full bidirectional ESP32-S3Jetson 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):
JSON telemetry /saltybot/imu, /saltybot/balance_state, /diagnostics
TX path:
/cmd_vel (geometry_msgs/Twist) C<speed>,<steer>\\n ESP32 BALANCE
Heartbeat timer (200ms) H\\n ESP32 BALANCE
Protocol:
H\\n heartbeat. ESP32 BALANCE reverts steer to 0 if gap > 500ms.
C<spd>,<str>\\n drive command. speed/steer: -1000..+1000 integers.
C command also refreshes ESP32 BALANCE heartbeat timer.
=======
RX path (50Hz from ESP32-S3): RX path (50Hz from ESP32-S3):
JSON telemetry /saltybot/imu, /saltybot/balance_state, /diagnostics JSON telemetry /saltybot/imu, /saltybot/balance_state, /diagnostics
@ -15,6 +32,7 @@ Protocol:
H\\n heartbeat. ESP32-S3 reverts steer to 0 if gap > 500ms. H\\n heartbeat. ESP32-S3 reverts steer to 0 if gap > 500ms.
C<spd>,<str>\\n drive command. speed/steer: -1000..+1000 integers. C<spd>,<str>\\n drive command. speed/steer: -1000..+1000 integers.
C command also refreshes ESP32-S3 heartbeat timer. C command also refreshes ESP32-S3 heartbeat timer.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Twist mapping (configurable via ROS2 params): Twist mapping (configurable via ROS2 params):
speed = clamp(linear.x * speed_scale, -1000, 1000) speed = clamp(linear.x * speed_scale, -1000, 1000)
@ -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-S3 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 = "esp32s322" 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.message = f"IMU fault errno={errno}"
diag.status.append(status)
self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32 BALANCE IMU fault: errno={errno}")
=======
status.hardware_id = "esp32s322" status.hardware_id = "esp32s322"
status.message = f"IMU fault errno={errno}" status.message = f"IMU fault errno={errno}"
diag.status.append(status) diag.status.append(status)
self._diag_pub.publish(diag) self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32-S3 IMU fault: errno={errno}") self.get_logger().error(f"ESP32-S3 IMU fault: errno={errno}")
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
# ── TX — command send ───────────────────────────────────────────────────── # ── TX — command send ─────────────────────────────────────────────────────
@ -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-S3 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-S3 USB CDC ROS2 topic publisher ESP32-S3 USB CDC ROS2 topic publisher
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Telemetry frame (50 Hz, newline-delimited JSON): Telemetry frame (50 Hz, newline-delimited JSON):
{"p":<pitch×10>,"r":<roll×10>,"e":<err×10>,"ig":<integral×10>, {"p":<pitch×10>,"r":<roll×10>,"e":<err×10>,"ig":<integral×10>,
@ -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-S3 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
@ -38,7 +46,7 @@ IMU_FRAME_ID = "imu_link"
class SerialBridgeNode(Node): class SerialBridgeNode(Node):
def __init__(self): def __init__(self):
super().__init__("stm32_serial_bridge") super().__init__("esp32_serial_bridge")
# ── Parameters ──────────────────────────────────────────────────────── # ── Parameters ────────────────────────────────────────────────────────
self.declare_parameter("serial_port", "/dev/ttyACM0") self.declare_parameter("serial_port", "/dev/ttyACM0")
@ -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-S3 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-S3 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-S3 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 = "esp32s322" 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.message = f"IMU fault errno={errno}"
diag.status.append(status)
self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32 BALANCE reported IMU fault: errno={errno}")
=======
status.hardware_id = "esp32s322" status.hardware_id = "esp32s322"
status.message = f"IMU fault errno={errno}" status.message = f"IMU fault errno={errno}"
diag.status.append(status) diag.status.append(status)
self._diag_pub.publish(diag) self._diag_pub.publish(diag)
self.get_logger().error(f"ESP32-S3 reported IMU fault: errno={errno}") self.get_logger().error(f"ESP32-S3 reported IMU fault: errno={errno}")
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
def destroy_node(self): def destroy_node(self):
self._close_serial() self._close_serial()

View File

@ -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-S3 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)
"stm32_cmd_node = saltybot_bridge.stm32_cmd_node:main",
=======
# Binary-framed ESP32-S3 command node (Issue #119) # Binary-framed ESP32-S3 command node (Issue #119)
"esp32_cmd_node = saltybot_bridge.esp32_cmd_node:main", "esp32_cmd_node = saltybot_bridge.esp32_cmd_node:main",
>>>>>>> 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-S3 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

@ -139,10 +139,10 @@ class TestModeGate:
MODE_ASSISTED = 1 MODE_ASSISTED = 1
MODE_AUTONOMOUS = 2 MODE_AUTONOMOUS = 2
def _apply_mode_gate(self, stm32_mode, current_speed, current_steer, def _apply_mode_gate(self, esp32_mode, current_speed, current_steer,
target_speed, target_steer, step=10): target_speed, target_steer, step=10):
"""Mirror of _control_cb mode gate logic.""" """Mirror of _control_cb mode gate logic."""
if stm32_mode != self.MODE_AUTONOMOUS: if esp32_mode != self.MODE_AUTONOMOUS:
# Reset ramp state, send zero # Reset ramp state, send zero
return 0, 0, 0, 0 # (current_speed, current_steer, sent_speed, sent_steer) return 0, 0, 0, 0 # (current_speed, current_steer, sent_speed, sent_steer)
new_s = _ramp_toward(current_speed, target_speed, step) new_s = _ramp_toward(current_speed, target_speed, step)

View File

@ -1,5 +1,9 @@
""" """
<<<<<<< HEAD
Unit tests for ESP32 BALANCE telemetry parsing logic.
=======
Unit tests for ESP32-S3 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

@ -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-S3 bridge consumes this topic. # Output: /cmd_vel (Twist) — ESP32-S3 bridge consumes this topic.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
bt_navigator: bt_navigator:
ros__parameters: ros__parameters:

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-S3 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-S3 (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= 2s cmd_vel bridge (consumes /cmd_vel, needs ESP32 bridge up)
=======
t= 0s ESP32-S3 bridge (serial port owner must be first) t= 0s ESP32-S3 bridge (serial port owner must be first)
t= 2s cmd_vel bridge (consumes /cmd_vel, needs ESP32-S3 bridge up) t= 2s cmd_vel bridge (consumes /cmd_vel, needs ESP32-S3 bridge up)
>>>>>>> 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.
cmd_vel bridge has 500ms deadman: stops robot if /cmd_vel goes silent.
ESP32 BALANCE AUTONOMOUS mode gate (md=2) in cmd_vel bridge robot stays still
until ESP32 BALANCE firmware is in AUTONOMOUS mode regardless of /cmd_vel.
=======
ESP32-S3 bridge must be up before cmd_vel bridge sends any command. ESP32-S3 bridge must be up before cmd_vel bridge sends any command.
cmd_vel bridge has 500ms deadman: stops robot if /cmd_vel goes silent. cmd_vel bridge has 500ms deadman: stops robot if /cmd_vel goes silent.
ESP32-S3 AUTONOMOUS mode gate (md=2) in cmd_vel bridge robot stays still ESP32-S3 AUTONOMOUS mode gate (md=2) in cmd_vel bridge robot stays still
until ESP32-S3 firmware is in AUTONOMOUS mode regardless of /cmd_vel. until ESP32-S3 firmware is in AUTONOMOUS mode regardless of /cmd_vel.
>>>>>>> 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-S3) /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-S3 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-S3 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-S3 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-S3 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

@ -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-S3 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-S3 USART 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-S3 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-S3 bidirectional bridge (JLINK USART1) # 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-S3 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-S3 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-S3 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-S3 bridge + sensors + motor daemon. """Minimal: ESP32-S3 bridge + sensors + motor daemon.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Safe drive control only. No AI, no nav, no social. Safe drive control only. No AI, no nav, no social.
Boot time ~4 s. RAM ~400 MB. Boot time ~4 s. RAM ~400 MB.

View File

@ -1,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-S3 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-S3 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-S3 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

@ -3,8 +3,7 @@
can_bridge_node.py ROS2 node bridging the SaltyBot Orin to the ESP32-S3 BALANCE motor can_bridge_node.py ROS2 node bridging the SaltyBot Orin to the ESP32-S3 BALANCE motor
controller and VESC motor controllers over CAN bus. controller and VESC motor controllers over CAN bus.
The node opens the SocketCAN interface (slcan0 by default), spawns a background Spec: docs/SAUL-TEE-SYSTEM-REFERENCE.md §6 (2026-04-04)
reader thread to process incoming telemetry, and exposes the following interface:
Subscriptions Subscriptions
------------- -------------
@ -19,9 +18,15 @@ Publications
/can/vesc/right/state std_msgs/Float32MultiArray Right VESC state /can/vesc/right/state std_msgs/Float32MultiArray Right VESC state
/can/connection_status std_msgs/String "connected" | "disconnected" /can/connection_status std_msgs/String "connected" | "disconnected"
Issue: https://gitea.vayrette.com/seb/saltylab-firmware/issues/674 Parameters
----------
can_interface str CAN socket name (default: slcan0)
speed_scale float /cmd_vel linear.x (m/s) motor units (default: 1000.0)
steer_scale float /cmd_vel angular.z (rad/s) motor units (default: -500.0)
command_timeout_s float watchdog zero-vel threshold (default: 0.5)
""" """
import json
import threading import threading
import time import time
from typing import Optional from typing import Optional
@ -30,8 +35,7 @@ import can
import rclpy import rclpy
from geometry_msgs.msg import Twist from geometry_msgs.msg import Twist
from rclpy.node import Node from rclpy.node import Node
from rcl_interfaces.msg import SetParametersResult from sensor_msgs.msg import BatteryState
from sensor_msgs.msg import BatteryState, Imu
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 (
@ -44,22 +48,19 @@ from saltybot_can_bridge.balance_protocol import (
ORIN_CAN_ID_FC_PID_ACK, ORIN_CAN_ID_FC_PID_ACK,
ORIN_CAN_ID_PID_SET, ORIN_CAN_ID_PID_SET,
MODE_DRIVE, MODE_DRIVE,
MODE_ESTOP,
MODE_IDLE, MODE_IDLE,
encode_drive_cmd,
encode_arm_cmd,
encode_estop_cmd, encode_estop_cmd,
encode_mode_cmd, decode_attitude,
encode_velocity_cmd, decode_battery,
encode_pid_set_cmd, decode_vesc_status1,
decode_battery_telem,
decode_imu_telem,
decode_pid_ack,
decode_vesc_state,
) )
# Reconnect attempt interval when CAN bus is lost # Reconnect attempt interval when CAN bus is lost
_RECONNECT_INTERVAL_S: float = 5.0 _RECONNECT_INTERVAL_S: float = 5.0
# Watchdog timer tick rate (Hz) # Watchdog tick rate (Hz); sends zero DRIVE when /cmd_vel is silent
_WATCHDOG_HZ: float = 10.0 _WATCHDOG_HZ: float = 10.0
@ -71,46 +72,37 @@ class CanBridgeNode(Node):
# ── Parameters ──────────────────────────────────────────────────── # ── Parameters ────────────────────────────────────────────────────
self.declare_parameter("can_interface", "slcan0") self.declare_parameter("can_interface", "slcan0")
self.declare_parameter("left_vesc_can_id", 56) self.declare_parameter("left_vesc_can_id", VESC_LEFT_ID)
self.declare_parameter("right_vesc_can_id", 68) self.declare_parameter("right_vesc_can_id", VESC_RIGHT_ID)
self.declare_parameter("mamba_can_id", 1) self.declare_parameter("speed_scale", 1000.0)
self.declare_parameter("steer_scale", -500.0)
self.declare_parameter("command_timeout_s", 0.5) self.declare_parameter("command_timeout_s", 0.5)
self.declare_parameter("pid/kp", 0.0)
self.declare_parameter("pid/ki", 0.0)
self.declare_parameter("pid/kd", 0.0)
self._iface: str = self.get_parameter("can_interface").value self._iface = self.get_parameter("can_interface").value
self._left_vesc_id: int = self.get_parameter("left_vesc_can_id").value self._left_vesc_id = self.get_parameter("left_vesc_can_id").value
self._right_vesc_id: int = self.get_parameter("right_vesc_can_id").value self._right_vesc_id = self.get_parameter("right_vesc_can_id").value
self._mamba_id: int = self.get_parameter("mamba_can_id").value self._speed_scale = self.get_parameter("speed_scale").value
self._cmd_timeout: float = self.get_parameter("command_timeout_s").value self._steer_scale = self.get_parameter("steer_scale").value
self._pid_kp: float = self.get_parameter("pid/kp").value self._cmd_timeout = self.get_parameter("command_timeout_s").value
self._pid_ki: float = self.get_parameter("pid/ki").value
self._pid_kd: float = self.get_parameter("pid/kd").value
# ── State ───────────────────────────────────────────────────────── # ── State ─────────────────────────────────────────────────────────
self._bus: Optional[can.BusABC] = None self._bus: Optional[can.BusABC] = None
self._connected: bool = False self._connected: bool = False
self._last_cmd_time: float = time.monotonic() self._last_cmd_time: float = time.monotonic()
self._lock = threading.Lock() # protects _bus / _connected self._lock = threading.Lock()
# ── Publishers ──────────────────────────────────────────────────── # ── Publishers ────────────────────────────────────────────────────
self._pub_imu = self.create_publisher(Imu, "/can/imu", 10) self._pub_attitude = self.create_publisher(String, "/saltybot/attitude", 10)
self._pub_balance = self.create_publisher(String, "/saltybot/balance_state", 10)
self._pub_battery = self.create_publisher(BatteryState, "/can/battery", 10) self._pub_battery = self.create_publisher(BatteryState, "/can/battery", 10)
self._pub_vesc_left = self.create_publisher( self._pub_vesc_left = self.create_publisher(Float32MultiArray,"/can/vesc/left/state", 10)
Float32MultiArray, "/can/vesc/left/state", 10 self._pub_vesc_right= self.create_publisher(Float32MultiArray,"/can/vesc/right/state", 10)
) self._pub_status = self.create_publisher(String, "/can/connection_status", 10)
self._pub_vesc_right = self.create_publisher(
Float32MultiArray, "/can/vesc/right/state", 10
)
self._pub_status = self.create_publisher(
String, "/can/connection_status", 10
)
# ── Subscriptions ───────────────────────────────────────────────── # ── Subscriptions ─────────────────────────────────────────────────
self.create_subscription(Twist, "/cmd_vel", self._cmd_vel_cb, 10) self.create_subscription(Twist, "/cmd_vel", self._cmd_vel_cb, 10)
self.create_subscription(Bool, "/estop", self._estop_cb, 10) self.create_subscription(Bool, "/estop", self._estop_cb, 10)
self.add_on_set_parameters_callback(self._on_set_parameters) self.create_subscription(Bool, "/saltybot/arm", self._arm_cb, 10)
# ── Timers ──────────────────────────────────────────────────────── # ── Timers ────────────────────────────────────────────────────────
self.create_timer(1.0 / _WATCHDOG_HZ, self._watchdog_cb) self.create_timer(1.0 / _WATCHDOG_HZ, self._watchdog_cb)
@ -128,46 +120,17 @@ class CanBridgeNode(Node):
self.get_logger().info( self.get_logger().info(
f"can_bridge_node ready — iface={self._iface} " f"can_bridge_node ready — iface={self._iface} "
f"left_vesc={self._left_vesc_id} right_vesc={self._right_vesc_id} " f"left_vesc={self._left_vesc_id} right_vesc={self._right_vesc_id} "
f"mamba={self._mamba_id}" f"speed_scale={self._speed_scale} steer_scale={self._steer_scale}"
) )
# -- PID parameter callback (Issue #693) --
def _on_set_parameters(self, params) -> SetParametersResult:
"""Send new PID gains over CAN when pid/* params change."""
for p in params:
if p.name == "pid/kp":
self._pid_kp = float(p.value)
elif p.name == "pid/ki":
self._pid_ki = float(p.value)
elif p.name == "pid/kd":
self._pid_kd = float(p.value)
else:
continue
try:
payload = encode_pid_set_cmd(self._pid_kp, self._pid_ki, self._pid_kd)
self._send_can(ORIN_CAN_ID_PID_SET, payload, "pid_set")
self.get_logger().info(
f"PID gains sent: Kp={self._pid_kp:.2f} "
f"Ki={self._pid_ki:.2f} Kd={self._pid_kd:.2f}"
)
except ValueError as exc:
return SetParametersResult(successful=False, reason=str(exc))
return SetParametersResult(successful=True)
# ── Connection management ────────────────────────────────────────────── # ── Connection management ──────────────────────────────────────────────
def _try_connect(self) -> None: def _try_connect(self) -> None:
"""Attempt to open the CAN interface; silently skip if already connected."""
with self._lock: with self._lock:
if self._connected: if self._connected:
return return
try: try:
bus = can.interface.Bus( self._bus = can.interface.Bus(channel=self._iface, bustype="socketcan")
channel=self._iface,
bustype="socketcan",
)
self._bus = bus
self._connected = True self._connected = True
self.get_logger().info(f"CAN bus connected: {self._iface}") self.get_logger().info(f"CAN bus connected: {self._iface}")
self._publish_status("connected") self._publish_status("connected")
@ -180,12 +143,10 @@ class CanBridgeNode(Node):
self._publish_status("disconnected") self._publish_status("disconnected")
def _reconnect_cb(self) -> None: def _reconnect_cb(self) -> None:
"""Periodic timer: try to reconnect when disconnected."""
if not self._connected: if not self._connected:
self._try_connect() self._try_connect()
def _handle_can_error(self, exc: Exception, context: str) -> None: def _handle_can_error(self, exc: Exception, context: str) -> None:
"""Mark bus as disconnected on any CAN error."""
self.get_logger().warning(f"CAN error in {context}: {exc}") self.get_logger().warning(f"CAN error in {context}: {exc}")
with self._lock: with self._lock:
if self._bus is not None: if self._bus is not None:
@ -200,9 +161,8 @@ class CanBridgeNode(Node):
# ── ROS callbacks ───────────────────────────────────────────────────── # ── ROS callbacks ─────────────────────────────────────────────────────
def _cmd_vel_cb(self, msg: Twist) -> None: def _cmd_vel_cb(self, msg: Twist) -> None:
"""Convert /cmd_vel Twist to VESC speed commands over CAN.""" """Convert /cmd_vel Twist to ORIN_CMD_DRIVE over CAN."""
self._last_cmd_time = time.monotonic() self._last_cmd_time = time.monotonic()
if not self._connected: if not self._connected:
return return
@ -228,8 +188,6 @@ class CanBridgeNode(Node):
"""Forward /estop to ESP32-S3 BALANCE over CAN.""" """Forward /estop to ESP32-S3 BALANCE over CAN."""
if not self._connected: if not self._connected:
return return
payload = encode_estop_cmd(msg.data)
self._send_can(MAMBA_CMD_ESTOP, payload, "estop")
if msg.data: if msg.data:
self._send_can( self._send_can(
MAMBA_CMD_MODE, encode_mode_cmd(MODE_ESTOP), "estop mode" MAMBA_CMD_MODE, encode_mode_cmd(MODE_ESTOP), "estop mode"
@ -239,34 +197,22 @@ class CanBridgeNode(Node):
# ── Watchdog ────────────────────────────────────────────────────────── # ── Watchdog ──────────────────────────────────────────────────────────
def _watchdog_cb(self) -> None: def _watchdog_cb(self) -> None:
"""If no /cmd_vel arrives within the timeout, send zero velocity.""" """If /cmd_vel is silent for command_timeout_s, send zero DRIVE (acts as keepalive)."""
if not self._connected: if not self._connected:
return return
elapsed = time.monotonic() - self._last_cmd_time if time.monotonic() - self._last_cmd_time > self._cmd_timeout:
if elapsed > self._cmd_timeout: self._send_can(ORIN_CMD_DRIVE, encode_drive_cmd(0, 0, MODE_IDLE), "watchdog")
self._send_can(
MAMBA_CMD_VELOCITY,
encode_velocity_cmd(0.0, 0.0),
"watchdog zero-vel",
)
self._send_can(
MAMBA_CMD_MODE, encode_mode_cmd(MODE_IDLE), "watchdog idle"
)
# ── CAN send helper ─────────────────────────────────────────────────── # ── CAN send helper ───────────────────────────────────────────────────
def _send_can(self, arb_id: int, data: bytes, context: str) -> None: def _send_can(self, arb_id: int, data: bytes, context: str,
"""Send a standard CAN frame; handle errors gracefully.""" extended: bool = False) -> None:
with self._lock: with self._lock:
if not self._connected or self._bus is None: if not self._connected or self._bus is None:
return return
bus = self._bus bus = self._bus
msg = can.Message(arbitration_id=arb_id, data=data,
msg = can.Message( is_extended_id=extended)
arbitration_id=arb_id,
data=data,
is_extended_id=False,
)
try: try:
bus.send(msg, timeout=0.05) bus.send(msg, timeout=0.05)
except can.CanError as exc: except can.CanError as exc:
@ -275,55 +221,41 @@ class CanBridgeNode(Node):
# ── Background CAN reader ───────────────────────────────────────────── # ── Background CAN reader ─────────────────────────────────────────────
def _reader_loop(self) -> None: def _reader_loop(self) -> None:
"""
Blocking CAN read loop executed in a daemon thread.
Dispatches incoming frames to the appropriate handler.
"""
while rclpy.ok(): while rclpy.ok():
with self._lock: with self._lock:
connected = self._connected connected, bus = self._connected, self._bus
bus = self._bus
if not connected or bus is None: if not connected or bus is None:
time.sleep(0.1) time.sleep(0.1)
continue continue
try: try:
frame = bus.recv(timeout=0.5) frame = bus.recv(timeout=0.5)
except can.CanError as exc: except can.CanError as exc:
self._handle_can_error(exc, "reader_loop recv") self._handle_can_error(exc, "reader_loop recv")
continue continue
if frame is None: if frame is None:
# Timeout — no frame within 0.5 s, loop again
continue continue
self._dispatch_frame(frame) self._dispatch_frame(frame)
def _dispatch_frame(self, frame: can.Message) -> None: def _dispatch_frame(self, frame: can.Message) -> None:
"""Route an incoming CAN frame to the correct publisher."""
arb_id = frame.arbitration_id arb_id = frame.arbitration_id
data = bytes(frame.data) data = bytes(frame.data)
vesc_l = (VESC_STATUS_1 << 8) | self._left_vesc_id
vesc_r = (VESC_STATUS_1 << 8) | self._right_vesc_id
try: try:
if arb_id == MAMBA_TELEM_IMU: if arb_id == ESP32_TELEM_ATTITUDE:
self._handle_imu(data, frame.timestamp) self._handle_attitude(data)
elif arb_id == ESP32_TELEM_BATTERY:
elif arb_id == MAMBA_TELEM_BATTERY: self._handle_battery(data)
self._handle_battery(data, frame.timestamp) elif arb_id == vesc_l:
t = decode_vesc_status1(self._left_vesc_id, data)
elif arb_id == VESC_TELEM_STATE + self._left_vesc_id: m = Float32MultiArray()
self._handle_vesc_state(data, frame.timestamp, side="left") m.data = [t.erpm, t.duty, 0.0, t.current]
self._pub_vesc_left.publish(m)
elif arb_id == VESC_TELEM_STATE + self._right_vesc_id: elif arb_id == vesc_r:
self._handle_vesc_state(data, frame.timestamp, side="right") t = decode_vesc_status1(self._right_vesc_id, data)
m = Float32MultiArray()
elif arb_id == ORIN_CAN_ID_FC_PID_ACK: m.data = [t.erpm, t.duty, 0.0, t.current]
gains = decode_pid_ack(data) self._pub_vesc_right.publish(m)
self.get_logger().debug(
f"FC PID ACK: Kp={gains.kp:.2f} Ki={gains.ki:.2f} Kd={gains.kd:.2f}"
)
except Exception as exc: except Exception as exc:
self.get_logger().warning( self.get_logger().warning(
f"Error parsing CAN frame 0x{arb_id:03X}: {exc}" f"Error parsing CAN frame 0x{arb_id:03X}: {exc}"
@ -331,52 +263,36 @@ class CanBridgeNode(Node):
# ── Frame handlers ──────────────────────────────────────────────────── # ── Frame handlers ────────────────────────────────────────────────────
def _handle_imu(self, data: bytes, timestamp: float) -> None: _STATE_LABEL = {0: "IDLE", 1: "RUNNING", 2: "FAULT"}
telem = decode_imu_telem(data)
msg = Imu() def _handle_attitude(self, data: bytes) -> None:
msg.header.stamp = self.get_clock().now().to_msg() """ATTITUDE (0x400): pitch, speed, yaw_rate, state, flags → /saltybot/attitude."""
msg.header.frame_id = "imu_link" t = decode_attitude(data)
now = self.get_clock().now().to_msg()
msg.linear_acceleration.x = telem.accel_x payload = {
msg.linear_acceleration.y = telem.accel_y "pitch_deg": round(t.pitch_deg, 2),
msg.linear_acceleration.z = telem.accel_z "speed_mps": round(t.speed, 3),
"yaw_rate": round(t.yaw_rate, 3),
msg.angular_velocity.x = telem.gyro_x "state": t.state,
msg.angular_velocity.y = telem.gyro_y "state_label": self._STATE_LABEL.get(t.state, f"UNKNOWN({t.state})"),
msg.angular_velocity.z = telem.gyro_z "flags": t.flags,
"ts": f"{now.sec}.{now.nanosec:09d}",
# Covariance unknown; mark as -1 per REP-145 }
msg.orientation_covariance[0] = -1.0 msg = String()
msg.data = json.dumps(payload)
self._pub_imu.publish(msg) self._pub_attitude.publish(msg)
self._pub_balance.publish(msg) # keep /saltybot/balance_state alive
def _handle_battery(self, data: bytes, timestamp: float) -> None:
telem = decode_battery_telem(data)
def _handle_battery(self, data: bytes) -> None:
"""BATTERY (0x401): vbat_mv, fault_code, rssi → /can/battery."""
t = decode_battery(data)
msg = BatteryState() msg = BatteryState()
msg.header.stamp = self.get_clock().now().to_msg() msg.header.stamp = self.get_clock().now().to_msg()
msg.voltage = telem.voltage msg.voltage = t.vbat_mv / 1000.0
msg.current = telem.current
msg.present = True msg.present = True
msg.power_supply_status = BatteryState.POWER_SUPPLY_STATUS_DISCHARGING msg.power_supply_status = BatteryState.POWER_SUPPLY_STATUS_DISCHARGING
self._pub_battery.publish(msg) self._pub_battery.publish(msg)
def _handle_vesc_state(
self, data: bytes, timestamp: float, side: str
) -> None:
telem = decode_vesc_state(data)
msg = Float32MultiArray()
# Layout: [erpm, duty, voltage, current]
msg.data = [telem.erpm, telem.duty, telem.voltage, telem.current]
if side == "left":
self._pub_vesc_left.publish(msg)
else:
self._pub_vesc_right.publish(msg)
# ── Status helper ───────────────────────────────────────────────────── # ── Status helper ─────────────────────────────────────────────────────
def _publish_status(self, status: str) -> None: def _publish_status(self, status: str) -> None:
@ -387,17 +303,10 @@ class CanBridgeNode(Node):
# ── Shutdown ────────────────────────────────────────────────────────── # ── Shutdown ──────────────────────────────────────────────────────────
def destroy_node(self) -> None: def destroy_node(self) -> None:
"""Send zero velocity and shut down the CAN bus cleanly."""
if self._connected and self._bus is not None: if self._connected and self._bus is not None:
try: try:
self._send_can( self._send_can(ORIN_CMD_DRIVE, encode_drive_cmd(0, 0, MODE_IDLE), "shutdown")
MAMBA_CMD_VELOCITY, self._send_can(ORIN_CMD_ARM, encode_arm_cmd(False), "shutdown")
encode_velocity_cmd(0.0, 0.0),
"shutdown",
)
self._send_can(
MAMBA_CMD_MODE, encode_mode_cmd(MODE_IDLE), "shutdown"
)
except Exception: except Exception:
pass pass
try: try:
@ -407,8 +316,6 @@ class CanBridgeNode(Node):
super().destroy_node() super().destroy_node()
# ---------------------------------------------------------------------------
def main(args=None) -> None: def main(args=None) -> None:
rclpy.init(args=args) rclpy.init(args=args)
node = CanBridgeNode() node = CanBridgeNode()

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-S3 BALANCE 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

@ -22,7 +22,10 @@ FC (ESP32-S3 BALANCE) → Orin telemetry (standard 11-bit, matching orin_can.h):
FC_IMU 0x402 8 bytes FC_IMU 0x402 8 bytes
FC_BARO 0x403 8 bytes FC_BARO 0x403 8 bytes
Mamba VESC internal commands (matching balance_protocol.py):
=======
ESP32-S3 BALANCE 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
@ -54,7 +57,10 @@ FC_IMU: int = 0x402
FC_BARO: int = 0x403 FC_BARO: int = 0x403
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Mamba → VESC internal command IDs (from balance_protocol.py)
=======
# ESP32-S3 BALANCE → VESC internal command IDs (from balance_protocol.py) # ESP32-S3 BALANCE → VESC internal command IDs (from balance_protocol.py)
>>>>>>> 9aed963 (fix: scrub remaining Mamba references in can_bridge and e2e test protocol files)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
MAMBA_CMD_VELOCITY: int = 0x100 MAMBA_CMD_VELOCITY: int = 0x100
@ -136,7 +142,10 @@ def build_estop_cmd(action: int = 1) -> bytes:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Frame builders — Mamba velocity commands (balance_protocol.py encoding)
=======
# Frame builders — ESP32-S3 BALANCE velocity commands (balance_protocol.py encoding) # Frame builders — ESP32-S3 BALANCE velocity commands (balance_protocol.py encoding)
>>>>>>> 9aed963 (fix: scrub remaining Mamba references in can_bridge and e2e test protocol files)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def build_velocity_cmd(left_mps: float, right_mps: float) -> bytes: def build_velocity_cmd(left_mps: float, right_mps: float) -> bytes:

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-S3 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

@ -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-S3, 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-S3 bridge online", {}) self.hardware_checks["stm32"] = ("OK", "ESP32-S3 bridge online", {})
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
def _check_servos(self): def _check_servos(self):
try: try:

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-S3 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-S3 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-S3 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-S3 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-S3", 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

@ -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-S3. 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,14 +1,24 @@
"""jlink_gimbal.py — JLINK binary frame codec for gimbal commands (Issue #548). """jlink_gimbal.py — JLINK binary frame codec for gimbal commands (Issue #548).
<<<<<<< HEAD
Matches the JLINK protocol defined in include/jlink.h (Issue #547 ESP32 side).
Command type (Jetson ESP32 BALANCE):
=======
Matches the JLINK protocol defined in include/jlink.h (Issue #547 ESP32-S3 side). Matches the JLINK protocol defined in include/jlink.h (Issue #547 ESP32-S3 side).
Command type (Jetson ESP32-S3): Command type (Jetson ESP32-S3):
>>>>>>> 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-S3 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)
@ -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
TLM_GIMBAL_STATE = 0x84 # ESP32 BALANCE → Jetson: measured state
=======
CMD_GIMBAL_POS = 0x0B # Jetson → ESP32-S3: set pan/tilt target CMD_GIMBAL_POS = 0x0B # Jetson → ESP32-S3: set pan/tilt target
TLM_GIMBAL_STATE = 0x84 # ESP32-S3 → Jetson: measured state TLM_GIMBAL_STATE = 0x84 # ESP32-S3 → Jetson: measured state
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
# Speed register: 0 = maximum servo speed; 4095 = slowest non-zero speed. # Speed register: 0 = maximum servo speed; 4095 = slowest non-zero speed.
# Map deg/s to this register: speed_reg = max(0, 4095 - int(deg_s * 4095 / 360)) # Map deg/s to this register: speed_reg = max(0, 4095 - int(deg_s * 4095 / 360))

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-S3 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-S3 is in RC_MANUAL. prevents autonomous commands when the ESP32-S3 is in RC_MANUAL.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
The bridge's existing ESC ramp handles hardware-level smoothing; The bridge's existing ESC ramp handles hardware-level smoothing;
the blend_alpha here provides the higher-level cmd_vel policy ramp. the blend_alpha here provides the higher-level cmd_vel policy ramp.

View File

@ -6,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.
"RAMP_TO_AUTO" Transitioning RCAUTO; blend_alpha 0.01.0 over ramp_s.
"AUTO" ESP32 BALANCE executing Jetson cmd_vel; RC sticks idle.
=======
"RC" ESP32-S3 executing RC pilot commands; Jetson cmd_vel blocked. "RC" ESP32-S3 executing RC pilot commands; Jetson cmd_vel blocked.
"RAMP_TO_AUTO" Transitioning RCAUTO; blend_alpha 0.01.0 over ramp_s. "RAMP_TO_AUTO" Transitioning RCAUTO; blend_alpha 0.01.0 over ramp_s.
"AUTO" ESP32-S3 executing Jetson cmd_vel; RC sticks idle. "AUTO" ESP32-S3 executing Jetson cmd_vel; RC sticks idle.
>>>>>>> 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-S3 mode. Parsed for RC link health (field "rc_link") and ESP32-S3 mode.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
<slam_fix_topic> (geometry_msgs/PoseWithCovarianceStamped) <slam_fix_topic> (geometry_msgs/PoseWithCovarianceStamped)
Any message received within slam_fix_timeout_s SLAM fix valid. Any message received within slam_fix_timeout_s SLAM fix valid.
@ -106,7 +110,7 @@ class ModeSwitchNode(Node):
self._last_joy_t: float = 0.0 # monotonic; 0 = never self._last_joy_t: float = 0.0 # monotonic; 0 = never
self._last_slam_t: float = 0.0 self._last_slam_t: float = 0.0
self._joy_axes: list = [] self._joy_axes: list = []
self._stm32_mode: int = 0 # from balance_state JSON self._esp32_mode: int = 0 # from balance_state JSON
# ── QoS ─────────────────────────────────────────────────────────────── # ── QoS ───────────────────────────────────────────────────────────────
best_effort = QoSProfile( best_effort = QoSProfile(
@ -187,7 +191,7 @@ class ModeSwitchNode(Node):
data = json.loads(msg.data) data = json.loads(msg.data)
# "mode" is a label string; map back to int for reference # "mode" is a label string; map back to int for reference
mode_label = data.get("mode", "RC_MANUAL") mode_label = data.get("mode", "RC_MANUAL")
self._stm32_mode = {"RC_MANUAL": 0, "RC_ASSISTED": 1, self._esp32_mode = {"RC_MANUAL": 0, "RC_ASSISTED": 1,
"AUTONOMOUS": 2}.get(mode_label, 0) "AUTONOMOUS": 2}.get(mode_label, 0)
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
pass pass

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)
right_can_id: 68 # right motor VESC CAN ID (ESP32 BALANCE)
=======
left_can_id: 56 # left motor VESC CAN ID (ESP32-S3 BALANCE) left_can_id: 56 # left motor VESC CAN ID (ESP32-S3 BALANCE)
right_can_id: 68 # right motor VESC CAN ID (ESP32-S3 BALANCE) right_can_id: 68 # right motor VESC CAN ID (ESP32-S3 BALANCE)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# ── State topic names (must match VESC telemetry publisher) ────────────── # ── State topic names (must match VESC telemetry publisher) ──────────────
left_state_topic: /vesc/left/state left_state_topic: /vesc/left/state

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-S3 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-S3 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-S3; 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-S3 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-S3 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-S3 or Raspberry Pi Pico GPIO PWM bridge. Communication: USB CDC serial to ESP32-S3 or Raspberry Pi Pico GPIO PWM bridge.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
ESC channel assignments (configurable): ESC channel assignments (configurable):
CH1 = left-front CH1 = left-front

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_input_topic: /cmd_vel_input # upstream velocity (remap as needed)
cmd_vel_output_topic: /cmd_vel # downstream (to ESP32 bridge)
=======
# cmd_vel_mux → /cmd_vel_safe → [safety_zone: cmd_vel_input] → /cmd_vel → ESP32-S3 # cmd_vel_mux → /cmd_vel_safe → [safety_zone: cmd_vel_input] → /cmd_vel → ESP32-S3
cmd_vel_input_topic: /cmd_vel_input # upstream velocity (remap as needed) cmd_vel_input_topic: /cmd_vel_input # upstream velocity (remap as needed)
cmd_vel_output_topic: /cmd_vel # downstream (to ESP32-S3 bridge) cmd_vel_output_topic: /cmd_vel # downstream (to ESP32-S3 bridge)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)

View File

@ -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-S3 # person_follower → /cmd_vel_raw → [speed_controller] → /cmd_vel → cmd_vel_bridge → ESP32-S3
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
# ── Controller ───────────────────────────────────────────────────────────────── # ── Controller ─────────────────────────────────────────────────────────────────
control_rate: 50.0 # Hz — 50ms tick, same as cmd_vel_bridge control_rate: 50.0 # Hz — 50ms tick, same as cmd_vel_bridge
@ -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
# reconfigured: max_linear_vel=8.0, ramp_rate=500 → consider ramp_rate=150
# at ride speed (slower ramp = smoother balance).
#
# 2. The ESP32 BALANCE balance PID gains likely need retuning for ride speed. Expect
=======
# 1. To enable ride profile, the Jetson → ESP32-S3 cmd_vel_bridge must also be # 1. To enable ride profile, the Jetson → ESP32-S3 cmd_vel_bridge must also be
# reconfigured: max_linear_vel=8.0, ramp_rate=500 → consider ramp_rate=150 # reconfigured: max_linear_vel=8.0, ramp_rate=500 → consider ramp_rate=150
# at ride speed (slower ramp = smoother balance). # at ride speed (slower ramp = smoother balance).
# #
# 2. The ESP32-S3 balance PID gains likely need retuning for ride speed. Expect # 2. The ESP32-S3 balance PID gains likely need retuning for ride speed. Expect
>>>>>>> 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-S3 person_follower /cmd_vel_raw [speed_controller] /cmd_vel cmd_vel_bridge ESP32-S3
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
Usage: Usage:
# Defaults (walk profile initially, adapts via UWB + GPS): # Defaults (walk profile initially, adapts via UWB + GPS):

View File

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

View File

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

View File

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

View File

@ -127,7 +127,7 @@ loop — USB would never enumerate cleanly.
| LED2 | PC15 | GPIO | | LED2 | PC15 | GPIO |
| Buzzer | PB2 | GPIO/TIM4_CH3 | | Buzzer | PB2 | GPIO/TIM4_CH3 |
MCU: STM32F722RET6 (MAMBA F722S FC, Betaflight target DIAT-MAMBAF722_2022B) MCU: ESP32RET6 (ESP32 BALANCE FC, Betaflight target DIAT-MAMBAF722_2022B)
--- ---

View File

@ -8,7 +8,7 @@ monitor_speed = 115200
board_build.mcu = stm32f722ret6 board_build.mcu = stm32f722ret6
board_build.f_cpu = 216000000L board_build.f_cpu = 216000000L
build_flags = build_flags =
-DSTM32F722xx -DESP32xx
-DUSE_HAL_DRIVER -DUSE_HAL_DRIVER
-DHSE_VALUE=8000000U -DHSE_VALUE=8000000U
-DUSE_USB_FS -DUSE_USB_FS

View File

@ -22,7 +22,7 @@ Requirements:
dfu-util >= 0.9 installed and in PATH dfu-util >= 0.9 installed and in PATH
Dual-bank note: Dual-bank note:
STM32F722 has single-bank 512 KB flash; hardware A/B rollback is not ESP32 has single-bank 512 KB flash; hardware A/B rollback is not
supported. Rollback is implemented here by saving a backup of the supported. Rollback is implemented here by saving a backup of the
previous binary (.firmware_backup.bin) before each flash. previous binary (.firmware_backup.bin) before each flash.
""" """
@ -36,11 +36,11 @@ import subprocess
import sys import sys
import time import time
# ---- STM32F722 flash constants ---- # ---- ESP32 flash constants ----
FLASH_BASE = 0x08000000 FLASH_BASE = 0x08000000
FLASH_SIZE = 0x80000 # 512 KB FLASH_SIZE = 0x80000 # 512 KB
# ---- DFU device defaults (STM32 system bootloader) ---- # ---- DFU device defaults (ESP32/STM32 system bootloader) ----
DFU_VID = 0x0483 # STMicroelectronics DFU_VID = 0x0483 # STMicroelectronics
DFU_PID = 0xDF11 # DFU mode DFU_PID = 0xDF11 # DFU mode
@ -62,16 +62,16 @@ def crc32_file(path: str) -> int:
def stm32_crc32(data: bytes) -> int: def stm32_crc32(data: bytes) -> int:
""" """
Compute CRC-32/MPEG-2 matching STM32F7 hardware CRC unit. Compute CRC-32/MPEG-2 matching ESP32 hardware CRC unit.
STM32 algorithm: ESP32/STM32 algorithm:
Polynomial : 0x04C11DB7 Polynomial : 0x04C11DB7
Initial : 0xFFFFFFFF Initial : 0xFFFFFFFF
Width : 32 bits Width : 32 bits
Reflection : none (MSB-first) Reflection : none (MSB-first)
Feed size : 32-bit words from flash (little-endian CPU read) Feed size : 32-bit words from flash (little-endian CPU read)
When the STM32 reads a flash word it gets a little-endian uint32; When the ESP32 BALANCE reads a flash word it gets a little-endian uint32;
the hardware CRC unit processes bits[31:24] first, then [23:16], the hardware CRC unit processes bits[31:24] first, then [23:16],
[15:8], [7:0]. This Python implementation replicates that behaviour. [15:8], [7:0]. This Python implementation replicates that behaviour.
@ -214,11 +214,11 @@ def main() -> int:
f'({FLASH_SIZE} bytes)', file=sys.stderr) f'({FLASH_SIZE} bytes)', file=sys.stderr)
return 1 return 1
# STM32 hardware CRC (for cross-checking with firmware telemetry) # ESP32/STM32 hardware CRC (for cross-checking with firmware telemetry)
with open(target, 'rb') as fh: with open(target, 'rb') as fh:
bin_data = fh.read() bin_data = fh.read()
crc_hw = stm32_crc32(bin_data.ljust(FLASH_SIZE, b'\xff')) crc_hw = stm32_crc32(bin_data.ljust(FLASH_SIZE, b'\xff'))
print(f'CRC-32 : 0x{crc_hw:08X} (MPEG-2 / STM32 HW, padded to {FLASH_SIZE // 1024} KB)') print(f'CRC-32 : 0x{crc_hw:08X} (MPEG-2 / ESP32/STM32 HW, padded to {FLASH_SIZE // 1024} KB)')
# Save backup before flashing (skip when rolling back) # Save backup before flashing (skip when rolling back)
if not args.rollback: if not args.rollback:

View File

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

View File

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

View File

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

View File

@ -3,7 +3,11 @@ test_ota.py — OTA firmware update utilities (Issue #124)
Tests: Tests:
- CRC-32/ISO-HDLC (crc32_file / binascii.crc32) - CRC-32/ISO-HDLC (crc32_file / binascii.crc32)
<<<<<<< HEAD
- CRC-32/MPEG-2 (stm32_crc32 matches ESP32 hardware CRC unit)
=======
- CRC-32/MPEG-2 (stm32_crc32 matches ESP32-S3 hardware CRC unit) - CRC-32/MPEG-2 (stm32_crc32 matches ESP32-S3 hardware CRC unit)
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
- CRC-16/XMODEM (_crc16_xmodem JLink frame integrity) - CRC-16/XMODEM (_crc16_xmodem JLink frame integrity)
- DFU_ENTER frame (JLINK_CMD_DFU_ENTER = 0x06, no payload) - DFU_ENTER frame (JLINK_CMD_DFU_ENTER = 0x06, no payload)
- Safety constants (BKP index, flash region, magic value) - Safety constants (BKP index, flash region, magic value)
@ -233,8 +237,13 @@ class TestOtaConstants:
BNO055_BKP_RANGE = range(0, 7) BNO055_BKP_RANGE = range(0, 7)
assert OTA_DFU_BKP_IDX not in BNO055_BKP_RANGE assert OTA_DFU_BKP_IDX not in BNO055_BKP_RANGE
<<<<<<< HEAD
def test_bkp_idx_valid_esp32(self):
"""ESP32 has 32 backup registers (BKP0RBKP31R)."""
=======
def test_bkp_idx_valid_esp32s3(self): def test_bkp_idx_valid_esp32s3(self):
"""ESP32-S3 has 32 backup registers (BKP0RBKP31R).""" """ESP32-S3 has 32 backup registers (BKP0RBKP31R)."""
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
OTA_DFU_BKP_IDX = 15 OTA_DFU_BKP_IDX = 15
assert 0 <= OTA_DFU_BKP_IDX <= 31 assert 0 <= OTA_DFU_BKP_IDX <= 31
@ -252,7 +261,11 @@ class TestOtaConstants:
assert DFU_VID == 0x0483 assert DFU_VID == 0x0483
def test_dfu_pid_dfu_mode(self): def test_dfu_pid_dfu_mode(self):
<<<<<<< HEAD
"""Default PID = 0xDF11 (ESP32 DFU mode)."""
=======
"""Default PID = 0xDF11 (ESP32-S3 DFU mode).""" """Default PID = 0xDF11 (ESP32-S3 DFU mode)."""
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references ESP32-S3 only)
assert DFU_PID == 0xDF11 assert DFU_PID == 0xDF11
def test_bkp_idx_not_zero(self): def test_bkp_idx_not_zero(self):

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Saltybot — CAN Monitor</title> <title>Saltybot — CAN Monitor</title>
<link rel="stylesheet" href="can_monitor_panel.css"> <link rel="stylesheet" href="can_monitor_panel.css">
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roslib@1.4.0/build/roslib.min.js"></script>
</head> </head>
<body> <body>

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Saltybot — System Diagnostics</title> <title>Saltybot — System Diagnostics</title>
<link rel="stylesheet" href="diagnostics_panel.css"> <link rel="stylesheet" href="diagnostics_panel.css">
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roslib@1.4.0/build/roslib.min.js"></script>
</head> </head>
<body> <body>

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Saltybot — Event Log</title> <title>Saltybot — Event Log</title>
<link rel="stylesheet" href="event_log_panel.css"> <link rel="stylesheet" href="event_log_panel.css">
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roslib@1.4.0/build/roslib.min.js"></script>
</head> </head>
<body> <body>

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Saltybot — Gamepad Teleop</title> <title>Saltybot — Gamepad Teleop</title>
<link rel="stylesheet" href="gamepad_panel.css"> <link rel="stylesheet" href="gamepad_panel.css">
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roslib@1.4.0/build/roslib.min.js"></script>
</head> </head>
<body> <body>

View File

@ -6,7 +6,7 @@
<title>Saltybot — Gimbal Control</title> <title>Saltybot — Gimbal Control</title>
<link rel="stylesheet" href="gimbal_panel.css"> <link rel="stylesheet" href="gimbal_panel.css">
<!-- roslib from CDN --> <!-- roslib from CDN -->
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roslib@1.4.0/build/roslib.min.js"></script>
<style> <style>
/* Cam button active state (can't use CSS-only with JS-toggled class without Tailwind) */ /* Cam button active state (can't use CSS-only with JS-toggled class without Tailwind) */
.cam-btn { padding: 3px 10px; border-radius: 4px; border: 1px solid #1e3a5f; .cam-btn { padding: 3px 10px; border-radius: 4px; border: 1px solid #1e3a5f;

View File

@ -7,7 +7,7 @@
<link rel="stylesheet" href="gps_map_panel.css"> <link rel="stylesheet" href="gps_map_panel.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roslib@1.4.0/build/roslib.min.js"></script>
</head> </head>
<body> <body>

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Saltybot — Dashboard</title> <title>Saltybot — Dashboard</title>
<link rel="stylesheet" href="dashboard.css"> <link rel="stylesheet" href="dashboard.css">
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roslib@1.4.0/build/roslib.min.js"></script>
</head> </head>
<body> <body>

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Saltybot — Map View</title> <title>Saltybot — Map View</title>
<link rel="stylesheet" href="map_panel.css"> <link rel="stylesheet" href="map_panel.css">
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roslib@1.4.0/build/roslib.min.js"></script>
</head> </head>
<body> <body>

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Saltybot — Settings</title> <title>Saltybot — Settings</title>
<link rel="stylesheet" href="settings_panel.css"> <link rel="stylesheet" href="settings_panel.css">
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roslib@1.4.0/build/roslib.min.js"></script>
</head> </head>
<body> <body>

View File

@ -310,7 +310,7 @@ function FirmwareView({ firmwareInfo, startFirmwareWatch, connected }) {
</div> </div>
{!firmwareInfo && connected && ( {!firmwareInfo && connected && (
<div className="text-amber-700 text-xs border border-amber-900 rounded p-2"> <div className="text-amber-700 text-xs border border-amber-900 rounded p-2">
Waiting for /diagnostics Ensure firmware diagnostics keys (stm32_fw_version etc.) are published. Waiting for /diagnostics Ensure firmware diagnostics keys (esp32_fw_version etc.) are published.
</div> </div>
)} )}
</div> </div>

View File

@ -9,7 +9,7 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- ROSLIB for robot GPS via rosbridge --> <!-- ROSLIB for robot GPS via rosbridge -->
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roslib@1.4.0/build/roslib.min.js"></script>
<style> <style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Saltybot — VESC Motor Dashboard</title> <title>Saltybot — VESC Motor Dashboard</title>
<link rel="stylesheet" href="vesc_panel.css"> <link rel="stylesheet" href="vesc_panel.css">
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/roslib@1.4.0/build/roslib.min.js"></script>
</head> </head>
<body> <body>