Device-side software for automated image capture on Raspberry Pi, integrated with the Reverse Engineering Lab platform.
- Installation Guide — Hardware, setup, and configuration
- Contributing — Development workflow and testing
- Platform Docs — Camera management in RELab
The plugin runs a lightweight FastAPI server on your Raspberry Pi that:
- Captures images from the connected camera module
- Publishes a low-latency LL-HLS preview through the local MediaMTX sidecar
- Connects to the RELab platform via WebSocket relay
- Exposes a REST API for manual testing and integration
Supports Raspberry Pi 5/4 with Camera Module 3/v2, running on Raspberry Pi OS (64-bit) with Python 3.13+.
The plugin is organized around a small runtime container that owns the long-lived process services for the app:
- FastAPI control plane: HTTP routes, setup UI, auth, and local-access helpers
- Camera service: capture, stream, focus/controls, upload fallback queue
- Preview/media path: Picamera2 lores encoder -> MediaMTX -> LL-HLS proxy
- Relay/pairing path: runtime-owned pairing state/service, outbound WebSocket relay service, local-access bootstrap
- Background services: upload queue drain, preview sleeper, thermal governor, stream health checks
Request flow is: router -> application service -> backend/adapter. Runtime-owned
background services use the same camera manager and preview pipeline instances as
the HTTP paths, so lifecycle and cleanup stay centralized in the app lifespan.
Relay activity and pairing state are also runtime-owned now, so preview wake/
hibernate logic and setup-page state do not depend on module-level globals.
PairingService and RelayService are the only production orchestration
entrypoints for those flows.
Static configuration still comes from app.core.config.Settings, but live
mutable process state now lives in app.core.runtime_state.RuntimeState
through AppRuntime. Relay credentials, local API keys, and derived auth
snapshots are runtime-owned, then rehydrated from the persisted credentials
file on the next boot.
The seam is split intentionally now:
- Public contract: the RELab backend owns the app-facing API and OpenAPI
- Private device contract: shared
relab_rpi_cam_modelsDTOs cover pairing, relay envelopes, local-access bootstrap, and Pi-initiated upload acks
Bootstrap precedence is explicit:
- env-backed
Settings - persisted credentials file
- generated first-boot defaults for local-only secrets
- Paired relay mode: outbound WebSocket relay to the RELab backend
- Local direct mode: Ethernet/LAN access with
X-API-Keyauth - Backend upload mode: captures pushed back to the RELab backend
- S3 upload mode: captures written to a configured S3-compatible bucket
The plugin connects to the RELab backend via WebSocket relay. The Pi opens an outbound connection to the backend — no public IP address or port forwarding is required.
Pairing is automatic in the native RELab app: set PAIRING_BACKEND_URL in your .env, start the plugin, and enter the 6-character code in the RELab app. The Pi generates an asymmetric key pair locally, registers the public key with the backend, and keeps the private key on-device. No API key is ever copied manually.
The browser-based RELab frontend is different: if it is served over HTTPS, modern browsers block fetch() calls to the Pi's plain HTTP API as mixed content. That means the web frontend cannot auto-probe or switch a camera into direct mode; this direct-connection path is for the native app or for manual clients that call the Pi directly.
- Prepare your Pi and install the plugin
- Set
PAIRING_BACKEND_URLin your.envand start the plugin - Enter the pairing code shown on
/setup(or in logs) in the RELab app - Visit
http://your-pi-ip:8018to test/setup— Pairing and status/hls/cam-preview/index.m3u8— LL-HLS live preview (via the MediaMTX sidecar on :8888)/stream/watch— YouTube viewer UI for an active YouTube stream/camera/controls— Discover/set camera controls (autofocus, exposure, etc.)/camera/focus— Friendly focus controls (continuous/auto/manual)/docs— API reference
If you use Docker Compose on the Pi, generate compose.override.yml with ./scripts/generate_compose_override.py. The override targets the existing app service from compose.yml, so the device mappings merge into the plugin container cleanly.
For headless setup, the active 6-character pairing code is also printed to stdout in a boxed PAIRING READY banner, so you can read it over SSH, docker compose logs, or journalctl without opening the browser UI.
If you need a fresh code during setup, open /setup and click the Generate a new pairing code button beside the current code. That rotates the code without unpairing the camera.
By default, Docker Compose runs only the camera plugin. Inspect logs with:
docker compose logs -f appOptional remote observability is available with the observability-ship profile. It runs Alloy on the Pi and ships the app's structured file logs to an external Loki-compatible endpoint. Add it to COMPOSE_PROFILES in your .env:
COMPOSE_PROFILES=observability-ship
LOKI_PUSH_URL=http://your-observability-host:3100/loki/api/v1/push
OBSERVABILITY_INSTANCE=pi-01Without the profile, logs are still written to Docker logs and the 7-day rotating app_logs volume. Local Loki/Grafana is not bundled with this plugin; use your platform's central observability stack when you need fleet log browsing.
Tracing is opt-in. When OTEL_ENABLED=true and OTEL_EXPORTER_OTLP_ENDPOINT
are set, the plugin instruments FastAPI and httpx and propagates request/relay
trace context across the backend -> plugin boundary. This repo does not bundle
or configure a full OTLP pipeline; that remains an environment concern.
For platform management and operation, see the RELab camera guide.
The plugin can also run fully standalone, writing captures straight to a local S3-compatible bucket (RustFS by default) instead of pushing them to the RELab backend. This is useful for hobbyist / bench / offline-first setups.
Everything lives in the same compose.yml. Standalone mode uses a separate
build target that includes the S3 client (aioboto3); the default paired build
does not. Set the following in .env and rebuild:
# Select the standalone build target and start the RustFS sidecar
APP_BUILD_TARGET=runtime-standalone
COMPOSE_PROFILES=standalone
# S3 sink and RustFS credentials — see .env.example for the full list
IMAGE_SINK=s3
S3_ENDPOINT_URL=http://host.docker.internal:9000
S3_BUCKET=rpi-cam
S3_ACCESS_KEY_ID=rustfsadmin
S3_SECRET_ACCESS_KEY=change-me-to-a-strong-password
RUSTFS_SECRET_KEY=change-me-to-a-strong-passworddocker compose build && docker compose up -dOnce up:
- Camera API at
http://<pi-lan-ip>:8018(same as paired mode) - Live LL-HLS preview at the same URL shape as paired mode (proxied through
the Pi's own
/hlsendpoint; no RELab backend needed) - RustFS console at
http://<pi-lan-ip>:9001— log in withRUSTFS_ACCESS_KEY/RUSTFS_SECRET_KEY - Captures browsable under
http://<pi-lan-ip>:9000/rpi-cam/
To point the plugin at an external S3-compatible service (Backblaze B2,
Cloudflare R2, Wasabi, AWS S3, …), update S3_ENDPOINT_URL, credentials, and
S3_PUBLIC_URL_TEMPLATE in .env and rebuild. No code changes required.
Set COMPOSE_PROFILES= (empty) to skip the RustFS sidecar when using a managed
bucket.
Profiles combine freely: COMPOSE_PROFILES=standalone,observability-ship
runs the RustFS sidecar and ships logs to your central Loki.
The local API key serves two independent use-cases:
| Use-case | Requires relay pairing? | What it does |
|---|---|---|
| RELab app latency boost | Yes | After relay pairing, the app fetches the key automatically through the relay and switches to Ethernet-direct when the Pi is on the same LAN — preview latency drops from ~2 s to ~0.4 s. No manual setup. Works in both the native app and modern browsers (Chrome/Edge). |
| Standalone / custom clients | No | Call the camera API directly with X-API-Key: <key> — no relay needed. Useful for scripts, custom dashboards, or standalone mode (see below). |
The local key does not replace relay pairing. To register a camera in the RELab app you still need to complete the relay pairing flow (6-character code). The local key is for latency improvement once already paired, or for non-RELab access.
Local mode is enabled by default. No .env changes are needed.
On first startup the plugin auto-generates a local API key and persists it to the credentials file. When the RELab app opens the camera detail screen and the camera is online, it automatically retrieves the key and candidate IP addresses through the relay and probes your local network. If the Pi is reachable via Ethernet, the app switches to direct mode silently — preview latency drops to ~0.4–0.8 s without any user action.
This works in both the native app (iOS/Android) and modern Chromium-based browsers. The Pi sends Access-Control-Allow-Private-Network: true on all responses so Chrome's Private Network Access policy is satisfied as enforcement ramps up.
To disable local mode entirely (opt-out):
LOCAL_MODE_ENABLED=falseOn startup the plugin logs a banner showing the current mode and how to retrieve the local API key:
══════════════════════════════════════════════════════
ReLab RPi Camera v1.x
Setup : http://my-pi.local:8018/setup
Mode : PAIRED camera_id=…
Local key: run: just show-key
══════════════════════════════════════════════════════
To print the key from an SSH session:
just show-key
# or without just, if the app is already running:
python3 -c "import json,pathlib; print(json.loads((pathlib.Path.home()/'.config/relab/relay_credentials.json').read_text()).get('local_api_key',''))"The RELab app discovers the Pi's IP addresses automatically via the relay. Avahi is not required, but it lets you access /setup and the API by hostname instead of IP on the local link:
sudo apt install avahi-daemon avahi-utilsCreate a service advertisement file:
<!-- /etc/avahi/services/relab-rpi-cam.service -->
<service-group>
<name>relab-rpi-cam-%h</name>
<service>
<type>_relab-rpi-cam._tcp</type>
<port>8018</port>
</service>
</service-group>sudo systemctl enable avahi-daemon && sudo systemctl restart avahi-daemonAfter this, the Pi is resolvable as <hostname>.local on macOS and Windows 10+ without any DNS configuration.
- Any Ethernet port — Works on all RPi models and any Linux SBC. Connect directly with a cable or through a USB-C to Ethernet adapter. Link-local addressing (169.254.x.x) is negotiated automatically if no DHCP is present.
- USB gadget mode (USB-C data) — Only available on RPi Zero 2W and some RPi 4 revisions (not RPi 5). Requires
dtoverlay=dwc2+g_ethermodule; Pi appears at192.168.7.1on the host. - Hardware-agnostic contract — Any camera device that implements
GET /camera,POST /images, andGET /hls/*withX-API-Keyauthentication works with the RELab frontend's local connection mode.
The local API key is the only authentication gate on the direct interface. Physical access to the cable is the primary trust boundary — appropriate for lab use. The key is distinct from relay credentials; disable local auth with LOCAL_MODE_ENABLED=false if needed.
Camera not detected? Run rpicam-hello --list-cameras
Won't connect? See INSTALL.md — Troubleshooting for connection-specific issues.
Want to contribute? See CONTRIBUTING.md for development setup.