feat(simulator): virtual drone fleet data generator
One process per drone: seeded patrol kinematics, sensor streams at realistic rates, detection events, hot current/ blocks sealed into Hive-partitioned Parquet, and the compact UDP state broadcast with peer RSSI derivation. Scales via docker compose --scale.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Virtual drone entry point: fly, sense, write, broadcast, listen, seal."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 __name__ == "__main__":
|
||||
run(Config())
|
||||
Reference in New Issue
Block a user