Repository navigation
Releases
v0.7.2
Added
- The local API on port 8018 can serve HTTPS. Set
TLS_CERT_FILEandTLS_KEY_FILEto a
certificate and key, mount them into the container and usehttpsinBASE_URL; see
"Serve the local API over HTTPS" in INSTALL.md. It is off by default. The app refuses to
start when only one is set or the pair does not load. The image now starts with
python -m app.serveinstead offastapi run. (#116)
Changed
- Release tags are signed:
just release-publishsigns the tag with the maintainer's key
and pushes it before drafting the release, instead of GitHub creating an unsigned tag. (#113) - The image and the install no longer pull in the deploy-only parts of
fastapi[standard]
(the FastAPI Cloud CLI,email-validator,httpx2,pydantic-extra-typesand their
dependencies). The app asks for what it uses:fastapi-cli,uvicorn[standard],jinja2
andpython-multipart. The runtime image is about 15 MB smaller. (#112)
Fixed
- A 2xx upload reply that is not JSON or lacks
image_idorimage_urlnow counts as
delivered, with a warning in the log. Before, the Pi retried the upload and the backend
stored the capture twice. (#108) - A 2xx upload reply with a valid
image_idbut a bad or missingimage_urlnow keeps the
backend's id. Before, the Pi fell back to its own capture id and lost the backend's. (#111) - A capture whose requeue after pairing was cut off by a crash is no longer lost when the
camera restarts and drains its queue before the next pairing. The queue now finishes the
requeue instead of deleting the image. A full disk while finishing one such capture no
longer stops the rest of the requeue or the queue drain. (#109) - A camera start that is cancelled (for example by a request timeout) no longer leaves the
camera claimed until the app restarts. The close now waits for the start, or for opening
the camera, to finish, because picamera2 cannot release a camera that is still starting. (#109) - A capture that runs out of upload attempts while a new capture is being queued is no longer
lost. The queue check could delete its metadata between the two file moves intodead/.
A dead-letter move cut off by a crash or shutdown is now finished on the next pass. (#109)
v0.7.1
Added
- Each capture's metadata records how it was made: the lens position, white-balance gains
and sensor crop, the plugin version and the commit the release image was built from, and
the SHA-256 of the libcamera tuning file. R9lab stores them with the image. (#100)
Changed
- Needs r9cam-models 0.6.1, which adds the provenance fields and the stricter paired poll
response. (#106) - The container image is about 220 MiB smaller unpacked (amd64 build: 750 to 529 MiB of files,
797 to 556 MB as Docker reports it; the arm64 0.7.0 image was 778 MiB of files). It drops
Mesa's GPU drivers with LLVM and Z3, which the libav libraries pull in but a headless image
never loads, and it runs on Debian's Python 3.13 (3.13.5) instead of a second interpreter
from the python:3.13-slim base. (#103)
Fixed
- Relayed commands no longer fail with 401 when the relay credentials come from environment
variables. The key the relay uses to call this camera's own API was never created or
authorized on that path, only when credentials were read from the credentials file. (#96) - A relayed command whose reply the relay cannot encode (for example an oversized
Content-Type) now gets a 500 reply at once. Before, the backend waited out its 30 second
command timeout. (#97) - The release image gets its SBOM and build provenance attestations again. The 0.7.0 image
was published without them, because the SBOM step looked for an amd64 image. ([#95]) - The citation metadata lists "research data" instead of "computer vision", which r9cam does
not do. - A queued upload whose metadata file is valid JSON of the wrong shape, such as a list or a
nullattempt count, is now skipped instead of stopping every queued upload and every new
capture. A timestamp without a timezone is read as UTC. (#102) - A capture left in the upload queue without its metadata file, after a crash or power cut
during queueing, is now removed after a minute. It used to count against the queue's size
limit forever. A requeue that a crash interrupted is finished on the next start. (#102) - Unpairing keeps the local API key. Before, it deleted the key with the relay credentials,
so the next start made a new one and LAN clients lost access. (#101) - When the camera fails to configure, for example on a memory error, or its start is cancelled
by a request timeout, it is released, so the next capture or stream opens it again. Before,
the camera stayed half set up until a restart. (#101) - The preview thumbnail keeps refreshing after an error. Before, one failed refresh stopped
the refresh loop until a restart, so the camera card showed an old image. (#101) - The relay waits before it reconnects when the backend accepts the connection and then drops
it at once. Before, it reconnected in a tight loop. The wait grows up to 60 seconds and
resets after a connection that stays up for 60 seconds. (#101) - A capture the backend accepted is no longer uploaded again when the backend's reply has an
image_urlthe camera cannot use, such as plain HTTP in production. The camera logs a
warning and returns the capture without a URL. Before, it queued the capture for retry and
the backend stored it twice. (#101)
v0.7.0
Upgrading:
- Upgrade the R9lab backend first. This release needs one that serves the
/v1/plugins/r9cam
routes and accepts ther9cam-relaytoken audience: the hostedapi.r9lab.iodoes, a
self-hosted R9lab v0.5.0 or older does not. Pis on 0.6 keep working with a current backend
until 2027-01-31, so upgrade them before then. - Docker Compose now pulls the release image: run
git pullfirst, thendocker compose pull
anddocker compose up -d. Standalone installs: setR9CAM_IMAGE=r9camin.envand run
docker compose buildinstead ofpull. - Paired cameras stay paired when Docker Compose is upgraded in place and on installs without
Docker. A fresh clone into anr9cam/directory starts with empty volumes. See "Upgrading
From 0.6" in INSTALL.md for both cases. - With S3 storage, new captures go under the
r9cam/key prefix. Objects already stored stay
underrpi-cam/; nothing moves them. Dashboards and alerts that filter
onjob="rpi-cam-plugin"orrpi_cam_*metrics must switch tojob="r9cam"andr9cam_*.
Added
- Each release publishes a signed linux/arm64 image to
ghcr.io/cmlplatform/r9cam, tagged
with the version,X.Yandlatest, so the Pi pulls it instead of building it. Its build
provenance and SBOM are attested:gh attestation verify oci://ghcr.io/cmlplatform/r9cam@<digest> --repo CMLPlatform/r9camchecks them.R9CAM_VERSIONin.envpins a release; the default islatest. (#87)
Changed
- Breaking: the plugin is now r9cam, and its shared models package is
r9cam-models
(wasrelab-rpi-cam-models). Renamed with it: theR9CAM_CREDENTIALS_FILEvariable, the
~/.config/r9camcredentials directory, the_r9cam._tcpmDNS service type, ther9cam
OpenTelemetryservice.name,/healthzservice, ther9cam_sessioncookie,
openapi.r9cam.jsonand the device token audiencer9cam-relay. INSTALL.md has the full
table. (#45, #48) - Breaking: the last
rpi-camnames follow the rename: Prometheus metrics arer9cam_*
(wasrpi_cam_*), the Alloyjoblabel and defaultinstancearer9cam(was
rpi-cam-plugin), the default S3 key prefix isr9cam/(wasrpi-cam/), the example bucket
isr9cam, and the container user isr9cam. (#86) - Breaking: the Pi calls only the
/v1/plugins/r9camroutes and the default pairing
backend ishttps://api.r9lab.io. (#79) - Pairing polls with POST and a JSON body, as the current backend expects. (#46)
- EXIF capture details (exposure, offset time, temperature) are written to the Exif block, and
Softwarenames the r9cam version.
Fixed
- The upload queue stays under its byte cap when captures arrive at the same time. (#61)
- Captures that ran out of upload retries, for example while the Pi was unpaired, are queued
again after a successful pairing. Any that no longer fit under the queue's limits stay in
data/queue/dead/. (#85) local_setup.shinstalls uv from a release archive it checks against its checksum. (#68)- Installs without Docker keep their pairing across the rename, and
RELAB_CREDENTIALS_FILE
still works with a warning. (#79) - The wheel and sdist include the AGPL license text. (#90)
- The relay keeps retrying when the backend refuses its handshake (401, 403, 404, 429 and
the like), with the usual backoff and a warning that names the status, instead of stopping
until the next restart. On a 401 or 403 the warning says the camera may need re-pairing. (#84)
Security
v0.6.2
What's Changed
- chore(deps): lock file maintenance by @renovate[bot] in #26
- chore(deps): update docker by @renovate[bot] in #25
- ci(container): gate image CVEs on the weekly scan, report them on PRs by @simonvanlierde in #29
- chore(deps): update docker by @renovate[bot] in #28
- chore(deps): update github-actions (major) by @renovate[bot] in #30
- chore(deps): lock file maintenance by @renovate[bot] in #27
- fix(pairing): pin the relay host to the paired backend by @simonvanlierde in #31
Full Changelog: v0.6.1...v0.6.2
v0.6.1
What's Changed
- chore(deps): lock file maintenance by @renovate[bot] in #23
- chore(deps): update github-actions by @renovate[bot] in #20
- fix: device seam validation and licensing update by @simonvanlierde in #24
- chore(deps): update dependency uv_build to >=0.12.3,<0.13 by @renovate[bot] in #22
- chore(deps): update docker by @renovate[bot] in #21
Full Changelog: v0.6.0...v0.6.1
v0.6.0
Relayed commands are dispatched in-process rather than over loopback TCP, so the
relay no longer depends on the local listener being reachable. The command
timeout is now a shared constant the backend can align against.
Also adds ADR 0001 (remote preview stays on LL-HLS) and test coverage for
camera payload validation, CSRF origin checks, and upload-queue recovery.
Requires relab-rpi-cam-models >= 0.5.0.
relab-rpi-cam-models v0.5.0
Adds RELAY_COMMAND_TIMEOUT_SECONDS — the Pi-side deadline for dispatching one
relayed command to its local API. The backend's per-command wait should exceed
this (plus grace) so the Pi's own timeout response reaches the caller instead of
a backend-side TimeoutError.
Additive only; no breaking changes from 0.4.0.
v0.5.0
What's new
Unauthenticated local-network liveness probe (GET /healthz).
The app discovers a direct Ethernet/USB link by probing candidate LAN addresses, but it only holds the device API key after bootstrapping one through the backend relay — which needs the camera to be online. Every endpoint that could serve as a liveness signal (GET /camera) required X-API-Key, so a keyless probe always got a 401 and a camera reachable only over the direct link could never be auto-detected.
GET /healthz closes that gap. It sits on the public router, is gated to local clients like the other unauthenticated surfaces (HLS preview, setup UI), and returns just {"status": "ok", "service": "relab-rpi-cam"} — enough for a prober to tell an RPi cam from any other LAN host that answers 200, and nothing more.
Security improvement. Because liveness no longer needs a credential, the app stops attaching the device API key to probe requests. The key is no longer sprayed at every server-supplied candidate URL during discovery — it now only ever reaches the single host that wins the probe, on real use.
Maintenance
Upgrading
Deploy this firmware before or together with the app release that consumes it. Cameras still on v0.4.0 will 404 the probe, and the app quietly falls back to relay transport — direct-connection discovery stops working, but nothing breaks.
No configuration changes. openapi.rpi-cam.json is regenerated; /healthz is the only added path.
v0.4.0
What's Changed
- Configure Renovate by @renovate[bot] in #1
- Small fixes by @simonvanlierde in #5
- Add local pairing flow and local-access improvements for the RPi camera plugin by @simonvanlierde in #6
- chore: update action versions in CI workflows and add codecov configu… by @simonvanlierde in #7
- Working branch by @simonvanlierde in #8
- Simplify pairing flow and clean up related config handling by @simonvanlierde in #9
- chore(deps): update dependency pyjwt to v2.13.0 [security] by @renovate[bot] in #10
- chore(deps): update dependency pydantic-settings to v2.14.2 [security] by @renovate[bot] in #11
- chore(deps): update github-actions by @renovate[bot] in #12
- feat: security-harden the RPi camera plugin and reorganize into delivery/streaming/observability by @simonvanlierde in #14
New Contributors
Full Changelog: https://github.com/CMLPlatform/relab-rpi-cam-plugin/commits/v0.4.0
relab-rpi-cam-models-v0.4.0
⚠️ Breaking
- Removed
SAFE_RELAY_TRACE_HEADERS→ useextract_safe_relay_headers(). capture_timenow requires an awaredatetime(naive values rejected; serialized as UTCZ).
Added
- Relay allowlist contract:
relay_command_is_allowed(),build_relay_command(),relay_content_type_is_binary(),RELAY_ALLOWED_*,RELAY_COMMAND_FORBIDDEN_DETAIL,RELAY_WS_TEXT_FRAME_LIMIT_BYTES. - Pairing contract:
PairingCode,PAIRING_CODE_ALPHABET/LENGTH/PATTERN