Development guide for the RPI Camera Plugin.
Prepare your development environment:
./scripts/local_setup.sh --devThis installs dependencies, configures your venv, and sets up pre-commit hooks.
Start with hot reload:
just devThe API will be available at http://localhost:8018/docs.
The app uses a feature-first layout: each domain is a self-contained package
with its HTTP router, schemas, dependencies, exceptions, and services colocated.
Cross-cutting infrastructure (relay, backend client, upload queue, image sinks,
media pipeline) lives as peer packages at app/ root.
app/
main.py # FastAPI app creation + wiring
router.py # Top-level HTTP router aggregator (public vs authed)
device_jwt.py # Shared device-assertion primitive
core/ # Runtime + config + lifespan/middleware wiring
runtime.py, runtime_state.py, runtime_context.py
config.py, settings.py, bootstrap.py
lifespan.py, middleware.py, templates_config.py
# Features (own a router.py that exports `public_router` and `router`)
camera/ # Camera controls, captures, HLS preview, streaming
router.py, routers/{controls,captures,hls,stream}.py
schemas.py, dependencies.py, exceptions.py
services/{manager,backend,picamera2_backend,hardware_protocols,hardware_stubs}.py
pairing/ # Device pairing flow + setup UI + local-access + local-key
router.py, routers/{setup,local_access,local_key}.py
services/{service,client}.py
auth/ # Session auth + request-auth dependency
router.py, dependencies.py
system/ # /metrics + /telemetry HTTP surfaces
router.py, routers/{metrics,telemetry}.py
frontend/ # Landing HTML page
router.py
# Infrastructure (cross-cutting — no HTTP routers)
backend/ # Backend HTTP client + factory + contract adapters
relay/ # Outbound WebSocket relay service + observable state
media/ # MediaMTX client, preview pipeline, stream helpers
upload/ # Persistent upload queue
image_sinks/ # Backend / S3 image sink implementations
observability/ # Logging, tracing (OTel), telemetry collector
utils/ # Generic helpers (files, network, task orchestration)
workers/ # Process-wide background tasks (preview sleeper, thermal, etc.)
static/, templates/ # Web assets
relab_rpi_cam_models/
src/ # Shared device-seam DTOs (separately published PyPI package)
tests/
unit/ # Mirrors app/ domain layout (camera/, pairing/, …, core/)
integration/ # ASGI app, routes, and lifespan behavior (flat)
support/ # Shared fakes and fixtures
scripts/
local_setup.sh # Local development setup
generate_compose_override.py # Docker device mapping for the compose `app` service
uv run pytest testsOr via just:
just test
just test-unit
just test-integration
just test-slowestCheck test coverage:
uv run pytest --cov=app testsThe project aims for >80% coverage. CI will fail if coverage drops.
Pre-commit hooks automatically run:
- Linting —
ruff checkandruff format - Type checking —
ty
Hooks run before every commit. To manually check:
pre-commit run --all-filesRecommended local commands:
just lint
just typecheck
just test
just test-slowest
just checkThe suite is intentionally split into two main layers:
tests/unit/: pure function/service tests and small collaboratorstests/integration/: ASGI app, route, and lifespan behavior
Custom pytest markers mirror that split:
@pytest.mark.unit@pytest.mark.integration@pytest.mark.slowfor intentionally longer worker/lifecycle tests
Prefer these patterns when adding tests:
- use the shared runtime/app fixtures from
tests/conftest.py - use typed helpers from
tests/support/for recurring fakes - assert behavior and public contracts before asserting internal call choreography
- patch private module internals only when there is no stable seam to target
When cleaning up old tests:
- delete tests whose only purpose was covering removed implementation details
- keep or replace tests that still protect externally meaningful behavior
- avoid snapshot-style broad response dumps when explicit assertions are clearer
- Find or create the owning feature folder (e.g.
app/camera/,app/pairing/) - Add a sub-router under its
routers/dir and register it in the feature'srouter.py - Keep HTTP translation in the router; put orchestration in the feature's
services/ - Attach schemas to
schemas.pyand feature-specific errors toexceptions.py - Wire dependencies via runtime-aware helpers rather than importing process globals
- Mirror the test placement under
tests/unit/<feature>/
- Camera backend contract:
app/camera/services/backend.py - Camera orchestration:
app/camera/services/manager.py - Picamera2 implementation:
app/camera/services/picamera2_backend.py - Runtime-owned service wiring:
app/core/runtime.py - Shared DTOs:
relab_rpi_cam_models/src/relab_rpi_cam_models/ - Tests:
tests/unit/camera/andtests/integration/
Shared cross-repo contract DTOs live in the relab_rpi_cam_models package. Keep runtime logic in the plugin repo. After contract changes:
- Update version in
relab_rpi_cam_models/pyproject.toml - Rebuild the main project's lock file:
uv lock --upgrade relab-rpi-cam-models
The workspace source override is for local co-development only. Treat the
published relab-rpi-cam-models package version as the actual cross-repo
contract baseline.
- Run tests:
uv run pytest tests - Check coverage: Aim for >80%
- Run pre-commit:
pre-commit run --all-files - Update docs: If adding features, update relevant docs
- Test on hardware: If you modified camera logic, test on an actual Pi
Runtime container (app/core/runtime.py::AppRuntime) owns all long-lived services: camera manager, relay service + state, pairing service + state, preview pipeline + sleeper + thumbnail worker, thermal governor, upload queue worker, observability handle, and managed background tasks. Attach new long-lived services here instead of adding module-level singletons.
Config vs runtime state. Settings (env-backed, static) holds operator config; RuntimeState holds live mutable relay/local-auth/derived-auth state. If a value changes while the app is running, it lives on the runtime side.
Orchestration entrypoints. PairingService and RelayService are the only production entrypoints for pairing and relay flows.
Contracts. Backend OpenAPI is the public frontend contract. relab_rpi_cam_models is the private backend↔plugin device seam — frontend code never imports from it, and new cross-repo payloads go into the shared package first.
Connection. The plugin opens an outbound WebSocket relay (app/relay/service.py) to the backend. No public IP or port forwarding.
Camera capture. Real hardware: libcamera via picamera2. Tests: synthetic image generation.
Streaming. YouTube RTMP only. The Pi publishes into MediaMTX locally; MediaMTX handles the egress to YouTube.
View Docker logs:
docker compose logs -f appIf you start the optional observability-ship profile, Alloy ships the app's structured file logs to the external Loki-compatible endpoint configured by LOKI_PUSH_URL. Local Loki/Grafana is not bundled with this plugin.
View direct server logs:
just dev- Swagger UI:
http://localhost:8018/docs - Setup page:
http://localhost:8018/setup
Configuration precedence is:
- environment variables
- relay credentials file (
~/.config/relab/relay_credentials.json) for generated/runtime secrets - generated defaults on first boot where applicable
Key debugging settings:
DEBUG=true— Enable debug loggingCAMERA_DEVICE_NUM=0— Switch camera device (if multi-camera setup)OTEL_ENABLED=trueandOTEL_EXPORTER_OTLP_ENDPOINT=...— enable trace export
Plugin application releases and relab_rpi_cam_models releases are versioned independently.
The plugin app release remains fully automated via commitizen and GitHub Actions.
- Write commits following Conventional Commits —
fix:bumps patch,feat:bumps minor,feat!:/BREAKING CHANGE:bumps major - Merge to
main— CI runs lint, tests, and dependency audit - On CI success, the release workflow automatically:
- Bumps the version in
pyproject.tomlandapp/__version__.py - Updates
CHANGELOG.md - Pushes a
vX.Y.Ztag - Creates a GitHub release with auto-generated notes
- Bumps the version in
If no commits since the last tag warrant a bump, the plugin release step skips silently.
The contract package publishes independently to PyPI.
- Update
relab_rpi_cam_models/pyproject.tomlto the package version you want to publish - Rebuild the workspace lock file:
uv lock --upgrade relab-rpi-cam-models - Merge the package changes to
main - Create and push a tag named
relab-rpi-cam-models-vX.Y.Z
The publish workflow verifies that the tag version matches relab_rpi_cam_models/pyproject.toml, reruns package-focused checks, builds the distributions, and publishes them to PyPI via GitHub trusted publishing.
- Check INSTALL.md for setup issues
- See README.md for project overview
- Review existing code and tests for patterns