8.5 KiB
8.5 KiB
Vigilar Architecture Overview
This document explains how Vigilar is put together for someone reading the
codebase for the first time. It is short on purpose — per-subsystem details
live under subsystems/.
Design principles
- Offline-first. No external calls in the critical path. Cloud integrations, if any, are opt-in and off the hot path.
- Subsystem isolation. Each subsystem runs in its own process. A crash in
one subsystem cannot take down another — the supervisor in
vigilar/main.pyrestarts crashed children with exponential backoff. - Loose coupling via MQTT. Subsystems do not call each other directly.
They publish and subscribe to a local Mosquitto broker on
127.0.0.1:1883. - SQLite (WAL) is the single durable store. Access goes through
SQLAlchemy Core expressions (
vigilar/storage/schema.py), not ORM mapped classes. WAL mode andsynchronous=NORMALare set on every connection invigilar/storage/db.py. - Adaptive cost. Cameras idle at 2 FPS and jump to 30 FPS on motion, with a 5-second ring buffer so the moment leading up to the trigger is kept.
- Configuration is typed.
config/vigilar.tomlis loaded and validated by Pydantic v2. Secrets are never inline — they are file paths under/etc/vigilar/secrets/.
Process topology
vigilar start loads config and calls run_supervisor() in vigilar/main.py.
The supervisor spawns every subsystem as a multiprocessing.Process (via the
SubsystemProcess wrapper) and monitors them in a 2-second restart loop.
Cameras are managed separately by CameraManager, which owns one child
process per configured camera.
systemd (vigilar.service)
|
v
vigilar start (supervisor, main.py)
|
+--------+-----------+-----------+-----------+----------+
| | | | | |
v v v v v v
web event- sensor- ups- presence- health-
(Flask) processor bridge monitor monitor monitor
CameraManager --> camera worker (front_door)
--> camera worker (backyard)
--> camera worker (side_yard)
--> camera worker (garage)
^ ^
| MQTT |
v v
mosquitto (127.0.0.1:1883, loopback only)
Every arrow touching the broker is a local TCP connection to loopback. The
web process is a Flask server (vigilar/web/app.py:create_app) with one
Blueprint per feature area under vigilar/web/blueprints/.
The MQTT bus
- Broker: Mosquitto, bound to loopback only (see
systemd/vigilar-mosquitto.conf:listener 1883 127.0.0.1,allow_anonymous true,persistence false). - Topic convention: every topic starts with
vigilar/and is defined invigilar/constants.pyvia theTopicsclass (either static strings or builder functions taking an ID). Real examples:vigilar/camera/{camera_id}/motion/startvigilar/camera/{camera_id}/motion/endvigilar/camera/{camera_id}/heartbeatvigilar/sensor/{sensor_id}/{event_type}vigilar/ups/status,vigilar/ups/power_loss,vigilar/ups/low_batteryvigilar/system/arm_state,vigilar/system/alert- Wildcard subscriptions:
vigilar/#,vigilar/camera/#,vigilar/sensor/#
- Payloads are JSON dicts. Publishers use
bus.publish_event(topic, **kwargs)fromvigilar/bus.py; new fields are callers' responsibility. - Why MQTT rather than an in-process queue: crash isolation, introspection with
mosquitto_sub, and the option to move subsystems to separate hosts later without changing the wire format.
Data flow: from motion to phone notification
vigilar/camera/worker.py:run_camera_worker— opens the RTSP stream viacv2.VideoCapture(..., cv2.CAP_FFMPEG)with reconnect/backoff, pushes every frame into a ring buffer, and drives the capture loop.vigilar/camera/motion.py:MotionDetector.detect— MOG2 background subtraction on a downscaled frame; when a new motion edge is found,worker.pypublishesvigilar/camera/{camera_id}/motion/startwith confidence and zone count.vigilar/camera/recorder.py:AdaptiveRecorder.start_motion_recording— stops any idle recording, launches a fresh FFmpeg subprocess atmotion_fps(default 30), and writes the flushed ring-buffer frames (default 5s of pre-roll) before the live frames. On stop, ifVIGILAR_ENCRYPTION_KEYis set, the MP4 is re-encrypted in place to.vgeviavigilar/storage/encryption.py:encrypt_file(AES-256-CTR).vigilar/events/processor.py:EventProcessor._handle_event— subscribes tovigilar/#, classifies the topic into anEventType/Severity, and writes a row to theeventstable viavigilar/storage/queries.py:insert_event. Wildlife and pet sightings also get rows inwildlife_sightings/pet_sightings.vigilar/events/rules.py:RuleEngine.evaluate— matches the event against configured[[rules]]fromvigilar.toml(AND/OR on arm state, sensor event, camera motion, time window), honours per-rule cooldowns, and returns a list of actions.vigilar/alerts/sender.py:send_alert— foralert_all/push_and_recordactions, builds a notification from the_CONTENT_MAPtable, loads the VAPID key from[alerts.web_push].vapid_private_key_file, and callspywebpush.webpushfor every row inpush_subscriptions. Successes and failures are recorded inalert_log; endpoints returning410 Goneare pruned.- Web UI — the browser holds an open SSE connection to a handler in
vigilar/web/blueprints/events.py(mimetype="text/event-stream"), which tails new event rows and pushes them to the timeline live.
Storage layout
vigilar.dbunder[system] data_dir(default/var/vigilar/data), SQLite in WAL mode. Tables defined invigilar/storage/schema.py:cameras,sensors,sensor_states,events,recordings,system_events,arm_state_log,alert_log,push_subscriptions,pets,pet_sightings,wildlife_sightings,package_events,pet_training_images,pet_rules,face_profiles,face_embeddings,visits,timelapse_schedules.- Recordings:
.vgefiles under[system] recordings_dir(default/var/vigilar/recordings), AES-256-CTR with a random 16-byte IV prefixed to each file. Key at/etc/vigilar/secrets/storage.key. Losing the key means losing the recordings — there is no recovery path. - HLS: rolling segments under
[system] hls_dir(default/var/vigilar/hls), written by the per-cameraHLSStreamerinvigilar/camera/hls.py. - Backups: DB +
/etc/vigilartarball viascripts/backup.sh.
Configuration and secrets
config/vigilar.tomlis the only configuration file the app reads (systemd pointsVIGILAR_CONFIGat/etc/vigilar/vigilar.tomlin production).- Validated by Pydantic v2 at startup (
vigilar/config.py). - Secrets never live in the TOML; they are file paths under
/etc/vigilar/secrets/(storage.key,vapid_private.pem). - The arm PIN and admin password are hashed; comparisons are constant-time
(see
vigilar/alerts/pin.py). The PIN hash is written into the TOML viavigilar config set-pin, never typed by hand.
The web tier
- Flask with Blueprints, one per feature area under
vigilar/web/blueprints/:cameras,events,kiosk,pets,recordings,sensors,system,visitors,wildlife. All registered invigilar/web/app.py:create_app. - Jinja2 templates under
vigilar/web/templates/, Bootstrap 5 dark theme, static assets undervigilar/web/static/. - Live view:
hls.jsgrid for bandwidth efficiency, MJPEG single view for low latency. - Live timeline updates via Server-Sent Events from
vigilar/web/blueprints/events.py. - PWA with VAPID web push — no Firebase, no Google Cloud Messaging. Service
worker at
vigilar/web/static/sw.js.
What is NOT in the critical path
- Remote access (
[remote]section) — optional, bandwidth-shaped HLS over a WireGuard tunnel. - Email alerts (
[alerts.email]) and webhook alerts ([alerts.webhook]) — optional, off by default. - Any cloud service — never.
Where to go next
- Conventions:
conventions.md - Per-subsystem details:
subsystems/