A lightweight ISR workbench: ingest -> normalize to ZMeta -> rules/alerts -> WebSocket -> live Leaflet map -> record to NDJSON.
- FastAPI backend: REST
/api/v1/ingest, UDP:5005, WebSocket/ws - Same-origin UI served at
/ui/live_map.html(root/redirects) - Adapters normalize simulator/KLV payloads to ZMeta
- YAML rules raise alerts (info/warn/crit) that pulse on the map
- Recorder writes NDJSON under
data/records/(gitignored)
Dev defaults: permissive CORS, no auth - great for local work. Lock down before exposing externally.
- Ingest -> Rules -> Alerts pipeline (served live at
/docs/local/ingest_pipelineand aliased at/docs/pipeline)
scripts\dev.ps1# Clone & enter
git clone https://github.com/JTC-byte/zmeta-stack.git
cd zmeta-stack
# Create venv + install deps
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
# Run the backend (IMPORTANT: use the venv's Python)
python -m uvicorn backend.app.main:app --reloadOpen the map: http://127.0.0.1:8000 -> redirects to /ui/live_map.html
Windows note: Every new terminal starts without the venv.
Reactivate before running:.\.venv\Scripts\Activate.ps1 python -m uvicorn backend.app.main:app --reload
A) RF simulator (module run)
# new terminal? activate venv again first
.\.venv\Scripts\Activate.ps1
python -m tools.simulators.rfB) Thermal simulator (module run)
.\.venv\Scripts\Activate.ps1
python -m tools.simulators.thermalC) KLV simulator (module run)
.\.venv\Scripts\Activate.ps1
python -m tools.simulators.klvD) GUI control panel (module run)
.\.venv\Scripts\Activate.ps1
python -m tools.gui_app
# Docs button opens http://127.0.0.1:8000/docs/pipelineE) Single REST packet (PowerShell)
$body = @{
sensor_id="rf_sim_001"; modality="rf"; timestamp="2025-01-01T00:00:00Z"
source_format="zmeta"; confidence=0.95
location=@{ lat=35.271; lon=-78.637 }
data=@{ type="rf_detection"; value=@{ frequency_hz=915000000; rssi_dbm=-45.2; bandwidth_hz=20000; dwell_s=0.8 } }
} | ConvertTo-Json -Depth 6
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/v1/ingest" -Method POST -ContentType "application/json" -Body $bodyYou should see a marker near [35.271, -78.637] (info markers = blue, warn = orange, crit = red). (blue=info, orange=warn, red=crit).
git clone https://github.com/JTC-byte/zmeta-stack.git
cd zmeta-stack
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# run through the venv's Python so the reloader uses the right interpreter
python -m uvicorn backend.app.main:app --reload
# in another shell (activate venv), run a simulator:
# python -m tools.simulators.rf
# python -m tools.simulators.klv- Windows:
scripts/dev.ps1(use-NoGuior-NoSimulator) - macOS/Linux:
scripts/dev.sh(supports--no-gui/--no-sim)
Both scripts ensure the virtual environment exists, install requirements, then launch the backend, GUI, and RF simulator. Stop them with Ctrl+C.
Environment variables let you tune ports, URLs, and simulator targets:
| Variable | Default | Purpose |
|---|---|---|
ZMETA_APP_TITLE |
ZMeta Backend |
UI title surfaced via /api/v1/status and docs. |
ZMETA_UDP_HOST |
0.0.0.0 |
Bind address for the UDP listener. |
ZMETA_UDP_PORT |
5005 |
UDP port for ingest + simulators. |
ZMETA_UDP_QUEUE_MAX |
4096 |
Max UDP queue depth for the background listener. |
ZMETA_UI_BASE_URL |
http://127.0.0.1:8000 |
Base URL used for helper prints and GUI hints. |
ZMETA_WS_GREETING |
Connected to ZMeta WebSocket |
Text sent after a client connects. |
ZMETA_CORS_ORIGINS |
* |
Comma-separated origins allowed by FastAPI CORS middleware. |
ZMETA_SIM_UDP_HOST |
(unset) | Optional override for simulator UDP target host. |
ZMETA_UDP_TARGET_HOST |
127.0.0.1 |
Default simulator UDP target when override not set. |
ZMETA_SHARED_SECRET |
(empty) | Optional shared secret required for /ingest and /ws. |
ZMETA_AUTH_HEADER |
x-zmeta-secret |
Header name to read the shared secret from. |
ZMETA_ENV |
dev |
Hint for environment-specific behavior (e.g., prod CORS tightening). |
ZMETA_WS_QUEUE |
64 |
Max per-client WebSocket buffer size before dropping messages. |
ZMETA_RECORDER_RETENTION_HOURS |
(unset) | If set, older NDJSON files are pruned after this many hours. |
A starter .env.example is included - copy it to .env and tweak as needed.
Set ZMETA_SHARED_SECRET (and optionally ZMETA_AUTH_HEADER) to require clients to present a shared secret.
- REST clients: include the header
X-ZMeta-Secret: <value>(or whichever header you configure). - WebSocket clients: pass the header or append
?secret=<value>to/ws. /healthzsurfacesauth_mode,auth_header, andallowed_originsso you can confirm the mode.
When hardening a deployment, also tighten ZMETA_CORS_ORIGINS and set ZMETA_ENV=prod.
- GET / -> redirects to /ui/live_map.html
- GET /ui/live_map.html -> live map UI (Leaflet + WS)
- GET /ui/ws_test.html -> prints every WebSocket message
- GET /favicon.ico -> alias to /ui/favicon.svg (tab icon)
- GET /api -> legacy redirect to /api/v1/status
- GET /healthz -> legacy redirect to /api/v1/healthz
- GET /api/v1/status -> lightweight service status ({ "status": ..., "clients": ... })
- GET /api/v1/healthz -> detailed ingest/WebSocket metrics
- POST /api/v1/ingest -> accepts JSON; validates as ZMeta or auto-adapts then validates
- GET /api/v1/rules -> list loaded rule names
- POST /api/v1/rules/reload -> reload config/rules.yaml without restarting
Location: tools/ingest_adapters.py
Flow:
- Try native ZMeta validation.
- If validation fails, try adapters:
- Simulated RF (MHz -> Hz) ->
data.type: "rf_detection" - Thermal (degC) ->
data.type: "thermal_hotspot" - KLV-like dicts -> via
tools/translators/klv_to_zmeta.py
- Simulated RF (MHz -> Hz) ->
Resulting ZMeta is:
- broadcast over WS,
- evaluated by rules,
- recorded to NDJSON.
- YAML file:
config/rules.yaml - Examples:
- RF in the 915 MHz ISM band
- High-confidence RF
- Thermal hotspot >= 70 degC
- AOI example near default sim coordinates
Map UI shows: You should see a marker near [35.271, -78.637] (info markers = blue, warn = orange, crit = red).
- Stats panel (WS status, clients, EPS, alerts)
- Marker tooltips with modality plus relative/ISO timestamps
- Track trails with age-based fade, plus auto-follow and Fit All controls
Reload rules at runtime:
Invoke-RestMethod -Method POST http://127.0.0.1:8000/rules/reloadWrites hourly-rotated NDJSON to data/records/YYYYMMDD_HH.ndjson.
Git hygiene:
data/records/is ignored by.gitignore- Do not commit NDJSON logs
backend/app/main.py # FastAPI app, WS hub, routes, favicon alias
config/rules.yaml # YAML rules
schemas/zmeta.py # ZMeta Pydantic models
tools/recorder.py # NDJSON recorder (hourly rotate)
tools/ingest_adapters.py # normalize inbound payloads -> ZMeta
tools/rules.py # load/apply rules; de-dup alerts
tools/simulators/{rf.py, klv.py} # simulators (module-run)
tools/translators/klv_to_zmeta.py # KLV -> ZMeta translator
zmeta_map_dashboard/live_map.html # Leaflet map UI (served from /ui/)
zmeta_map_dashboard/ws_test.html # WebSocket message viewer
zmeta_map_dashboard/favicon.svg # tab icon (aliased at /favicon.ico)
data/records/ # recorder outputs (gitignored)
requirements.txt # runtime/dev deps
Why: ensure teammates/CI get the same versions every time.
A) Quick freeze
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
pip freeze | Out-File -Encoding utf8 requirements.lock.txt
# later:
pip install -r requirements.lock.txtB) pip-tools with hashes
pip install pip-tools
pip-compile --generate-hashes -o requirements.lock.txt requirements.txt
pip install --require-hashes -r requirements.lock.txt
# refresh later:
pip-compile --upgrade --generate-hashes -o requirements.lock.txt requirements.txt-
"WS: connected" but stats don't move
Always open via backend origin:http://127.0.0.1:8000/ui/live_map.html.
Opening the file directly withfile://...blocks/healthz. -
No markers
Check/healthz->validated_totalshould increase.
If onlyudp_received_totalrises, payload likely needs an adapter tweak. -
No alert pulses
Confirm adapted packet matches rule fields.
Use/ui/ws_test.htmlto see live WS messages (feature + alert JSON). -
On Windows:
ModuleNotFoundError: No module named 'yaml'
That's uvicorn using a global Python.
Activate the venv and launch via:.\.venv\Scripts\Activate.ps1 python -m uvicorn backend.app.main:app --reload
-
Too many repeated alerts
Short-window de-dup is set to 3s - adjustAlertDeduper(ttl_s=3.0)inbackend/app/main.py.
- Use a virtualenv
- Keep
data/records/out of Git - Ignore
__pycache__/,.pyc(already in.gitignore) - Consider
ruff/black/pre-commitfor consistent formatting
- Bash:
scripts/replay.sh data/records/20250101_12.ndjson http://127.0.0.1:8000 - PowerShell:
scripts/replay.ps1 -Path data/records/20250101_12.ndjson -BaseUrl http://127.0.0.1:8000
- Build locally:
docker build -t zmeta . - Dev compose:
docker-compose up(hot reload on port 8000).
Here is what we have queued up, in rough priority order:
- Phone Tracker Integration – ingest mobile device feeds for live map display and alerting.
- GUI Auth Support – shared-secret entry in the desktop control panel with secure header/query handling.
- Expanded Health Dashboard – surface adapter counts, WebSocket queue drops, and per-client stats.
- Rules Management UI – list/add/reload detection rules from the GUI instead of YAML-only workflow.
- Electron/Tauri Frontend – TypeScript/React desktop bundle with built-in map, health, and rules pages.
- Enhanced Visualization – clustering, timeline playback, and live metrics charts.