cam01 Vision Capture Pipeline¶
Status: Tier 2 Architecture Document Last Updated: 2026-05-03 WI: WI-395, WI-400
Context¶
caneast-site1-ot2-cam01 is a Raspberry Pi 5 4GB vision node deployed in zone OT-2 (main floor residential). Its primary purpose is event-driven door-entry detection: monitoring a door via an MC-38 reed switch (GPIO 17) and capturing still images on open events for vision-language model inference to classify what entered or exited.
The pipeline combines hardware sensors, local frame capture, continuous RTSP streaming, Frigate NVR ingestion, and tiered vision inference with local-first sovereignty.
Architecture Overview (Implemented, as of WI-400)¶
Pi 5 (caneast-site1-ot2-cam01 -- REDACTED)
|
+-- MC-38 Reed Switch (GPIO 17, pull_up=True)
| |
| v
| cam01-capture.service (runs as cam01:cam01, video+gpio groups)
| |
| +-- on_door_open():
| | 1. MQTT publish door_state=1, capture_triggered=1
| | 2. sudo systemctl stop cam01-mediamtx [release camera device]
| | 3. rpicam-still -> /tmp/cam01_latest.jpg (1920x1080)
| | 4. sudo systemctl start cam01-mediamtx [restore stream, try/finally]
| | 5. HTTP POST JPEG to Ollama /api/generate (qwen3-vl:4b)
| | 6. MQTT publish inference_result (JSON)
| | 7. JSONL append to /var/log/cam01/inference.jsonl
| |
| +-- on_door_closed():
| MQTT publish door_state=0
|
+-- cam01-mediamtx.service (runs as mediamtx:mediamtx, SupplementaryGroups=video)
|
+-- MediaMTX v1.18.1 (rpiCamera source, softwareH264, 1920x1080, 5fps)
|
v
rtsp://REDACTED:[REDACTED]/cam01
|
v
Frigate 0.14.1 (archon-vision namespace, caneast-site1-node4)
[Records continuous stream, motion events to MQTT]
|
v
https://frigate.peries.ca (Traefik IngressRoute, TLS)
Camera contention model (Option A): MediaMTX holds the camera device 100% of the time during
normal streaming. On a door-open event, capture.py stops MediaMTX via sudo systemctl, takes a
single still with rpicam-still --immediate, then restarts MediaMTX. The try/finally guard
ensures the stream is always restored even if capture fails. Stream gap per door event is
approximately 2 seconds.
Node Provisioning¶
OS: Raspberry Pi OS Lite 64-bit Trixie (Debian 13, kernel 6.12 LTS, aarch64). IP: REDACTED (DHCP-reserved). Camera: Camera Module 3 (IMX708, /dev/media2 + /dev/media0 ISP).
Bootstrap pattern follows the fleet-wide two-user model:
| Phase | SSH user | How provisioned | Inventory ansible_user |
|---|---|---|---|
| Imager flash | operator | Pi Imager -- operator pubkey loaded from Infisical | operator |
| Bootstrap play | operator | Same key | operator (transitional) |
| Ongoing automation | ansible-ot-svc-account | Created by bootstrap play; key from Infisical | ansible-ot-svc-account |
Steps to reach automation-ready state:
1. Flash Pi 5 with Raspberry Pi OS Lite 64-bit Trixie via Pi Imager
2. Imager advanced settings: username operator, paste operator SSH public key from Infisical
3. Boot Pi, reserve DHCP IP, set ansible_host: REDACTED in ansible/inventories/ot/hosts.yml
4. Run ansible/playbooks/ot/cam01-bootstrap.yml (creates ansible-ot-svc-account, deploys key, restricted sudoers)
5. Update ansible_user: ansible-ot-svc-account in ot/hosts.yml
6. Run ansible/playbooks/ot/cam01-mediamtx.yml then ansible/playbooks/ot/cam01-capture.yml
Python isolation: Trixie enforces PEP 668 (externally-managed Python). The capture pipeline
runs from a virtualenv at /opt/cae/cam01/.venv created with --system-site-packages so that
apt-managed packages (gpiozero, paho-mqtt, requests) are accessible. lgpio (python3-lgpio)
is system-installed; the service sets WorkingDirectory=/run/cam01 so lgpio can create its
notification pipe (Pi 5/Trixie requires writable CWD for lgpio, not HOME).
Pipeline Stages¶
1. Event Source: MC-38 Reed Switch (GPIO 17)¶
The MC-38 magnetic contact switch is wired to GPIO 17 on the Pi 5 with pull_up=True.
gpiozero uses the lgpio backend (python3-lgpio, v0.2.2) to access /dev/gpiochip*.
Event trigger logic:
- when_pressed (GPIO pulled low = door open): triggers full capture + inference cycle
- when_released (GPIO high = door closed): publishes door_state=0, no capture
2. MQTT Publish (Pre-Capture)¶
Before capture, publish to MQTT broker at REDACTED:1883:
- caneast/ot-zone/cam01/door_state = "1" (retained)
- caneast/ot-zone/cam01/capture_triggered = "1"
3. Frame Capture: rpicam-still subprocess¶
capture.py releases the camera from MediaMTX, then runs:
The --immediate flag skips the AEC/AWB settling delay for fast event capture.
After capture (success or failure), MediaMTX is restarted via the try/finally guard.
4. NVR Ingest: MediaMTX RTSP to Frigate¶
MediaMTX runs as a dedicated systemd service (cam01-mediamtx.service) with the
mediamtx system user in the video group. It streams the IMX708 camera at
1920x1080, 5fps, softwareH264 (OpenH264) to Frigate.
See mediamtx-cam01.md for full MediaMTX configuration and the Pi 5 hardware H264 limitation that requires softwareH264.
Frigate role: - Continuous recording with configurable retention - Motion event detection, MQTT publish on events - Web UI at https://frigate.peries.ca and HA integration - Live stream at rtsp://REDACTED:[REDACTED]/cam01, HLS at :8888, WebRTC at :8889
5. VL Inference: Tiered Chain¶
Tier 1: Ollama (Active, Primary)¶
- Endpoint:
http://REDACTED:[REDACTED]/api/generate - Model:
qwen3-vl:4b - Latency target: < 90s (timeout)
Tiers 2-4: Cloud Fallbacks (Stub -- not yet implemented)¶
The inference loop in capture.py has the tier structure defined but Tiers 2-4
are commented stubs. When Tier 1 exhausts, inference_failed is published.
Cloud API clients will be added in a future WI when offline resilience is required.
6. MQTT Result Publish and JSONL Logging¶
{
"label": "person",
"confidence": 0.91,
"description": "A person is visible entering through the doorway",
"tier_used": 1,
"model": "qwen3-vl:4b",
"latency_ms": 4820,
"ts": "2026-05-03T04:30:00.123Z"
}
Published to caneast/ot-zone/cam01/inference_result (retained).
Appended to /var/log/cam01/inference.jsonl with daily logrotate, 90-day retention.
Design Decisions¶
Option A: MediaMTX Owns Camera, rpicam-still for Events¶
Chosen over: - Option B (picamera2 unified): picamera2 switch-mode is complex; MediaMTX can't use the camera when picamera2 holds it. - Option C (libcamera-vid pipe): brittle coordination via named pipes; failure recovery is complex.
Option A gives MediaMTX full ownership during streaming, and rpicam-still provides
clean single-shot captures with --immediate. Stream gap is ~2s per door event, which is
acceptable for a 5fps NVR stream.
softwareH264 on Pi 5¶
The Pi 5 (BCM2712/RP1) does not expose the VC4-style V4L2 M2M H264 encoder that Pi 4 had.
MediaMTX's hardwareH264 codec fails with encoder_create(): unable to open device.
softwareH264 (OpenH264 via MediaMTX's bundled libcamera v0.5.0+59) works reliably at 5fps
on the Pi 5's quad-core Cortex-A76.
lgpio WorkingDirectory¶
The python3-lgpio library on Pi OS Trixie creates notification pipes relative to the
process CWD (not HOME). System users running under systemd have no home directory.
The service sets RuntimeDirectory=cam01 (creates /run/cam01 owned by cam01) and
WorkingDirectory=/run/cam01 so lgpio can write its .lgd-nfy* pipes.
Ollama First, Cloud Stubs¶
Per LLMOPS-0002 addendum (2026-05-02): OT-2 is residential/consumer; cloud inference is permitted as fallback. Ollama on the CanEast AI Node workstation (REDACTED:[REDACTED], LAN-bound) is the primary sovereign tier with zero data egress in the normal path.
Failure Modes¶
| Mode | Impact | Mitigation |
|---|---|---|
| MediaMTX fails to restart after capture | Stream down until service auto-restarts | Restart=on-failure, RestartSec=5 in systemd unit |
| All inference tiers fail | inference_failed published |
Alert on label=inference_failed |
| Ollama unavailable | No cloud fallback (tiers 2-4 are stubs) | Currently: inference_failed; future WI adds cloud fallback |
| Camera device busy | rpicam-still fails, exception raised | try/finally still restarts MediaMTX; capture_triggered without inference_result |
| MQTT broker down | Results not published but JSONL still written locally | paho reconnect on next event |
References¶
- OT-0001 (2026-05-02 addendum) -- caneast-site1-ot2-cam01 hardware spec, GPIO map, MQTT topics
- LLMOPS-0002 (2026-05-02 addendum) -- Residential OT-2 zone sovereignty clarification
- platform/mediamtx-cam01.md -- MediaMTX deployment, Pi 5 notes, softwareH264 rationale
- platform/frigate-caneast-site1-node4.md -- Frigate NVR Kubernetes deployment reference
- platform/ollama-alienware-bind.md -- Ollama LAN bind, model inventory
- internal/vl-inference-cam01.md -- qwen3-vl:4b inference details and synthetic test results
- WI-395 -- caneast-site1-ot2-cam01 Phase 1 documentation
- WI-400 -- cam01 MediaMTX integration and Option A camera contention implementation