"""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())