chore: remove Mamba F722S / STM32 / BlackPill legacy hw refs #716
200
docs/AGENTS.md
200
docs/AGENTS.md
@ -5,17 +5,18 @@ You're working on **SaltyLab**, a self-balancing two-wheeled indoor robot. Read
|
||||
## Project Overview
|
||||
|
||||
A hoverboard-based balancing robot with two compute layers:
|
||||
1. **FC (Flight Controller)** — MAMBA F722S (STM32F722RET6 + 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 Nano** — AI brain. ROS2, SLAM, person tracking. Sends velocity commands to FC via UART. Not safety-critical — FC operates independently.
|
||||
1. **ESP32-S3 BALANCE** — runs the PID balance loop. Safety-critical, operates independently of the Orin.
|
||||
2. **ESP32-S3 IO** — handles I/O: motor commands to VESCs, sensor polling, CAN/UART comms.
|
||||
3. **Orin Nano Super** — AI brain. ROS2, SLAM, person tracking. Sends velocity commands to ESP32-S3 BALANCE via UART. Not safety-critical.
|
||||
|
||||
```
|
||||
Jetson (speed+steer via UART1) ←→ ELRS RC (UART3, kill switch)
|
||||
Orin Nano Super (speed+steer via UART) ←→ ELRS RC (kill switch)
|
||||
│
|
||||
▼
|
||||
MAMBA F722S (MPU6000 IMU, PID balance)
|
||||
ESP32-S3 BALANCE (IMU, PID balance loop)
|
||||
│
|
||||
▼ UART2
|
||||
Hoverboard ESC (FOC) → 2× 8" hub motors
|
||||
▼ CAN / UART
|
||||
ESP32-S3 IO → VESC 68 (left) + VESC 56 (right)
|
||||
```
|
||||
|
||||
## ⚠️ SAFETY — READ THIS OR PEOPLE GET HURT
|
||||
@ -26,7 +27,7 @@ This is not a toy. 8" hub motors + 36V battery can crush fingers, break toes, an
|
||||
2. **Tilt cutoff at ±25°** — motors to zero, require manual re-arm. No retry, no recovery.
|
||||
3. **Hardware watchdog (50ms)** — if firmware hangs, motors cut.
|
||||
4. **RC kill switch** — dedicated ELRS channel, checked every loop iteration. Always overrides.
|
||||
5. **Jetson UART timeout (200ms)** — if Jetson disconnects, motors cut.
|
||||
5. **Orin UART timeout (200ms)** — if Orin disconnects, motors cut.
|
||||
6. **Speed hard cap** — firmware limit, start at 10%. Increase only after proven stable.
|
||||
7. **Never test untethered** until PID is stable for 5+ minutes on a tether.
|
||||
|
||||
@ -35,31 +36,16 @@ This is not a toy. 8" hub motors + 36V battery can crush fingers, break toes, an
|
||||
## Repository Layout
|
||||
|
||||
```
|
||||
firmware/ # STM32 HAL firmware (PlatformIO)
|
||||
├── src/
|
||||
│ ├── main.c # Entry point, clock config, main loop
|
||||
│ ├── icm42688.c # ICM-42688-P SPI driver (backup IMU — currently broken)
|
||||
│ ├── bmp280.c # Barometer driver (disabled)
|
||||
│ └── status.c # LED + buzzer status patterns
|
||||
├── include/
|
||||
│ ├── config.h # Pin definitions, constants
|
||||
│ ├── icm42688.h
|
||||
│ ├── mpu6000.h # MPU6000 driver header (primary IMU)
|
||||
│ ├── hoverboard.h # Hoverboard ESC UART protocol
|
||||
│ ├── crsf.h # ELRS CRSF protocol
|
||||
│ ├── bmp280.h
|
||||
│ └── status.h
|
||||
├── lib/USB_CDC/ # USB CDC stack (serial over USB)
|
||||
│ ├── src/ # CDC implementation, USB descriptors, PCD config
|
||||
│ └── include/
|
||||
└── platformio.ini # Build config
|
||||
esp32/ # ESP32-S3 firmware (ESP-IDF)
|
||||
├── balance/ # ESP32-S3 BALANCE — PID loop, IMU, safety
|
||||
└── io/ # ESP32-S3 IO — VESC CAN, sensors, comms
|
||||
|
||||
cad/ # OpenSCAD parametric parts (16 files)
|
||||
├── dimensions.scad # ALL measurements live here — single source of truth
|
||||
├── assembly.scad # Full robot assembly visualization
|
||||
├── motor_mount_plate.scad
|
||||
├── battery_shelf.scad
|
||||
├── fc_mount.scad # Vibration-isolated FC mount
|
||||
├── esp32_balance_mount.scad # Vibration-isolated ESP32-S3 BALANCE mount
|
||||
├── jetson_shelf.scad
|
||||
├── esc_mount.scad
|
||||
├── sensor_tower_top.scad
|
||||
@ -82,55 +68,55 @@ PLATFORM.md # Hardware platform reference
|
||||
|
||||
## Hardware Quick Reference
|
||||
|
||||
### MAMBA F722S Flight Controller
|
||||
### ESP32-S3 BALANCE
|
||||
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| MCU | STM32F722RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) |
|
||||
| Primary IMU | MPU6000 (WHO_AM_I = 0x68) |
|
||||
| IMU Bus | SPI1: PA5=SCK, PA6=MISO, PA7=MOSI, CS=PA4 |
|
||||
| IMU EXTI | PC4 (data ready interrupt) |
|
||||
| IMU Orientation | CW270 (Betaflight convention) |
|
||||
| Secondary IMU | ICM-42688-P (on same SPI1, CS unknown — currently non-functional) |
|
||||
| Betaflight Target | DIAT-MAMBAF722_2022B |
|
||||
| USB | OTG FS (PA11/PA12), enumerates as /dev/cu.usbmodemSALTY0011 |
|
||||
| VID/PID | 0x0483/0x5740 |
|
||||
| LEDs | PC15 (LED1), PC14 (LED2), active low |
|
||||
| Buzzer | PB2 (inverted push-pull) |
|
||||
| Battery ADC | PC1=VBAT, PC3=CURR (ADC3) |
|
||||
| DFU | Hold yellow BOOT button + plug USB (or send 'R' over CDC) |
|
||||
| MCU | ESP32-S3 (dual-core Xtensa LX7, 240MHz, 512KB SRAM, 8MB flash) |
|
||||
| Primary IMU | MPU6000 (SPI) |
|
||||
| Role | PID balance loop, tilt cutoff, arming |
|
||||
| Comms to Orin | UART (velocity commands in, telemetry out) |
|
||||
| Flash | `idf.py -p /dev/ttyUSB0 flash` |
|
||||
|
||||
### UART Assignments
|
||||
### ESP32-S3 IO
|
||||
|
||||
| UART | Pins | Connected To | Baud |
|
||||
|------|------|-------------|------|
|
||||
| USART1 | PA9/PA10 | Jetson Nano | 115200 |
|
||||
| USART2 | PA2/PA3 | Hoverboard ESC | 115200 |
|
||||
| USART3 | PB10/PB11 | ELRS Receiver | 420000 (CRSF) |
|
||||
| UART4 | — | Spare | — |
|
||||
| UART5 | — | Spare | — |
|
||||
| Spec | Value |
|
||||
|------|-------|
|
||||
| MCU | ESP32-S3 |
|
||||
| Role | VESC CAN driver, sensor polling, peripheral I/O |
|
||||
| VESC IDs | 68 (left), 56 (right) |
|
||||
| Motor bus | CAN 1Mbit/s |
|
||||
| Flash | `idf.py -p /dev/ttyUSB1 flash` |
|
||||
|
||||
### UART Assignments (ESP32-S3 BALANCE)
|
||||
|
||||
| UART | Connected To | Baud |
|
||||
|------|-------------|------|
|
||||
| UART0 | Orin Nano Super | 115200 |
|
||||
| UART1 | ESP32-S3 IO | 115200 |
|
||||
| UART2 | ELRS Receiver | 420000 (CRSF) |
|
||||
|
||||
### Motor/ESC
|
||||
|
||||
- 2× 8" pneumatic hub motors (36V, hoverboard type)
|
||||
- Hoverboard ESC with FOC firmware
|
||||
- UART protocol: `{0xABCD, int16 speed, int16 steer, uint16 checksum}` at 115200
|
||||
- Speed range: -1000 to +1000
|
||||
- 2× VESC motor controllers (CAN IDs 68, 56)
|
||||
- VESC CAN protocol: standard SET_DUTY / SET_CURRENT / SET_RPM
|
||||
- Speed range: -1.0 to +1.0 (duty cycle)
|
||||
|
||||
### Physical Dimensions (from `cad/dimensions.scad`)
|
||||
|
||||
| Part | Key Measurement |
|
||||
|------|----------------|
|
||||
| FC mounting holes | 25.5mm spacing (NOT standard 30.5mm!) |
|
||||
| FC board size | ~36mm square |
|
||||
| ESP32-S3 BALANCE board | ~55×28mm (DevKit form factor) |
|
||||
| ESP32-S3 IO board | ~55×28mm (DevKit form factor) |
|
||||
| Hub motor body | Ø200mm (~8") |
|
||||
| Motor axle | Ø12mm, 45mm long |
|
||||
| Jetson Nano | 100×80×29mm, M2.5 holes at 86×58mm |
|
||||
| Orin Nano Super | 100×79mm, M2.5 holes at 86×58mm |
|
||||
| RealSense D435i | 90×25×25mm, 1/4-20 tripod mount |
|
||||
| RPLIDAR A1 | Ø70×41mm, 4× M2.5 on Ø67mm circle |
|
||||
| Kill switch hole | Ø22mm panel mount |
|
||||
| Battery pack | ~180×80×40mm |
|
||||
| Hoverboard ESC | ~80×50×15mm |
|
||||
| VESC (each) | ~70×50×15mm |
|
||||
| 2020 extrusion | 20mm square, M5 center bore |
|
||||
| Frame width | ~350mm (axle to axle) |
|
||||
| Frame height | ~500-550mm total |
|
||||
@ -147,7 +133,7 @@ PLATFORM.md # Hardware platform reference
|
||||
| sensor_tower_top | ASA | 80% |
|
||||
| lidar_standoff (Ø80×80mm) | ASA | 40% |
|
||||
| realsense_bracket | PETG | 60% |
|
||||
| fc_mount (vibration isolated) | TPU+PETG | — |
|
||||
| esp32_balance_mount (vibration isolated) | TPU+PETG | — |
|
||||
| bumper front + rear (350×50×30mm) | TPU | 30% |
|
||||
| handle | PETG | 80% |
|
||||
| kill_switch_mount | PETG | 80% |
|
||||
@ -159,99 +145,75 @@ PLATFORM.md # Hardware platform reference
|
||||
|
||||
### 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.
|
||||
2. **DCache breaks SPI on STM32F7** — disable DCache or use cache-aligned DMA buffers with clean/invalidate. We disable it.
|
||||
3. **`-(int)0 == 0`** — checking `if (-result)` to detect errors doesn't work when result is 0 (success and failure look the same). Always use explicit error codes.
|
||||
4. **NEVER auto-run untested code on_boot** — we bricked the NSPanel 3x doing this. Test manually first.
|
||||
5. **USB CDC needs ReceivePacket() primed in CDC_Init** — without it, the OUT endpoint never starts listening. No data reception.
|
||||
1. **NEVER auto-run untested code on_boot** — we bricked the NSPanel 3x doing this. Test manually first.
|
||||
2. **`-(int)0 == 0`** — checking `if (-result)` to detect errors doesn't work when result is 0. Always use explicit error codes.
|
||||
3. **USB CDC needs RX primed in init** — without it, the OUT endpoint never starts listening.
|
||||
4. **Watchdog must be fed every loop iteration** — if balance loop stalls, motors must cut within 50ms.
|
||||
5. **Never change PID and speed limit in the same test** — one variable at a time.
|
||||
|
||||
### DFU Reboot (Betaflight Method)
|
||||
|
||||
The firmware supports reboot-to-DFU via USB command:
|
||||
1. Send `R` byte over USB CDC
|
||||
2. Firmware writes `0xDEADBEEF` to RTC backup register 0
|
||||
3. `NVIC_SystemReset()` — clean hardware reset
|
||||
4. On boot, `checkForBootloader()` (called after `HAL_Init()`) reads the magic
|
||||
5. If magic found: clears it, remaps system memory, jumps to STM32 bootloader at `0x1FF00000`
|
||||
6. Board appears as DFU device, ready for `dfu-util` flash
|
||||
|
||||
### Build & Flash
|
||||
### Build & Flash (ESP32-S3)
|
||||
|
||||
```bash
|
||||
cd firmware/
|
||||
python3 -m platformio run # Build
|
||||
dfu-util -a 0 -s 0x08000000:leave -D .pio/build/f722/firmware.bin # Flash
|
||||
# Balance board
|
||||
cd esp32/balance/
|
||||
idf.py build && idf.py -p /dev/ttyUSB0 flash monitor
|
||||
|
||||
# IO board
|
||||
cd esp32/io/
|
||||
idf.py build && idf.py -p /dev/ttyUSB1 flash monitor
|
||||
```
|
||||
|
||||
Dev machine: mbpm4 (seb@192.168.87.40), PlatformIO project at `~/Projects/saltylab-firmware/`
|
||||
|
||||
### Clock Configuration
|
||||
|
||||
```
|
||||
HSE 8MHz → PLL (M=8, N=432, P=2, Q=9) → SYSCLK 216MHz
|
||||
PLLSAI (N=384, P=8) → CLK48 48MHz (USB)
|
||||
APB1 = HCLK/4 = 54MHz
|
||||
APB2 = HCLK/2 = 108MHz
|
||||
Fallback: HSI 16MHz if HSE fails (PLL M=16)
|
||||
```
|
||||
Dev machine: mbpm4 (seb@192.168.87.40)
|
||||
|
||||
## Current Status & Known Issues
|
||||
|
||||
### Working
|
||||
- USB CDC serial streaming (50Hz JSON: `{"ax":...,"ay":...,"az":...,"gx":...,"gy":...,"gz":...}`)
|
||||
- Clock config with HSE + HSI fallback
|
||||
- Reboot-to-DFU via USB 'R' command
|
||||
- LED status patterns (status.c)
|
||||
- IMU streaming (50Hz JSON: `{"ax":...,"ay":...,"az":...,"gx":...,"gy":...,"gz":...}`)
|
||||
- VESC CAN communication (IDs 68, 56)
|
||||
- LED status patterns
|
||||
- Web UI with WebSerial + Three.js 3D visualization
|
||||
|
||||
### Broken / In Progress
|
||||
- **ICM-42688-P SPI reads return all zeros** — was the original IMU target, but SPI communication completely non-functional despite correct pin config. May be dead silicon. Switched to MPU6000 as primary.
|
||||
- **MPU6000 driver** — header exists but implementation needs completion
|
||||
- **PID balance loop** — not yet implemented
|
||||
- **Hoverboard ESC UART** — protocol defined, driver not written
|
||||
- **ELRS CRSF receiver** — protocol defined, driver not written
|
||||
- **Barometer (BMP280)** — I2C init hangs, disabled
|
||||
### In Progress
|
||||
- PID balance loop tuning
|
||||
- ELRS CRSF receiver integration
|
||||
- Orin UART integration
|
||||
|
||||
### TODO (Priority Order)
|
||||
1. Get MPU6000 streaming accel+gyro data
|
||||
1. Tune PID balance loop on ESP32-S3 BALANCE
|
||||
2. Implement complementary filter (pitch angle)
|
||||
3. Write hoverboard ESC UART driver
|
||||
4. Write PID balance loop with safety checks
|
||||
5. Wire ELRS receiver, implement CRSF parser
|
||||
6. Bench test (ESC disconnected, verify PID output)
|
||||
7. First tethered balance test at 10% speed
|
||||
8. Jetson UART integration
|
||||
9. LED subsystem (ESP32-C3)
|
||||
3. Wire ELRS receiver, implement CRSF parser
|
||||
4. Bench test (VESCs disconnected, verify PID output)
|
||||
5. First tethered balance test at 10% speed
|
||||
6. Orin UART integration
|
||||
7. LED subsystem (ESP32-S3 IO)
|
||||
|
||||
## Communication Protocols
|
||||
|
||||
### Jetson → FC (UART1, 50Hz)
|
||||
### Orin → ESP32-S3 BALANCE (UART0, 50Hz)
|
||||
```c
|
||||
struct { uint8_t header=0xAA; int16_t speed; int16_t steer; uint8_t mode; uint8_t checksum; };
|
||||
// mode: 0=idle, 1=balance, 2=follow, 3=RC
|
||||
```
|
||||
|
||||
### FC → Hoverboard ESC (UART2, loop rate)
|
||||
```c
|
||||
struct { uint16_t start=0xABCD; int16_t speed; int16_t steer; uint16_t checksum; };
|
||||
// speed/steer: -1000 to +1000
|
||||
```
|
||||
### ESP32-S3 IO → VESC (CAN, loop rate)
|
||||
- Standard VESC CAN protocol (SET_DUTY / SET_CURRENT / SET_RPM)
|
||||
- Node IDs: VESC 68 (left), VESC 56 (right)
|
||||
|
||||
### FC → Jetson Telemetry (UART1 TX, 50Hz)
|
||||
### ESP32-S3 BALANCE → Orin Telemetry (UART0 TX, 50Hz)
|
||||
```
|
||||
T:12.3,P:45,L:100,R:-80,S:3\n
|
||||
// T=tilt°, P=PID output, L/R=motor commands, S=state (0-3)
|
||||
```
|
||||
|
||||
### FC → USB CDC (50Hz JSON)
|
||||
### ESP32-S3 → USB (50Hz JSON, debug/tuning)
|
||||
```json
|
||||
{"ax":123,"ay":-456,"az":16384,"gx":10,"gy":-5,"gz":3,"t":250,"p":0,"bt":0}
|
||||
// Raw IMU values (int16), t=temp×10, p=pressure, bt=baro temp
|
||||
{"ax":123,"ay":-456,"az":16384,"gx":10,"gy":-5,"gz":3,"t":250,"p":0}
|
||||
// Raw IMU values (int16), t=temp×10, p=PID output
|
||||
```
|
||||
|
||||
## LED Subsystem (ESP32-C3)
|
||||
## LED Subsystem (ESP32-S3 IO)
|
||||
|
||||
ESP32-C3 eavesdrops on FC→Jetson telemetry (listen-only tap on UART1 TX). No extra FC UART needed.
|
||||
ESP32-S3 IO eavesdrops on BALANCE→Orin telemetry (listen-only). No extra UART needed.
|
||||
|
||||
| State | Pattern | Color |
|
||||
|-------|---------|-------|
|
||||
@ -275,7 +237,7 @@ ESP32-C3 eavesdrops on FC→Jetson telemetry (listen-only tap on UART1 TX). No e
|
||||
1. **Read SALTYLAB.md fully** before making any design decisions
|
||||
2. **Never remove safety checks** from firmware — add more if needed
|
||||
3. **All measurements go in `cad/dimensions.scad`** — single source of truth
|
||||
4. **Test firmware on bench before any motor test** — ESC disconnected, verify outputs on serial
|
||||
4. **Test firmware on bench before any motor test** — VESCs disconnected, verify outputs on serial
|
||||
5. **One variable at a time** — don't change PID and speed limit in the same test
|
||||
6. **Document what you change** — update this file if you add pins, change protocols, or discover hardware quirks
|
||||
7. **Ask before wiring changes** — wrong connections can fry the FC ($50+ board)
|
||||
7. **Ask before wiring changes** — wrong connections can fry an ESP32 or VESC
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""stm32_protocol.py — Binary frame codec for Jetson↔STM32 communication.
|
||||
"""esp32_protocol.py — Binary frame codec for Jetson↔STM32 communication.
|
||||
|
||||
Issue #119: defines the binary serial protocol between the Jetson Nano and the
|
||||
STM32F722 flight controller over USB CDC @ 921600 baud.
|
||||
@ -55,7 +55,7 @@ from sensor_msgs.msg import Imu
|
||||
from std_msgs.msg import String
|
||||
from std_srvs.srv import SetBool, Trigger
|
||||
|
||||
from .stm32_protocol import (
|
||||
from .esp32_protocol import (
|
||||
FrameParser,
|
||||
ImuFrame, BatteryFrame, MotorRpmFrame, ArmStateFrame, ErrorFrame,
|
||||
encode_heartbeat, encode_speed_steer, encode_arm, encode_set_mode,
|
||||
|
||||
@ -29,7 +29,7 @@ import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from saltybot_bridge.stm32_protocol import (
|
||||
from saltybot_bridge.esp32_protocol import (
|
||||
STX, ETX, CmdType, TelType,
|
||||
encode_speed_steer, encode_heartbeat, encode_arm, encode_pid_update,
|
||||
_build_frame, _crc16_ccitt,
|
||||
@ -219,10 +219,10 @@ class TestMockSerialTX:
|
||||
class TestMockSerialRX:
|
||||
"""Test RX parsing path using MockSerial with pre-loaded telemetry data."""
|
||||
|
||||
from saltybot_bridge.stm32_protocol import FrameParser
|
||||
from saltybot_bridge.esp32_protocol import FrameParser
|
||||
|
||||
def test_rx_imu_frame(self):
|
||||
from saltybot_bridge.stm32_protocol import FrameParser, ImuFrame
|
||||
from saltybot_bridge.esp32_protocol import FrameParser, ImuFrame
|
||||
raw = _imu_frame_bytes(pitch=500, roll=-200, yaw=100, ax=0, ay=0, az=981)
|
||||
ms = MockSerial(rx_data=raw)
|
||||
parser = FrameParser()
|
||||
@ -241,7 +241,7 @@ class TestMockSerialRX:
|
||||
assert f.accel_z == pytest.approx(9.81)
|
||||
|
||||
def test_rx_battery_frame(self):
|
||||
from saltybot_bridge.stm32_protocol import FrameParser, BatteryFrame
|
||||
from saltybot_bridge.esp32_protocol import FrameParser, BatteryFrame
|
||||
raw = _battery_frame_bytes(v_mv=10500, i_ma=1200, soc=45)
|
||||
ms = MockSerial(rx_data=raw)
|
||||
parser = FrameParser()
|
||||
@ -257,7 +257,7 @@ class TestMockSerialRX:
|
||||
assert f.soc_pct == 45
|
||||
|
||||
def test_rx_multiple_frames_in_one_read(self):
|
||||
from saltybot_bridge.stm32_protocol import FrameParser
|
||||
from saltybot_bridge.esp32_protocol import FrameParser
|
||||
raw = (_imu_frame_bytes() + _arm_state_frame_bytes() + _battery_frame_bytes())
|
||||
ms = MockSerial(rx_data=raw)
|
||||
parser = FrameParser()
|
||||
@ -271,7 +271,7 @@ class TestMockSerialRX:
|
||||
assert parser.frames_error == 0
|
||||
|
||||
def test_rx_bad_crc_counted_as_error(self):
|
||||
from saltybot_bridge.stm32_protocol import FrameParser
|
||||
from saltybot_bridge.esp32_protocol import FrameParser
|
||||
raw = bytearray(_arm_state_frame_bytes(state=1))
|
||||
raw[-3] ^= 0xFF # corrupt CRC
|
||||
ms = MockSerial(rx_data=bytes(raw))
|
||||
@ -282,7 +282,7 @@ class TestMockSerialRX:
|
||||
assert parser.frames_error == 1
|
||||
|
||||
def test_rx_resync_after_corrupt_byte(self):
|
||||
from saltybot_bridge.stm32_protocol import FrameParser, ArmStateFrame
|
||||
from saltybot_bridge.esp32_protocol import FrameParser, ArmStateFrame
|
||||
garbage = b"\xDE\xAD\x00\x00"
|
||||
valid = _arm_state_frame_bytes(state=1)
|
||||
ms = MockSerial(rx_data=garbage + valid)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""test_stm32_protocol.py — Unit tests for binary STM32 frame codec.
|
||||
"""test_esp32_protocol.py — Unit tests for binary STM32 frame codec.
|
||||
|
||||
Tests:
|
||||
- CRC16-CCITT correctness
|
||||
@ -12,7 +12,7 @@ Tests:
|
||||
- Speed/steer clamping in encode_speed_steer
|
||||
- Round-trip encode → decode for all known telemetry types
|
||||
|
||||
Run with: pytest test/test_stm32_protocol.py -v
|
||||
Run with: pytest test/test_esp32_protocol.py -v
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -25,7 +25,7 @@ import os
|
||||
# ── Path setup (no ROS2 install needed) ──────────────────────────────────────
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from saltybot_bridge.stm32_protocol import (
|
||||
from saltybot_bridge.esp32_protocol import (
|
||||
STX, ETX,
|
||||
CmdType, TelType,
|
||||
ImuFrame, BatteryFrame, MotorRpmFrame, ArmStateFrame, ErrorFrame,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
mamba_protocol.py — CAN message encoding/decoding for the Mamba motor controller
|
||||
balance_protocol.py — CAN message encoding/decoding for the Mamba motor controller
|
||||
and VESC telemetry.
|
||||
|
||||
CAN message layout
|
||||
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
can_bridge_node.py — ROS2 node bridging the SaltyBot Orin to the Mamba motor
|
||||
can_bridge_node.py — ROS2 node bridging the SaltyBot Orin to the ESP32-S3 BALANCE
|
||||
controller and VESC motor controllers over CAN bus.
|
||||
|
||||
The node opens the SocketCAN interface (slcan0 by default), spawns a background
|
||||
@ -34,7 +34,7 @@ from rcl_interfaces.msg import SetParametersResult
|
||||
from sensor_msgs.msg import BatteryState, Imu
|
||||
from std_msgs.msg import Bool, Float32MultiArray, String
|
||||
|
||||
from saltybot_can_bridge.mamba_protocol import (
|
||||
from saltybot_can_bridge.balance_protocol import (
|
||||
MAMBA_CMD_ESTOP,
|
||||
MAMBA_CMD_MODE,
|
||||
MAMBA_CMD_VELOCITY,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unit tests for saltybot_can_bridge.mamba_protocol.
|
||||
Unit tests for saltybot_can_bridge.balance_protocol.
|
||||
|
||||
No ROS2 or CAN hardware required — tests exercise encode/decode round-trips
|
||||
and boundary conditions entirely in Python.
|
||||
@ -11,7 +11,7 @@ Run with: pytest test/test_can_bridge.py -v
|
||||
import struct
|
||||
import unittest
|
||||
|
||||
from saltybot_can_bridge.mamba_protocol import (
|
||||
from saltybot_can_bridge.balance_protocol import (
|
||||
MAMBA_CMD_ESTOP,
|
||||
MAMBA_CMD_MODE,
|
||||
MAMBA_CMD_VELOCITY,
|
||||
|
||||
@ -6,7 +6,7 @@ Orin↔Mamba↔VESC integration test suite.
|
||||
All IDs and payload formats are derived from:
|
||||
include/orin_can.h — Orin↔FC (Mamba) protocol
|
||||
include/vesc_can.h — VESC CAN protocol
|
||||
saltybot_can_bridge/mamba_protocol.py — existing bridge constants
|
||||
saltybot_can_bridge/balance_protocol.py — existing bridge constants
|
||||
|
||||
CAN IDs used in tests
|
||||
---------------------
|
||||
@ -22,7 +22,7 @@ FC (Mamba) → Orin telemetry (standard 11-bit, matching orin_can.h):
|
||||
FC_IMU 0x402 8 bytes
|
||||
FC_BARO 0x403 8 bytes
|
||||
|
||||
Mamba ↔ VESC internal commands (matching mamba_protocol.py):
|
||||
Mamba ↔ VESC internal commands (matching balance_protocol.py):
|
||||
MAMBA_CMD_VELOCITY 0x100 8 bytes left_mps (f32) | right_mps (f32) big-endian
|
||||
MAMBA_CMD_MODE 0x101 1 byte mode (0=idle,1=drive,2=estop)
|
||||
MAMBA_CMD_ESTOP 0x102 1 byte 0x01=stop
|
||||
@ -54,7 +54,7 @@ FC_IMU: int = 0x402
|
||||
FC_BARO: int = 0x403
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Mamba → VESC internal command IDs (from mamba_protocol.py)
|
||||
# Mamba → VESC internal command IDs (from balance_protocol.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MAMBA_CMD_VELOCITY: int = 0x100
|
||||
@ -136,14 +136,14 @@ def build_estop_cmd(action: int = 1) -> bytes:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Frame builders — Mamba velocity commands (mamba_protocol.py encoding)
|
||||
# Frame builders — Mamba velocity commands (balance_protocol.py encoding)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_velocity_cmd(left_mps: float, right_mps: float) -> bytes:
|
||||
"""
|
||||
Build a MAMBA_CMD_VELOCITY payload (8 bytes, 2 × float32 big-endian).
|
||||
|
||||
Matches encode_velocity_cmd() in mamba_protocol.py.
|
||||
Matches encode_velocity_cmd() in balance_protocol.py.
|
||||
"""
|
||||
return struct.pack(">ff", float(left_mps), float(right_mps))
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ _pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if _pkg_root not in sys.path:
|
||||
sys.path.insert(0, _pkg_root)
|
||||
|
||||
# Also add the saltybot_can_bridge package so we can import mamba_protocol.
|
||||
# Also add the saltybot_can_bridge package so we can import balance_protocol.
|
||||
_bridge_pkg = os.path.join(
|
||||
os.path.dirname(_pkg_root), "saltybot_can_bridge"
|
||||
)
|
||||
@ -60,7 +60,7 @@ def loopback_can_bus():
|
||||
@pytest.fixture(scope="function")
|
||||
def bridge_components():
|
||||
"""
|
||||
Return the mamba_protocol encode/decode callables and a fresh mock bus.
|
||||
Return the balance_protocol encode/decode callables and a fresh mock bus.
|
||||
|
||||
Yields a dict with keys:
|
||||
bus — MockCANBus instance
|
||||
@ -69,7 +69,7 @@ def bridge_components():
|
||||
encode_estop — encode_estop_cmd(stop) → bytes
|
||||
decode_vesc — decode_vesc_state(data) → VescStateTelemetry
|
||||
"""
|
||||
from saltybot_can_bridge.mamba_protocol import (
|
||||
from saltybot_can_bridge.balance_protocol import (
|
||||
encode_velocity_cmd,
|
||||
encode_mode_cmd,
|
||||
encode_estop_cmd,
|
||||
|
||||
@ -28,7 +28,7 @@ from saltybot_can_e2e_test.protocol_defs import (
|
||||
parse_velocity_cmd,
|
||||
parse_fc_vesc,
|
||||
)
|
||||
from saltybot_can_bridge.mamba_protocol import (
|
||||
from saltybot_can_bridge.balance_protocol import (
|
||||
encode_velocity_cmd,
|
||||
encode_mode_cmd,
|
||||
)
|
||||
|
||||
@ -32,7 +32,7 @@ from saltybot_can_e2e_test.protocol_defs import (
|
||||
parse_velocity_cmd,
|
||||
parse_fc_status,
|
||||
)
|
||||
from saltybot_can_bridge.mamba_protocol import (
|
||||
from saltybot_can_bridge.balance_protocol import (
|
||||
encode_velocity_cmd,
|
||||
encode_mode_cmd,
|
||||
encode_estop_cmd,
|
||||
|
||||
@ -30,7 +30,7 @@ from saltybot_can_e2e_test.protocol_defs import (
|
||||
parse_fc_vesc,
|
||||
parse_vesc_status,
|
||||
)
|
||||
from saltybot_can_bridge.mamba_protocol import (
|
||||
from saltybot_can_bridge.balance_protocol import (
|
||||
VESC_TELEM_STATE as BRIDGE_VESC_TELEM_STATE,
|
||||
decode_vesc_state,
|
||||
)
|
||||
|
||||
@ -33,7 +33,7 @@ from saltybot_can_e2e_test.protocol_defs import (
|
||||
build_velocity_cmd,
|
||||
parse_velocity_cmd,
|
||||
)
|
||||
from saltybot_can_bridge.mamba_protocol import (
|
||||
from saltybot_can_bridge.balance_protocol import (
|
||||
encode_velocity_cmd,
|
||||
encode_mode_cmd,
|
||||
encode_estop_cmd,
|
||||
|
||||
@ -27,7 +27,7 @@ from saltybot_can_e2e_test.protocol_defs import (
|
||||
build_velocity_cmd,
|
||||
parse_velocity_cmd,
|
||||
)
|
||||
from saltybot_can_bridge.mamba_protocol import (
|
||||
from saltybot_can_bridge.balance_protocol import (
|
||||
encode_velocity_cmd,
|
||||
encode_mode_cmd,
|
||||
encode_estop_cmd,
|
||||
@ -189,7 +189,7 @@ class TestModeCommandEncoding:
|
||||
"""build_mode_cmd in protocol_defs must produce identical bytes."""
|
||||
for mode in (MODE_IDLE, MODE_DRIVE, MODE_ESTOP):
|
||||
assert build_mode_cmd(mode) == encode_mode_cmd(mode), \
|
||||
f"protocol_defs.build_mode_cmd({mode}) != mamba_protocol.encode_mode_cmd({mode})"
|
||||
f"protocol_defs.build_mode_cmd({mode}) != balance_protocol.encode_mode_cmd({mode})"
|
||||
|
||||
|
||||
class TestInvalidMode:
|
||||
@ -218,8 +218,8 @@ class TestInvalidMode:
|
||||
accepted = sm.set_mode(-1)
|
||||
assert accepted is False
|
||||
|
||||
def test_mamba_protocol_invalid_mode_raises(self):
|
||||
"""mamba_protocol.encode_mode_cmd must raise on invalid mode."""
|
||||
def test_balance_protocol_invalid_mode_raises(self):
|
||||
"""balance_protocol.encode_mode_cmd must raise on invalid mode."""
|
||||
with pytest.raises(ValueError):
|
||||
encode_mode_cmd(99)
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
@ -13,7 +13,7 @@ Telemetry type (STM32 → Jetson):
|
||||
uint16 pan_speed_raw + uint16 tilt_speed_raw +
|
||||
uint8 torque_en + uint8 rx_err_pct (10 bytes)
|
||||
|
||||
Frame format (shared with stm32_protocol.py):
|
||||
Frame format (shared with esp32_protocol.py):
|
||||
[STX=0x02][CMD][LEN][PAYLOAD...][CRC16_hi][CRC16_lo][ETX=0x03]
|
||||
CRC16-CCITT: poly=0x1021, init=0xFFFF, covers CMD+LEN+PAYLOAD bytes.
|
||||
"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user