PhotoCar — Complete Build & Wiring Guide

The single source of truth for building the PhotoCar — consolidates the former assembly, MD25HV/Pi wiring, and Bluetooth-integration guides (now retired). Published at photocar.userinterfaces.shop. Control-side facts (pins, PWM, service env) are read directly from the running firmware in motor-control/; part specs are the modeled PhotoCar parts.

Pin note (verified against firmware): the deployed service uses PWM = GPIO18 (PWM0, physical pin 12) and DIR = GPIO23 (physical pin 16), sharing GND on pin 14 — three consecutive outer-row pins, so the Pi side is a single 1×3 plug on pins 12/14/16. GPIO12 is dead/shorted-to-ground on this board, so PWM stays on GPIO18. The 433 MHz keyfob input is on GPIO17 (pin 11). Source: MotorController.ts defaults used by the running photocar-ble.service.
ComputePi Zero 2 W
Motor driverCytron MD25HV
Control pins12 / 14 / 16
Power2× 12 V LiFePO4
InterfaceBLE + WiFi
PWM = GPIO18 (PWM0), pin 12 DIR = GPIO23, pin 16 GND = pin 14 (any ground) 1 kHz hardware PWM Chain drive, single center wheel Never wire motor power to GPIO

1. Bill of Materials & Specs

PartQtySpecificationTerminals / role
Raspberry Pi Zero 2 W (or 2 WH)165 × 30 mm; 4× M2.5 on 58 × 23 mm; BCM43430 BLE 4.2 + WiFi; 40-pin J8 headerRuns the photocar-ble service; PWM/DIR out on GPIO18/23
Cytron MD25HV motor driver1Single channel, 7–58 V, 25 A, PWM/DIR mode (not I²C); PCB 111.76 × 60.96 × 1.6 mm; 4× Ø3 on 106.68 × 55.88 mmPower VB+/VB−, motor MA/MB; control GND/5VO/PWM/DIR
12 V wiper-style gear motor1~100 RPM; ~2.4 Ω → ~5.3 A / ~68 W running at 12.8 V; ~179 mm overall, Ø9.5 D-shaftTwo armature leads → MA/MB; park-switch taps unused
12 V 10 Ah LiFePO4 battery (A & B)212.8 V nominal, 20 A limit; 151 × 65 × 94 mm; F2 spade terminalsPOS/NEG → A/B/OFF selector
A/B/OFF battery selector1Rotary selector, red ringChooses pack A, pack B, or OFF
Emergency stop + master On/Off (DPST)1 eaE-stop cutoff; DPST master switchIn series between Main(+) bus and Switched(+) bus
Bus bars4Main(+), Main(−), Switched(+), Switched(−)Distribution to MD25HV + buck
5 V DC-DC buck converter167 × 38 × 14 mm; holes 61.05 × 31.45 mm; set output 5.1 V, ≥ 3 AINP/INN in, OUTP/OUTN out → Pi 5 V
Onboard LiFePO4 charger + inlet114.6 V 10 A LiFePO4 charger; 120 V AC charge inletCharges the pack(s) in place
Electronics mounting plate1180 × 180 × 2 mm PLA, 22 holes (Pi M2.5 Ø3.4, MD25/buck M3 Ø3.8, grid/corner M4 Ø4.7)Bolts the board stack to the deck
3D-printed board enclosure1Bolt-down case, side port cutout (board_case_*)Houses MD25HV / Pi / buck
2.54 mm connector housings + signal wire122–26 AWG; one 1×3 for PWM+GND+DIRPi control: PWM pin 12, GND pin 14, DIR pin 16 (consecutive)
Single center knobby wheel, chain, sprocketssetOne-stage chain reductionMotor → chain → center drive wheel
RGB LED module1Onboard resistors (no external resistors); common-cathode; only R, G, and - wired (B leg unconnected); 1×3 housingBoot status indicator on pins 35 / 37 / 39

2. Parts — Reuse, Buy & Budget

Conservative list including the small wiring and mounting parts that usually get missed. Buy the genuine Raspberry Pi from a trustworthy listing; save money on generic wiring, housings, heat-shrink, standoffs, fuse holders, and buck converters — never by removing the Pi fuse, undersizing the 5 V regulator, or powering the Pi from the MD25HV 5VO.

Reuse from the existing car

PartUseNotes
Cytron MD25HV motor driverMotor power stageKeep existing motor + battery screw-terminal wiring if it already runs the motor.
Charger, XT60 leads, power distributionCharging / wiringA LiFe-capable charger handles the 4S LiFePO4 pack; reuse XT60s and distribution.
12 VDC gear motor + connectorDrive motorLeave motor wiring as heavy-current wiring — never route motor current into GPIO harnesses.
Enclosure, glands, lever connectors, loomMounting / serviceReuse where practical; add strain relief where wires leave the box.

Buy / add

PartQtyMinimum specWhy
4S LiFePO4 battery pack1–212.8 V nominal (14.6 V full), 10 Ah, balance lead; BMS ≥ 20 A continuousMain power. 12.8 V matches the 12 V motor. Two packs + A/B/OFF selector for runtime.
Raspberry Pi Zero 2 WH (or 2 W + soldered header)1Onboard BLE, 40-pin headerWH saves soldering. Runs the photocar-ble service.
microSD card116–32 GB, reputable brandRaspberry Pi OS + control program.
5 V DC-DC buck converter1Input 8–30 V; output 5.1 V; ≥ 3 A (LM2596/MP1584)Powers the Pi from the 12.8 V bus. Set to 5.1 V before connecting the Pi.
Inline fuse holder + fuses1Mini blade, 2 A to startProtects the buck/Pi branch.
Pre-crimped 1-pin Dupont leads20–4022–26 AWG, 2.54 mmEach conductor starts as a 1-pin lead, then loads into housings.
2.54 mm connector housingsassort.1×3, 1×4, plus 1×2 / 2×nBuild keyed groups: the Pi control plug and power plugs.
18 AWG red/black wire6–10 ftStranded copperBattery branch to fuse, switch, buck input.
22–26 AWG signal wire3–6 ftBlue / black / purplePWM, GND, DIR control leads.
Ferrules / heat-shrink / zip ties / M2.5 nylon standoffssetsMatch gaugesClean terminations, strain relief, and a non-conductive Pi mount.
Multimeter1DC volts + continuityRequired to verify buck output before plugging the Pi.

Optional add-ons

PartInclude when
Pi Camera Module + Zero CSI ribbonThe car should capture photos/video (narrow Pi-Zero ribbon).
Micro-USB OTG adapterSetup / rescue access before BLE is stable.
Low-current status LED/buzzer (via resistor/driver)Boot/control status feedback — never drive high current from GPIO.
Budget: a minimum cart (new 4S LiFePO4 pack, genuine Pi Zero 2 WH, microSD, buck, fuse, switch, Dupont kit, wire, heat-shrink, standoffs) runs roughly $130–210 on Amazon US, assuming the motor driver, charger, enclosure, and basic tools are reused. Verify live prices, and confirm a genuine 4S LiFePO4 pack (not LiPo) with a ≥ 20 A BMS and real Pi Zero 2 W/WH branding.

3. System Architecture

High-current drive power and low-current control signals stay on separate paths. The selected battery feeds a main bus, through the E-stop and master switch to a switched bus, which feeds both the MD25HV power input and the buck converter. The buck powers the Pi at 5 V. The Pi commands the MD25HV with three signal wires: PWM, DIR, and a shared GND.

Power current and control signals stay on separate paths Battery A / B 12.8 V LiFePO4 A/B/OFF selector E-stop + On/Off (DPST) via main bus Switched bus + / − MD25HV VB± in, MA/MB out Wiper Motor 12 V gear motor → chain → wheel 5 V Buck 5.1 V, ≥ 3 A MA/MB Pi Zero 2 W control firmware BLE + WiFi (7777) GPIO18 / GPIO23 5 V PWM / DIR / GND
Hard safety rule: never connect motor power, battery, or charger output to a Raspberry Pi GPIO pin. GPIO is a 3.3 V signal interface. Motor current lives only on the MD25HV power/motor screw terminals. The Pi sends only PWM and DIR plus a shared ground.

4. Control Wiring — Pi to MD25HV

The MD25HV runs in PWM/DIR mode. Three wires connect it to the Pi: PWM = GPIO18 (physical pin 12), DIR = GPIO23 (physical pin 16), and a shared GND (pin 14). GPIO18 is used because GPIO12 is dead/shorted-to-ground on this board. Pins 12, 14, and 16 are three consecutive outer-row pins, so the whole Pi side is a single 1×3 housing.

Pi Zero header → MD25HV

Pi physical pinPi signalMD25HV terminalWire
Pin 12GPIO18 (PWM0)PWMBlue — motor speed
Pin 14GNDGNDBlack — common reference (next to PWM)
Pin 16GPIO23DIRPurple — direction
no connection5VOLeave empty / unwired

Verified in MotorController.ts (defaults PWM_GPIO=18, DIR_GPIO=23, used by the running service) and provision-photocar.sh. Any ground pin works for GND; pin 14 is used because it sits between PWM and DIR.

Connector plan

  • Pi side: one 1×3 housing on pins 12 / 14 / 16 (PWM = GPIO18, GND, DIR = GPIO23) — three consecutive outer-row pins.
  • MD25HV side: land on the green control screw terminal at the PWM, GND, and DIR positions; use ferrules.
  • The 5VO terminal stays empty. Do not use it to power the Pi.
  • Do not use loose single-pin jumpers — they shake loose in a moving car. Use the 1×3 housing.
  • Pi GPIO is 3.3 V and the MD25HV accepts it directly. No level shifter needed.
Pi Zero header — PWM pin 12 + GND pin 14 + DIR pin 16 (one 1×3) Outer (even) row shown, pins 2…40 left→right. PWM, GND, and DIR are consecutive. 2 12 14 16 40 1×3: PWM + GND + DIR Pin 12 = GPIO18 / PWM0 → MD25HV PWM Pin 14 = GND → MD25HV GND (adjacent to PWM) Pin 16 = GPIO23 → MD25HV DIR (next to GND)

MD25HV board — connect here, not there

The MD25HV has more than one control area. Ignore the small manual potentiometer/rocker headers (those were for standalone manual control). For Pi/software control, land on the green control screw terminal silk-screened GND / 5VO / PWM / DIR. Battery power goes to the high-current terminals on the opposite edge: VB+, VB−, and the motor on MA / MB. Match the printed labels on your physical board before powering anything.

MD25HV terminal map — control on one edge, power/motor on the other Cytron MD25HV PWM/DIR mode control (green) GND 5VO × PWM DIR high current VB+ VB− MA MB Pi → PWM / GND / DIR · leave 5VO empty battery → VB± · motor → MA/MB
Motor leads: the wiper gear motor's connector carries the two armature (drive) leads plus unused park-switch taps. Only the two armature leads go to MA / MB. The 5VO terminal stays empty — never use it to power the Pi.
Direction sense and PWM mode: firmware default is DIR_FORWARD_LEVEL=1 (forward = DIR high). PWM is hardware-timed at 1 kHz on PWM channel 0 via dtoverlay=pwm,pin=18,func=2; on boot the firmware sets GPIO18 to its ALT5 PWM function with pinctrl. If the motor spins the wrong way, swap the two motor leads or flip DIR_FORWARD_LEVEL.

5. Power Wiring

The car's power path is: selected battery → main bus → E-stop → master On/Off (DPST) → switched bus → MD25HV power input and buck converter. Negatives return through the Main(−) and Switched(−) bus bars. The buck output powers the Pi at 5 V, which also establishes the Pi–MD25HV common ground through the control cable.

FromToMethodNotes
Battery A/B POS / NEGA/B/OFF selectorF2 spadeSelector picks pack A, B, or OFF.
Selector out (+)Main(+) busBus barSingle main positive distribution.
Main(+) busE-stop → On/Off (DPST)InlineE-stop then master switch in series.
On/Off outSwitched(+) busBus barEverything downstream is switch-controlled.
Switched(+) busMD25HV VB+Screw / ferruleHeavy wire (motor current).
Switched(+) busBuck INPScrew / solderBattery voltage in — not from MD25HV 5VO.
Battery NEGMain(−) / Switched(−) busBus barCommon ground.
Switched(−) busMD25HV VB− + Buck INNScrew / ferruleMotor + buck return.
Buck OUTP/OUTNPi 5 V inputExisting Pi power methodVerify 5.1 V with a meter before plugging the Pi.
MD25HV MA/MBWiper motor armature leadsScrew terminalSwap leads to invert direction.
Charge inlet (120 V AC)Onboard 14.6 V LiFePO4 charger → packInlet + leadsCharges in place; do not drive while charging.

Soldered Y-split harness (positive & negative distribution)

Where the switched bus feeds both the MD25HV and the buck, a clean removable harness is a soldered Y-split rather than soldering directly to fixed components:

  1. Cut one trunk wire (from the switched bus / switch output) and two branch wires (MD25HV + buck).
  2. Slide adhesive-lined heat-shrink onto the trunk before soldering.
  3. Strip enough insulation that the three copper ends overlap mechanically; twist the two branches together, then wrap with the trunk.
  4. Solder so it flows through the joint, not just on the outside; let it cool without movement.
  5. Cover with the adhesive heat-shrink; add an outer sleeve or cable wrap if the harness will vibrate.
  6. Repeat for the negative side. Use heavier wire on the MD25HV branch (motor current) than on the buck branch.
Fuse choice: size the fuse for your wiring and expected motor current, not the MD25HV's 25 A ceiling. The driver can pass far more than your harness; protect the wire, connectors, and battery harness. Start conservative and validate under load.
Before first power-up: measure the pack at full charge, verify MD25HV power polarity and buck input polarity, set the buck to 5.1 V and confirm it at the Pi end, and confirm MD25HV 5VO is not wired to the Pi. Lift the wheel off the ground for the first motor test.

6. Software & Control

The Pi runs motor-control (TypeScript/Node) as the systemd service photocar-ble.service, installed by scripts/provision-photocar.sh. It accepts drive commands over Bluetooth LE (from the PhotoCar tablet/phone app) and a WiFi TCP control port.

Provisioning a fresh Pi — two ways, same result

Both paths end at the same place: the Pi-side provisioner scripts/provision-photocar.sh runs as root, installs packages, writes the PWM overlay, builds the app, installs photocar-ble.service, and reboots once if the boot config changed. It is idempotent — safe to re-run for updates too.

Step 0 — flash the right OS onto the microSD

  1. Install Raspberry Pi Imager on any computer with an SD card reader.
  2. Choose Device: Raspberry Pi Zero 2 W.
  3. Choose OS: Raspberry Pi OS (Bookworm, Debian 12). This matters: the provisioner and its PWM/boot-config handling target Bookworm — do not pick a Trixie (Debian 13) image. If the Imager's default entry says Trixie, go to Raspberry Pi OS (other) and pick a release explicitly labelled Bookworm (the “Legacy” entries once Trixie becomes the default). 32-bit or 64-bit both work on the Zero 2 W.
  4. Click the gear / “Edit Settings” before writing and set:
    • Hostname: photorcar (this is what deploy.ps1 and its -Scan mode look for).
    • Username / password: dealermade / the usual password (the provisioner assumes user dealermade exists).
    • WiFi: your SSID + password, and set the wireless LAN country (e.g. US) — without a country the radio can stay rfkill-blocked.
    • Services tab: enable SSH with password authentication.
  5. Write the card, put it in the Pi, and power up. First boot takes a couple of minutes (it resizes the filesystem and joins WiFi).

Step 0.5 — SSH into the Pi

# from any machine on the same WiFi network
ssh dealermade@photorcar.local

Way 1 — from the PC: deploy.ps1 (the single deploy script)

Way 2 — on the car itself, over WiFi with curl (no PC needed)

The guide site hosts a bootstrap script and the full motor-control payload, so the Pi can fetch and install everything itself. In the SSH session from Step 0.5:

# 1) download the setup script from the guide site
curl -fsSL https://photocar.userinterfaces.shop/setup.sh -o /tmp/photocar-setup.sh

# 2) execute it (root required — it installs apt packages and a systemd service)
sudo bash /tmp/photocar-setup.sh

Or as a one-liner:

curl -fsSL https://photocar.userinterfaces.shop/setup.sh | sudo bash

What the install does, in order:

  1. setup.sh downloads photocar-motor.tar.gz from the same site and extracts it to /home/dealermade/photocar-motor.
  2. It hands off to scripts/provision-photocar.sh provision — the exact same provisioner Way 1 runs: apt packages (bluetooth bluez nodejs npm build-essential… — the apt step takes several minutes on a Zero 2 W), PWM overlay in config.txt, npm install && npm run build, then installs and enables photocar-ble.service and the boot LED service.
  3. On a fresh image the boot config changed, so the Pi reboots itself once (your SSH session drops — that's expected). After the reboot the service starts on its own; when the status LED goes solid green the car is ready to drive.
  4. Re-running the same two commands later is safe — it re-downloads the latest payload and updates in place.

Verifying either way

Service environment (the source of truth for pins)

VariableValueMeaning
PWM_GPIO18Throttle PWM on GPIO18 / PWM0 (physical pin 12). GPIO12 is dead on this board.
DIR_GPIO23Direction on GPIO23 (physical pin 16).
PWM_FREQUENCY_HZ10001 kHz hardware PWM (period 1 ms). Keep ≤~1 kHz — the MD25HV only responds at 100% duty near 20 kHz.
MAX_THROTTLE100Throttle is clamped 0–100%.
WATCHDOG_TIMEOUT_MS1200Motor stops if no command arrives within 1.2 s.
DIRECTION_CHANGE_PAUSE_MS20Ramps to 0 then pauses before reversing.
WIFI_CONTROL_PORT / HOST7777 / 0.0.0.0WiFi TCP control listener.
BLUETOOTH_ENABLED1BLE GATT control enabled (BCM43430, HCI 4.2).
PHOTOCAR_REAL_MOTOR11 = drive real GPIO/PWM; 0 = dry run.

Boot config line written by the provisioner: dtoverlay=pwm,pin=18,func=2 (GPIO18 reaches the PWM block via ALT5). Reboot once after first adding it.

Driving commands: over BLE or the WiFi port the car accepts JSON like {"cmd":"drive","dir":"forward","throttle":50,"seq":1}, plus stop, ping, and (when ALLOW_SHUTDOWN=1) power-off. The 1.2 s watchdog stops the motor if commands stop arriving, so a dropped link fails safe. Pack-voltage telemetry on the UI requires an ADC/voltage divider wired to the Pi (BATTERY_VOLTAGE_FILE); until then the UI marks battery as not configured.

Boot status LEDs

A single RGB LED module shows boot state: red blinks from the first seconds of kernel boot, then goes out and green comes on solid once the receiver (photocar-ble.service) is up and the car is ready to drive. If the service later stops, it falls back to blinking red. The early blink is the kernel itself — the installer adds dtoverlay=gpio-led,…,trigger=timer lines to config.txt (one reboot required after install), so red blinks long before the OS is up; photocar-bootled.service then takes over and swaps red for green when ready.

LEDRed (R)Green (G)Common (-)
Status LEDpin 35 (GPIO19)pin 37 (GPIO26)pin 39 (GND)

Only three legs are wired — R, G, and the common -; the blue leg gets no wire. Pins 35 / 37 / 39 are 3 physically adjacent inner-row header pins, so the harness is a single 1×3 housing (R first, GND last). These pins avoid everything in use: GPIO18 (PWM, pin 12), GPIO23 (DIR, pin 16), GPIO17 (keyfob, pin 11), and dead GPIO12. The module has onboard resistors — no external resistors needed (a bare LED would need ~330 Ω per leg). Common-cathode default (GPIO high = lit); for common-anode modules set PHOTOCAR_LED_ACTIVE_HIGH=0. The firmware's GPIO env vars are comma-separated lists and also drive GPIO9/GPIO11 by default (a second, unfitted LED position) — those pins simply stay unwired.

7. Assembly Steps

1. Bench-test the Pi before installing it

microSD Raspberry Pi OS Pi Zero pair Bluetooth test GPIO output Ready

Image Raspberry Pi OS Bookworm with WiFi + SSH configured, then provision: either run .\deploy.ps1 from the PC, or SSH in and run curl -fsSL https://photocar.userinterfaces.shop/setup.sh | sudo bash (see §6).

Pair the tablet/controller while on USB bench power.

Confirm PWM hardware drives: bash scripts/provision-photocar.sh selftest before touching car wiring.

2. Mount the Pi and buck converter

Vacuum debris from the enclosure; mount the Pi on non-conductive standoffs on the electronics plate.

Place the buck close enough to keep the 5 V leads short; keep the Pi antenna area clear of metal and large bundles.

Keep low-current electronics away from the battery terminals and motor leads.

3. Add the fused, switched buck branch

Switched bus+ / − Fusenear tap SwitchPi branch 5 V BuckINP / INN

Install the fuse close to the bus tap, not next to the Pi; verify buck input polarity before connecting power.

Set buck output to 5.1 V with a meter before plugging into the Pi.

4. Build the control harness from 1-pin pre-crimp leads

Build a single 1×3 housing for PWM + GND + DIR (GPIO18 pin 12 / GND pin 14 / GPIO23 pin 16), in the same order as the wiring table.

Tug-test each lead after insertion; if a crimp backs out, replace that lead. Label the harness before plugging it in.

Do not solder loose wires directly to the Pi header — serviceable connectors survive a moving car.

5. Connect to the MD25HV control terminal only

Pi Zero GPIO18, GPIO23, GND pins 12 / 14 / 16 MD25HV PWM, DIR, GND PWM DIR GND Never connect motor output to GPIO

Land on the MD25HV green control screw terminal (GND / 5VO / PWM / DIR), not the heavy motor terminals; leave 5VO empty.

Keep existing motor screw-terminal wiring intact. After plugging in, check continuity from Pi GND to MD25HV GND.

First motor test at low throttle with the wheel lifted off the ground.

8. Bring-Up Checklist

Source Notes

Control-side facts (pins, PWM mode, frequency, service env, provisioning) are read from the live firmware in motor-control/ — primarily scripts/provision-photocar.sh and src/MotorController.ts. Part specifications (MD25HV, Pi Zero 2 W, buck, battery, motor, mounting plate, bus/selector/E-stop power chain) are the modeled PhotoCar parts.