FIELD MANUAL · § 02.1 PHASE 1 · WALK · REV 0.2FM 18-DOF / SPEC-P1-DEEP
★ PROJECT HEXAPOD ★ INTERNAL

Phase 1 — Walk. Deep dive.

GOAL: TRIPOD GAIT FWD/BACK/ROTATE ON USB SERIAL · ~2 WEEKS · NOOB → GURU

At the end of Phase 1, you will have a hexapod that stands on its own legs, accepts commands over USB serial from your laptop, and walks forward / backward / turns cleanly on the tripod gait. No Bluetooth, no camera, no autonomy yet — just mechanics + power + locomotion. That's half the project done.

Everything here is bench work — no cables to a browser, no game controller, no network. You plug a USB cable into the ESP32, open the serial monitor in VS Code, and type WALK 1. The robot walks.

Tasks T1 through T7 land this. Rough week 1 = T1–T4 (firmware plumbing + calibration). Week 2 = T5–T7 (gait, battery safety, gait variants). Work on firmware (T1, T3, T6) while prints finish and parts arrive — that's free time.

30 A inline fuse on LiPo+ lead
Never skip. A direct short on an unfused 2S 3300 mAh pack can ignite the pack in seconds.
Store and charge inside the fireproof bag
LiPos fail rarely but catastrophically. The bag contains the fire long enough to get the pack outside.
Balance charge only (IMAX B6)
Use the balance plug every charge. Cells that drift more than 0.05 V apart over a few cycles are early failure signals.
Low-voltage alarm connected to balance plug
Redundant to the ESP32 monitor. Below 3.0 V/cell permanently damages the pack.
Set buck output to 6.0 V BEFORE connecting servos
Verify with a multimeter. Exceeding 6 V cooks MG996Rs; stall torque climbs with heat and kills gears.
Tie ESP32 GND to servo-rail GND
Separate grounds cause random PWM glitches. This has confused many people. Tie them at the buck GND.
Never power servos from the ESP32 3.3 V or 5 V pins
The onboard LDO cannot handle the current. Let out the magic smoke exactly once and learn forever.
First power-on: no servos plugged in
Boot ESP32 + PCA9685s only. Verify 0x40 and 0x41 over serial. Then plug in one servo at a time.
Non-negotiable rules
LiPo ONLY charges inside the fireproof bag, on a non-flammable surface, with the IMAX B6 in balance mode at 2S / 1C (~3.3 A max for the OnBO 3300). Never leave it unattended. Never store above 4.20 V/cell for more than a week — storage-charge to 3.80 V/cell if idle > 3 days.

Spend an hour on these before cutting any wire. You'll understand 90% of the terminology in T1–T7 just from the top three.

◆ TUTORIAL
Random Nerd Tutorials — VS Code + PlatformIO for ESP32
https://randomnerdtutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/
The canonical walkthrough to get PlatformIO installed, create a first project, and upload Blink. If you've never used PlatformIO, read this end-to-end before T1.
◆ TUTORIAL
Random Nerd Tutorials — ESP32 DevKit V1 pinout reference
https://randomnerdtutorials.com/esp32-pinout-reference-gpios/
Which pins are safe to use, which are boot-strap pins, which can do ADC/I²C. Keep this open in a tab.
◆ TUTORIAL
Adafruit Learn — 16-Channel PWM / Servo Driver
https://learn.adafruit.com/16-channel-pwm-servo-driver
The authoritative PCA9685 hookup + Arduino library guide. Wiring diagrams, chaining jumpers, µs-per-tick arithmetic.
◆ TUTORIAL
DroneBot Workshop — Using the PCA9685 PWM Driver
https://dronebotworkshop.com/pca9685/
Video + written tutorial. Bill explains PWM frequency choice and shows the Adafruit library in action.
▶ CHANNEL
YouTube — Andreas Spiess (ESP32 deep-dives)
https://www.youtube.com/@AndreasSpiess
The Swiss guy with a Swiss accent. Start with any ESP32 video — power supply quirks, brown-out causes, Wi-Fi + BT coexistence.
▶ CHANNEL
YouTube — Paul McWhorter Arduino Lesson Series
https://www.youtube.com/@paulmcwhorter
Slow, patient, beginner-paced. If servos/PWM still feel magical, watch his servo lessons.
▶ CHANNEL
YouTube — James Bruton (robotics builds)
https://www.youtube.com/@jamesbruton
Prolific UK roboticist. Several hexapod + gait videos — useful for intuition on leg geometry and tripod timing.
◆ ARTICLE
SparkFun — Battery Technologies (LiPo safety)
https://learn.sparkfun.com/tutorials/battery-technologies
Read the LiPo section before you unwrap your battery. Non-negotiable.

Every term you'll hear in Phase 1, in plain English. Skim once, refer back any time your eyes glaze over at PCA9685 / DMP / NVS.

PWM
Pulse-Width Modulation. A digital signal where the HIGH time within each cycle encodes a value. Hobby servos sample the high time of a 50 Hz pulse: 1.0 ms ≈ full-left, 1.5 ms ≈ centre, 2.0 ms ≈ full-right.
I²C
Inter-Integrated Circuit. A two-wire bus (SDA = data, SCL = clock) where each peripheral has a 7-bit address. The PCA9685 is 0x40 by default; bridge the A0 solder jumper to make the second one 0x41.
PCA9685
NXP 16-channel, 12-bit PWM driver. Takes I²C commands, outputs clean PWM suitable for servos or LEDs. At 50 Hz, one tick ≈ 4.88 µs; 4096 ticks per frame.
MG996R
Tower Pro's analog metal-gear servo. 9.4 kg·cm stall torque @ 6 V, 4.8–6.6 V operating range, ~500 mA idle / up to ~1.2 A under stall per servo. Analog (50 Hz PWM).
ESP32
Dual-core 240 MHz microcontroller with Wi-Fi + Bluetooth. The classic ESP32 DevKit V1 is what we're using — NOT the S3 variant, because Bluepad32's BT Classic stack needs the original.
PlatformIO
A VS Code extension that manages embedded toolchains (compilers, uploaders, libraries) declaratively via a platformio.ini file. Replaces the Arduino IDE for anything non-trivial.
NVS
Non-Volatile Storage — ESP32's key/value store on flash. Survives reboots and re-flashes. We use it to save calibration trim + direction values so you don't re-tune after every boot.
Trim / direction
Trim is a per-servo angular offset that corrects for mechanical install error (e.g. horn splined ~3° off). Direction is a ±1 flag that flips sign if the servo was mounted reversed.
FK / IK
Forward Kinematics: joint angles → foot XYZ. Inverse Kinematics: desired foot XYZ → joint angles. The 3-DOF hexapod leg has a closed-form trigonometric IK solution.
Coxa / femur / tibia
Insect-anatomy names for the three leg segments. Coxa rotates around vertical (hip-yaw). Femur is the upper segment, rotating up/down from the coxa. Tibia is the lower segment that touches the ground.
Tripod gait
The fastest and most statically-stable hexapod gait. Legs split into two sets of three (0/2/4 vs 1/3/5). One tripod is always planted while the other swings — the robot is always balanced on a triangle.
Swing / stance
Each leg's cycle has a swing phase (foot in the air, moving forward) and a stance phase (foot on the ground, pushing the body forward).
Cycloidal trajectory
A smooth swing path that looks like the curve a point on a rolling wheel traces. Zero foot velocity at lift-off and touch-down reduces slipping.
Stall torque / current
The torque at which the motor can no longer rotate — and the peak current drawn at that instant. For MG996R @ 6 V, stall is ~11 kg·cm and ~1.2 A. Multiply by 18 for the worst-case battery spec.
2S LiPo / C-rating
Lithium-Polymer battery. '2S' = 2 cells in series (nominal 7.4 V, full 8.4 V, empty-and-damaging-below 6.0 V). 'C' = continuous-discharge multiplier. 25C × 3.3 Ah = 82.5 A peak, plenty for 18 servos.
XT60
The yellow power plug ubiquitous on RC LiPos. Rated ~60 A continuous. One male + one female pigtail = battery-side and robot-side leads.
Hysteresis
A dead-band on a threshold to stop chatter. Battery cutoff: halt at 6.6 V, only resume above 7.0 V — because voltage sags under load and rebounds when idle; without hysteresis you'd bounce.
Bench PSU
Bench-top power supply with adjustable voltage + current limit. Use this for every first-power test BEFORE wiring the LiPo — set 6.0 V / 1 A limit and you can't cook anything.

Tools + supplies you need on the bench before starting. Most are separate from the BOM (we assume you own a soldering iron and multimeter; the shopping pages flag the rest).

Multimeter with continuity beep
Verify GND is common, check divider ratios, sanity-check LiPo voltage.
HAVE
Bench PSU (5–12 V, ≥3 A, current-limited)
First-power safety. A LAB bench PSU is ideal but any buck-based variable supply works.
HAVE
USB-C cable, data-capable (not charge-only)
ESP32 DevKit V1 needs a data cable for programming. Charge-only cables silently fail.
HAVE
Soldering iron with fine + chisel tips
Chisel tip installs heat-set inserts. Fine tip for PCB headers.
HAVE
Solder, flux, desoldering wick
You WILL make a bad joint. Wick saves parts.
HAVE
Helping-hands / PCB vice
Three-hand work on servo leads and PCA9685 headers.
OPTIONAL
Bench stand / vice / clamp for the hexapod
NEVER calibrate with feet on the floor. Tibia rotation under body weight strips gears.
SHOULD BUY
Calipers (digital, 150 mm)
Verify leg link lengths match the IK constants.
OPTIONAL
Cable ties + mini zip-ties
Loom the 18 servo leads before you lose your mind.
SHOULD BUY
Heat-shrink assortment
Every solder joint on a power lead gets shrink.
SHOULD BUY

17 line items · subtotal R4 874.42. Tick off as they arrive on your bench. Full BOM in Shop.

MG996R metal-gear servo
qty 18 · PiShop
R1 942.20
PCA9685 16-ch I²C PWM module
qty 2 · Micro Robotics
R158.70
DC-DC Buck converter, 300 W 20 A
qty 1 · Micro Robotics
R147.20
OnBO 3300 mAh 2S 25C LiPo (XT60)
qty 1 · Rclipo
R572
IMAX B6 balance charger (clone)
qty 1 · Bobshop
R250
Fireproof LiPo safe bag
qty 1 · Africa Drone Kings
R200
XT60 male+female pigtails (pair)
qty 2 · Communica
R60
2S low-voltage alarm buzzer
qty 1 · Communica
R50
Inline 30 A blade fuse + holder
qty 1 · Communica
R53
M3 screw/nut assortment kit (6–25 mm)
qty 1 · Communica
R150
M2.5 servo-horn screws (100 pack)
qty 1 · Communica
R80
M3 heat-set threaded inserts (50 pack)
qty 1 · DIYElectronics
R120
Male-female Dupont jumpers (40 × 20 cm)
qty 2 · Multiple
R80
Silicone wire 18 AWG, 1 m red + 1 m black
qty 1 · Rclipo
R80
Creality PETG filament, black, 1 kg
qty 1 · PiShop
R231.32
Shipping across ~3 suppliers (Jo'burg)
qty 1 · Various
R400
Contingency (solder, heat-shrink, 1 DOA servo, cable ties)
qty 1 ·
R300

The checkable task list. Deep guides for each are in section 08.

T1BOM inventory + bench safety
Acceptance:All BOM items physically present; LiPo bag, fire blanket, and CO₂/dry-powder extinguisher within arm's reach of the bench.
T2PlatformIO project scaffold· depends T1
Acceptance:GPIO 2 LED blinks at 1 Hz; serial monitor shows `HEXAPOD V1 / boot / hello` at 115200 baud.
T3Build the power harness· depends T1
Acceptance:No exposed copper; all joints heat-shrunk; continuity test clean; LVC buzzer sounds on a single-cell test trip.
T4LiPo storage charge + safety check· depends T3
Acceptance:Pack at 8.40 ± 0.05 V; cells within 0.02 V of each other; stored in LiPo bag, labelled.
T5Buck converter calibration· depends T3, T4
Acceptance:Buck reads 6.00 V no-load, ≥ 5.8 V at 5 A load; trim pot mechanically locked with paint or nail polish.
T6I²C scan finds both PCA9685s· depends T2, T5
Acceptance:0x40 and 0x41 both reported every scan iteration over USB serial.
T7Drive one servo from ESP32· depends T6
Acceptance:Servo sweeps smoothly through 0→90→180→90; no jitter at 90°; buck output stays ≥ 5.8 V during stalls.
T8Bench-test all 18 servos· depends T7
Acceptance:18 working servos labelled S00–S17 with masking tape; any duds set aside and noted in docs/servo-test-log.md.

One section per T-task. Plain-English explanation, prerequisites, sub-steps with code you can copy, numbers to expect, curated reference links, and the pitfalls you will absolutely hit.

T1BOM inventory + bench safety
Acceptance:All BOM items physically present; LiPo bag, fire blanket, and CO₂/dry-powder extinguisher within arm's reach of the bench.
What this task does

Install the development environment, create an empty firmware project, and upload the microcontroller equivalent of 'hello world' — a blinking LED plus a serial print. If this works, every other task is just more code on top.

Why it matters

~80% of 'the code doesn't upload' problems come from a broken toolchain, bad USB cable, missing driver, or wrong COM port — NOT the code. You need a baseline that proves the chain works so when real bugs appear later, you know to look at the code, not the setup.

Before you start
  • VS Code installed (https://code.visualstudio.com).
  • An ESP32 DevKit V1 board (classic ESP32, not S3 — Bluepad32 needs BT Classic).
  • A DATA-capable USB cable (charge-only cables cause silent upload failures).
  • Mac / Windows / Linux — PlatformIO runs on all three.
Step-by-step
  1. STEP 1
    Install the PlatformIO extension in VS Code

    Open VS Code → Extensions panel (⇧⌘X) → search 'PlatformIO IDE' → Install. First install downloads the Python-based core (~200 MB) and can take 3–10 min. Let it finish; you'll see a 'PlatformIO: Home' tab when done.

    EXPECT: The PlatformIO icon (ant head) appears in the VS Code activity bar (left strip).
  2. STEP 2
    Install the CP2102 / CH340 USB-serial driver (macOS / Windows)

    Most DevKit V1 boards use the Silicon Labs CP2102. On macOS you need the VCP driver from Silicon Labs. On Windows, plug the board in — if it shows as a yellow-bang in Device Manager, install the CP2102 or CH340 driver matching the onboard chip. Linux recognises both natively.

    EXPECT: A new /dev/cu.SLAB_USBtoUART (macOS) or COMn (Windows) appears when you plug the board in.
  3. STEP 3
    Create a new PlatformIO project

    PIO Home → New Project. Name: hexa-firmware. Board: 'Espressif ESP32 Dev Module'. Framework: Arduino. Location: anywhere outside OneDrive/iCloud sync folders (those break uploads).

    EXPECT: VS Code opens a folder with platformio.ini, src/main.cpp, and a lib/ folder.
  4. STEP 4
    Edit platformio.ini

    Replace the generated file with the config from docs/04_SOFTWARE_PLAN.md. Key settings: monitor_speed = 115200, upload_speed = 921600, and the lib_deps block that pulls in the Adafruit PCA9685 driver.

    [env:esp32dev]
    platform = espressif32
    board = esp32doit-devkit-v1
    framework = arduino
    monitor_speed = 115200
    upload_speed = 921600
    lib_deps =
      adafruit/Adafruit PWM Servo Driver Library@^3.0.0
    EXPECT: PlatformIO will re-index and silently download the library on first build.
  5. STEP 5
    Write a minimal main.cpp

    Blink GPIO 2 (the blue onboard LED) and print a banner to Serial.

    #include <Arduino.h>
    
    void setup() {
      Serial.begin(115200);
      pinMode(2, OUTPUT);
      delay(200);
      Serial.println("=== HEXA FIRMWARE BOOT ===");
    }
    
    void loop() {
      digitalWrite(2, HIGH);
      delay(500);
      digitalWrite(2, LOW);
      delay(500);
      Serial.print(".");
    }
    EXPECT: Compiles cleanly.
  6. STEP 6
    Build, upload, open the serial monitor

    Click the ✓ (Build) icon in the PlatformIO toolbar. Then → (Upload). When you see 'Hard resetting via RTS pin…', click the plug icon (Serial Monitor). Some DevKit V1s need you to HOLD the BOOT button briefly while upload starts.

    EXPECT: Blue LED blinks at 1 Hz. Serial monitor shows 'HEXA FIRMWARE BOOT' followed by dots.
Numbers you'll want
Serial baud
115200
Upload baud
921600 (drop to 460800 on flaky USB)
LED pin on DevKit V1
GPIO 2 (blue, onboard)
First-build library fetch
~30–60 s
Common pitfalls
▲ SYMPTOM
'Failed to connect to ESP32: Timed out waiting for packet header' during upload.
Cause:Board isn't in bootloader mode, or charge-only USB cable, or wrong serial port selected.
Fix:Hold BOOT, press-release EN, release BOOT while upload prints 'Connecting...'. If that fails, swap the USB cable. If still failing, check that no other tool (Arduino IDE, minicom) has the serial port open.
▲ SYMPTOM
Serial monitor shows garbage or ������.
Cause:Baud mismatch between platformio.ini and the monitor.
Fix:Confirm monitor_speed = 115200 in platformio.ini AND that Serial.begin(115200) matches.
▲ SYMPTOM
LED doesn't blink but serial prints fine.
Cause:Some DevKit V1 clones wire the onboard LED to a different pin, or have no onboard LED.
Fix:Try GPIO 5 instead. If still nothing, wire an external LED + 330 Ω resistor to GPIO 2.
▲ SYMPTOM
Upload succeeds but serial is silent.
Cause:Board boot-looping in a brown-out.
Fix:Power the board from a powered USB hub, not a laptop port. Remove any attached servos for this test.
You're done with this task when…
Blue LED pulses at 1 Hz. Serial monitor opens at 115200 and you see 'HEXA FIRMWARE BOOT' followed by a dot every 500 ms. No red underlines in main.cpp. pio run -t upload completes without errors.
T2PlatformIO project scaffold
Acceptance:GPIO 2 LED blinks at 1 Hz; serial monitor shows `HEXAPOD V1 / boot / hello` at 115200 baud.
What this task does

Get the two PCA9685 servo driver boards talking to the ESP32 over I²C, confirm both show up at addresses 0x40 and 0x41, and make servo #0 physically move to its mechanical centre.

Why it matters

The PCA9685 is the bridge between your code and every servo. If I²C doesn't scan clean or the PWM frequency is wrong, nothing downstream works. Getting one servo to centre proves the entire signal chain (code → I²C → driver → PWM → servo).

Before you start
  • T1 complete (board boots, serial works).
  • One MG996R servo with horn OFF (don't install horns until after calibration).
  • Both PCA9685 boards soldered to their headers (or bought pre-assembled).
  • Buck converter set to ~5.5 V (measure with multimeter!) OR a bench PSU at 5.5 V, 1 A limit.
  • DANGER: never power PCA9685 V+ from the ESP32's 5 V pin. One servo under load will brown out the board.
Step-by-step
  1. STEP 1
    Bridge the A0 solder-jumper on the SECOND PCA9685 only

    Each PCA9685 has six address-select pads (A0..A5). Default = 0x40. Bridge A0 on driver #2 with a small solder blob → it becomes 0x41. Leave the first one alone. Bridge a wrong one and you'll clash with the IMU (0x68) or the camera's EEPROM.

    EXPECT: Visibly shiny solder over the A0 pad on driver #2. None of the A1..A5 pads touched.
  2. STEP 2
    Wire the I²C daisy-chain

    ESP32 GPIO 21 → SDA of driver #1 → SDA of driver #2. ESP32 GPIO 22 → SCL of driver #1 → SCL of driver #2. ESP32 3V3 → VCC of both drivers (logic power — low current). ESP32 GND → GND of both drivers. Keep these wires short: <20 cm each.

    EXPECT: Both drivers' green power LED lights when ESP32 is powered.
  3. STEP 3
    Wire servo power — SEPARATELY

    Buck converter + output → V+ pin on driver #1 → V+ pin on driver #2. Buck converter − output → GND of driver #1 → GND of driver #2 → ESP32 GND (all grounds MUST be common). Put a 470 µF–1000 µF electrolytic cap across V+/GND near driver #1.

    EXPECT: With no servos plugged in, V+ reads 5.3–5.6 V at both drivers.
  4. STEP 4
    Plug ONE servo into channel 0 of driver #1

    Signal = orange (or yellow), +V = red, GND = brown (or black). Double-check the pin order on your driver silkscreen — some have GND on the outside, some inside.

    EXPECT: Servo doesn't move yet. No buzz.
  5. STEP 5
    Run an I²C scan on boot

    Add this to setup() after Serial.begin:

    #include <Wire.h>
    void i2cScan() {
      Wire.begin(21, 22);
      Wire.setClock(400000);
      Serial.println("I2C scan:");
      for (uint8_t a = 1; a < 127; a++) {
        Wire.beginTransmission(a);
        if (Wire.endTransmission() == 0) Serial.printf("  0x%02X found\n", a);
      }
    }
    EXPECT: Boot-time serial log: '0x40 found' and '0x41 found'.
  6. STEP 6
    Initialise at 50 Hz, set servo 0 to centre

    Use the Adafruit PCA9685 library. 50 Hz is the MG996R's native frame rate. Centre = ~1500 µs pulse = tick count 307 at 50 Hz / 12-bit.

    #include <Adafruit_PWMServoDriver.h>
    
    Adafruit_PWMServoDriver drv0(0x40);
    uint16_t usToTicks(uint16_t us) {
      return (uint16_t)((us * 4096UL) / 20000UL);
    }
    
    void setup() {
      Serial.begin(115200);
      Wire.begin(21, 22);
      drv0.begin();
      drv0.setPWMFreq(50);
      drv0.setPWM(0, 0, usToTicks(1500)); // centre
    }
    EXPECT: Servo flicks to centre position within one frame (<20 ms) and holds there.
  7. STEP 7
    Wrap it behind a setAngle API

    Deg 0..180 maps to ~500..2500 µs on a typical analog servo. Clamp at the ends to avoid gear strain.

    void setAngle(uint8_t ch, uint8_t deg) {
      deg = constrain(deg, 0, 180);
      uint16_t us = map(deg, 0, 180, 500, 2500);
      if (ch < 16) drv0.setPWM(ch, 0, usToTicks(us));
      else drv1.setPWM(ch - 16, 0, usToTicks(us));
    }
    EXPECT: setAngle(0, 0) → hard-left. setAngle(0, 180) → hard-right. setAngle(0, 90) → centre.
Numbers you'll want
PCA9685 default address
0x40 (bridge A0 for 0x41)
I²C clock for short bus
400 kHz (drop to 100 kHz if bus is >40 cm)
PWM frequency for MG996R
50 Hz
MG996R pulse range
~500 µs (0°) … ~2500 µs (180°), centre ~1500 µs
Ticks per µs at 50 Hz
4096 / 20000 = 0.2048
MG996R idle current
~5–20 mA (no load)
MG996R stall current @ 6 V
~1.2 A (one servo)
Common pitfalls
▲ SYMPTOM
Only 0x40 shows up on the scan; 0x41 missing.
Cause:A0 solder jumper on driver #2 not actually bridged, or cold joint.
Fix:Under a bright light, re-flow A0 with fresh solder + flux. Run the scan again.
▲ SYMPTOM
Servo buzzes but doesn't hold position.
Cause:Brown-out: V+ rail dropping below ~4.5 V under servo load.
Fix:Bigger bulk cap (1000 µF) right at V+ of driver #1. Confirm buck converter is set to 5.5 V under load. Never power from the ESP32's 5V pin.
▲ SYMPTOM
All servos on driver #2 jitter or refuse to move.
Cause:Missed the common ground: buck converter GND and ESP32 GND aren't tied.
Fix:Connect buck converter − directly to an ESP32 GND pin with a short thick wire.
▲ SYMPTOM
Scan shows 0x40 AND 0x41 AND random other addresses (0x70, 0x77…).
Cause:Floating SDA/SCL, or missing pull-ups. Most PCA9685 breakouts have onboard pull-ups, but if you bought a bare chip, add 4.7 kΩ to 3V3 on each line.
Fix:Check for pull-up resistors on the PCA9685 board. If absent, add 4.7 kΩ from SDA→3V3 and SCL→3V3.
▲ SYMPTOM
Servo moves in the wrong direction or has limited travel.
Cause:You will fix this in T4 with direction flag + trim. For now, ignore.
Fix:(deferred to T4)
You're done with this task when…
Boot log lists 0x40 and 0x41. setAngle(0, 0), (0, 90), (0, 180) each moves servo #0 to three visibly different positions. Bench PSU shows < 150 mA steady, no browning, no LED flicker on the ESP32.
T3Build the power harness
Acceptance:No exposed copper; all joints heat-shrunk; continuity test clean; LVC buzzer sounds on a single-cell test trip.
What this task does

Write the math that turns 'put the foot at this XYZ point' into 'coxa=this angle, femur=that, tibia=that'. Then prove it with a unit test that runs ON YOUR LAPTOP (not on the robot), using PlatformIO's native test environment.

Why it matters

IK is the brain of the whole walker. Get it right once, the rest of the robot is just sequencing. Test it NATIVELY — if you only discover a sign error in IK by watching a leg punch the floor, you've already stripped a gear.

Before you start
  • T2 complete.
  • Measured link lengths (coxa, femur, tibia) with calipers. For Antdroid defaults: coxa ≈ 28 mm, femur ≈ 83 mm, tibia ≈ 121 mm — but MEASURE yours.
  • Paper + pencil (you will draw the triangle).
  • A willingness to re-read high-school trigonometry (atan2, law of cosines).
Step-by-step
  1. STEP 1
    Draw the leg on paper

    Top view: coxa rotates around the vertical axis; its angle = atan2(y, x). Side view: the femur and tibia form a triangle — given foot radius r and depth z, solve the triangle with the law of cosines. Write the diagram + label link lengths L1, L2, L3 before you touch code.

  2. STEP 2
    Port the analytical IK from rasheeddo

    The rasheeddo/hexapod_dev_esp32 repo has a clean closed-form IK you can literally copy. Adjust signs if your body frame defines +Z as UP vs. DOWN.

    struct Angles { float coxa, femur, tibia; bool reachable; };
    
    Angles solveIK(float x, float y, float z,
                   float L1, float L2, float L3) {
      Angles a{};
      a.coxa = atan2f(y, x);
      float r = sqrtf(x*x + y*y) - L1;
      float d = sqrtf(r*r + z*z);
      if (d > (L2 + L3) || d < fabsf(L2 - L3)) {
        a.reachable = false;
        return a;
      }
      float alpha = atan2f(z, r);
      float beta  = acosf((L2*L2 + d*d - L3*L3) / (2*L2*d));
      a.femur = alpha + beta;
      a.tibia = acosf((L2*L2 + L3*L3 - d*d) / (2*L2*L3));
      a.reachable = true;
      return a;
    }
  3. STEP 3
    Add the matching FK

    FK is the other direction: plug angles in, get (x, y, z) out. Good for sanity testing (IK then FK should round-trip).

  4. STEP 4
    Set up a native (desktop) test env in platformio.ini

    Running unit tests on your laptop is ~100× faster than flashing to hardware for every fix.

    [env:native]
    platform = native
    test_framework = unity
    build_flags = -std=gnu++17
    test_build_src = yes
  5. STEP 5
    Write the round-trip test

    Pick 10 varied points within the reachable envelope. Run IK → FK. Expect recovery within 0.1 mm.

    void test_ik_fk_roundtrip() {
      const float pts[][3] = {{150, 0, -100}, {100, 80, -120},
                              {50, -50, -80}};
      for (auto& p : pts) {
        auto a = solveIK(p[0], p[1], p[2], L1, L2, L3);
        TEST_ASSERT_TRUE(a.reachable);
        float x, y, z; solveFK(a, x, y, z);
        TEST_ASSERT_FLOAT_WITHIN(0.1, p[0], x);
        TEST_ASSERT_FLOAT_WITHIN(0.1, p[1], y);
        TEST_ASSERT_FLOAT_WITHIN(0.1, p[2], z);
      }
    }
  6. STEP 6
    Run the test

    Terminal in VS Code: pio test -e native. You get a pass/fail summary in under 2 seconds.

    EXPECT: 3 tests pass. If one fails with 'Expected 150 got -150', you have a sign error — probably in atan2 arg order or the z convention.
  7. STEP 7
    Check reachability explicitly

    Commanding an unreachable point should return reachable=false, not NaN angles. NaNs will propagate and do wild things at PWM time.

Numbers you'll want
Typical Antdroid coxa length
~28 mm
Typical Antdroid femur length
~83 mm
Typical Antdroid tibia length
~121 mm
Max foot radius
L1 + L2 + L3 (straight leg)
Min foot radius
|L2 − L3| + L1
Native test turnaround
< 2 s (vs 20–30 s flash)
References · learn until it clicks
Common pitfalls
▲ SYMPTOM
IK returns NaN for points that should be reachable.
Cause:acosf() fed a value outside [-1, 1] due to rounding or a wrong law-of-cosines denominator.
Fix:Clamp the argument to [-1.0, 1.0] before acosf. Re-derive the law-of-cosines term on paper.
▲ SYMPTOM
FK round-trip is off by a constant (e.g. every x is +28 mm too big).
Cause:Forgot to subtract the coxa length L1 before computing r.
Fix:r = sqrt(x² + y²) − L1, not sqrt(x² + y²).
▲ SYMPTOM
A positive z moves the foot UP but your code expected DOWN.
Cause:Body-frame convention mismatch (gravity along +Z vs −Z).
Fix:Pick one convention and stick to it across IK, FK, gait, and serial commands. We use 'foot below body = negative z' throughout.
▲ SYMPTOM
The native test env won't compile because Arduino.h is missing.
Cause:Test files must avoid any hardware dependencies.
Fix:Put IK/FK math in pure C++ files with NO Arduino includes. The test env only compiles those files.
You're done with this task when…
pio test -e native → 10/10 tests pass in under 2 seconds. Round-trip error max < 0.1 mm. solveIK(x=9999, y=0, z=0) returns reachable=false (not NaN).
T4LiPo storage charge + safety check
Acceptance:Pack at 8.40 ± 0.05 V; cells within 0.02 V of each other; stored in LiPo bag, labelled.
What this task does

Add a serial command that forces every servo to neutral 90° and lets you adjust per-servo trim + direction flags until the robot looks mechanically straight. Save those values to ESP32 flash so they survive reboots.

Why it matters

No matter how careful you are, servo horns can ONLY install in 6° or 15° increments (25-tooth vs 24-tooth splines). So mechanical neutral ≠ 90° electrical. Trim is how you reconcile this. Without calibration, IK gives correct ANGLES but the leg physically isn't where the code thinks it is — and the walker will drift or fall.

Before you start
  • T2 complete (servo bus works).
  • Robot assembled and MOUNTED ON A STAND. Feet must not touch the floor.
  • One person at a keyboard, one hand on the EN (reset) button.
  • Every servo labelled (leg 0..5, joint coxa/femur/tibia) before calibration.
Step-by-step
  1. STEP 1
    Implement the CAL serial protocol

    On receiving 'CAL\n': every servo goes to 90° − trim, with direction applied. While in CAL mode, accept commands: 'T <idx> <deg>' to adjust trim, 'D <idx>' to flip direction, 'SAVE' to write NVS, 'Q' to exit.

    // Pseudo-protocol:
    // CAL         → enter calibration hold
    // T 3 -5      → servo 3 trim = −5°
    // D 3         → flip servo 3 direction
    // SAVE        → commit trims + dirs to NVS
    // Q           → exit CAL; resume normal control
  2. STEP 2
    Wire trim + direction into setAngle

    Inside setAngle(), apply direction first, then trim. Clamp the final value to [0, 180] to avoid mechanical stall.

    int8_t SERVO_DIR[18] = { /* +1 or −1 */ };
    float  SERVO_TRIM[18] = { /* −15.0 .. +15.0 deg */ };
    
    void setAngle(uint8_t ch, float deg) {
      float d = 90.0f + SERVO_DIR[ch] * (deg - 90.0f) + SERVO_TRIM[ch];
      d = constrain(d, 0.0f, 180.0f);
      /* ... PWM write ... */
    }
  3. STEP 3
    Persist trim + dir to NVS using Preferences

    ESP32's Preferences library wraps NVS in a simple key/value API.

    #include <Preferences.h>
    Preferences prefs;
    void saveCal() {
      prefs.begin("cal", false);
      prefs.putBytes("trim", SERVO_TRIM, sizeof(SERVO_TRIM));
      prefs.putBytes("dir",  SERVO_DIR,  sizeof(SERVO_DIR));
      prefs.end();
    }
    void loadCal() {
      prefs.begin("cal", true);
      prefs.getBytes("trim", SERVO_TRIM, sizeof(SERVO_TRIM));
      prefs.getBytes("dir",  SERVO_DIR,  sizeof(SERVO_DIR));
      prefs.end();
    }
  4. STEP 4
    Walk through every leg, joint by joint

    For each leg (0..5), for each joint (coxa, femur, tibia): command 90°. If it moves to the wrong side, issue 'D'. Then adjust 'T' by ±1° until the joint is mechanically straight. Coxa parallel to ground, femur straight out horizontal, tibia pointing straight down. Print each value as you go — that's your backup.

  5. STEP 5
    SAVE and power-cycle

    Issue SAVE. Power the board off, then on. Re-enter CAL. Verify every joint comes up in the same position. If it drifts, loadCal() isn't being called in setup().

    EXPECT: All 18 joints hold their calibrated pose from cold boot.
  6. STEP 6
    Export a backup as JSON

    Print the calibration as JSON on serial with a 'DUMP' command. Paste that into a git-committed calibration_backup.json file. If the NVS ever wipes (flash erase, bad partition), you have the values.

Numbers you'll want
MG996R horn tooth count
25T (3.6° smallest install increment / spline)
Typical trim range used
±15° is plenty; >15° = re-install the horn
NVS write cycles (conservative)
> 100 000 per cell — don't hammer saveCal() in a loop
NVS write latency
2–20 ms per putBytes() call
Common pitfalls
▲ SYMPTOM
Trim values 'stick' for a few reboots then suddenly reset to zero.
Cause:NVS partition corrupt or missing; or Preferences namespace string changed.
Fix:Run 'pio run -t erase' once, then re-flash. Keep the namespace literal ('cal') consistent across save + load.
▲ SYMPTOM
A positive trim moves the joint the wrong way.
Cause:That servo's direction flag should be inverted.
Fix:Flip SERVO_DIR for that channel, re-test. Don't try to fight it with negative trim — the code will surprise you elsewhere.
▲ SYMPTOM
Trim needs to be > 20° to get mechanical neutral.
Cause:Horn installed on the wrong spline — you're >1 tooth off.
Fix:Pop the horn off, rotate one tooth, press back on. Retry trim adjustment — should now be within a few degrees.
▲ SYMPTOM
Servos 'kick' at power-on before CAL is entered.
Cause:setAngle() is called before loadCal() runs, so trims are zero.
Fix:Call loadCal() as the FIRST thing in setup(), before any PWM writes.
You're done with this task when…
From cold boot, all 18 joints sit in visually-correct neutral pose. Coxa parallel to ground, femur horizontal, tibia vertical. 'DUMP' returns the full trim + dir table. Power-cycle 3 times, no drift.
T5Buck converter calibration
Acceptance:Buck reads 6.00 V no-load, ≥ 5.8 V at 5 A load; trim pot mechanically locked with paint or nail polish.
What this task does

Combine IK (T3) + servo bus (T2) + calibration (T4) into a tripod gait. On a stand, all 6 feet should trace smooth arcs. Tripod A (legs 0, 2, 4) and Tripod B (legs 1, 3, 5) should alternate cleanly.

Why it matters

This is the 'it walks' moment. Every prior task has been plumbing. Tripod is the simplest stable gait because a triangle is always balanced — other gaits are harder because they require inertia or body shifting.

Before you start
  • T3 (IK) tests passing.
  • T4 calibration saved.
  • Robot on stand, feet hanging free.
  • Fire extinguisher / LiPo bag nearby (we're now running servos continuously).
Step-by-step
  1. STEP 1
    Parametrise the gait

    Expose: cycleMs (full swing+stance duration), strideMm (step length), swingHeightMm (arc height), bodyHeightMm (nominal z), duty (fraction of cycle spent in stance). Start conservative: cycleMs=600, stride=40, swingHeight=25, bodyHeight=90, duty=0.5.

  2. STEP 2
    Generate one leg's trajectory

    Swing phase: foot traces a cycloid from (x0−stride/2, −bodyHeight) up over (x0, −bodyHeight + swingHeight) to (x0+stride/2, −bodyHeight). Stance: foot slides straight back from (x0+stride/2, −bodyHeight) to (x0−stride/2, −bodyHeight). Feed each (x, y, z) into solveIK() → setAngle().

    // Normalised phase t in [0,1) within cycle.
    // Stance for t < duty; swing for t >= duty.
    // Cycloidal swing (smooth start/end):
    //   s = (t - duty) / (1 - duty);
    //   x = -stride/2 + stride * s;
    //   z = -bodyH + swingH * sin(PI * s);
  3. STEP 3
    Phase-offset the two tripods

    Tripod A (legs 0, 2, 4) uses phase t. Tripod B (legs 1, 3, 5) uses phase (t + 0.5) mod 1. That means whenever A is swinging, B is in stance, and vice versa — the body is always on three feet.

  4. STEP 4
    Handle 'walk in place'

    When commanded stride = 0, swing height stays but x delta is zero. Feet lift and drop in place — a great visual test on the stand.

  5. STEP 5
    Run on the stand first

    Issue 'WALK 0.0' (stride 0). Watch all 6 feet. Each should lift exactly once per cycle with clean arcs. Tripod A and B should alternate visibly. If any foot doesn't lift at all, its femur direction or trim is wrong.

  6. STEP 6
    Timer task, not loop()

    Run the gait update in a FreeRTOS task or esp_timer at a fixed 50 Hz. Don't rely on loop() — it's whatever-your-code-allows Hz and will jitter.

    // In setup():
    // esp_timer_create_args_t tArgs = {.callback=&gaitTick, ...};
    // esp_timer_start_periodic(timer, 20000); // 20 ms = 50 Hz
Numbers you'll want
Starting cycle time
600 ms (= ~1.7 Hz)
Starting stride
30–40 mm
Starting swing height
20–25 mm
Starting body height
80–95 mm above ground
Gait update rate
50 Hz (20 ms period)
Common pitfalls
▲ SYMPTOM
All 6 feet lift together — no support polygon.
Cause:Tripod phase mapping wrong. You've grouped legs as (0, 1, 2) vs (3, 4, 5) — geometrically adjacent — instead of alternating.
Fix:Tripod A = legs 0, 2, 4 (front-left, middle-right, rear-left). Tripod B = legs 1, 3, 5.
▲ SYMPTOM
A single leg lags visibly.
Cause:That leg's coxa direction is flipped — it's tracing the arc in reverse.
Fix:Go back to CAL, flip SERVO_DIR for that coxa.
▲ SYMPTOM
Servos cook (hot to touch) after 30 s on the stand.
Cause:Body height or stride too aggressive → femur holding against gravity at a hard angle → continuous stall-region current.
Fix:Drop bodyHeight to 80 mm. Widen stance (move x0 outward). Slow cycle to 800 ms.
▲ SYMPTOM
Foot slides / drags through stance instead of lifting.
Cause:Swing height too low OR swing/stance phase boundaries swapped.
Fix:Raise swingHeight to 30 mm as a test. If still bad, re-check the duty boundary: stance for t < duty, swing afterwards.
▲ SYMPTOM
Jittery, jerky motion.
Cause:Gait running in loop() without rate limit — I²C writes saturate the bus at random rates.
Fix:Move the update to a 50 Hz timer. Confirm with a scope or logic analyser on SDA.
You're done with this task when…
Robot on stand, issue WALK 0 → all 6 feet lift once per ~600 ms cycle in a clean arc. Issue WALK 1 (forward) → tripod A plants while B swings and vice versa. No servo overheating after 2 min of walking in place.
T6I²C scan finds both PCA9685s
Acceptance:0x40 and 0x41 both reported every scan iteration over USB serial.
What this task does

Monitor the LiPo voltage through a simple resistor divider into an ADC pin, and halt all servo motion when voltage drops below 6.6 V (3.3 V per cell). Resume above 7.0 V. Beep a buzzer + blink an LED on low voltage.

Why it matters

Discharging a LiPo below 3.0 V/cell permanently damages it — a single deep-discharge event and the pack is scrap (R600+ lost). Below 2.5 V/cell LiPos can puff, vent, or catch fire. Battery monitor is the single most-important safety feature after the fuse.

Before you start
  • T1 complete.
  • 2S LiPo charged (nominal 7.4 V, full 8.4 V).
  • Two 10 kΩ ±1% resistors (one 10k/10k divider → ADC sees ≤ ½ × battery voltage, safely under 3.3 V).
  • Soldering iron, heat-shrink.
  • A buzzer from the BOM OR the low-voltage alarm as backup.
Step-by-step
  1. STEP 1
    Build a 10k / 10k voltage divider

    From LiPo+ → 10 kΩ → ADC pin (GPIO 34) AND from same node → 10 kΩ → GND. At 8.4 V battery, ADC sees 4.2 V — TOO HIGH for ESP32. Use 20k/10k instead, OR 10k/4.7k — something that keeps ADC input < 3.0 V at 8.4 V input. Calculate: ADC_V = V_bat × (R2 / (R1 + R2)).

    EXPECT: At 8.4 V input, ADC input ≤ 3.0 V. At 6.0 V input, ADC input ≤ 2.2 V.
  2. STEP 2
    Verify with multimeter BEFORE connecting to ESP32

    Connect the divider to the LiPo (inside fireproof bag, fuse in circuit). Measure the divider midpoint. Must match your calculation ± 0.1 V. Only after this passes, connect to GPIO 34.

  3. STEP 3
    Read ADC and convert back to battery voltage

    ESP32 ADC is 12-bit (0..4095) → 0..3.3 V reference. Scale by the divider ratio to recover V_battery.

    const float DIV_RATIO = (10000.0f + 10000.0f) / 10000.0f; // for 10k/10k
    // or use DIV_RATIO = (20000 + 10000) / 10000 = 3.0 for a 20k/10k
    float readBattery() {
      int raw = analogRead(34);
      float v_adc = (raw / 4095.0f) * 3.3f;
      return v_adc * DIV_RATIO;
    }
    EXPECT: readBattery() returns ±0.2 V of the multimeter reading.
  4. STEP 4
    Apply moving average (ADC is noisy)

    A 16-sample moving average over ~160 ms smooths the ADC enough to make hysteresis reliable.

    float filteredV() {
      static float buf[16] = {0};
      static int i = 0;
      buf[i] = readBattery();
      i = (i + 1) & 15;
      float s = 0; for (float x : buf) s += x;
      return s / 16;
    }
  5. STEP 5
    Implement hysteresis cutoff

    Two thresholds, not one. Halt at ≤ 6.6 V. Only re-arm above ≥ 7.0 V. State is sticky — you need a deliberate recovery.

    enum BatteryState { OK, LOW };
    BatteryState bs = OK;
    bool gaitEnabled() {
      float v = filteredV();
      if (bs == OK  && v <= 6.60f) bs = LOW;
      if (bs == LOW && v >= 7.00f) bs = OK;
      return bs == OK;
    }
  6. STEP 6
    Feedback: buzzer + GPIO 2 LED

    On LOW: pulse GPIO 2 fast (100 ms on / 100 ms off), drive a buzzer pin HIGH with a 1 kHz square wave. Keep the standalone LVC buzzer ALSO plugged in — redundancy is cheap.

  7. STEP 7
    Test with a bench PSU (not the LiPo!)

    Sweep the PSU from 8.0 V down to 6.0 V while watching the serial log. Motion halts at 6.6 V. Sweep back up — doesn't resume until 7.0 V. Beep + blink align with the halt.

    EXPECT: No resume chatter near the threshold. Serial logs state transitions.
Numbers you'll want
2S LiPo nominal
7.4 V
2S LiPo fully charged
8.4 V
2S LiPo our cutoff
6.6 V (= 3.3 V / cell)
Absolute damage threshold
< 6.0 V (3.0 V / cell)
ESP32 ADC reference
3.3 V (12-bit, 0..4095)
ESP32 ADC2 vs ADC1
Use ADC1 pins (32–39) — ADC2 conflicts with Wi-Fi
GPIO 34 capability
ADC1_CH6, input-only, safe for this use
Common pitfalls
▲ SYMPTOM
ADC reads a constant ~4095 (saturated).
Cause:Divider ratio too low — ADC input > 3.3 V.
Fix:Re-check resistor values. With a 20k/10k divider, 8.4 V input → 2.8 V ADC (safe). Anything that can exceed 3.3 V on the pin is too much.
▲ SYMPTOM
Voltage reading is 0.5–1 V lower than the multimeter.
Cause:ESP32's default ADC is non-linear near the rails — this is a known characteristic.
Fix:Use analogReadMilliVolts() (Arduino-ESP32 ≥ 2.0.x) — it applies the factory calibration automatically.
▲ SYMPTOM
System chatters between OK and LOW near 6.6 V.
Cause:No hysteresis, or hysteresis band too narrow (< 0.3 V).
Fix:Keep the cutoff at 6.6 V, recovery at 7.0 V — 0.4 V band is reliable with moving-average filtering.
▲ SYMPTOM
Works on bench PSU, fails on LiPo (reading drops under load).
Cause:Real — LiPo sags hard under servo stall. The filter should absorb it; if it doesn't, your average is too short.
Fix:Increase the filter window to 32 samples (~320 ms). Or gate the read on 'all servos idle' if you want a less twitchy reading.
You're done with this task when…
Bench PSU slowly ramped 8.0 V → 6.0 V. Serial logs 'BATT LOW' at 6.6 V; gait task halts within 200 ms. Ramp back up; 'BATT OK' appears at 7.0 V and motion resumes. Buzzer sounds + LED pulses during LOW state.
T7Drive one servo from ESP32
Acceptance:Servo sweeps smoothly through 0→90→180→90; no jitter at 90°; buck output stays ≥ 5.8 V during stalls.
What this task does

Extend the tripod gait state machine to also support ripple (one leg at a time, 6 × 1/6-phase) and wave (one leg at a time, slower, more stable). Let the operator switch between them via serial command or later, via the dashboard.

Why it matters

Tripod is fast but commits all weight to a triangle — unstable on uneven ground. Wave gait is slower but keeps 5/6 feet planted at all times — useful for carrying payloads or climbing. Shipping all three is low-effort and impressive in demos.

Before you start
  • T5 complete (tripod working).
  • Understanding of gait phase tables (see docs 06).
Step-by-step
  1. STEP 1
    Generalise phase to a 6-entry table

    Each leg has a phase offset in [0, 1). Tripod: [0, 0.5, 0, 0.5, 0, 0.5]. Ripple: [0, 1/6, 2/6, 3/6, 4/6, 5/6] — but in the insect-inspired order 0, 3, 1, 4, 2, 5 (opposite leg pairs alternate). Wave: [0, 1/6, 2/6, 3/6, 4/6, 5/6] in sequential leg order but with duty = 5/6 (each leg swings 1/6 of cycle, planted 5/6).

  2. STEP 2
    Make duty (stance fraction) a per-gait parameter

    Tripod: duty = 0.5. Ripple: duty = 2/3. Wave: duty = 5/6. Everything else in the gait function stays the same — just a different phase table + duty.

  3. STEP 3
    Add a serial 'GAIT <name>' command

    'GAIT TRIPOD' / 'GAIT RIPPLE' / 'GAIT WAVE'. Switching mid-walk should finish the current cycle before applying the new table (cleaner transition).

  4. STEP 4
    Latency: don't reset phase on switch

    If you reset phase to 0 on switch, all legs snap to a new pose. Let phase keep advancing; the new phase table takes effect on the next cycle.

  5. STEP 5
    Stability sanity check

    For each gait, at every moment in the cycle, the centre of mass projection should fall inside the support polygon formed by the planted feet. For tripod that's a triangle (most fragile). For wave it's always a pentagon (most stable).

Numbers you'll want
Tripod duty
~0.5 (3 swing, 3 stance)
Ripple duty
~0.67 (1 swing, 5 stance at any instant, but overlapping)
Wave duty
~0.83 (only 1 leg in air at a time)
Typical tripod cycle
400–600 ms
Typical wave cycle
900–1500 ms (slower, more stable)
Common pitfalls
▲ SYMPTOM
Switching from tripod to wave causes the robot to lurch or fall.
Cause:Phase table reset on switch.
Fix:Preserve the global phase accumulator; only swap the per-leg phase offset table + duty.
▲ SYMPTOM
Wave gait looks jerky — legs 'hop'.
Cause:Cycle time too short for wave's longer stance phase.
Fix:Slow cycle to 1200 ms. Swing phase is now 1/6 × 1200 = 200 ms — tight, but reachable.
▲ SYMPTOM
Ripple works only in one direction.
Cause:Phase table encodes direction; reversing velocity needs either reversing the table or inverting the stance slide.
Fix:Invert the stance x-slide sign based on vx. The phase table stays the same.
You're done with this task when…
Serial command switches between three visually distinct gaits without falls. Tripod: fast, hopping feel. Ripple: smoother, like walking. Wave: careful, like walking on ice. Each starts/stops cleanly.

The short version of what T4 walks you through. Print this page. Stick it on the wall next to the bench.

  1. STEP 1
    Multimeter polarity check before LiPo plug-in
    Before connecting the LiPo for the first time, set the multimeter to DC volts and verify polarity on the buck IN+/IN- terminals. One reversed cell or a flipped XT60 will destroy the buck instantly.
  2. STEP 2
    Lock the 6.0 V trim with paint
    Once the buck reads exactly 6.0 V no-load, dab the trim pot with nail polish or a paint pen. Vibration will otherwise drift it over time.
  3. STEP 3
    Single ground reference is mandatory
    Tie buck OUT- to ESP32 GND. Without this, servo PWM signals reference a floating ground and the servos will twitch or fail to centre.
  • → docs/03_ARCHITECTURE.md — wiring rules, bus allocation
  • → docs/04_SOFTWARE_PLAN.md — module layout, platformio.ini
  • → docs/06_KINEMATICS_AND_GAITS.md — IK + gait math
  • → docs/07_CALIBRATION_PROCEDURE.md — step-by-step calibration
  • → docs/08_BUILD_SEQUENCE.md — full-project bring-up timeline
  • → docs/10_SAFETY_CHECKLIST.md — LiPo + power pre-flight
  • → docs/11_PRIOR_ERRORS.md — mistakes we already know about
Ship when all of these are true
  • All 7 task acceptance criteria pass.
  • Calibration saved to NVS + JSON backup committed to git.
  • Robot walks forward 2 m on smooth floor with < 10° drift.
  • Robot turns left and right on command within 3 s.
  • Battery cutoff triggers at 6.6 V (bench-verified), recovers at 7.0 V.
  • No servo overheats (touch-test, hot-but-holdable) after 2 min of walking.
  • Video of the above archived — you'll want it for Phase 2 debugging reference.