From b25f41ef1c6338b28c7c8f78edd306e0ff114cac Mon Sep 17 00:00:00 2001 From: sl-webui Date: Sat, 4 Apr 2026 08:38:59 -0400 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20rename=20stm32=5Fprotocol.py?= =?UTF-8?q?=E2=86=92esp32=5Fprotocol.py,=20mamba=5Fprotocol.py=E2=86=92bal?= =?UTF-8?q?ance=5Fprotocol.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove all legacy STM32/Mamba names from source files. Update all imports, comments, and docstring references across the codebase. - stm32_protocol.py → esp32_protocol.py (inter-board UART codec) - mamba_protocol.py → balance_protocol.py (Orin↔ESP32 BALANCE CAN codec) - Updated: stm32_cmd_node.py, can_bridge_node.py, all test files, conftest.py, protocol_defs.py, jlink_gimbal.py Co-Authored-By: Claude Sonnet 4.6 --- .../{stm32_protocol.py => esp32_protocol.py} | 2 +- .../saltybot_bridge/stm32_cmd_node.py | 2 +- .../saltybot_bridge/test/test_stm32_cmd_node.py | 14 +++++++------- .../saltybot_bridge/test/test_stm32_protocol.py | 6 +++--- .../{mamba_protocol.py => balance_protocol.py} | 2 +- .../saltybot_can_bridge/can_bridge_node.py | 4 ++-- .../saltybot_can_bridge/test/test_can_bridge.py | 4 ++-- .../saltybot_can_e2e_test/protocol_defs.py | 10 +++++----- .../src/saltybot_can_e2e_test/test/conftest.py | 6 +++--- .../test/test_drive_command.py | 2 +- .../src/saltybot_can_e2e_test/test/test_estop.py | 2 +- .../test/test_fc_vesc_broadcast.py | 2 +- .../test/test_heartbeat_timeout.py | 2 +- .../test/test_mode_switching.py | 8 ++++---- .../saltybot_gimbal/jlink_gimbal.py | 2 +- 15 files changed, 34 insertions(+), 34 deletions(-) rename jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/{stm32_protocol.py => esp32_protocol.py} (99%) rename jetson/ros2_ws/src/saltybot_can_bridge/saltybot_can_bridge/{mamba_protocol.py => balance_protocol.py} (98%) diff --git a/jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_protocol.py b/jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_protocol.py similarity index 99% rename from jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_protocol.py rename to jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_protocol.py index ed98326..fc8496c 100644 --- a/jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_protocol.py +++ b/jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/esp32_protocol.py @@ -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. diff --git a/jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py b/jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py index 4e3d63c..930dd05 100644 --- a/jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py +++ b/jetson/ros2_ws/src/saltybot_bridge/saltybot_bridge/stm32_cmd_node.py @@ -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, diff --git a/jetson/ros2_ws/src/saltybot_bridge/test/test_stm32_cmd_node.py b/jetson/ros2_ws/src/saltybot_bridge/test/test_stm32_cmd_node.py index 00d98b6..ff893f7 100644 --- a/jetson/ros2_ws/src/saltybot_bridge/test/test_stm32_cmd_node.py +++ b/jetson/ros2_ws/src/saltybot_bridge/test/test_stm32_cmd_node.py @@ -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) diff --git a/jetson/ros2_ws/src/saltybot_bridge/test/test_stm32_protocol.py b/jetson/ros2_ws/src/saltybot_bridge/test/test_stm32_protocol.py index 4e33f11..42fc30e 100644 --- a/jetson/ros2_ws/src/saltybot_bridge/test/test_stm32_protocol.py +++ b/jetson/ros2_ws/src/saltybot_bridge/test/test_stm32_protocol.py @@ -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, diff --git a/jetson/ros2_ws/src/saltybot_can_bridge/saltybot_can_bridge/mamba_protocol.py b/jetson/ros2_ws/src/saltybot_can_bridge/saltybot_can_bridge/balance_protocol.py similarity index 98% rename from jetson/ros2_ws/src/saltybot_can_bridge/saltybot_can_bridge/mamba_protocol.py rename to jetson/ros2_ws/src/saltybot_can_bridge/saltybot_can_bridge/balance_protocol.py index 804ae29..61520c6 100644 --- a/jetson/ros2_ws/src/saltybot_can_bridge/saltybot_can_bridge/mamba_protocol.py +++ b/jetson/ros2_ws/src/saltybot_can_bridge/saltybot_can_bridge/balance_protocol.py @@ -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 diff --git a/jetson/ros2_ws/src/saltybot_can_bridge/saltybot_can_bridge/can_bridge_node.py b/jetson/ros2_ws/src/saltybot_can_bridge/saltybot_can_bridge/can_bridge_node.py index fe62740..151e112 100644 --- a/jetson/ros2_ws/src/saltybot_can_bridge/saltybot_can_bridge/can_bridge_node.py +++ b/jetson/ros2_ws/src/saltybot_can_bridge/saltybot_can_bridge/can_bridge_node.py @@ -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, diff --git a/jetson/ros2_ws/src/saltybot_can_bridge/test/test_can_bridge.py b/jetson/ros2_ws/src/saltybot_can_bridge/test/test_can_bridge.py index 2b8f9bf..6bddca2 100644 --- a/jetson/ros2_ws/src/saltybot_can_bridge/test/test_can_bridge.py +++ b/jetson/ros2_ws/src/saltybot_can_bridge/test/test_can_bridge.py @@ -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, diff --git a/jetson/ros2_ws/src/saltybot_can_e2e_test/saltybot_can_e2e_test/protocol_defs.py b/jetson/ros2_ws/src/saltybot_can_e2e_test/saltybot_can_e2e_test/protocol_defs.py index 41bdd6c..c8d37ed 100644 --- a/jetson/ros2_ws/src/saltybot_can_e2e_test/saltybot_can_e2e_test/protocol_defs.py +++ b/jetson/ros2_ws/src/saltybot_can_e2e_test/saltybot_can_e2e_test/protocol_defs.py @@ -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)) diff --git a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/conftest.py b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/conftest.py index db8c471..2617741 100644 --- a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/conftest.py +++ b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/conftest.py @@ -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, diff --git a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_drive_command.py b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_drive_command.py index dfbee87..b647b26 100644 --- a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_drive_command.py +++ b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_drive_command.py @@ -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, ) diff --git a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_estop.py b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_estop.py index b77581e..746840f 100644 --- a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_estop.py +++ b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_estop.py @@ -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, diff --git a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_fc_vesc_broadcast.py b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_fc_vesc_broadcast.py index a7e9f9a..cde51fe 100644 --- a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_fc_vesc_broadcast.py +++ b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_fc_vesc_broadcast.py @@ -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, ) diff --git a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_heartbeat_timeout.py b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_heartbeat_timeout.py index adf7db2..7b7f3a6 100644 --- a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_heartbeat_timeout.py +++ b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_heartbeat_timeout.py @@ -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, diff --git a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_mode_switching.py b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_mode_switching.py index d87ab29..9e8da58 100644 --- a/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_mode_switching.py +++ b/jetson/ros2_ws/src/saltybot_can_e2e_test/test/test_mode_switching.py @@ -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): diff --git a/jetson/ros2_ws/src/saltybot_gimbal/saltybot_gimbal/jlink_gimbal.py b/jetson/ros2_ws/src/saltybot_gimbal/saltybot_gimbal/jlink_gimbal.py index 9915624..c3d3bac 100644 --- a/jetson/ros2_ws/src/saltybot_gimbal/saltybot_gimbal/jlink_gimbal.py +++ b/jetson/ros2_ws/src/saltybot_gimbal/saltybot_gimbal/jlink_gimbal.py @@ -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. """ -- 2.47.2 From c52dc786a20f62e154b9bbf06f4c96b1cfd22695 Mon Sep 17 00:00:00 2001 From: sl-android Date: Sat, 4 Apr 2026 08:47:05 -0400 Subject: [PATCH 2/2] chore: remove all Mamba F722S / STM32 / BlackPill refs from docs/AGENTS.md Replace old hardware (Mamba F722S, STM32F722, Jetson Nano, hoverboard ESC) with new architecture: Orin Nano Super, ESP32-S3 BALANCE, ESP32-S3 IO, VESC IDs 68/56. Update architecture diagram, hardware tables, UART assignments, firmware build instructions, protocol docs, and 3D parts list. Co-Authored-By: Claude Sonnet 4.6 --- docs/AGENTS.md | 200 ++++++++++++++++++++----------------------------- 1 file changed, 81 insertions(+), 119 deletions(-) diff --git a/docs/AGENTS.md b/docs/AGENTS.md index f3ac992..5043599 100644 --- a/docs/AGENTS.md +++ b/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 -- 2.47.2