Skip to the content
PCBKitNew board
2 layers83.8 × 134.6 mmFpv rover, the printed circuit board in 3D

Monorepo for an FPV rover (LEGO Audi e-tron) controlled from a web UI with live video, telemetry OSD, and optional physical gamepad input.

Size
83.8 × 134.6 mm
Copper layers
2
Thickness
1.6 mm
Parts
42
Nets
37
Pads
240
Tracks, vias
26, 0
Routed
Yes
KiCad
KiCad 10.0 (file 20260206)
Created
7 Jun 2026
Updated
9 Oct 2026

Overview

Fpv rover is an open-source printed circuit board by Aspience, from the Aspience/fpv-rover repository on GitHub. It is a 2-layer board of 83.8 × 134.6 mm with 42 parts on 37 nets, routed with 26 tracks and 0 vias.

It is built around the INA219AID. Its parts include 12 ICs, 4 connectors and 1 switch. It belongs with the robots and motor control boards of this library.

Main components

1

README

README.md

FPV Rover

Monorepo for an FPV rover (LEGO Audi e-tron) controlled from a web UI with live video, telemetry OSD, and optional physical gamepad input.

Contents


Overview

The project follows a modular architecture: each hardware capability (motion, power, thermal, IMU, light, camera, gamepad) is an independent backend module under backend/modules/<name>/. Gamepads connect over USB or Bluetooth (the bluetooth module handles wireless pairing; gamepad reads input via evdev). Modules are toggled at runtime via ROVER_MODULES_* feature flags in .env — only enabled modules are loaded, initialized, and exposed to the UI.

This design lets you mix and match hardware configurations without changing application code. A minimal bench setup might enable only camera and motion; a fully instrumented rover can enable every sensor. The frontend reads module flags from GET /config on startup and shows or hides controls accordingly.

The stack runs on Raspberry Pi (reference build: Pi Zero 2 W) inside Docker Compose: FastAPI backend, React SPA, nginx reverse proxy, MediaMTX for WebRTC video, and a pigpio sidecar for GPIO/PWM motor control.


Hardware requirements

The reference rover is built around a Raspberry Pi (64-bit). Peripherals connect over the buses listed below.

ComponentModule / roleBus / interface
Raspberry Pi Zero 2 W (or compatible Pi 3/4/5)Host — runs Docker stack—
Arducam IMX462 (Pivariety)FPV camera, night modeCSI-2 → libcamera / V4L2 (/dev/video0)
GY-302 (BH1750)Ambient light sensor; triggers camera night mode below thresholdI2C (default address 0x23)
DS18B20Temperature sensors (motors, BMS, BEC, charger)1-Wire (default GPIO 4 via dtoverlay=w1-gpio)
TB6612FNG × 3Motor drivers — front drive, rear drive, steering (LEGO Control+ hubs)GPIO / PWM via pigpio daemon; encoder tachos on GPIO
GY-521 (MPU6050)Gyroscope + accelerometer (orientation OSD)I2C (default address 0x68)
INA219Battery voltage and current monitoringI2C (default address 0x40)
BMS 2S battery charge controller2S Li-ion pack — 2P2S layout (four 18650 cells)Passive power path; temperature via DS18B20 on 1-Wire
iFlight Micro 2–8S BECSwitched regulator — 5 V supply for the PiPower only (no data bus)
TOF400CLaser ToF distance sensorI2C (planned; not yet implemented in software)
Gamepad (USB or Bluetooth)Physical input (optional); Bluetooth is used to pair wireless controllers, then input is read via evdevBluetooth (pairing, host netns via nsenter) → evdev (/dev/input/event*)

Host interfaces to enable

Before deployment, enable these on the Pi (see Prerequisites on the Pi):

InterfaceUsed by
I2C (/dev/i2c-1)INA219, BH1750, MPU6050, TOF400C
1-Wire (/sys/bus/w1)DS18B20
GPIO / pigpio (/dev/gpiochip0)TB6612FNG motor drivers
CSI camera + Arducam overlayIMX462
BluetoothGamepad pairing (wireless controllers)
evdev (/dev/input)Gamepad input (USB and paired Bluetooth)

Motion and gamepad modules require the pigpiod Docker sidecar. Without pigpio or when the daemon is unreachable, motion falls back to mock hardware (useful for local dev).


KiCad project

Electrical design lives in hardware/ as a KiCad 10 project. The schematic captures the wiring described in SCHEMA.md (main board + expansion board buses, power path, motor drivers, sensors); SCHEMA.md remains the physical wiring source of truth.

FileWhat it is
hardware/fpv-rover.kicad_proKiCad project — open this in KiCad
hardware/fpv-rover.kicad_schSchematic (editable source)
hardware/fpv-rover.kicad_pcbPCB layout (placeholder — build uses perfboard, see SCHEMA.md)
hardware/fpv-rover-schematic.pdfExported schematic — view without KiCad
hardware/fpv-rover-schematic.svg / .pngSame schematic as vector / raster image

To view the design, open the PDF/SVG exports. To edit, open fpv-rover.kicad_pro in KiCad 10+ and re-export the PDF/SVG afterwards so the rendered files stay in sync.


Software requirements

RequirementNotes
Linux 64-bitRequired for production images and MediaMTX arm64 build
Raspberry Pi OS Lite 64-bit (recommended)Bookworm or Trixie; Bullseye supported on Pi 4 and older
Docker Engine + Docker Compose pluginProduction deployment and OTA updates
git, curlBootstrap and OTA scripts

For local development on any OS: Python 3.12+ with uv, Node.js 20+ for the frontend.


Installation and updates

On Raspberry Pi, backend, frontend, and mediamtx images are built in GitHub Actions (arm64) and pulled at runtime — no local npm run build or docker compose build on the device.

Prerequisites on the Pi

Bootstrap requires git, curl, Docker, and the Docker Compose plugin. On a fresh Raspberry Pi OS image these are often missing.

1. Git and curl
bash
sudo apt update
sudo apt install -y git curl
2. Docker and Docker Compose
bash
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER"
newgrp docker

Log out and back in (or run newgrp docker) so your user can run Docker without sudo.

Verify:

bash
docker --version
docker compose version
docker ps
3. I2C and 1-Wire

Backend expects /dev/i2c-1 and /sys/bus/w1 on the host. Enable both before bootstrap — sensors do not need to be connected yet.

bash
sudo raspi-config
# Interface Options → I2C → Enable
# Interface Options → 1-Wire → Enable
sudo reboot

Or add to /boot/firmware/config.txt (older images: /boot/config.txt):

text
dtparam=i2c_arm=on
dtoverlay=w1-gpio,gpiopin=4

After reboot, verify:

bash
ls /dev/i2c-1
ls /sys/bus/w1

gpiopin=4 matches the default ROVER_W1_GPIO=4 in .env.example.

4. Arducam IMX462 camera drivers

The IMX462 is an Arducam Pivariety module. It requires Arducam's custom libcamera stack and a device-tree overlay on the host (outside Docker). Follow the Arducam Pivariety Quick Start Guide.

Step 1 — download the install script:

bash
wget -O install_pivariety_pkgs.sh https://github.com/ArduCAM/Arducam-Pivariety-V4L2-Driver/releases/download/install_script/install_pivariety_pkgs.sh
chmod +x install_pivariety_pkgs.sh

Step 2 — install Arducam libcamera (on the Pi host, before or after bootstrap):

bash
./install_pivariety_pkgs.sh -p libcamera_dev
./install_pivariety_pkgs.sh -p libcamera_apps

No reboot yet — proceed to Step 3.

Step 3 — enable the camera overlay in /boot/firmware/config.txt (Pi 4/5 Bookworm/Trixie; older images may use /boot/config.txt):

text
camera_auto_detect=0
dtoverlay=arducam-pivariety

On Pi 5 / CM5 with the camera on the CAM0 port, use dtoverlay=arducam-pivariety,cam0 instead.

Save and reboot:

bash
sudo reboot

Verify after reboot:

bash
rpicam-still --list-cameras    # Bookworm / Trixie
# or: libcamera-still --list-cameras   # Bullseye
ls /dev/video0

The MediaMTX container ships with Arducam libcamera libraries baked in (infra/mediamtx/Dockerfile); the host overlay and libcamera packages are still required for the camera device node to appear.

If you see Configuration file 'arducam-pivariety.json' not found for IPA module 'rpi/pisp', it can be safely ignored — the config is embedded in camera firmware.

5. SSH deploy key

Bootstrap can generate one interactively, or configure manually:

bash
ssh-keygen -t ed25519 -f ~/.ssh/fpv_rover_deploy -N ""
cat ~/.ssh/fpv_rover_deploy.pub

Add the public key in GitHub → Settings → Deploy keys (read-only).

~/.ssh/config:

sshconfig
Host github.com
  IdentityFile ~/.ssh/fpv_rover_deploy
  IdentitiesOnly yes
6. GHCR login (only if images are private)
bash
echo "$GITHUB_PAT" | docker login ghcr.io -u YOUR_GITHUB_USER --password-stdin

PAT needs read:packages. Public GHCR packages do not require login.

First install (bootstrap)

bash
sudo mkdir -p /opt/fpv-rover && sudo chown "$USER" /opt/fpv-rover
git clone git@github.com:Aspience/fpv-rover.git /opt/fpv-rover
cd /opt/fpv-rover
./scripts/bootstrap.sh --enable-systemd
# or pin a release tag:
./scripts/bootstrap.sh --tag v0.1.5 --enable-systemd

Bootstrap creates .env from .env.example with production values (ROVER_OTA_ENABLED=true, VITE_RPI_HOST, IMAGE_TAG, etc.). It does not create .env.local.

Before pulling images, bootstrap pauses so you can edit env interactively (opens ${EDITOR:-nano} on request). Set module flags, confirm VITE_RPI_HOST, and optionally create .env.local for hardware IDs — then press Enter to continue.

Example .env.local (do not copy the full .env.example — .env.local overrides .env and would reset OTA settings to example defaults):

text
ROVER_MODULES_MOTION_ENABLED=true
ROVER_MODULES_GAMEPAD_ENABLED=true
ROVER_MODULES_CAMERA_ENABLED=true
ROVER_THERMAL_SENSOR_IDS={"motor_steering":"28-..."}

Use --non-interactive to skip the env edit pause when env is preconfigured.

Bootstrap options
FlagDefaultDescription
--install-dir PATH/opt/fpv-roverInstall path on the device
--tag TAGlatest (or $IMAGE_TAG)Git tag and Docker image tag to deploy
--repo URLgit@github.com:Aspience/fpv-rover.gitGit remote for clone/fetch
--non-interactiveoffFail fast if SSH/GitHub/GHCR setup is missing
--skip-cloneoffSkip clone when repo is already in --install-dir
--skip-sshoffSkip SSH key generation and GitHub deploy-key prompt
--skip-ghcr-loginoffSkip docker login ghcr.io
--skip-deployoffSetup only (env, clone) — no compose pull/up
--enable-systemdoffInstall and enable infra/systemd/fpv-rover.service
--quietoffLess stdout; log file stays verbose
Env varUsed for
VITE_RPI_HOSTBrowser-facing Pi IP/hostname (non-interactive bootstrap)
IMAGE_TAGDefault tag when --tag is omitted
GHCR_USER + GHCR_TOKENOptional GHCR login (public packages skip this)

Logs: /opt/fpv-rover/logs/bootstrap.log

bash
tail -100 /opt/fpv-rover/logs/bootstrap.log

Updating to a new version

From the UI

Open Settings → Check for updates → Install update. The UI shows a fullscreen overlay while the rover restarts and polls GET /health every 3 seconds until the backend is back.

Requires ROVER_OTA_ENABLED=true (set by bootstrap on production installs).

Manual update / rollback (OTA script)
bash
cd /opt/fpv-rover
./scripts/ota_update.sh v0.2.0        # upgrade (prompts for confirmation)
./scripts/ota_update.sh v0.1.0        # rollback (prompts for confirmation)
./scripts/ota_update.sh v0.2.0 --yes  # skip the confirmation prompt

The script waits 3 seconds (so the API can respond), fetches the git tag, resolves per-service image tags from image-tags.env (with nearest-tag fallback for older releases), pulls Docker images, and runs docker compose up -d. It never overwrites .env or .env.local.

Before any destructive step the script asks:

text
Update from v0.1.0 to v0.2.0? [y/N]

The prompt is skipped when running non-interactively (backend OTA API), with --yes/-y, or when ROVER_OTA_ASSUME_YES=1.

Logs: /opt/fpv-rover/logs/ota.log

bash
tail -100 /opt/fpv-rover/logs/ota.log

Technical reference

Repository structure

text
fpv-rover/
├── backend/          # Python — FastAPI, hardware modules, WebSocket
│   ├── api/          # REST routes, WebSocket handler, command/telemetry schemas
│   ├── core/         # Settings, event bus, module registry, startup
│   ├── modules/      # Pluggable hardware modules (see below)
│   └── tests/
├── frontend/         # React SPA — live video, OSD, motion levers, settings
├── infra/            # Nginx reverse proxy, MediaMTX, systemd unit
├── scripts/          # Bootstrap and OTA update scripts
├── .env.example      # Shared environment template (backend + frontend)
└── docker-compose.yml

Each backend module typically has *.module.py, *.schema.py, *.config.py, and optional *.utils.py / *.service.py.

Backend modules

ModuleRole
motionLEGO Control+ drive (front/rear) + steering via TB6612FNG + pigpio; PID closed loop, homing calibration
gamepadPhysical gamepad over Linux evdev (/dev/input); maps to motion commands
powerINA219 battery monitoring (I2C)
thermalDS18B20 temperature sensors (1-Wire)
imuMPU6050 orientation (I2C)
lightBH1750 ambient lux (I2C); triggers camera night mode below threshold
cameraMediaMTX integration — recording and stream config
bluetoothBluetooth gamepad pairing (host netns via nsenter)

When ROVER_MODULES_MOTION_ENABLED=true, the backend runs steering calibration automatically on startup (core/startup.py). Physical gamepad input requires both ROVER_MODULES_GAMEPAD_ENABLED and ROVER_MODULES_MOTION_ENABLED.

Motion tuning constants (PID loop rate, homing power, throttle limits) live in motion.config.py. Gamepad evdev codes and poll intervals — in gamepad.config.py. GPIO pins, PID gains, and speed limits are runtime settings via ROVER_* env.

Environment variables

Both backend and frontend read from a single root .env file:

bash
cp .env.example .env

On the Pi or in Docker, optional .env.local overrides .env without touching git-tracked files.

Feature flags
VariableDefaultDescription
ROVER_MODULES_POWER_ENABLEDfalseINA219 power module
ROVER_MODULES_MOTION_ENABLEDfalseMotion module (LEGO Control+ + TB6612FNG)
ROVER_MODULES_GAMEPAD_ENABLEDfalsePhysical gamepad via evdev
ROVER_MODULES_THERMAL_ENABLEDfalseDS18B20 thermal module
ROVER_MODULES_IMU_ENABLEDfalseMPU6050 IMU module
ROVER_MODULES_LIGHT_ENABLEDfalseBH1750 light module
ROVER_MODULES_CAMERA_ENABLEDfalseMediaMTX camera integration
ROVER_MODULES_BLUETOOTH_ENABLEDfalseBluetooth gamepad pairing

Backend loads ROVER_* via pydantic-settings (backend/core/config.py).
Frontend loads VITE_* via Vite with envDir pointing at the repo root (frontend/vite.config.ts).

Note: VITE_* variables are embedded at build time. After changing them, rebuild the frontend (npm run build or docker compose build frontend).

Motion & pigpio
VariableDefaultDescription
ROVER_PIGPIO_HOSTpigpiodpigpio daemon hostname (Docker service name)
ROVER_PIGPIO_PORT8888pigpio daemon TCP port
ROVER_MOTION_FRONT_PWMA_GPIO18Front drive — PWM pin (TB6612FNG PWMA)
ROVER_MOTION_FRONT_AIN1_GPIO23Front drive — direction AIN1
ROVER_MOTION_FRONT_AIN2_GPIO24Front drive — direction AIN2
ROVER_MOTION_FRONT_TACHO_A_GPIO17Front drive — encoder channel A
ROVER_MOTION_FRONT_TACHO_B_GPIO27Front drive — encoder channel B
ROVER_MOTION_REAR_PWMA_GPIO12Rear drive — PWM pin
ROVER_MOTION_REAR_AIN1_GPIO16Rear drive — direction AIN1
ROVER_MOTION_REAR_AIN2_GPIO20Rear drive — direction AIN2
ROVER_MOTION_REAR_TACHO_A_GPIO5Rear drive — encoder channel A
ROVER_MOTION_REAR_TACHO_B_GPIO6Rear drive — encoder channel B
ROVER_MOTION_STEER_PWMA_GPIO13Steering — PWM pin
ROVER_MOTION_STEER_AIN1_GPIO19Steering — direction AIN1
ROVER_MOTION_STEER_AIN2_GPIO26Steering — direction AIN2
ROVER_MOTION_STEER_TACHO_A_GPIO21Steering — encoder channel A
ROVER_MOTION_STEER_TACHO_B_GPIO22Steering — encoder channel B
ROVER_MOTION_MAX_SPEED_TICKS800Max drive speed in encoder ticks/s at full throttle
ROVER_MOTION_STEER_MAX_DEG45Maximum steering angle (°)
ROVER_MOTION_PID_KP0.8PID proportional gain
ROVER_MOTION_PID_KI0.05PID integral gain
ROVER_MOTION_PID_KD0.01PID derivative gain
Global backend
VariableDefaultDescription
ROVER_LOG_LEVELINFOLogging level
ROVER_HOST0.0.0.0FastAPI bind address
ROVER_PORT8000FastAPI port
ROVER_I2C_BUS1Linux I2C bus number
ROVER_W1_GPIO41-Wire GPIO pin (must match dtoverlay=w1-gpio)
ROVER_WS_TELEMETRY_HZ20WebSocket telemetry broadcast rate (Hz)
ROVER_HEARTBEAT_TIMEOUT_SEC1.0Watchdog heartbeat timeout
ROVER_IO_RETRY_DELAY_SEC2.0Hardware I/O retry delay
MediaMTX
VariableDefaultDescription
ROVER_MEDIAMTX_API_URLhttp://mediamtx:9997MediaMTX control API
ROVER_MEDIAMTX_RECORD_START_PATH/v3/recordings/start/{stream_path}Start recording API path template
ROVER_MEDIAMTX_RECORD_STOP_PATH/v3/recordings/stop/{stream_path}Stop recording API path template
ROVER_MEDIAMTX_STREAM_CONFIG_PATH/v3/config/paths/patch/{stream_path}Patch stream config path template
ROVER_MEDIAMTX_STREAM_CONFIG_GET_PATH/v3/config/paths/get/{stream_path}Get stream config path template
Hardware addresses & devices
VariableDefaultDescription
ROVER_POWER_I2C_ADDRESS0x40INA219 I2C address
ROVER_IMU_I2C_ADDRESS0x68MPU6050 I2C address
ROVER_LIGHT_I2C_ADDRESS0x23BH1750 I2C address
ROVER_W1_BASE_PATH/sys/bus/w1/devices1-Wire sysfs base path
ROVER_THERMAL_W1_SLAVE_FILEw1_slaveFilename inside each sensor directory
ROVER_THERMAL_SENSOR_IDS(JSON map)DS18B20 ROM IDs keyed by sensor name
ROVER_CAMERA_V4L2_DEVICE/dev/video0Camera V4L2 device
ROVER_CAMERA_V4L2_CTL_BINv4l2-ctlPath to v4l2-ctl binary
ROVER_CAMERA_STREAM_PATHroverMediaMTX stream path name
Frontend (build-time)
VariableDefaultDescription
VITE_RPI_HOSTlocalhostDev server proxy target; production WebRTC fallback
VITE_API_PORT8000Dev server proxy target for REST/WebSocket
VITE_WEBRTC_PORT8889MediaMTX WebRTC (WHEP) port in the browser
Docker / OTA (production)
VariableDefaultDescription
ROVER_OTA_ENABLEDfalseAllow POST /update/apply (set true on Pi)
ROVER_OTA_INSTALL_DIR/opt/fpv-roverInstall path on the device
ROVER_OTA_SCRIPT/opt/fpv-rover/scripts/ota_update.shUpdate script path
ROVER_OTA_SSH_KEY_PATH(empty)Deploy key path; bootstrap sets $HOME/.ssh/fpv_rover_deploy
ROVER_OTA_ASSUME_YES(empty)Set 1/true to skip manual-update confirmation
ROVER_GITHUB_OWNERaspienceGitHub owner for release check and GHCR image path
ROVER_GITHUB_REPOfpv-roverGitHub repo for release check and GHCR image path
ROVER_GITHUB_TOKEN(empty)Optional PAT for GitHub API rate limits
FPV_ROVER_IMAGE_REGISTRYghcr.ioContainer registry for production images
IMAGE_TAGlatestTarget release tag for git checkout and app version
BACKEND_IMAGE_TAG(auto)Pin backend image tag — override in .env.local
FRONTEND_IMAGE_TAG(auto)Pin frontend image tag — override in .env.local
MEDIAMTX_IMAGE_TAG(auto)Pin mediamtx image tag — override in .env.local

Per-service tag precedence: on deploy/OTA, tags are resolved as (1) explicit pin in .env.local, then (2) image-tags.env from the checked-out release, then (3) nearest existing GHCR tag. To pin a specific service tag, put it in .env.local (not .env, which the OTA script rewrites).

Compose also uses PIGPIOD_IMAGE (default zinen2/alpine-pigpiod:pigpio-v79) for the GPIO sidecar.

Production images are pulled from ${FPV_ROVER_IMAGE_REGISTRY}/${ROVER_GITHUB_OWNER}/${ROVER_GITHUB_REPO}-{backend,frontend,mediamtx}.

Local development

Backend
bash
uv sync --project backend
uv run --project backend pytest
uv run --project backend ruff check
uv run --project backend ruff format
uv run --project backend fpv-rover

API: http://localhost:8000 — OpenAPI docs at /docs.

Install pre-commit hooks (from repo root):

bash
uv run --project backend pre-commit install
Frontend

React 19 SPA: live video (WebRTC), telemetry OSD, on-screen throttle/steer levers, keyboard shortcuts, light and camera controls. Physical gamepads are handled on the backend (evdev).

Stack: React 19, TypeScript, Vite 8, Tailwind CSS 4, Zustand, Zod, Axios.

bash
cd frontend
npm ci
npm run dev          # http://localhost:5173
npm run build        # production bundle → dist/
npm run lint
npm run preview

Run backend and frontend in separate terminals:

bash
# terminal 1
uv run --project backend fpv-rover

# terminal 2
cd frontend && npm run dev

Vite proxies /api and /ws to the backend using VITE_RPI_HOST and VITE_API_PORT.

Backend ↔ frontend sync

The UI talks to the backend over three channels. Keep their contracts aligned when you change either side.

1. Shared configuration (.env)

Module flags (ROVER_MODULES_*) are exposed via GET /config; the frontend fetches them on startup.

When deploying on a Pi or via Docker, REST and WebSocket use nginx on port 80 (/api, /ws). WebRTC/WHEP uses window.location.hostname and VITE_WEBRTC_PORT (not proxied through nginx).

2. REST — module flags
BackendFrontend
EndpointGET /configfetchConfig() in frontend/src/api/http.ts
Schemabackend/core/schemas/config.py → ConfigResponsefrontend/src/types/schemas.ts → ConfigResponseSchema
TypesOpenAPI (/openapi.json)frontend/src/types/contracts.ts

After changing the REST API:

  1. Update Pydantic models in backend/modules/<name>/<name>.schema.py or backend/core/schemas/.
  2. Run uv run --project backend pytest.
  3. Mirror the shape in frontend/src/types/schemas.ts (Zod) and frontend/src/types/contracts.ts.
  4. Optionally regenerate REST types (backend must be running): cd frontend && npm run generate:api
3. WebSocket — telemetry and commands
BackendFrontend
EndpointWS /wsfrontend/src/api/websocket.ts
Server → clientbackend/modules/<name>/<name>.schema.py, backend/api/schemas/telemetry.pyTelemetryMessageSchema, ErrorMessageSchema
Client → serverbackend/api/schemas/commands.pyClientCommandSchema

Message types:

  • Telemetry (default 20 Hz): { type: "telemetry", modules: { power?, motion?, light?, thermal?, imu?, bluetooth?, gamepad? } }
  • Commands (frontend): heartbeat (every 500 ms), move, calibrate, set_brightness
  • Commands (backend only today): record — not yet sent from the UI
  • Errors: { type: "error", message: string }

Motion control from the UI, keyboard, or physical gamepad converges on the same backend path. Use GET /ws-protocol on a running backend to inspect documented JSON shapes.

Note: STEER_MAX_DEG in frontend/src/constants.ts is hardcoded (default 45°). Keep it aligned with ROVER_MOTION_STEER_MAX_DEG when you change steering limits.

4. Video — WebRTC (WHEP)

Video does not go through FastAPI. MediaMTX serves the stream; the frontend connects via WHEP:

  • Stream path: rover (see infra/mediamtx/mediamtx.yml)
  • Frontend: POST http://{host}:{VITE_WEBRTC_PORT}/rover/whep
  • Backend camera module controls recording via ROVER_MEDIAMTX_API_URL

Keep VITE_WEBRTC_PORT aligned with MediaMTX webrtcAddress (default 8889).

Sync checklist

When you add or change an API field:

  • Pydantic model in backend/modules/<name>/<name>.schema.py (or backend/api/schemas/)
  • Zod schema in frontend/src/types/schemas.ts
  • TypeScript interface in frontend/src/types/contracts.ts (if applicable)
  • Backend tests (pytest)
  • Manual check: backend running + npm run dev or built frontend

Infra / Docker

bash
cp .env.example .env
docker compose up --build
ServicePort(s)Role
pigpiod8888 (internal)pigpio daemon — GPIO/PWM for motion motors
backend8000FastAPI, hardware modules
frontend3000Static SPA (Caddy)
nginx80Reverse proxy: / → frontend, /api/ and /ws → backend
mediamtx8554, 8889, 8189/udp, 9997RTSP, WebRTC, ICE UDP, control API (Arducam/Pivariety libcamera)

Pi device mounts in docker-compose.yml: /dev/i2c-1, /dev/video0, /dev/input (gamepad), /sys/bus/w1 (1-Wire), /dev/gpiochip0 (pigpiod sidecar).

Production UI on port 80: REST → /api, WebSocket → /ws. WebRTC/WHEP → http://<pi-ip>:8889/rover/whep.

Locally, mediamtx is built from infra/mediamtx/Dockerfile; production pulls a pre-built image from GHCR.

Releasing a new version

bash
git tag v0.2.0
git push origin v0.2.0

GitHub Actions (.github/workflows/release.yml) builds arm64 backend, frontend, and mediamtx images and pushes them to GHCR. Each service is rebuilt only when its context changed since the previous v* tag. Skipped services keep their previous image tag.

The workflow writes image-tags.env with resolved tags per service. Release assets include image-tags.env, compose files, infra/**/*, OTA/bootstrap scripts, and .env.example.

Boot persistence (optional)

Bootstrap can install the systemd unit with --enable-systemd, or manually:

bash
sudo cp infra/systemd/fpv-rover.service /etc/systemd/system/
sudo systemctl enable --now fpv-rover

Parts

42
QtyValueFootprintReferences
118650 A1 (bottom)Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_VerticalBT1
118650 A2 (top)Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_VerticalBT2
118650 B1 (bottom)Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_VerticalBT3
118650 B2 (top)Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_VerticalBT4
1BMS 2S ProtectionConnector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_VerticalU2
1BUS_GND_TIENetTie:NetTie-2_THT_Pad1.0mmNT1
1CN3903 DC-DC 5V/3AConnector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_VerticalU4
1GY-302 BH1750Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_VerticalU10
1GY-521 MPU6050Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_VerticalU6
1INA219Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_VerticalU5
1J_EXP XH 8p (exp)Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_VerticalJ_EXP_XH_8p_exp
1J_EXP XH 8p (main)Connector_JST:JST_XH_B8B-XH-A_1x08_P2.50mm_VerticalJ1
1J_LIGHT XH 5pConnector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_VerticalJ_LIGHT XH 5p
1J_PI 2x20 male header - Raspberry Pi Zero 2 WConnector_PinHeader_2.54mm:PinHeader_2x20_P2.54mm_VerticalU1
1J_TOF1 XH 6pConnector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_VerticalJ_TOF1 XH 6p
1J_TOF2 XH 6pConnector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_VerticalJ_TOF2 XH 6p
1J_TOF3 XH 6pConnector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_VerticalJ_TOF3 XH 6p
1J_TOF4 XH 6pConnector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_VerticalJ_TOF4 XH 6p
1LEGO Hub FrontConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ8
1LEGO Hub RearConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ9
1LEGO Hub SteerConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ10
1SW_MAINConnector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_VerticalSW1
1TB6612FNG SteerConnector_PinHeader_2.54mm:PinHeader_1x14_P2.54mm_VerticalU9
1TOF400C VL53L1X #1Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_VerticalU11
1TOF400C VL53L1X #2Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_VerticalU12
1TOF400C VL53L1X #3Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_VerticalU13
1TOF400C VL53L1X #4Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_VerticalU14
1Type-C 2S Charger IP2326Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_VerticalU3

The 28 most common of 42 parts.

Questions

How big is the Fpv rover PCB?

It measures 83.8 × 134.6 mm and has 2 copper layers, 1.6 mm thick.

What parts does the Fpv rover use?

42 parts, built around the INA219AID. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the Fpv rover KiCad files?

From the Aspience/fpv-rover repository on GitHub, at the commit this page was made from: the board is hardware/fpv-rover.kicad_pcb and its schematic hardware/fpv-rover.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the Fpv rover design in my own project?

Its repository states no licence, so its author keeps all rights: ask Aspience before reusing the design.

Similar boards

  • 4 layers82.6 × 82.5 mmJumperless2, the printed circuit board in 3D

    Jumperless2

    Architeuthis-Flux/Jumperless

    A jumperless breadboard

    208 parts914
  • 4 layers82.6 × 82.6 mmJumperlessV5r8, the printed circuit board in 3D

    JumperlessV5r8

    Architeuthis-Flux/JumperlessV5

    The next generation jumperless breadboard

    650 parts332
  • 2 layers74.4 × 51.2 mmEsp32-wroom timer, the printed circuit board in 3D

    Esp32-wroom timer

    AlessandroAU/Chorus32-ESP32LapTimer

    This is an ESP32 port of the popular Chorus RF Laptimer

    66 parts102GPL-3.0
  • 2 layers169.3 × 128 mmPSU PCB, the printed circuit board in 3D

    PSU PCB

    ThomasVDD/SmartSupply

    SmartSupply is a digital battery powered lab powersupply. It is Arduino compatible and can be controlled via PC over USB.

    134 parts96
  • 4 layers100 × 54 mmKbox, the printed circuit board in 3D

    Kbox

    sarfata/kbox-hardware

    Open-source electronics for your boat

    139 parts19
  • 2 layers50 × 100 mmPSU PCB, the printed circuit board in 3D

    PSU PCB

    ThomasVDD/SmartSupplyMini

    118 parts16
  • 2 layers101.6 × 53.3 mmBatteryAnalyzer MkII, the printed circuit board in 3D

    BatteryAnalyzer MkII

    Moragor/BatteryAnalyzer_MkII

    Updated Version of the battery analyzer, now with constant current

    25 parts12MIT
  • 2 layers100 × 100 mmWLEDController, the printed circuit board in 3D

    WLEDController

    LostInCompilation/WLED-Controller-PCB

    WLED Controller Board with ESP32 - 4 Channels - Current and Power Monitoring - INMP441 Microphone

    69 parts8