Files
swarm-house/simulator/virtual_drone/main.py
T
eSlider ea062af86a
CI & Release / Verify simulator (push) Successful in 11s
CI & Release / Trivy scan (push) Successful in 9s
CI & Release / Semantic Release (push) Successful in 5s
perf: bound lake scans and add platform Grafana dashboard
Explorer and exporter were scanning the full Parquet lake every 1–5s
(~2000 files, 200MB+), driving ~2.2 CPU cores. Limit metrics to the
last N flights, cache DuckDB views and the partition tree, slow live
polls to 2s, and keep drones alive after seal to stop restart churn.

Add node-exporter, scan-duration metrics, and a Swarm Platform Grafana
dashboard for node CPU/memory and scan health.
2026-07-08 21:19:53 +01:00

120 lines
4.3 KiB
Python

"""Virtual drone entry point: fly, sense, write, broadcast, listen, seal."""
from __future__ import annotations
import os
import random
import select
import time
from pathlib import Path
from .broadcast import decode_state, encode_state, open_socket
from .config import Config
from .flight import initial_pose, make_waypoints, step
from .sensors import (
baro_row,
battery_row,
detection_row,
imu_row,
state_row,
temp_row,
)
from .writer import make_writers
TICK_HZ = 10.0
SENSOR_NAMES = ["imu", "baro", "temp", "battery", "rssi"]
def run(cfg: Config) -> None:
rng = random.Random(f"{cfg.seed}:{cfg.drone_id}")
root = Path(cfg.data_dir)
writers = make_writers(root, cfg.flight_id, cfg.drone_id, SENSOR_NAMES)
sock = open_socket(cfg.udp_port)
now_ns = time.time_ns()
pose = initial_pose(cfg, rng, now_ns)
waypoints = make_waypoints(cfg, rng)
wp_index = 1
dt = 1.0 / TICK_HZ
ticks_total = int(cfg.duration_s * TICK_HZ)
# Emission accumulators: sensor name -> carry-over fraction of a sample
acc = {name: 0.0 for name in ["imu", "baro", "temp", "battery", "state", "detection"]}
rates = {
"imu": cfg.imu_hz, "baro": cfg.baro_hz, "temp": cfg.temp_hz,
"battery": cfg.battery_hz, "state": cfg.state_hz, "detection": cfg.detection_rate_hz,
}
peers_seen: dict[str, int] = {}
frames_sent = 0
print(f"[{cfg.drone_id}] flight={cfg.flight_id} uuid={cfg.flight_uuid} "
f"duration={cfg.duration_s}s speedup={cfg.speedup}x data={root}")
for tick in range(ticks_total):
sim_elapsed = tick * dt
ts_ns = now_ns + int(sim_elapsed * 1e9)
pose, reached = step(pose, waypoints[wp_index], cfg, rng, dt, ts_ns)
if reached:
wp_index = (wp_index + 1) % len(waypoints)
def due(name: str) -> int:
acc[name] += rates[name] * dt
n = int(acc[name])
acc[name] -= n
return n
for i in range(due("imu")):
writers["imu"].append(imu_row(pose, rng, ts_ns + i * int(1e9 / max(cfg.imu_hz, 1))))
for _ in range(due("baro")):
writers["baro"].append(baro_row(pose, rng, ts_ns))
for _ in range(due("temp")):
writers["temp"].append(temp_row(pose, rng, ts_ns))
for _ in range(due("battery")):
writers["battery"].append(battery_row(pose, rng, ts_ns, sim_elapsed, cfg.duration_s))
for _ in range(due("detection")):
writers["detections"].append(detection_row(pose, rng, ts_ns))
for _ in range(due("state")):
frame = encode_state(cfg.drone_id, pose)
try:
sock.sendto(frame, (cfg.broadcast_addr, cfg.udp_port))
frames_sent += 1
except OSError:
pass # lossy link is part of the model
writers["state"].append({**state_row(pose, ts_ns), "peer_id": cfg.drone_id, "direction": "sent"})
# Drain incoming peer frames; record them and derive RSSI-like signal
while True:
readable, _, _ = select.select([sock], [], [], 0)
if not readable:
break
payload, _addr = sock.recvfrom(256)
decoded = decode_state(payload)
if decoded is None or decoded["peer_id"] == cfg.drone_id:
continue
peers_seen[decoded["peer_id"]] = decoded["ts_ns"]
writers["state"].append({**decoded, "direction": "received"})
dist = max(1.0, ((decoded["pos_x"] - pose.x) ** 2 + (decoded["pos_y"] - pose.y) ** 2) ** 0.5)
writers["rssi"].append({
"ts_ns": ts_ns,
"peer_id": decoded["peer_id"],
"rssi_dbm": -40.0 - 20.0 * (dist ** 0.5) / 10.0 + rng.gauss(0.0, 2.0),
"distance_m": dist,
})
time.sleep(dt / max(cfg.speedup, 0.01))
for writer in writers.values():
writer.seal()
print(f"[{cfg.drone_id}] done: {frames_sent} state frames sent, "
f"{len(peers_seen)} peers seen {sorted(peers_seen)}; sealed to {root}")
if os.environ.get("KEEP_ALIVE", "0") == "1":
print(f"[{cfg.drone_id}] KEEP_ALIVE=1 — idle after seal (no pod restart churn)")
while True:
time.sleep(3600)
if __name__ == "__main__":
run(Config())