Files
swarm-house/simulator/README.md
T
eSlider 8f98a0c994 feat(simulator): Grafana monitoring over generated Parquet
Monitoring compose profile: dependency-free Prometheus exporter that
scans the Parquet output with DuckDB (rows, detections, pose frames,
battery, RSSI, link distance, bytes on disk), Prometheus scrape config
and a provisioned Grafana dashboard. Demonstrates the observability
doctrine: fleet statistics derived from the data platform itself.
2026-07-08 13:27:53 +01:00

84 lines
3.5 KiB
Markdown

# Virtual drone fleet
A data generator that exercises the **exact on-board pipeline** described in [03 — Data platform](../docs/03-data-platform.md): seeded kinematics along a patrol route, sensor streams at realistic rates, detection events, the hot `current/` → sealed Parquet write path, and the compact UDP state broadcast between drones.
One process = one drone. Scaling the swarm is a Compose flag.
## Run a swarm
```bash
# 5 drones (default), 2-minute flight, shared flight id
FLIGHT_ID=$(date -u +%Y%m%dT%H%MZ)-sim docker compose up --build --scale drone=5
# 10 drones, 5-minute flight, 4x accelerated
DRONE_COUNT=10 DURATION_S=300 SPEEDUP=4 docker compose up --build --scale drone=10
```
Output lands in `./data/` with the standard layout:
```
data/dataset=telemetry/flight=…/drone=…/sensor=imu/year=…/…/hour=…/data.parquet
data/dataset=detections/flight=…/drone=…/…
data/dataset=state/flight=…/drone=…/… ← sent + received broadcasts
```
## Run a single drone without Docker
```bash
pip install -r requirements.txt
DRONE_ID=dr-01 DURATION_S=30 SPEEDUP=10 DATA_DIR=./data python -m virtual_drone.main
```
## Query the results
```bash
python - <<'EOF'
import duckdb
con = duckdb.connect()
print(con.sql("""
SELECT drone, sensor, count(*) AS rows,
to_timestamp(min(ts_ns)/1e9) AS first_row,
to_timestamp(max(ts_ns)/1e9) AS last_row
FROM read_parquet('data/dataset=telemetry/**/*.parquet', hive_partitioning=true)
GROUP BY drone, sensor ORDER BY drone, sensor
"""))
print(con.sql("""
SELECT drone, direction, count(*) AS frames, count(DISTINCT peer_id) AS peers
FROM read_parquet('data/dataset=state/**/*.parquet', hive_partitioning=true)
GROUP BY drone, direction ORDER BY drone, direction
"""))
EOF
```
## Monitoring: Grafana over the generated data
The `monitoring` profile spins up a small metrics chain — a DuckDB-based exporter that scans the generated Parquet every few seconds, Prometheus, and a pre-provisioned Grafana dashboard:
```bash
docker compose --profile monitoring up -d # exporter + prometheus + grafana
# generate some flights in parallel or beforehand:
FLIGHT_ID=$(date -u +%Y%m%dT%H%MZ)-sim docker compose up --scale drone=5
```
- Grafana: `http://localhost:3000` (anonymous admin — demo only) → dashboard **Swarm Fleet — generated data**
- Prometheus: `http://localhost:9090` · exporter: `http://localhost:9105/metrics`
Panels: telemetry rows by drone/sensor, detections by class, pose frames sent/received, battery per drone, RSSI and estimated distance per link, Parquet bytes/files on disk. The exporter is deliberately a demonstration of the observability doctrine from [07 — Observability](../docs/07-observability.md): fleet statistics are *derived from the data platform itself* — no agent on the drone, just SQL over the same Parquet everyone else reads.
## Reproducibility
Every run is deterministic per `(SEED, DRONE_ID)`: same route jitter, same sensor noise, same detection sequence. A bug report is a seed and a config, not a description.
## Knobs
| Env | Default | Meaning |
| --- | --- | --- |
| `DRONE_ID` | derived from hostname | Unique per container automatically under `--scale` |
| `FLIGHT_ID` | generated | Set explicitly so all drones share one flight partition |
| `SEED` | `42` | Determinism root |
| `DURATION_S` | `120` | Simulated flight length |
| `SPEEDUP` | `1.0` | Wall-clock acceleration |
| `IMU_HZ` | `50` | High-rate sensor load |
| `STATE_HZ` | `5` | Broadcast frequency |
| `AREA_SIZE_M` | `400` | Patrol square side |