Blog / Reference
reference
Handoff
Neato D10 Brain-Transplant — Handoff / Continuation Doc
Purpose: snapshot of where this project stands so it can be resumed in a fresh conversation without re-deriving anything. Pairs with the main build doc: neato-d10-brain-transplant.md.
Last updated: 2026-08-18
One-paragraph context
The user has a Neato D10 robot vacuum. Neato went bust; Vorwerk killed the cloud (Q4 2025). Working plan: “brain transplant” — rebuild as a ROS 2 robot on a Raspberry Pi 4 + ESP32, controlled from Home Assistant as an MQTT vacuum, keeping the mechanically/electrically standard parts. The user wants full vacuum function (keep brush + blower motors), and intends to blog the findings on GitHub eventually.
Premise correction (2026-08-04), resolved: the transplant was originally justified by “bare-metal LPC51U68, no OS.” That was wrong — lifting the RF shield revealed a full NXP i.MX 8M Nano SoloLite Linux computer beside the LPC51U68 (a two-brain design). But the conclusion still holds: the D8/D9/D10 boot chain is locked (OpenNeato excludes them; encrypted/signed firmware; i.MX HAB secure boot), so reusing the existing brain is not viable. Transplant confirmed.
The user is hands-on: soldering gear, a multimeter, a Pi 4, an Elegoo Arduino starter kit, comfortable opening hardware. Teardown is complete — every motor measured, every driver chosen, vetted, ordered and now ARRIVED.
Session 2 (2026-08-06): the whole power stage arrived — 2× DRV8871, Cytron MD13S, side-brush MOSFET, plus the speaker amp. That unblocks build step 2 (real wheel motor + encoder). This session’s bench work was sensors: the digital safety switches are fully characterised, and the cliff/wall sensor is characterised via prior art (the multimeter couldn’t crack it — see below). Focus now: build step 2 (encoders → odometry) — the drivers are here, so it’s the top real-hardware milestone.
Session 3 (2026-08-06): battery + charging chapter effectively CLOSED. Pack opened → cells confirmed reusable (Samsung INR18650-35E, 4S2P, alive at 14.23 V, soldered taps → no spot-welder). Dock confirmed dumb + always-on 19.54 V (top contact = +), and the rear-contact 2-pin harness is reusable (just unplug from the old board). The reuse-cells + dumb-BMS + dock-fed-charger path is now fully specced (BOM under Parts status). Only physical work left on the battery = the board swap when the new parts land.
Session 4 (2026-08-06): battery BOM fully SOURCED — mostly by salvage, not purchase. Every line item decided (see Parts status): ordered = 4S BMS (DollaTek 40A, common-port + balancer), CC/CV charger (XL4015 buck), inline fuse (Bst4U blade-fuse kit, 20A). No purchase = voltage sense (build a 10k+2k divider from the Elegoo kit — the generic 25V module clips the ESP32’s 3.3V ADC at 16.8V), pack disconnect (salvage the OEM battery connector), bench safety (pack lives in nested terracotta pots on a concrete garage floor), hookup wire (salvage stranded PC-PSU peripheral cables — NOT solid-core twin-and-earth, which fatigue-cracks under vibration; keep the PSU whole to double as the step-2 12V bench supply). Net: nothing confirmed left to buy. Also drew the first power-spine block diagram (Mermaid) — which surfaced the open topology questions now queued (charger tie-in point, fuse placement, common-ground). Focus still: build step 2 (encoders → odometry).
Session 6 (2026-08-07): STEP 2 DRIVE HALF DONE — a real Neato wheel motor spun bidirectionally off the salvaged PSU + a DRV8871. The bench PSU turned out to be a proprietary Dell OptiPlex SFF unit (B255ES-01, 255 W, Bestec), NOT standard ATX — exactly the risk session 5 flagged. Decoded safely with the multimeter instead of guessing: it’s 12V-only (main output is +12VA/+12VB, ~21 A total, no meaningful 5V/3.3V rail — the motherboard’s VRM made those). 8-pin P1 connector colours: white ×3 = +12V (switched), black ×2 = GND, lime-green = PS_ON#, purple = +12V standby (always live), one empty pin (PWR_OK not populated). Turn-on = bridge green→PS_ON to a black (Dupont/paperclip, ~0 current). Verified pre-bridge: purple↔black = 12.04 V standby (confirms decode + ground); whites = 0 V until bridged, then 12.0 V. ⚠️ The session-5 “10Ω dummy load on 5V/red” note does NOT apply — this PSU has no 5V rail; any keep-alive load goes on 12V. Also confirmed a yellow+black peripheral lead = easy 12V tap (12.04 V) → cut its connector, fat stranded wire straight into the DRV8871 VM terminal. Soldered the DRV8871 logic header (4-pin: NC/IN2/IN1/GND; continuity-checked no bridges). Motor is powered THROUGH the encoder board (no separate motor leads — the motor terminals are soldered to the encoder PCB, harness feeds power in); continuity-traced red+black = motor power (the other 4 = encoder, taped aside). Wired two-zone as planned (muscle = screw terminals, brains = Dupont), common ground via the DRV8871’s shared power/logic ground → one Dupont to ESP32 GND. Firmware rewritten L293D→DRV8871 (see below). Dead-end worth remembering: F 60 and F 120 did nothing from rest — I chased a wiring fault (found+fixed a D26/D27 swap, but that only flips direction, wasn’t the blocker). The real cause was stiction: a stopped wheel motor needs a hard shove to break free; F 255 started it, then it sustains at low duty. → firmware needs a kickstart pulse + min-PWM floor (saved to memory motor-pwm-calibration). Focus now: the encoder (odometry) — the whole point of step 2.
Session 11 (2026-08-18): ENCODER ALIVE + WHEEL ODOMETRY CONFIRMED — the whole “dead sensor” thread was WIRING, and the session-7 pinout was BACKWARDS. The logic analyzer arrived (cheap 8-ch 24 MHz FX2 clone). Set up sigrok-cli (Homebrew; PulseView is no longer packaged — CLI + fx2lafw firmware blob installed and working) and wrote hardware-scripts/test-scripts/encoder-la.py (captures raw A/B, x4 quadrature decode, ASCII waveform + verdict, validated end-to-end via the demo driver + synthetic captures). Ran the “right test” episode 19 promised: raw A/B on the analyzer during a spin. Sequence of a near-miss: (1) five flat captures → nearly declared DEAD; (2) DID the discipline — grounding CH0 pulled it LOW, proving analyzer + channel + common-ground all good, so the flat reads looked real (was ~to order an A3423); (3) then the user caught it: the encoder power was wired REVERSED (brown/orange swapped on the rails). A reverse-powered hall doesn’t switch → flat, indistinguishable from dead. Flip it → A=35 / B=34 edges, both channels, immediately. ⚠️ The 2026-08-07 resistance-derived pinout had Vcc/GND BACKWARDS. Bench-corrected: orange = Vcc = +5 V, brown = GND (the config that produces clean edges IS correct polarity, by definition). Then the full ESP32 odometry test: flashed firmware. Found there were NO divider resistors in the board (5 V straight into GPIOs; ESP32 clamp diodes saved it) — built the 2k+3k dividers, both channels then counted. Speed sweep: duty 150 → A=3088/B=3620/pos=+342 (TRACKS); duty 190 → pos=+1; duty 230 → pos=−2 and only 110 edges. Fewer edges at higher speed = the divider is bandwidth-limited (R + long leads = low-pass; fast edges rounded off below threshold → missed → quadrature breaks). ⚠️ NOISE PROBLEM found via an F 0 test (motor commanded, NOT moving): phantom counts ran away, B≫A (7864 vs 2806, steady — not coast) → the noise floor is NOT clean once the 12 V stage is energized (the earlier “clean” floor was measured before that coupling; coupling path = motor red/black share the encoder harness + high-Z divider on long leads). ⚠️ ODOMETRY NOT YET SIGNED OFF: pos tracked +342 at duty 150 (real motion punched through) but on a dirty floor. No A3423 needed. Memory updated (wheel-encoder-pinout). Episode 20. (pending: confirm the wheel was truly still during the F0 test — B≫A runaway strongly says noise regardless.)
Session 10 (2026-08-11): THE WHOLE ENCODER TEST METHOD WAS WRONG — these are DYNAMIC (motion-only) hall sensors; static/meter testing can’t diagnose them. Session-9’s “weak disc” verdict is now in DOUBT. Set out to run the session-9 decider (motor #2’s disc on the proven rig). Instead: all evidence pointed at a systematic test error, not dead hardware. Sequence: (1) confirmed power (5.00 V at pads) and both wheel-encoder outputs idle at 4.40 V; (2) a static magnet on the sensors → nothing; motor #2’s disc (the clean control, never magnet-abused) hand-turned → also nothing, flat 4.40 V → this kills the “we wiped disc #1 ourselves” theory (two independent discs can’t both be coincidentally dead); (3) re-verified the wiring from scratch — CONFIRMED CORRECT (brown=Vcc, orange=GND, blue/yellow=outputs; the fingerprint = two signal lines parked together at 4.40 V just below the rail — crossed wires can’t produce that); (4) even a strong neodymium magnet held on the chips → nothing; (5) the roller-brush Hall tacho (a THIRD hall sensor, on a third motor, never touched today) → same 4.40 V, same silence. Three independent sensors, identical dead-flat = systematic, not three dead sensors. ROOT CAUSE (from research): the Neato wheel encoder uses Allegro A3423-class DYNAMIC, DIFFERENTIAL hall sensors — they respond ONLY to a CHANGING (moving) field, NOT to a static magnet, by design. This explains EVERYTHING: static-magnet tests were doomed; session-8’s “magnet gave edges” worked because the magnet was moving in/out (changing field); the meter reads flat 4.40 V on a spin because the real pulses are far faster than a ~3 Hz multimeter can show (it averages them — the same trap as session 8’s “dead-flat” readings). ⚠️ The user’s repeated instinct “does it need to be spinning?” was CORRECT — this dynamic type genuinely needs motion (I wrongly said hall = static detector). Chip correction: the two hall parts (U1/U2) are single-channel 3-pin devices (2 pins one side, 1 the other), two of them making the A/B quadrature — NOT a single dual-channel A3423 (exact part unreadable; behaviour fits the dynamic-differential family). CONSEQUENCES: (a) a DC multimeter CANNOT test these — static gives nothing, spinning is too fast to display; the only valid tests are logic-analyzer/scope on a spin, the ESP32 edge-counter on a sustained clean spin, or a meter’s Hz/frequency mode during a spin (MS8233A likely lacks it); (b) don’t buy linear hall sensors (49E/SS49E — wrong type, static/analog) — the correct test/replacement part is an Allegro A3423 (dynamic dual-channel, community-proven drop-in that reuses the existing disc, mounts next to the old chip). NET: both “SETTLED (weak disc)” and today’s “3 dead sensors” are probably WRONG — sensors likely FINE, never given a test they could pass. The one thing still needing an answer is session-9’s zero-edges-on-a-fast-spin — which the logic analyzer settles in one screen. Sources: linorobot group iA07r2NniK0, mikeferguson/neato_robot issue #9, industrialmonitordirect encoder-test article. Episode 19.
Session 9 (2026-08-10, all day): ENCODER INVESTIGATION SETTLED — three-layer verdict, circuit rebuilt from scratch, ONE suspect part left. ⚠️ SUPERSEDED by session 10 — the “settled/weak-disc” verdict is now in doubt (see above); the disc was never fairly tested because the sensors are motion-only. The session-8 “encoder counts” were RETRACTED: every drive-time count was PWM electrical noise, never rotation (~46–52k phantom edges/s whenever PWM switched — ~1 phantom edge per 20 kHz PWM cycle per channel; proven by counting BELOW the stiction duty where the disc provably doesn’t move, and by coast tests where edges die the instant PWM cuts while the wheel still spins). Root causes, both fixed: (1) the “divider” was miswired as a SERIES resistor (a GPIO draws ~no current → no voltage drop → pins saw the full 5 V, and the pin-side node floated = an antenna next to motor wiring); (2) ground bounce through the daisy-chained ground (motor return + signal reference on shared copper). Software glitch-filtering PROVEN a dead end twice (firmware gained a runtime G <µs> filter cmd — spacing filters just downsample the noise; the dips are as wide as the PWM off-time). Circuit REBUILT on a breadboard, meter-verified at every step: dedicated 5 V rails (5.00 V) → sensor power at the PCB pads (5.00 V) → proper two-stage dividers per channel (2k series + 2k+1k shunt → ~2.9 V high / ~0.4 V low at the nodes) → star ground (− rail = quiet bus; ONE link to the H-bridge GND screw where PSU black lands). ⚠️ Big model correction: the sensor outputs are PUSH-PULL (hard-driven), NOT open-drain with the internal ~2.4 kΩ pull-up the earlier sessions assumed — a shunt-only “divider” does nothing against them (5 V → 4.97 V); the two-resistor divider is mandatory. RESULT: noise = STONE DEAD (full battery: 0 edges at rest, 0 below stiction at F40/80/120, 0 at brake — vs 46–52k/s before) and BOTH channels PROVEN ALIVE — magnet presses fired A=10 and B=10 edges through the entire new chain (first-ever proof of channel B). THE ONE REMAINING FAULT: the magnet disc itself — motor spinning past two proven sensors = zero edges; a magnetised screwdriver over the same sensors = counts. The disc’s field doesn’t trip its own chips (weak/wiped ferrite pole pattern — possibly worsened by the booster-magnet stacking + screwdriver passes this session; ⚠️ keep strong magnets AWAY from the disc from now on). Full evidence: hardware-scripts/test-scripts/hall-probe/ENCODER-DIAGNOSIS.md + diag/HYPOTHESES.md (12-hypothesis ledger) + episodes 17–18. Focus next: hand-spin motor #2’s encoder disc on the proven breadboard rig — if it counts, diagnosis confirmed AND we hold a working spare.
Session 8 (2026-08-09): ENCODER CONFIRMED ALIVE — the step-2 blocker is broken. Answered the session-7 “is it dead?” question the right way: not with the multimeter (which can’t follow the fast toggle and just averages it to a flat blur — every earlier “dead-flat” reading was the meter shrugging, not the sensor), but by counting edges on the ESP32. Added ISR edge-counting to the firmware (A→GPIO32, B→GPIO33) + a host script hardware-scripts/test-scripts/encoder-test.py --spin that drives the motor and prints counts. On a spin, channel A (blue) counted 4→14→101→346→422 edges climbing steadily, pos ticking → SENSOR WORKS. ⚠️ Channel B (yellow) stayed at 0 the whole time (isolated to B’s divider/wiring — NOT a dead sensor). The divider was the real fight: 5 V outputs into a non-5V-tolerant ESP32, and the sensor’s internal ~2.4 kΩ pull-up sits in series so a textbook 1k+2k lands too low (~1.9 V). Only had 330 Ω at first (too small — needs ~10 in series); the kit turned out to be an assortment (2K/5K1/1M, labels on the tape). Tuned by meter to ~3.0–3.4 V idle at the GPIO using a single ~2 kΩ output→GND per channel. ⚠️ Different encoder units have different pull-up values → re-meter the idle when swapping motors; a LOWER pull-up pushes idle HIGHER — keep it under 3.6 V before connecting the GPIO. Also: the bench Uno is confirmed DEAD (didn’t survive the session-7 ICSP short) → used the Elegoo MB102 breadboard power module for the clean 5 V (feed it from the 12 V, not USB, and set the rail jumper to 5 V). Key lesson: zero counts is NOT proof of a dead sensor — but here the shaft+disc were visually confirmed spinning, so A’s 422 is real. Focus next: re-run the working motor to confirm A + chase why B is mute, then swap the OTHER motor onto the identical rig for the same duration to see if ITS sensor counts at all (that one gave 0/0 — genuinely suspect). ⚠️ UPDATE (later same evening): the good encoder REGRESSED after a disconnect/reinstall — now 0 counts at every speed/direction/gearbox state. Root-caused the count chain as PERFECT (manual GND-short of GPIO32/33 flew both A & B counts up), so the sensor isn’t switching under a spin. Leading suspect = encoder 5 V sagging below 4.5 V under motor load (shared 12 V rail). See NEXT ACTIONS #1 — Vcc-sag test first.
Session 7 (2026-08-07): encoder pinout DECODED cold from the meter — but the sensor won’t toggle, and the bench Uno got zapped. Cracked the label-less encoder connector with resistance alone: the matrix on the 4 thin wires showed matched pull-ups brown→blue (2.4 kΩ) and brown→yellow (2.43 kΩ) ⇒ brown = Vcc, blue/yellow = the two open-drain outputs; orange = GND by elimination (brown↔orange open = rails not shorted, safe to power); the wired-pad photo corroborates the order (motor+, Vcc, GND, A, B). Full map (saved to memory wheel-encoder-pinout): red = motor +, brown = Vcc, orange = GND, blue = A, yellow = B, black = motor −. ⚠️ BUT the outputs never toggled — dead-flat at 3.3 V (2.93 V) and at 5 V (4.52 V off ESP32 VIN, then 4.57 V off a clean 4.96 V Uno rail), whether hand-turned, inched, or “spun.” Research reframed it: it’s an 8-pole magnet disc + a unipolar A3144-class Hall switch → needs ≥4.5 V (so every 3.3 V attempt was doomed) and only pulls LOW when a SOUTH pole faces it; 8 poles = one edge per 45°. Two unknowns left open: (a) only a fridge magnet on hand (~50 gauss, below the A3144 trip point) — its silence proves nothing; need a strong HDD/speaker neodymium magnet; (b) never actually watched the motor shaft physically rotate during the “spin under power” tests, so the magnet may never have moved. Then the night ended badly: the encoder GND wire fell onto the Uno’s ICSP header (5 V + GND adjacent) → shorted the 5 V rail, flash, all LEDs died. Unplugged at once; Uno health untested (may survive on USB — packed away for the weekend). Also this session: revived the button/UI-board thought experiment (see queued discussion) and re-ran the motor demo. Focus Monday: strong magnet + confirm the shaft spins → settle whether the sensor is dead or has simply never seen a moving magnet.
Session 5 (2026-08-06): talk session — nailed down HOW the salvaged PSU powers the step-2 bench rig. Procedure: jump PS_ON (green) → any GND (black) on the 24-pin to force the PSU on without a motherboard; tap +12V (yellow) → DRV8871 VIN, GND (black) → DRV8871 GND. Wheel motor is 14.4V nominal but runs fine on 12V for a bench encoder test; 12V rail sources 15–18A ≫ 2.4A stall. Gotchas captured: (a) older PSUs may need a minimum load on 5V (red→black, ~10Ω) or they click off; (b) ⚠️ Dell OptiPlex proprietary-pinout risk — some OptiPlex PSUs DON’T follow ATX colour/pin standard, so green→black can short. CHECK THE CONNECTOR BEFORE JUMPING: standard 20/24-pin ATX colour mix = safe; odd/all-black/8-pin main = likely proprietary, look up the model first. Always meter yellow→black for ~12V before wiring the motor. Drew the step-2 bench-hookup Mermaid, then refined it into a two-zone layout after settling the breadboard question: MUSCLE zone = direct wire + the DRV8871’s 3.5mm screw terminals (12V→VM, OUT→motor, GND→PSU) — the motor’s ~2.4A stall must NOT run through breadboard clips (rated ~1–2A; they heat, sag, and bounce → glitchy encoder counts + brownouts); BRAINS zone = breadboard for the mA-level logic (IN1/IN2, encoder A/B, Vcc/GND, the logic ground reference). 12V itself is fine for a breadboard — the limit is CURRENT, not voltage. Add a bulk cap (100–470µF, ≥25V) across VM/GND at the driver for inrush/back-EMF — reservoir that covers the millisecond stall/inrush surge the PSU can’t push through the wire fast enough, and soaks up PWM-switching + back-EMF spikes (which can ring above 12V). NO PURCHASE expected — check the Elegoo kit first: it ships electrolytics, usually 10µF + 100µF, often 50V. A 100µF (parallel two for 200µF) works; only disqualifier is a <25V rating. Stripe/short leg = −/GND. Buy only if the kit’s caps are under-rated/missing (any 100–470µF ≥35V, pennies). 🔑 Common ground = one real wire (DRV8871 GND → PSU GND → ESP32 GND); the breadboard rail carries only the signal reference, never motor return current. This split is how the final robot wires too, not a bench-only hack. Zoned bench-hookup diagram (clickable, self-contained): https://mermaid.live/edit#pako:eNqNVVtvokAU_isnPNG0qOBu2jTbJqAsMREwXtiHZR9GGJWUizsMmqbtf98zYLU4muw8DPF858x837mMb0pUxFR5BGWVFvtoQxiHuRXmgKuslmtGthuY-L_s6e9QcRezwdgGDeKE0YjDHj9wC2XE6B44ZVmSk7QE1fPnsGSUxMuCsPgmVP40B4rVhCZFDuPpyTqZLXQDb8Av3OpGAOorTZFQOxZR5-DkeENQlymJXtouA3OCHssqfYGIbEHv9bRv973qJ5CIFWUJgdutQ5-fjO9BO_QodzgNxCH4eXi411FuWyCUSUxbkWIFLoYELnQhGHkSOsRLEcddgnwdAX8x12XEaBCjhdA8Pv1w_Tn6eJTwAvYbSlPICl4wLEoXeat8k0QvwGjcvZCqOufw9PT8Xmd8RZqCvqOUdsprH5G1k4vQ00o6aJ0mwx2tFX8GteN8XRwtRHyxGW1bLfesHy3fnIp0WlNz5M1Q6KnZQE2LdRJhAmguOpvdQWZqKd3R9H_68GsPjFs90OvoSQ4bvIeyyx0w8kQlcb-AGDViSMj42BefxBld3Vyv95GfPRMtinvfAHUxs7RtsadYaFixIoOUbHmxvZGucwRDZzLyQXuGS0wdo-UgE3bMT4cfGpgybH2BLQm2-33E-51-IEPOhfm4ot0boG_T7ocqg_q3IjEjvGJUlm0HAxERRBGoO8qS1SsgB9iVcP4GNEyuzqot1MuqbaH6gloTNEyjY557N2ZLyk1tF2RlQg3kXEmNo9e4qOjJZhxshjRIYgrroQ6Vge-6vgfO1F-IS7Cz83VK6ym_g7qnYJ3HkFHKSwTX4v1DQ6i8H9mMa3qdmp9yB0qG7yRJYvGf8hYqfEMznJVHCJWYrkiV8lD5EG6k4sXsNY8Q4qzC25RqGxNOhwnBMmcH88c_F_K8Qw — not yet a repo file. Focus still: build step 2 (encoders → odometry) — now with the PSU-power path fully understood; first physical step tomorrow = verify the OptiPlex connector isn’t proprietary.
Key confirmed facts (from hands-on teardown)
| Thing | Finding |
|---|---|
| Main application processor | NXP i.MX 8M Nano SoloLite (MIMX8MN1DVTJZAA, industrial) under the RF shield — 1× Cortex-A53 (Linux) + 1× Cortex-M7 (real-time), no GPU/VPU/NPU. + NANYA DDR3L + Kingston 4 GB eMMC + NXP PCA9450B PMIC. A Linux computer, but boot chain locked → discard. Photos: board-imx8m-soc.jpg, board-nanya-dram-and-lpc51u68.jpg, board-kingston-emmc.jpg, board-soc-cluster-overview.jpg. |
| Real-time MCU | NXP LPC51U68 (Cortex-M0+) — the body controller, not the whole brain. Discarded with the board. |
| Mainboard P/N | 520-0394 Rev.B, USB-C “SW Update” service port. Discard (reuse ruled out). |
| LiDAR | Neato LDS 2.2 (290-1044 REV 4, © 2019). 8N1 UART, 3.3 V, 115200 baud. J2 MAIN (5 V power + serial), J3 MOTOR (host-driven PWM, closed-loop off reported RPM). Protocol documented (Piccolo/XV-11 family) — expect a match on capture. |
| Battery | Li-ion 14.4 V nominal 4S2P, 6200 mAh / 89 Wh. Real range 12 V (empty) → 16.8 V (full). “Smart battery” — BMS / protection / balancing lives INSIDE the pack (D-series moved it off the mainboard), talking SMBus (I²C-like) to a TI fuel-gauge IC, with serial-number authentication. 6-pin colours: red, white, black, black, blue, yellow (all same gauge) — likely red = +, black×2 = − / gnd, yellow = thermistor, blue + white = SMBus data/clock. The charge circuit was on the discarded mainboard. Measured 2026-08-06: initially red↔black = 0 V (BMS FET latched); RE-READ 14.23 V → FET un-latched, cells ALIVE. Pack OPENED 2026-08-06 — cells = Samsung INR18650-35E (3500 mAh, 18650), 4S2P (8 cells, factory spot-welded nickel — KEEP those welds). OEM smart BMS = small GLBLi9 PCB in the centre plastic spine (where the 6-pin harness terminates), joined to the cells by SOLDERED taps → removable/reusable with just a soldering iron, no spot-welder. Bay = triangular prism ~4 cm/side × 22 cm (~150 cm³, ~8× 18650). Dock = 19.5 V / 1.5 A (label 905-0575). Full battery/charging plan (reuse cells + dumb BMS) in the Battery/charging note below. |
| Drive wheel motors | 260-0016, 14.4 V, brushed DC. Stall MEASURED: L ≈ 2.1 A (R 6.7 Ω), R ≈ 2.4 A (R 5.9 Ω). Matched pair. → DRV8871 ×2 (ARRIVED). |
| Wheel encoders | ✅ ALIVE — confirmed 2026-08-18 via logic analyzer (A=35 / B=34 edges, both channels, hand-twist). Every prior “dead” verdict was WIRING, not the sensor. ⚠️ CORRECTED WIRING — the session-7 pinout had Vcc/GND BACKWARDS: 🟠 orange = Vcc = +5 V · 🟤 brown = GND (−) · 🔴 red = motor + · ⚫ black = motor − · 🔵 blue = A → analyzer CH0 / ESP32 GPIO32 · 🟡 yellow = B → CH1 / GPIO33. POWER: ORANGE→+5 V, BROWN→ground. Reverse it and both channels read flat HIGH = looks dead but isn’t → meter orange = +5 V before every session. No A3423 swap needed; odometry (step 2) UNBLOCKED. Test tool: hardware-scripts/test-scripts/encoder-la.py (fx2lafw logic analyzer). (Below = superseded history, kept for the trail — note it repeats the OLD backwards brown=Vcc/orange=GND; the table above is correct.) LEGO WHEEL ENCODER ASY: 915-1055 REV, STD-3. Solid disc → magnetic (Hall). Harness CONFIRMED: 6 wires = 2 thick power (red/black) + 4 encoder = QUADRATURE (Vcc/GND/A/B), direction-aware. Good for odometry + slam_toolbox. The motor is powered THROUGH this board (motor terminals soldered to the encoder PCB); red+black = motor power (continuity-confirmed 2026-08-07) → the other 4 (brown/orange/blue/yellow — an earlier “black” note was wrong) are the encoder set. Board silk = LEGO WHEEL ENCODER ASY: 915-1055 REV, STD-3, C3, © 2020 — NO voltage or pin labels (an earlier “ENCODER 5V” note was a MISREAD — corrected 2026-08-07). PINOUT IDENTIFIED 2026-08-07 (unpowered resistance test + photos encoder-board-wired-pinout-1/2.jpg) — full 6-wire mapping in pad order: red = motor +, brown = Vcc, orange = GND, blue = A, yellow = B, black = motor −. Method: two matched ~2.4 kΩ pull-ups (brown→blue and brown→yellow) = Vcc feeding two open-drain quadrature outputs; orange = GND by elimination; brown↔orange open = Vcc/GND not shorted (safe to power). Needs ≥4.5 V supply, 5 V logic → NOT ESP32-safe, must divide before a GPIO. ⚠️ CORRECTED 2026-08-10: outputs are PUSH-PULL (hard-driven both ways), NOT open-drain behind that ~2.4 kΩ (that resistance was an unpowered artifact) — so a shunt-only resistor does NOT divide (metered 5 V→4.97 V); the required divider per channel = 2k SERIES + 2k+1k SHUNT to GND, GPIO taps the midpoint → ~2.9 V high / ~0.4 V low (meter the node ≤3.4 V before connecting a pin). Sensors behave latch-like (level flips and holds per magnet pole). ⚠️ Session-8’s “422 edges / SENSOR ALIVE” verdict RETRACTED 2026-08-10 — all drive-time counts were PWM noise (ground bounce + the series-resistor miswire); see session-9 paragraph. ✅ What IS proven (2026-08-10, on the rebuilt breadboard circuit): BOTH channels A AND B work end-to-end (magnet press → A=10 / B=10 edges, clean level flips) and the circuit is noise-free at every duty. ❌ A3423-class): they respond ONLY to a MOVING field, not a static magnet — so a held magnet, a slow hand-turn watched on a slow meter, and a DC-meter reading of a spin ALL fail by design, regardless of sensor/disc health. Three independent hall sensors (both wheel encoders + the roller tacho) all read identical flat 4.40 V to static tests → systematic method error, not dead hardware. The disc was never fairly tested. The U1/U2 parts are two single-channel 3-pin hall chips (not one dual-channel A3423; exact P/N unreadable). ⚠️ Keep strong magnets away from the disc anyway (ferrite pole patterns erase). Correct test = logic analyzer / ESP32 counter on a SPIN (multimeter is the wrong instrument). Replacement if needed = A3423 (dynamic dual-channel), NOT linear hall (49E/SS49E). Firmware = esp32-firmware (A→GPIO32, B→GPIO33, ISR count, E/Z/G <µs> cmds); test via hall-probe (Rust) or hardware-scripts/test-scripts/hall-probe/diag/quad-diag.py (noise battery; below-stiction phases must read 0). Full evidence: hall-probe/ENCODER-DIAGNOSIS.md, diag/HYPOTHESES.md. Photos: encoder-board-wired-pinout-1/2.jpg, encoder-board-silk-1/2/3.jpg, encoder-board-connector.jpg. |
| Roller brush motor | 905-0460-RoHS 14.4VDC, brushed DC. Winding R MEASURED: ~1.9 Ω → stall ≈ 7.6 A. Beefy — 3× the wheels. → Cytron MD13S (ARRIVED). Harness: 2 thick power + 3 thin = Hall tacho (Vcc/GND/signal) → free brush-jam / RPM detection into an ESP32 GPIO. Bidirectional driver → auto-unjam in software later. Tacho supply voltage TBD. |
| Side brush motor | Small brushed DC can, front-corner sweeper. 2 wires only, EMI cap, no sensor. Winding R MEASURED: 20–30 Ω → stall ≈ 0.5–0.7 A. → logic-level MOSFET module (ARRIVED). Photo: brush-motor.jpg. |
| Blower/vacuum | EVERFLOW F121225BU (AFX19bR) — DC14.4V 2.0AMP on the label. …BU = 4-wire PWM family → brushless w/ integrated driver, needs NO H-bridge. 4 wires CONFIRMED. |
| Front bumper switches | BUMP SWITCH 290-0056 REV.8 — mechanical tactile click-switch. User counts 4. CONFIRMED 2026-08-06: normally-open (beeps when pressed) → GPIO + INPUT_PULLUP, active-low, LOW = hit, ~5 ms debounce. Photo: front-bumper-switch.jpg. |
| Wheel-drop / lift switch | DT-08 lever microswitch (3A 125VAC), wheel arch — the “dead-man’s” switch. CONFIRMED 2026-08-06: normally-open. Mechanism: on the ground the wheel is held up → lever open (HIGH); lift the robot → wheel falls → presses the lever (closed, LOW). So LOW = wheel dropped / robot lifted → stop drive. Same wiring + polarity as bumpers (GPIO + pull-up, active-low). One per drive wheel. Photo: wheel-arch-dead-mans-switch.jpg. |
| Cliff / drop sensors | LOUIE DRP SENSOR 290-1023 REV 2 (© 2017) — downward IR reflectance, discrete IR emitter + phototransistor (two clear windows). CHARACTERISED 2026-08-06 (via prior art + teardown): runs on ~3.3 V (signal swings 0–3 V → native ESP32 ADC, no level shifter); wire convention black = GND, red = Vcc, brown/yellow/green = emitter-drive + signal. Host-driven: emitter is strobed/modulated + phototransistor sampled in sync (ambient rejection) — replicate in firmware; a static DC diode test is defeated by an on-board decoupling cap (don’t bother). ≥2 sensors (two gang into a 10-pin at the old board; vendors sell as a “2× set”). Connector = JST ZH 1.5 mm, 5-pin. Photos: under-carriage-sensor.jpg, cliff-sensor-front.jpg, cliff-sensor-connector.jpg, cliff-sensor-harness-cut-tails.jpg. |
| Side / wall sensor | CONFIRMED 2026-08-06: same LOUIE DRP 290-1023 board as the cliff sensor (silkscreen matches). Same ~3.3 V analog reflectance, same ADC + firmware strobe/sample treatment. Overrides the forum “Sharp GP2Y0A51 wall sensor” lead — that does not apply to this D10. One sensor type covers cliff + wall. |
| Rear speaker | Small speaker at the back. Keep/drive for audio cues (stuck/docked/bin-full). Plan: class-D mono amp (PAM8302-class, ARRIVED) fed from an ESP32 DAC pin. Nice-to-have, no build dependency. |
| Multimeter | MS8233A, 2000 counts. 10 A jack IS fused (MAX 10A FUSED). Ω 200 range = 0.1 Ω resolution. Diode mode open-circuit ~2 V (so a charging cap reads ~1.7 then OL — a false “diode”). |
| Bench PSU | Salvaged Dell B255ES-01, 255 W, 80+ Bronze (Bestec, DP/N 02XK8W) — PROPRIETARY OptiPlex SFF, NOT ATX. 12V-only (+12VA/+12VB, ~21 A total; no usable 5V/3.3V rail). 8-pin P1: white ×3 = +12V switched, black ×2 = GND, lime-green = PS_ON#, purple = +12V standby, 1 empty pin. Turn-on = green→black bridge. Verified: standby 12.04 V, switched rail 12.0 V. Also a yellow+black peripheral lead = 12V tap (cut + strip → DRV8871 VM). ⚠️ no 5V rail → keep-alive load (if it clicks off) goes on 12V, not 5V. |
Important gotcha: the “19.5 V 1.5 A” device plate is the dock charge input, NOT the battery. Tap the 14.4 V battery for the buck converter.
Winding-resistance method (proven): alligator clips (not hand probes), rotate shaft → STOP → settle → read lowest stable value. Subtract 0.1 Ω leads. stall ≈ 14.4 V ÷ R.
Cliff-sensor probing (learned the hard way): don’t try to DC-diode-test it. The fine-pitch JST fouls Dupont, the old board’s ESD clamps contaminate readings through the ground plane, and an on-board cap fakes a diode. It’s an analog, host-strobed reflectance sensor — to finish the pinout, power it and observe, don’t meter it statically.
Architecture (decided)
- Home Assistant — scheduling + notifications. Robot = HA MQTT Vacuum entity.
- Raspberry Pi 4 — the brain. ROS 2 + Nav2 + slam_toolbox + a ~100-line MQTT↔ROS bridge node.
- ESP32 (micro-ROS) — real-time co-processor: motor PWM, encoder counting, sensor polling (bump/drop switches, cliff/wall ADC), battery voltage. Joins the ROS 2 graph over USB serial.
Build infrastructure — set up & proven (2026-08-06)
Step 1 (toolchain + first motor) is DONE. What exists on the bench + in the repo:
| Thing | State |
|---|---|
| ESP32 board | ESP32-D0WD-V3 (rev 3.1), dual-core 240 MHz, Wi-Fi+BT, 4 MB flash. CH340C USB-serial, no driver needed on macOS. Auto-reset via RTS works — no BOOT/EN dance. Pins labelled D25/D26/D27 = GPIO25/26/27. |
| Serial port | ⚠️ The port name CHANGES on replug. Currently /dev/cu.usbserial-110 (2026-08-10; was -10 earlier same day — ⚠️ a stale usbserial-21420 node is a red herring). Always ls /dev/cu.usbserial* first. The diag python scripts auto-detect the port; hall-probe (Rust) + platformio.ini hard-code it — update/override on change. |
| Toolchain | PlatformIO (chosen over Arduino IDE — smoother road to micro-ROS). Project venv at .esp-venv/ (gitignored). Xtensa toolchain cached. |
| Firmware | esp32-firmware/ — platformio.ini (board esp32dev, 115200) + src/main.cpp. Now the STEP-2 DRV8871 driver (rewritten from L293D 2026-08-07): no EN pin — two LEDC PWM channels, one on IN1 (GPIO26) + one on IN2 (GPIO27), 20 kHz, 8-bit. PWM rides on the active-direction input, the other sits LOW; brake = both HIGH; coast = both LOW. F/R/S/B protocol unchanged. LED (GPIO2) heartbeats. Build+flash: .esp-venv/bin/pio run -d esp32-firmware -t upload --upload-port <port>. Flashed + proven driving a real wheel motor 2026-08-07. ✅ ENCODER COUNTING ADDED 2026-08-09 — ISR on GPIO32 (A) + GPIO33 (B), CHANGE edges, signed quadrature pos; new serial cmds E (print counts) / Z (zero); streams [enc] A=.. B=.. pos=.. while counts change. ✅ GLITCH-FILTER CMD ADDED 2026-08-10 — G <µs> sets a stability filter in the encoder ISR (G 0 = off/default; suppressed count reported in E output). Kept as a tool; unnecessary on the clean breadboard circuit. ⚠️ Session-8’s “proved channel A on a spin” = retracted (noise). Still TODO: kickstart pulse / min-PWM floor. |
| Bench test harness | hardware-scripts/test-scripts/motor-test.py (+ README). --demo ramp+reverse, --cmd "F 200" one-shot, no args = interactive motor> prompt. --port <port> to override (default -110 is stale — pass the current port). Run via ../../.esp-venv/bin/python3 motor-test.py. ⚠️ docstring/DEFAULT_PORT still say step-1/L293D + -110 — cosmetic, update when convenient. encoder-test.py added 2026-08-09 — same folder: --spin [--duty N --secs N] drives + counts edges → ALIVE/PARTIAL/DEAD verdict; --watch streams counts while hand-turning; --cmd "E" reads once; no args = interactive. Autodetects the port. |
Step-1 bench rig (retire it — it was only the toolchain proof): ESP32 D25→L293D EN, D26→IN1, D27→IN2, VIN→Vcc1, motor across the bridge, common ground. Do NOT put a real wheel motor on the L293D (stall >2 A cooks it) — that’s what the DRV8871s (now arrived) are for.
Motor driver plan — COMPLETE, VETTED & ARRIVED
Full vacuum = 5 motors, 4 drivers (blower needs none).
| Motor(s) | Stall | Driver | Confirmed part | Status |
|---|---|---|---|---|
| 2× drive wheels | ~2.1 / 2.4 A, quadrature encoders | 2× DRV8871 | Adafruit ADA3190 (6.5–45 V in, 3.6 A peak, IN1/IN2, default 30 kΩ Rlim ≈ 2 A) | ✅ ARRIVED + PROVEN — one drove a real wheel motor bidirectionally on the bench 2026-08-07 (12V PSU rail, 20 kHz PWM). |
| Roller brush | ~7.6 A, + Hall tacho | 1× Cytron MD13S (bidirectional) | Pi Hut SKU 106189, 6–30 V, 13 A cont / 30 A peak, 3.3 V & 5 V logic, PWM+DIR, 20 kHz | ✅ ARRIVED |
| Side brush | ~0.5–0.7 A, 2-wire | 1× logic-level MOSFET module | DFRobot Gravity MOSFET Power Controller (3.3 V trigger, VIN 5–36 V/20 A). ⚠️ 1 kHz max → on/off only (fine for side brush). ⚠️ add flyback diode (Elegoo kit). | ✅ ARRIVED |
| Blower/vacuum | 2.0 A, 4-wire PWM brushless | NONE — GPIO PWM (~25 kHz) + free tach | — | ✅ resolved |
Blower wiring (verify before trusting): black = GND, red = +14.4 V, yellow = tach out, blue = PWM in. Do not H-bridge it; do not measure its winding resistance.
Key principle: size drivers to stall current, not running. (OEM XV-11 drove the wheels with a ~2.8 A A3950, so DRV8871 at 3.6 A is correctly sized.)
Buck converter / 5 V rail — PART ORDERED (⚠️ in transit)
Chosen: 7 A switching UBEC (RC-style, 2–7S in, 5.25 V ±0.5 V out, 300 kHz, ~92% eff., shielded). Feeds Pi 4 + 2× ESP32 + LiDAR (~3.5 A sustained est.).
- ⚠️ Setup step: output is fixed ~5.25 V — meter it first, confirm ~5.1–5.3 V, then wire the Pi.
- ⚠️ On arrival: confirm it’s the 7 A variant (multi-variant listing “FPV RC UBEC 5V 3A 5A 7A 15A”).
- Rejected: 3 A modules (brown out a Pi 4 under Nav2). XL4015 = bench-only. Pololu D36V50F5 correct but £38.
Elegoo kit — what’s useful here
- L293D (dual H-bridge, ~600 mA/ch) → step-1 toolchain proof only (now done). Too weak for final use.
- PN2222 NPN (×2) + flyback diode (×2) → LiDAR spin-motor drive circuit. Diodes also cover the side-brush MOSFET flyback if the Gravity module lacks one.
- Thermistor → reference when mapping the battery 6-pin connector.
- UNO R3, breadboard, jumpers, sensors → general bench use.
Parts status
Already have: Pi 4, soldering iron+solder, multimeter, breadboard, Arduino/Elegoo kit.
Arrived 2026-08-06:
- ✅ 2× ESP32 WROOM-32 (USB-C, CH340C) — one flashed with step-1 firmware, on the bench.
- ✅ 2× Adafruit DRV8871 (ADA3190) — drive wheels.
- ✅ 1× Cytron MD13S (SKU 106189) — roller brush.
- ✅ 1× DFRobot Gravity MOSFET Power Controller — side brush.
- ✅ Speaker amp (PAM8302-class class-D mono) — rear speaker audio cues.
→ The whole power stage is now in hand. Build step 2 is unblocked.
Battery/charging BOM (specced 2026-08-06, reuse-cells path) — parts now selected, procurement in progress:
- 4S 16.8 V BMS, balancing, common-port, ≥20 A discharge → ✅ ORDERED: DollaTek 4S 16.8V 40A (common-port, w/ balancer), ETA ~2026-08-13. Common-port confirmed from product photos (single P−/P+ ⊖/⊕ pads, no separate C−; 43 Ω
430bleed balancers). 40 A = deliberate headroom over the ~16 A worst-case transient (roller stall 7.6 A + wheels + blower + 5 V rail). - CC/CV step-down charger, in ≥19.5 V → out 16.8 V, ~1.5 A → ✅ ORDERED: XL4015 CC/CV buck board (5–32 V in, 0.8–30 V / 0–5 A out, two pots = voltage + current, Blue LED = charging / Red LED = full, ~95% eff). Input range covers the 19.5 V dock feed; 5 A ≫ 1.5 A charge. Fed from the reused dock harness (19.5→16.8 V = 2.7 V drop, fine for a buck).
- ⚠️ Bench setup on arrival — do this BEFORE connecting the cells, on a bench PSU:
- Feed ~19.5 V into IN +/−.
- Turn the voltage pot until OUT reads 16.8 V (no load).
- Short OUT through an ammeter, turn the current pot until it limits at ~1.5 A (the CC setpoint).
- Then wire to the pack. Blue LED = charging → flips to Red = full when the pack hits 16.8 V and current tapers.
- ⚠️ Bench setup on arrival — do this BEFORE connecting the cells, on a bench PSU:
- Voltage sensor / divider → NO PURCHASE, build from the Elegoo kit. The generic “25 V voltage sensor module” divides by 5 → 3.36 V at 16.8 V, just over the ESP32’s 3.3 V ADC ceiling (clips the full-charge end). Instead build a divider: R1 = 10 kΩ (top, to batt+), R2 = 2 kΩ (bottom, to GND) → 16.8 V→2.80 V, 12 V→2.00 V (clean range, low source impedance). Alt if no 2k in kit: 5.1k+1k → 2.75 V. Add 100 nF across R2. ~1.4 mA draw (negligible vs 89 Wh; gate the divider GND via a spare GPIO if zero-drain wanted).
- Inline fuse (pack-output over-current backstop) — ✅ ORDERED: Bst4U inline fuse holder 6-pack (14 AWG) + 100pcs blade fuse kit (2A–35A incl. 20A), ASIN B087BT1PH3. Install a 20 A blade fuse on pack
+out → holder → BMS P+/load bus. ⚠️ Install before unattended/autonomous running (safe to defer during supervised bench work — BMS still covers dead-short + over/under-voltage). The 40 A BMS trip is coarser than the cells’ ~16 A continuous safe limit (35E ~8 A ×2P), so this fuse tuned to the pack is the real over-current protection; the BMS handles short-circuit + over/under-voltage + balancing. - Pack disconnect — SALVAGE FIRST (don’t buy yet): plan to reuse a Neato connector for the keyed pack disconnect — best candidate is the OEM battery 6-pin harness shell, re-terminated onto the new BMS output (it carried full pack current in OEM use). ⚠️ Verify during the BMS swap: (a) power-pin wire gauge rated for ~16 A peak, (b) polarity-keyed. XT60 as fallback only (YIXISI 10-pair, ASIN B0CF89QJGR, £6.99) if salvage is under-rated/fiddly. JST-XH balance connector dropped — on-bot BMS balances via soldered cell-tap wires (add a JST-XH port only if you later want an external iMAX balance-charger). Reuse the 2-pin dock-contact harness as the charge input.
- Hookup wire — SALVAGE FROM PC PSU (buy silicone only as fallback): need stranded wire for the four high-current runs (BMS B+/B− → pack, P+/P− → fuse/disconnect/bus; ~16 A peak). Plan: harvest the UNUSED PSU peripheral cables (SATA/Molex/spare PCIe — stranded, ~16–18 AWG, yellow=12V / black=GND); double-up 18 AWG for the ~16 A path. Keep the 24-pin + one PCIe cable INTACT so the PSU doubles as the step-2 bench supply (jump green PS_ON→black to power on — a genuinely useful free bench PSU for driving the wheel motor + DRV8871). ⚠️ Must be STRANDED — do NOT use UK twin-and-earth: it’s rated for the current (2.5mm²≈13 AWG, 20–27 A) but solid core fatigue-cracks under robot vibration; T&E only OK for static/bench runs. Fallback: Gruiqrd 14 AWG silicone (ASIN B0CZRG3QMK, £10.89) or £9.59 Amazon’s Choice 14 AWG 5m. The 3 middle balance taps (~100 mA) use thin salvaged wire / Elegoo jumpers (no purchase). ⚠️ Elegoo Dupont/jumper wire (~24–26 AWG) too thin for the power path.
- Bench safety for first charge — NO PURCHASE (LiPo bag dropped): user stores/charges the pack in a terracotta plant pot (fired clay = non-combustible → covers the fireproof-container job). Cells are steel-can 18650s + first charge supervised. ⚠️ During charge: sit pot on a ceramic tile / metal tray (drainage hole = spark escape) + keep clear above/around (thermal-shock crack; upward flame jet). 4S low-voltage alarm also dropped (needs a JST-XH balance lead we removed; MS8233A covers per-cell checks).
- Design note: CC/CV module does the 16.8 V limiting (the charging); the BMS is the backup overcharge/imbalance cutoff + balancer; the fuse is the pack-tuned over-current backstop. Belt-and-suspenders, correct.
- Still to buy: NOTHING confirmed outstanding — battery BOM is fully sourced. (BMS + charger + fuse ordered; voltage sensor = build from kit; pack disconnect = salvage OEM connector; bench-safety = terracotta pot; hookup wire = salvage PC PSU peripheral cables. Buy triggers ONLY if a salvage proves inadequate at the BMS-swap bench session — fallbacks: XT60 B0CF89QJGR, 14 AWG silicone B0CZRG3QMK.)
Ordered (still arriving):
8-ch 24 MHz logic analyzer✅ ARRIVED + WORKING (session 11) — cheap FX2 clone, driven viasigrok-cli+fx2lafw(Homebrew; ⚠️ PulseView no longer packaged in brew — CLI only, or grab the macOS DMG from sigrok.org). Tool:hardware-scripts/test-scripts/encoder-la.py. Already settled the encoder; still gates LiDAR decode (step 3).- 1× 7 A switching UBEC (eBay
26-14963-25714, £6.60, ETA 10–17 Aug, slowest part). ⚠️ confirm 7 A + meter to ~5.25 V before wiring the Pi. Not on critical path (bench is USB-powered). - Heat-shrink assortment, Dupont jumper assortment.
- [NEW session 11 — for the encoder divider] Metal-film 1/4 W 1% resistor kit (full leads, tight tolerance to match A/B channels) + perfboard/stripboard + 2.54 mm breakaway male header pins to solder the two dividers as a tidy plug-in module. UK: resistor kit ASIN
B07VCWT1F2(600pc, 30 values) or proto-pic.co.uk equivalent. Values needed:680Ω+1kΩ(or470Ω+680Ω) per channel. ⚠️ kit lacks a 1.5 kΩ value — use 680/1k or 470/680 from what it has. Plus small ceramic caps (1–10 nF) per channel for node→GND RC filtering (⚠️ check the Elegoo kit first — it ships assorted ceramics, likely no purchase needed).
No longer needed:
JST ZH pigtail for the cliff sensor— the harness was cut and the sensor keeps its native mated 5-pin plug as a permanent pigtail. (A JST kit is still handy for encoder/tacho/battery plugs — pitches TBD.)
On hold until measured / confirmed:
- Logic level shifter — cliff/wall sensors resolved (3.3 V, none needed); only if the encoders / roller tacho turn out 5 V logic (TBD).
- JST kit for encoder/tacho/battery plugs — blocked on measuring those pitches.
- T10 Torx long-reach driver — recessed case screws (optional).
- Standoffs/mounts — blocked on measuring free internal space + thermal plan.
NEXT ACTIONS (resume here)
✅ DONE session 11 (2026-08-18): encoder ALIVE + wheel odometry CONFIRMED at low speed; pinout corrected; analyzer tooling stood up. Logic analyzer working via sigrok-cli + new hardware-scripts/test-scripts/encoder-la.py. Root cause of ALL prior “dead” reads = reversed encoder power (the 2026-08-07 pinout was backwards): corrected to orange = Vcc = +5 V, brown = GND. Full ESP32 test: dead-clean zero noise floor (star ground holds — session-9’s phantom counts gone), both channels count, and pos tracks (+342) at duty 150 → odometry works at vacuum speed. Divider is bandwidth-limited (loses edges above ~duty 150 → pos stops tracking). No A3423 needed. Stiction confirmed breaks at ~duty 150 (not 255).
⚠️ Bench rig as left: breadboard, encoder powered orange→+5 V / brown→GND (⚠️ meter orange = +5 V FIRST — reversed = flat/looks-dead), A(blue)→2k series + 3k shunt→GPIO32, B(yellow)→same→GPIO33, ESP32 GND on the common star (ONE link to the H-bridge/PSU GND screw; never two = loop), DRV8871 IN1→26 / IN2→27, motor red/black on DRV8871 OUT, 12 V from the Dell PSU, port usbserial-110. Firmware flashed (E/Z/F/R/S/B/G cmds; leave G 0). Test tools: encoder-la.py (analyzer) + encoder-test.py --spin (ESP32 count).
- [TOP PRIORITY — the one thing left for clean odometry] Rebuild the divider for NOISE and bandwidth. Two faults in the current
2k+3kbreadboard divider: (a) bandwidth — long leads = low-pass, drops fast edges (pos tracks at duty 150, breaks at 190/230); (b) noise pickup — theF 0test showed phantom counts under power (motor wires couple into the high-Z nodes). Fix both:- ⭐ MODEL IT IN FRITZING FIRST (user bought Fritzing 2026-08-18) — lay out the divider module (schematic + breadboard view) and sanity-check values/topology before touching hardware. Optionally sim the RC behaviour in Falstad (falstad.com/circuit): confirm ~3 V node + that the node cap passes a ~kHz edge but rolls off the HF coupling.
- Lower values + short leads, soldered onto perfboard as a plug-in module (male headers so it plugs into the breadboard): per channel
680Ωseries +1kΩshunt (~3.0 V node, ~3 mA) or470Ω+680Ωfor more bandwidth. Meter each node ~3.0 V (<3.3 V) before connecting a GPIO. - Add a small node→GND cap (~1–10 nF) per channel — RC low-pass that kills the HF coupling but passes the slow real edges (this is the missing piece).
- Route encoder wires away from the motor red/black (they share the harness = the coupling path).
- Verify order:
F 0must read a FLAT 0 first (clean noise floor under power), then re-run the speed sweep (encoder-test.py --spin --duty 150/190/230) —posshould track at ALL speeds. Only then is build step 2 closed. - Parts to buy: metal-film 1/4 W resistor kit + small ceramic caps (1–10 nF) + perfboard + header pins (see Parts status).
- [After the divider — step 2 then DONE] Wire odometry into micro-ROS / the ROS 2 graph. ESP32 publishes wheel odometry (counts +
pos) over USB serial → feeds slam_toolbox / Nav2 on the Pi. - [Firmware polish, do alongside] kickstart pulse + min-PWM floor (
motor-pwm-calibration) — note stiction breaks at ~duty 150 (confirmed session 11), so the floor/kickstart can be gentler than the old “F255” assumption.G <µs>glitch filter stays available but unnecessary on the clean star-ground circuit — leaveG 0. - [Quick, ~10 min, do when convenient] Finish the cliff-sensor pinout functionally. Power the sensor: red → 3.3 V, black → GND; then tie each of brown/yellow/green high through a resistor while watching the emitter window through a phone camera (IR shows as a purple/white glow) → that’s the emitter-drive wire; the remaining wire read on the ADC is the signal. Resolves the last residual. (Not blocked on anything — just wasn’t worth grinding with a meter.)
- [Roller-tacho still open] Tacho Vcc. Wheel-encoder supply is settled: 5 V, breadboard-verified. The roller-brush Hall tacho supply (3.3 vs 5 V) is still unmetered → decides level-shifter need on that signal line when the roller goes on the bench.
- [When logic analyzer arrives] LiDAR bring-up: power the LDS, drive spin motor (PN2222 + diode), clip analyzer on
J2, confirm LDS 2.2 packet format vs documented Piccolo/XV-11. - [BOM SOURCED 2026-08-06 — awaiting parts, then SWAP] Battery: reuse OEM cells + separate on-bot BMS (option 3). ✅ (a) charge test: red↔black climbed
0 V → 14.23 V→ OEM cells ALIVE / reusable. ✅ (b) cell type:Samsung INR18650-35E, 18650,4S2P. ✅ (c) construction: soldered taps,GLBLi9OEM BMS lifts out with an iron. ✅ dock: dumb, always-on 19.54 V (top pad = +); 2-pin harness reusable, red = + (top/19.5 V), black = − (bottom/GND). BOM fully sourced (session 4): BMS ✅ ordered ETA ~2026-08-13, charger + fuse ✅ ordered; voltage sense / disconnect / wire / bench-safety = salvage or build-from-kit (see Parts status). Remaining, when parts land: the board swap — pull the OEM BMS, land the dumb 4S BMS on the existing soldered taps, wire the CC/CV charger to the reused dock harness. ⚠️ verify the salvaged OEM connector (gauge/keying) + PSU wire (stranded, gauge) at this bench session; buy fallbacks only if inadequate. ⚠️ do the swap in ONE session so the pack is never left bare/unprotected; don’t leave the first charge unattended (nested terracotta pots on concrete, MS8233A on the cells).- [DESIGN, do before the swap] Finalise the power topology → then schemdraw. First Mermaid block diagram drawn this session (dock → charger → cells+BMS → fuse → disconnect → 14.4 V bus → UBEC/5 V rail + 5 motor loads +
10k+2kADC sense). Open decisions to nail: (a) where the CC/CV charger ties in — into BMSP+/P−(pack side, as drawn) vs downstream of the fuse so charge current is fused too; (b) fuse placement — right atP+(protects everything) vs load-branch only; (c) how the ESP32 senses “docked” (read dock contacts vs charger output — feeds the map-pose auto-dock plan); (d) common-ground plan (all grounds → pack−). Resolve these, revise the Mermaid, then graduate to schemdraw for the proper schematic (WaveDrom for any timing/protocol diagrams).
- [DESIGN, do before the swap] Finalise the power topology → then schemdraw. First Mermaid block diagram drawn this session (dock → charger → cells+BMS → fuse → disconnect → 14.4 V bus → UBEC/5 V rail + 5 motor loads +
- [When UBEC arrives, ETA 10–17 Aug] Power rail: confirm 7 A variant + meter to ~5.25 V before wiring the Pi.
- [When board removed] Measure internal mounting space + plan cooling — see thermal risk below. Also count the full set of cliff sensors (≥2 confirmed; front corners + centre TBD).
🗣️ QUEUED FOR DISCUSSION (not yet explored):
- Power-on / info button + LED (OEM
neatoUI board) — thought experiment started 2026-08-07, direction sketched, needs a front-side photo to decide. The board (photobutton-ui-board-back-testpoints.jpg) is a 5 V device (silkGND/5Vpads by the connector) on a 6-pin JST harness with only ~4 signal wires (blue/green look like a data/clock pair) + 22 test points → strong smell of a “smart” board with a controller / I²C LED-driver-or-GPIO-expander, not discrete switches. Decode fork = same as the battery: dumb → wire raw button/LED pads straight to the ESP32; smart-standard-I²C → ESP32 as I²C master reuses the whole board; custom Neato MCU → infeasible protocol, fall back to tapping raw button/LED pads (path B) or replacing with our own button + NeoPixel (path C). The feature is never blocked — only the elegance is at stake. Next: photograph the FRONT (component side) to read the controller chip’s part number — that decides the path. Two layers to build: (1) soft power = button as an ESP32 input event (“start clean”) + LEDs as a status display — trivial, same as the bump switches; (2) hard power = a soft-latch load switch (P-FET / LTC2954 / Pololu-pushbutton pattern) between the 14.4 V bus and the UBEC, gated by an ESP32 keep-alive GPIO, so a press boots the robot and a long-press triggers a graceful Pi shutdown before cutting power (protects the SD card). Fold layer 2 into the battery power-topology decisions. - Battery / charging — investigated 2026-08-06; DIRECTION DECIDED, detail deferred to next session.
- Contact charging (dock rings,
19.5 V / 1.5 A— confirmed off the dock label905-0575 Rev B, supplyS030BBM1950150; photodock-input-output-label.jpg), NOT wireless/inductive. That19.5 Vis what our own charger will be fed;1.5 Ais the charge-current ceiling. - OEM pack is a “smart battery”: BMS + protection + balancing + a TI fuel-gauge IC live INSIDE the pack, talking SMBus (I²C-like, 2 wires) to the host, with serial-number authentication so only genuine packs work. Original charge circuit was on the discarded mainboard. Pack reads 0 V (BMS FET latched off).
- Why NOT the OEM path: keeping it means either the old board alive as a charger (hogs the board bay the Pi needs) or reverse-engineering an authenticated SMBus handshake (likely infeasible; the arriving logic analyzer can read SMBus but can’t defeat auth). Also note the Pi/board bay — not the battery bay — is where the brain goes, so the space win comes from ditching the old board, not shrinking the battery.
- ✅ DECIDED (drone/RC model): dumb cells + BMS-on-the-bot. Chain:
dumb 4S cells → on-bot 4S BMS (protection + balance) → 14.4 V power bus + a dock-fed 4S CC/CV charger (19.5 V in → 16.8 V out, ≤1.5 A) → ESP32 ADC voltage-divider for SoC / return-to-dock. No auth, no SMBus RE, swappable, Pi-controlled charging. Try option 3 first: reuse the OEM 18650 cells + a separate dumb BMS; build a fresh 18650 pack only if the cells are dead. - Battery bay: triangular prism, ~4 cm/side × 22 cm (~150 cm³) → holds ~8× 18650 = the OEM
4S2P; cylinders nest in a triangle (the “stacked triangle” seen). No catalogue pack is triangular → replacement is cell-reuse or DIY-shaped.4S1P= half runtime/easier fit;4S2P= OEM runtime. - Parts to search (specs to confirm in brackets):
4S 16.8V Li-ion BMS balancing 20A[4S · balancing · ≥20 A · common-port];DC-DC buck CC CV 16.8V 4S charger[input covers 19.5 V · out 16.8 V · CC/CV · ~1.5 A];18650 cells+4S holder/spot welder(if building);voltage sensor module 25Vor a resistor divider (keep <3.3 V into the ESP32 ADC);XT60+JST-XH balanceconnectors; bench safety —iMAX B6 balance charger,LiPo safe bag,4S low-voltage alarm. - 4S/2P meaning: S = series (adds voltage; 4 × 3.6 V ≈ 14.4 V nom, 16.8 V full); P = parallel (adds capacity/runtime). 4S2P = 8 cells.
- Contact charging (dock rings,
- Speaker integration detail — amp now in hand; ESP32 DAC wiring, what cues to play. Nice-to-have.
- Voice control via onboard mic array (Phase 2+) —
HK-ARRAY MIC-V1.1USB mic array (UAC, driverless on the Pi; real connector is a 5-pin header carrying USB lines). Plan:wyoming-satelliteon the Pi → HA Assist (STT→intent→TTS) → vacuum entity command → Nav2. Caveats: too loud to voice-control while cleaning (docked/idle only); named zones need SLAM/Nav2 up first (slot after steps 5–6 of build order); extra always-on process → thermal budget; mic ports need an air path (mount up top). Photo:microphone-array.jpg. - Auto-docking — direction decided 2026-08-06 (execution is late-stage, build step 6+). The OEM dock has an IR homing beacon behind a dark IR-transparent window at the top (IR LEDs emitting coded left/right fields; original robot behaviour = drive to dock → 360 scan → reverse rear-first onto the contacts). BUT the IR receivers + homing logic were on the discarded OEM brain, and Neato’s IR codes are proprietary → we do NOT reuse the OEM IR beacon (at least not initially). Plan A (chosen): dock via Nav2 map pose (robot knows the dock’s location on the SLAM map, navigates there) + a dead-simple final approach — reverse slowly until the ESP32 detects the dock’s 19.5 V appear on the charge contacts = “docked”, stop. That terminal signal reuses hardware we’re already building (rear contacts + pack-voltage ADC sense). Optional later upgrade: add our own cheap TSOP IR receiver + reverse-engineer the beacon codes (logic analyzer) if map-pose docking isn’t precise enough. Quick future test: phone camera at the dark dock window shows the IR LEDs glowing (purple/white).
Build order (for reference)
- ✅ DONE (2026-08-06) — ESP32 + L293D + stand-in motor → serial drive with PWM speed + direction. Toolchain proven.
- DRIVE half ✅ DONE (2026-08-07) — real Neato wheel motor + DRV8871 + 12V PSU → bidirectional serial drive proven. ENCODER half 🔶 circuit DONE, sensor verdict REOPENED (2026-08-11): breadboard rig rebuilt + proven noise-free. Sensors are
A3423-class dynamic (motion-only) hall → the “weak disc” call is doubtful because every static/meter test was invalid by design. Next: raw A/B on the logic analyzer while the motor spins — see NEXT ACTIONS #1. - LiDAR bring-up → power, spin, capture
J2, confirm/adapt packet decoder. (Waits on logic analyzer.) - Wire the digital sensors (bump ×4, wheel-drop) + cliff/wall ADC → safety inputs. (Switches + cliff/wall already characterised; cliff emitter-vs-signal pin is a 10-min functional test here.)
- Bare-bones MQTT bridge → HA
startdrives robot forward. (Proves full HA→Pi→ESP32→motor pipeline before SLAM.) - slam_toolbox + Nav2 → mapping, localization, coverage. (Nav2 tuning is the time-sink.)
Open questions / risks
- i.MX 8M reuse — RESOLVED: not viable. Three community projects all draw the line right below the D10: OpenNeato/renjfk (D3–D7); brainslug (gen2/3, “gen4… cannot interface directly”); 94-psy got a D7 working then suspended. Firmware encrypted/signed + i.MX HABv4 secure boot. Full transplant is the only path.
- Cliff / wall sensor — largely RESOLVED 2026-08-06. Vcc ~3.3 V (native ADC, no shifter); analog reflectance; host-strobed; ≥2 sensors gang-of-2; wall sensor is the same part. Residual: full sensor count (pending full undercarriage) + which of brown/yellow/green is emitter-drive vs signal (10-min functional test — action 2).
- Wheel-encoder supply voltage — RESOLVED: 5 V part (min 4.5 V; 3.3 V attempts were doomed). ⚠️ CORRECTED 2026-08-10: outputs are PUSH-PULL, not open-drain — divider per channel must be
2kseries +2k+1kshunt (shunt-only does nothing; series-only leaves 5 V on the pin). Roller-brush Hall tacho — bench-tested 2026-08-11: powers + idles at4.40 Voff 5 V (like the wheel encoders), 3 thin wires (Vcc/GND/signal). It too is a dynamic (motion-only) hall → won’t respond to a static magnet; test it by spinning the roller, not holding a magnet on it. - Is the wheel encoder working? STILL OPEN — reframed 2026-08-11. ⚠️ The session-9 “sensors YES / disc NO” verdict is doubtful: the sensors are
A3423-class DYNAMIC differential hall — motion-only (no response to a static magnet or slow hand-turn, by design), so every static/meter test to date was invalid. Confirmed this session: power good (5.00 V), wiring correct (re-verified), but three independent hall sensors (both wheel encoders + roller tacho) all sit at flat4.40 Vto static tests = systematic method error, not dead hardware. Decider = raw A/B on the logic analyzer while the motor SPINS (see NEXT ACTIONS #1). Likely outcome: sensors fine. If dead →A3423replacement, not linear hall. Evidence:hall-probe/ENCODER-DIAGNOSIS.md,diag/HYPOTHESES.md; research in linorobotiA07r2NniK0+ mikeferguson neato_robot #9. - ✅ Bench Uno — confirmed DEAD 2026-08-09 (didn’t survive the session-7 ICSP 5 V short). Not in the final robot anyway; clean 5 V for bench now comes from the MB102 module (or a phone charger / UBEC).
- Battery 6-pin pinout — partially mapped 2026-08-06: red, white, black, black, blue, yellow (all same gauge); likely red = +, black×2 = − / gnd, yellow = thermistor, white/blue = thermistor / serial. BMS is in-pack (smart battery); serial interface on the blue wire. Pack reads 0 V (latched) — see charging note.
- Bump/wheel-drop switch logic — RESOLVED (both NO, active-low, LOW = event).
- Internal mounting space for Pi+ESP32 — TBD once old board removed.
- ⚠️ THERMAL — closest prior art (
94-psy/OpenNeato, D7→SBC+ROS2+Nav2) was SUSPENDED partly because its SBC cooked at 85 °C+ inside the sealed chassis. The Pi 4 runs hotter → cooling is a design constraint, not an afterthought. - ⚠️ POWER — a 3 A 5 V rail browns out a Pi 4 under Nav2 (reboots / SD corruption). Hence the 7 A UBEC.
- ✅ Architecture validated by that failure — 94-psy’s other fatal flaw was driving Neato’s factory serial-diagnostic port for real-time control. Our design guts that board and runs the real-time loop on our own ESP32 (micro-ROS) → sidesteps it.
- LDS 2.2 packet format — largely solved by research; confirm on capture.
- Nav2 tuning — known hard/fiddly.
- “Lost” status detection (localization loss) — least clean signal to detect.
Reference links
- Neato XV-11 / Piccolo LDS protocol — https://github.com/ssloy/neato-xv11-lidar
- 94-psy/OpenNeato — closest prior art (D7 → SBC + ROS 2 + Nav2). Suspended; thermal + serial post-mortem — https://github.com/94-psy/OpenNeato
- renjfk/OpenNeato — D3–D7 cloud replacement; debug-port pinout
RX/3.3V/TX/GND— https://github.com/renjfk/OpenNeato - Philip2809/neato-brainslug — gen2/gen3 local control; confirms gen4 (D8/D9/D10) locked — https://github.com/Philip2809/neato-brainslug
- Neato drop/cliff sensor (290-1023 LOUIE DRP) discussion — Robot Reviews “D5 Cliff Sensors Not Responding”: https://robotreviews.com/chat/viewtopic.php?t=22133 ; vendor confirming 2×-set: https://casello.de/products/neato-botvac-d-connected-2x-drp-sensor-kabel-290-1023-louie-drop-rev2
- Neato smart-battery / in-pack BMS (D-series) — Robot Reviews “Fix/replace Battery BMS”: https://www.robotreviews.com/chat/viewtopic.php?t=22794 ; “Bricked my Botvac Connected battery pack”: https://robotreviews.com/chat/viewtopic.php?t=22714
- XV-11 LDS on Raspberry Pi (C++) — https://github.com/berndporr/neato-xv11-lidar
- ROS 2 Nav2 — https://docs.nav2.org
- slam_toolbox — https://github.com/SteveMacenski/slam_toolbox
- micro-ROS — https://micro.ros.org
- HA MQTT Vacuum — https://www.home-assistant.io/integrations/vacuum.mqtt/
- Cytron MD13S (roller driver) — Pi Hut SKU 106189, Cytron lib: https://github.com/CytronTechnologies/CytronMotorDriver
Photos captured so far (for the GitHub writeup)
-
Battery label (14.4 V 4S2P) + 6-pin connector
-
Mainboard both angles + RF shield removed (
shielding-removed.jpg) + chip close-ups:board-imx8m-soc.jpg,board-nanya-dram-and-lpc51u68.jpg,board-kingston-emmc.jpg,board-soc-cluster-overview.jpg -
Button/UI board (TP1–TP22)
-
LiDAR LDS 2.2 base board — underside (
290-1044 REV 4,J2 MAIN,J3 MOTOR) + chip side (LM393) -
Elegoo kit contents sheet
-
Drive wheel:
left-wheel-motor.jpg,left-wheel-motor-2.jpg,left-wheel-motor-wiring.jpg,left-wheel-chassis.jpg,left-wheel-chassis-with-motor-and-wiring.jpg,right-wheel-chassis-connected.jpg; encoder close-ups (LEGO … 915-1055,STD-3) -
Left motor winding-resistance measurement (
measure-left-motor-winding-resistance.jpg) -
Multimeter panel (
multimeter.jpg) -
Roller brush motor (
roller-brush-motor.jpg,roller-motor.jpg) + board/top-down (board-and-topdown-spinning-brush-motor.jpg) -
Side brush motor (
brush-motor.jpg) — 2-wire small can, EMI cap -
Blower label (EVERFLOW
F121225BU,DC14.4V 2.0AMP) +blower-motor.jpg -
Sensors (2026-08-05): front bumper switch (
front-bumper-switch.jpg,290-0056), wheel-arch dead-man’s microswitch (wheel-arch-dead-mans-switch.jpg,DT-08), under-carriage cliff sensor (under-carriage-sensor.jpg,LOUIE DRP 290-1023) -
Cliff sensor deep-dive (NEW 2026-08-06):
cliff-sensor-front.jpg(two emitter/detector windows),cliff-sensor-connector.jpg(5-wire JST + silkscreen),cliff-sensor-harness-clamp.jpg(ferrite/strain-relief clamshell + white plug),cliff-sensor-harness-cut-tails.jpg(harness cut, native plug kept as pigtail — hero shot for episode 12) -
Battery / dock (NEW 2026-08-06):
dock-input-output-label.jpg(dock19.5 V / 1.5 A, PN905-0575 Rev B);battery-pack-intact-shrinkwrapped.jpg(pack in hand, harness → JST);battery-pack-opened-bms-and-cells.jpg+battery-samsung-inr18650-35e-cells.jpg(opened —Samsung INR18650-35E, 4S2P, welded nickel);battery-bms-pcb-closeup.jpg+battery-bms-pcb-junction.jpg(GLBLi9OEM BMS in the centre spine, soldered taps — hero shots for the battery episode);mainboard-dock-charge-2pin-connector.jpg(2-pin dock-contact harness on the old board, reusable). Cell diameter no longer needs shooting (35E = 18650, confirmed off the label). -
Mic array (2026-08-05):
HK-ARRAY MIC-V1.1USB microphone array (microphone-array.jpg) -
Step 1 bench build (2026-08-06): ESP32 pinout D25/D26/D27 (
esp32-devkit-pinout.jpg), L293D on breadboard (l293d-breadboard-step1.jpg), first motor + fan spinning (step1-first-motor-fan-test.jpg— hero for episode 11) -
Step 2 / bench PSU (NEW 2026-08-07):
psu-dell-label.jpg+psu-dell-label-closeup.jpg(B255ES-01, 255 W, 80+ Bronze),psu-main-connector-p1.jpg(proprietary 8-pin — hero for episode 14),psu-secondary-connector.jpg(yellow+black 12V peripheral tap),psu-rear-iec.jpg. Drive-motor + encoder board shots (260-0016; encoder silkLEGO WHEEL ENCODER ASY 915-1055 REV/STD-3, no voltage marked):encoder-board-silk-1.jpg,encoder-board-silk-2-topdown.jpg,encoder-board-silk-3.jpg,encoder-board-connector.jpg. -
Session 7 (2026-08-07):
encoder-board-wired-pinout-1.jpg+encoder-board-wired-pinout-2.jpg(encoder board with the 6 wires soldered to the pad row — the hero shots that show the decoded pinout order: motor+, Vcc, GND, A, B, motor−).button-ui-board-back-testpoints.jpg(neatoUI board, trace side —GND/5Vpads, TP1–22, 6-pin JST → smart-board evidence). -
Session 8 (2026-08-09):
encoder-board-hall-chips-u1-u2.jpg— the encoder PCB with the magnet disc still on, showing the two separate Hall ICsU1+U2(one per quadrature channel) + the tiny pull-ups, and the yellowish blob near the 6-wire solder pads. Hero for episode 16 and the key evidence for the shared-cracked-joint diagnosis. -
Session 9 (2026-08-10):
breadboard-divider-rebuild.jpg— the rebuilt encoder circuit on the breadboard (rails, divider rows, jumpers) mid-build. Hero for episode 18.
Still to photograph: button/UI board FRONT (component side) to read the controller chip part number (decides the reuse path); LiDAR turret top/laser module markings (optional); internal space with mainboard removed; individual connectors close-up (JST pitch sizing for encoder/tacho/battery); wheel-encoder + brush-tacho Vcc pins while metering; full undercarriage showing all cliff-sensor positions; cliff-sensor emitter glowing on a phone camera during the functional pinout test. (✅ encoder board silk + connector now shot — encoder-board-silk-1/2/3.jpg, encoder-board-connector.jpg; silk carries no pinout, so Vcc/GND/A/B still comes from the bench toggle test.)