salty 02217443ea chore: merge CAD files and design docs from seb/saltylab seed repo
Consolidating seb/saltylab into saltylab-firmware before deleting the seed repo.
- 16 OpenSCAD CAD models → cad/
- Design docs (SALTYLAB.md, PLATFORM.md, AGENTS.md, board-viz.html) → docs/
2026-03-07 10:04:24 -05:00

18 KiB
Raw Permalink Blame History

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)

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

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)

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