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.
120 lines
4.3 KiB
Python
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())
|