Architecture¶
One broker, one monitor process, one SQLite file, one read-only dashboard.
Everything a sensor publishes arrives on a single MQTT topic
(meshtastic/receive by default); the monitor routes it, classifies it,
stores it, and decides whether it alerts.
The pipeline¶
flowchart TB
IN["MQTT message"] --> ROUTE{route}
ROUTE -->|control topic| ARM["arm / disarm / auto<br/>(secret-gated)"]
ROUTE -->|heartbeat topic| WD["sensor watchdog"]
ROUTE -->|sensor event| EV["handle_sensor_event<br/>vehicle · vibration · contact"]
ROUTE -->|MAC sighting| MAC["MAC pipeline<br/>RSSI floor → EMA → whitelist → dwell"]
EV --> LOG[("events table")]
MAC --> DET[("detections table")]
MAC --> LOG
EV --> COR["correlation buffer"]
MAC -->|alert-worthy| COR
EV --> AL["alert dispatch"]
MAC -->|unknown + armed + dwelled| AL
COR -->|"≥2 distinct types in window"| HC["HIGH CONFIDENCE alert"]
AL --> NLOG[("notifications table")]
HC --> NLOG
Canonical events (v0.2)¶
Every accepted wire format is normalized at ingest into one canonical dict; storage, correlation, and alerting consume only canonical events, so deployed sensors never need reflashing when the backend evolves:
{
"type": "vibration", # vehicle | vibration | contact | wireless_presence
"node": "fence-e",
"sensor": "piezo",
"event": "shake", # detected | knock | shake | trigger | ...
"value": 9, # peak / hits / magnitude (type-dependent)
"ts": "<iso8601 utc>",
"meta": {"mac": ..., "rssi": ...} # extras ride here
}
Accepted wire formats, all handled by mqtt/events.py:
| Wire format | Source | Normalized to |
|---|---|---|
{"v":1,"type":...,"node":...} |
bridges, future sensors | as-is; unknown keys → meta |
{"event":"knock","from":...,"peak":N} |
sensor firmware over WiFi/MQTT | vibration/knock etc. |
{"mac","from","rssi"} |
all MAC sources | wireless_presence, after the MAC pipeline |
Compact lines V,123 K,812 S,9 AABBCC112233,-64 |
LoRa relay | expanded by the bridges |
| Detection Sensor mesh packets | stock Meshtastic | contact/trigger |
The type registry (TYPE_REGISTRY) maps each (type, event) to its alert
text, cooldown config key, and alertability. Adding a sensor type is adding a
row.
The MAC pipeline¶
MAC sightings get extra treatment before they become events, because phones randomize MACs and radio is noisy:
- RSSI floor (
RSSIMin) drops far-away chatter - EMA smoothing (
EMAlpha) stabilizes flapping signal strength - Whitelist (
config/whitelist.txt, hot-reloaded) marks known gear - Dwell (
DwellSeconds) separates a passer-by from someone loitering
Storage¶
logs/detections.db (SQLite) holds three tables:
detections: the original MAC log:mac, node, rssi, timestamp, lat, lonevents: every canonical event, MAC and non-MAC alike:ts, node, type, sensor, event, value, lat, lon, meta(JSON). Pre-v0.2 detections are backfilled in once, so history search reaches all the way back.notifications: one row per alert delivery attempt per channel:ts, channel, target, ok, error, message
All three are pruned by [Files] RetentionDays (0 = keep forever). Writes are
committed at least every 5 seconds so the dashboard stays fresh.
Correlation¶
A rolling buffer records recent alertable events: ones that passed
armed/whitelist/dwell checks, even if their own per-type cooldown muted the
notification. When events from at least CorrelationMinTypes distinct
types land within CorrelationWindowSeconds, one combined HIGH CONFIDENCE
alert lists the contributors. Individual alerts are never held back;
correlation is pure escalation. See Configuration.