# SaltyLab β€” Self-Balancing Indoor Bot πŸ”¬ Two-wheeled, self-balancing robot for indoor AI/SLAM experiments. ## ⚠️ SAFETY β€” TOP PRIORITY **This robot can cause serious injury.** 8" hub motors with 36V power can crush toes, break fingers, and launch the frame if control is lost. Every design decision must prioritize safety. ### Mandatory Safety Systems 1. **Hardware kill switch** β€” physical big red button, wired inline with battery. Cuts ALL power instantly. Must be reachable without approaching the wheels. 2. **Software tilt cutoff** β€” if pitch exceeds Β±25Β° (not 30Β°), motors go to zero immediately. No retry, no recovery. Requires manual re-arm. 3. **Startup arming sequence** β€” motors NEVER spin on power-on. Requires deliberate arming: hold button for 3 seconds while robot is upright and stable. 4. **Watchdog timeout** β€” if FC firmware hangs or crashes, hardware watchdog resets to safe state (motors off) within 50ms. 5. **Current limiting** β€” hoverboard ESC max current set conservatively. Start low, increase gradually. 6. **Tether during development** β€” ceiling rope/strap during ALL balance testing. No free-standing tests until PID is proven stable for 5+ minutes tethered. 7. **Speed limiting** β€” firmware hard cap on max speed. Start at 10% throttle, increase in 10% increments only after stable testing. 8. **Remote kill** β€” Jetson can send emergency stop via UART. If Jetson disconnects (UART timeout >200ms), FC cuts motors automatically. 9. **Bumpers** β€” TPU bumpers on all sides, mandatory before any untethered operation. 10. **Test area** β€” clear 3m radius, no pets/kids/cables. Shoes mandatory. 11. **RC kill channel** β€” ELRS receiver connected to FC UART. Dedicated switch on radio = instant disarm. Works independently of Jetson. Always have radio in hand during testing. ### Safety Rules for Development - **Never reach near wheels while powered** β€” even "stopped" motors can spike - **Never test new firmware untethered** β€” tether FIRST, always - **Never increase speed and change PID in the same test** β€” one variable at a time - **Log everything** β€” FC sends telemetry (pitch, PID output, motor commands) to Jetson for post-crash analysis - **Two people for early tests** β€” one at the kill switch, one observing ## Parts | Part | Status | |------|--------| | 2x 8" pneumatic hub motors (36 PSI) | βœ… Have | | 1x hoverboard ESC (FOC firmware) | βœ… Have | | 1x Drone FC (STM32F745 + MPU-6000) | βœ… Have β€” balance brain | | 1x Jetson Nano + Noctua fan | βœ… Have | | 1x RealSense D435i | βœ… Have | | 1x RPLIDAR A1M8 | βœ… Have | | 1x battery pack (36V) | βœ… Have | | 1x DC-DC 5V converter | βœ… Have | | 1x DC-DC 12V converter | βœ… Have | | 1x ESP32-C3 (LED controller) | ⬜ Need (~$3) | | WS2812B LED strip (60/m) | ⬜ Need | | BNO055 9-DOF IMU | βœ… Have (spare/backup) | | MPU6050 | βœ… Have (spare/backup) | | 1x Big red kill switch (NC, inline with battery) | ⬜ Need | | 1x Arming button (momentary, with LED) | ⬜ Need | | 1x Ceiling tether strap + carabiner | ⬜ Need | | 1x BetaFPV ELRS 2.4GHz 1W TX module | βœ… Have β€” RC control + kill switch | | 1x ELRS receiver (matching) | βœ… Have β€” mounts on FC UART | ### Drone FC Details β€” GEPRC GEP-F7 AIO - **MCU:** STM32F722RET6 (216MHz Cortex-M7, 512KB flash, 256KB RAM) - **IMU:** TDK ICM-42688-P (6-axis, 32kHz gyro, ultra-low noise, SPI) ← the good one! - **Flash:** 8MB Winbond W25Q64 (blackbox, unused) - **OSD:** AT7456E (unused) - **4-in-1 ESC:** Built into AIO board (unused β€” we use hoverboard ESC) - **DFU mode:** Hold yellow BOOT button while plugging USB - **Firmware:** Custom balance firmware (PlatformIO + STM32 HAL) - **UART pads (confirmed from silkscreen):** - T1/R1 (bottom) β†’ USART1 (PA9/PA10) β†’ Jetson - T2/R2 (right top) β†’ USART2 (PA2/PA3) β†’ Hoverboard ESC - T3/R3 (bottom) β†’ USART3 (PB10/PB11) β†’ ELRS receiver - T4/R4 (bottom) β†’ UART4 β†’ spare - T5/R5 (right bottom) β†’ UART5 β†’ spare ## Architecture ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ RPLIDAR A1 β”‚ ← 360Β° scan, top-mounted β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β” β”‚ RealSense β”‚ ← Forward-facing depth+RGB β”‚ D435i β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Jetson Nano β”‚ ← AI brain: navigation, person tracking β”‚ β”‚ Sends velocity commands via UART β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Drone FC β”‚ ← Balance brain: IMU + PID @ 8kHz β”‚ F745+MPU6000 β”‚ Custom firmware, UART out to ESC β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Battery 36V β”‚ β”‚ + DC-DCs β”‚ β”œβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€ β”Œβ”€β”€β”€β”€β”€β”€ ESC (FOC) β”œβ”€β”€β”€β”€β”€β” β”‚ β”‚ Hoverboard β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”΄β”€β”€β” β”Œβ”€β”€β”΄β”€β”€β” β”‚ 8" β”‚ β”‚ 8" β”‚ β”‚ LEFTβ”‚ β”‚RIGHTβ”‚ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ ``` ## Self-Balancing Control β€” Custom Firmware on Drone FC ### Why a Drone FC? The F745 board is just a premium STM32 dev board with a high-quality IMU (MPU-6000) already soldered on, proper voltage regulation, and multiple UARTs broken out. We write a lean custom balance firmware (~50 lines of C). ### Architecture ``` Jetson (speed+steer via UART1) β”‚ β–Ό Drone FC (F745 + MPU-6000) β”‚ - Reads IMU @ 8kHz (SPI) β”‚ - Runs PID balance loop β”‚ - Mixes balance correction + Jetson commands β”‚ - Outputs speed+steer via UART2 β–Ό Hoverboard ESC (FOC firmware) β”‚ - Receives UART commands β”‚ - Drives hub motors β–Ό Left + Right wheels ``` - **No motor outputs used** β€” FC talks UART directly to hoverboard ESC - **Custom firmware only** β€” no third-party flight software - **Dead motor output irrelevant** β€” not using any PWM channels ### Wiring ``` Jetson UART1 Drone FC (UART1) ──────────── ──────────────── TX (Pin 8) ──→ RX RX (Pin 10) ──→ TX GND ──→ GND Drone FC (UART2) Hoverboard ESC ──────────────── ────────────── TX ──→ RX (serial input) GND ──→ GND 5V (BEC) ←── ESC 5V out (powers FC) ELRS Receiver Drone FC (UART3) ───────────── ──────────────── TX ──→ RX RX ←── TX (for telemetry/binding) GND ──→ GND 5V ←── 5V ``` ### Custom Firmware (STM32 C) ```c // Core balance loop β€” runs in timer interrupt @ 1-8kHz void balance_loop(void) { // 1. Read pitch angle from MPU-6000 (complementary filter) float pitch = get_pitch_angle(); // SPI read + filter // 2. Get velocity command from Jetson (updated async via UART1 RX) float target_speed = jetson_cmd.speed; // -1000 to 1000 float target_steer = jetson_cmd.steer; // -1000 to 1000 // 3. PID on pitch error // Target angle shifts with speed command (lean forward = go forward) float target_angle = target_speed * SPEED_TO_ANGLE_FACTOR; float error = target_angle - pitch; integral += error * dt; integral = clamp(integral, -MAX_I, MAX_I); // anti-windup float derivative = (error - prev_error) / dt; prev_error = error; float output = Kp * error + Ki * integral + Kd * derivative; // 4. Mix balance + steering β†’ hoverboard ESC UART command int16_t left = clamp(output + target_steer, -1000, 1000); int16_t right = clamp(output - target_steer, -1000, 1000); // 5. Send to hoverboard ESC via UART2 send_hoverboard_cmd(left, right); // 6. Safety: kill motors if tipped beyond recovery if (fabs(pitch) > MAX_TILT_DEG) { send_hoverboard_cmd(0, 0); disarm(); } // 7. Safety: RC kill switch (ELRS channel, checked every loop) if (rc_channels.arm_switch == DISARMED) { send_hoverboard_cmd(0, 0); disarm(); } // 8. Safety: kill if Jetson UART heartbeat lost if (millis() - jetson_last_rx > JETSON_TIMEOUT_MS) { send_hoverboard_cmd(0, 0); disarm(); } // 8. Safety: clamp output to max allowed speed left = clamp(left, -max_speed_limit, max_speed_limit); right = clamp(right, -max_speed_limit, max_speed_limit); } ``` ### Hoverboard ESC UART Protocol ```c typedef struct { uint16_t start; // 0xABCD int16_t speed; // -1000 to 1000 (left) int16_t steer; // -1000 to 1000 (right) uint16_t checksum; // XOR of all bytes } HoverboardCmd; // 115200 baud, send at loop rate ``` ### Jetson β†’ FC Protocol (simple custom) ```c typedef struct { uint8_t header; // 0xAA int16_t speed; // -1000 to 1000 int16_t steer; // -1000 to 1000 uint8_t mode; // 0=idle, 1=balance, 2=follow, 3=RC uint8_t checksum; } JetsonCmd; // 115200 baud, ~50Hz from Jetson is plenty ``` ### PID Tuning | Param | Starting Value | Notes | |-------|---------------|-------| | Kp | 30-50 | Main balance response | | Ki | 0.5-2 | Drift correction | | Kd | 0.5-2 | Damping oscillation | | Loop rate | 1-8 kHz | Start at 1kHz, increase if needed | | Max tilt | Β±25Β° | Beyond this = cut motors, require re-arm | | JETSON_TIMEOUT_MS | 200 | Kill motors if Jetson stops talking | | max_speed_limit | 100 | Start at 10% (100/1000), increase gradually | | SPEED_TO_ANGLE_FACTOR | 0.01-0.05 | How much lean per speed unit | ## LED Subsystem (ESP32-C3) ### Architecture The ESP32-C3 eavesdrops on the FCβ†’Jetson telemetry UART line (listen-only, one wire). No extra UART needed on the FC β€” zero firmware change. ``` FC UART1 TX ──┬──→ Jetson RX └──→ ESP32-C3 RX (listen-only, same wire) β”‚ └──→ WS2812B strip (via RMT peripheral) ``` ### Telemetry Format (already sent by FC at 50Hz) ``` T:12.3,P:45,L:100,R:-80,S:3\n ^-- State byte: 0=disarmed, 1=arming, 2=armed, 3=fault ``` ESP32-C3 parses the `S:` field and `L:/R:` for turn detection. ### LED Patterns | State | Pattern | Color | |-------|---------|-------| | Disarmed | Slow breathe | White | | Arming | Fast blink | Yellow | | Armed idle | Solid | Green | | Turning left | Sweep left | Orange | | Turning right | Sweep right | Orange | | Braking | Flash rear | Red | | Fault | Triple flash | Red | | RC signal lost | Alternating flash | Red/Blue | ### Turn/Brake Detection (on ESP32-C3) ``` if (L - R > threshold) β†’ turning right if (R - L > threshold) β†’ turning left if (L < -threshold && R < -threshold) β†’ braking ``` ### Wiring ``` FC UART1 TX pin ──→ ESP32-C3 GPIO RX (e.g. GPIO20) ESP32-C3 GPIO8 ──→ WS2812B data in ESC 5V BEC ──→ ESP32-C3 5V + WS2812B 5V GND ──→ Common ground ``` ### Dev Tools - **Flashing:** STM32CubeProgrammer via USB (DFU mode) or SWD - **IDE:** PlatformIO + STM32 HAL, or STM32CubeIDE - **Debug:** SWD via ST-Link (or use FC's USB as virtual COM for printf debug) ## Physical Design ### Frame: Vertical Tower ``` SIDE VIEW FRONT VIEW β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ RPLIDAR β”‚ ~500mm β”‚ RPLIDAR β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ RealSense β”‚ ~400mm β”‚ [RealSense] β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Jetson β”‚ ~300mm β”‚ [Jetson] β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Drone FC β”‚ ~200mm β”‚ [Drone FC] β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Battery β”‚ ~100mm β”‚ [Battery] β”‚ β”‚ + ESC β”‚ LOW! β”‚ [ESC+DCDC] β”‚ β”œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”€β”˜ β””β”€β”€β”€β”€β”€β”˜β”€ β”€β”˜ 8" 8" β””β”€β”€β”˜β”€ ═══════════════ ═══ ═══ GROUND L R ``` ### Key Dimensions - **Height:** ~500-550mm total (sensor tower top) - **Width:** ~350mm (axle to axle, constrained by motors) - **Depth:** ~150-200mm (thin profile for doorways) - **Weight target:** <10kg including battery - **Center of gravity:** AS LOW AS POSSIBLE β€” battery + ESC at bottom ### Critical: Center of Mass - Battery is the heaviest component β†’ mount at axle height or below - Jetson + sensors are light β†’ can go higher - Lower CoG = easier to balance, less aggressive PID needed - If CoG is too high β†’ oscillations, falls easily ### Frame Material - **Main spine:** Aluminum extrusion 2020, vertical - **Motor mount plate:** 3D printed PETG, 6mm thick, reinforced - **Component shelves:** 3D printed PETG, bolt to spine - **Fender/bumper:** 3D printed TPU (flexible, absorbs falls) ### 3D Printed Parts | Part | Size (mm) | Material | Qty | |------|-----------|----------|-----| | Motor mount plate | 350Γ—150Γ—6 | PETG 80% | 1 | | Battery shelf | 200Γ—100Γ—40 | PETG 60% | 1 | | ESC mount | 150Γ—100Γ—15 | PETG 40% | 1 | | Jetson shelf | 120Γ—100Γ—15 | PETG 40% | 1 | | Sensor tower top | 120Γ—120Γ—10 | ASA 80% | 1 | | LIDAR standoff | Ø80Γ—80 | ASA 40% | 1 | | RealSense bracket | 100Γ—50Γ—40 | PETG 60% | 1 | | FC mount (vibration isolated) | 30Γ—30Γ—15 | TPU+PETG | 1 | | Bumper front | 350Γ—50Γ—30 | TPU 30% | 1 | | Bumper rear | 350Γ—50Γ—30 | TPU 30% | 1 | | Handle (for carrying) | 150Γ—30Γ—30 | PETG 80% | 1 | | Kill switch mount | 60Γ—60Γ—40 | PETG 80% | 1 | | Tether anchor point | 50Γ—50Γ—20 | PETG 100% | 1 | | LED diffuser ring | Ø120Γ—15 | Clear PETG 30% | 1 | | ESP32-C3 mount | 30Γ—25Γ—10 | PETG 40% | 1 | ## Software Stack ### Jetson Nano - **OS:** JetPack 4.6.1 (Ubuntu 18.04) - **ROS2 Humble** (or Foxy) for: - `nav2` β€” navigation stack - `slam_toolbox` β€” 2D SLAM from LIDAR - `realsense-ros` β€” depth camera - `rplidar_ros` β€” LIDAR driver - **Person following:** SSD-MobileNet-v2 via TensorRT (~20 FPS) - **Balance commands:** ROS topic β†’ UART bridge to drone FC ### Modes 1. **Idle** β€” self-balancing in place, waiting for command 2. **RC** β€” manual control via ELRS radio (primary testing mode) 3. **Follow** β€” tracks person with RealSense, follows at set distance 4. **Explore** β€” autonomous SLAM mapping, builds house map 5. **Patrol** β€” follows waypoints on saved map 6. **Dock** β€” returns to charging station (future) **Mode priority:** RC override always wins. If radio sends stick input, it overrides Jetson commands. Kill switch overrides everything. ## Build Order ### Phase 1: Balance (Week 1) **Safety first β€” no motor spins without kill switch + tether in place.** - [ ] Install hardware kill switch inline with 36V battery (NC β€” press to kill) - [ ] Set up ceiling tether point above test area (rated for >15kg) - [ ] Clear test area: 3m radius, no loose items, shoes on - [ ] Set up PlatformIO project for STM32F745 (STM32 HAL) - [ ] Write MPU-6000 SPI driver (read gyro+accel, complementary filter) - [ ] Write PID balance loop with ALL safety checks: - Β±25Β° tilt cutoff β†’ disarm, require manual re-arm - Watchdog timer (50ms hardware WDT) - Speed limit at 10% (max_speed_limit = 100) - Arming sequence (3s hold while upright) - [ ] Write hoverboard ESC UART output (speed+steer protocol) - [ ] Flash firmware via USB DFU (boot0 jumper on FC) - [ ] Write ELRS CRSF receiver driver (UART3, parse channels + arm switch) - [ ] Bind ELRS TX ↔ RX, verify channel data on serial monitor - [ ] Map radio: CH1=steer, CH2=speed, CH5=arm/disarm switch - [ ] **Bench test first** β€” FC powered but ESC disconnected, verify IMU reads + PID output + RC channels on serial monitor - [ ] Wire FC UART2 β†’ hoverboard ESC UART - [ ] Build minimal frame: motor plate + battery + ESC + FC - [ ] Power FC from ESC 5V BEC - [ ] **First balance test β€” TETHERED, kill switch in hand, 10% speed limit** - [ ] Tune PID at 10% speed until stable tethered for 5+ minutes - [ ] Gradually increase speed limit (10% increments, 5 min stable each) ### Phase 2: Brain (Week 2) - [ ] Mount Jetson + power (DC-DC 5V) - [ ] Set up JetPack + ROS2 - [ ] Add Jetson UART RX to FC firmware (receive speed+steer commands) - [ ] Wire Jetson UART1 β†’ FC UART1 - [ ] Python serial bridge: send speed+steer, read telemetry - [ ] Test: keyboard teleoperation while balancing ### Phase 3: Senses (Week 3) - [ ] Mount RealSense + RPLIDAR - [ ] SLAM mapping of a room - [ ] Person detection + tracking (SSD-MobileNet-v2 via TensorRT) - [ ] Follow mode: maintain 1.5m distance from person ### Phase 4: Polish (Week 4) - [ ] Print proper enclosures, bumpers, diffuser ring - [ ] Wire ESP32-C3 to FC telemetry TX line (listen-only tap) - [ ] Flash ESP32-C3: parse telemetry, drive WS2812B via RMT - [ ] Mount LED strip around frame with diffuser - [ ] Test all LED patterns: disarmed/arming/armed/turning/fault - [ ] Speaker for audio feedback - [ ] WiFi status dashboard (ESP32-C3 can serve this too) - [ ] Emergency stop button