Test server

This commit is contained in:
Anatoly Antonov 2026-07-04 04:18:36 +09:00
parent 43c120df4c
commit 1e2a276126
3 changed files with 317 additions and 6 deletions

View file

@ -11,14 +11,24 @@ Usage:
--speedup compresses flight time: 20 means one wall-clock second advances the
flight by 20 s (a 3 h flight replays in ~9 min).
Push mode (--push): instead of serving, connect to the Django backend as a
ground station and push the same synthetic flight there. Both leaflet_svelte
(tracking) and this dashboard can then read the shared stream from
ws://<host>/api/ws/satellite/<uuid>/telemetry/:
python3 test_server.py --push ws://localhost:8000 \
--satellite <uuid> --token <drf-token> --speedup 20
"""
import argparse
import asyncio
import bisect
import json
import math
import random
import time
from datetime import datetime
import websockets
@ -28,14 +38,51 @@ GROUND_ALT = 200.0
BASE_LAT, BASE_LON = 61.66, 129.38
START_WALL = time.time()
# Random lat/lon drift around the trajectory (metres): each packet takes a
# random step; past DRIFT_MAX_M the next step is forced back toward zero.
DRIFT_STEP_M = 150.0
DRIFT_MAX_M = 5000.0
def sample(flight_s: float) -> dict:
TRAJ = None # [(t_s, lat, lon, alt), ...] from --trajectory, else synthetic
_TRAJ_TIMES = []
_drift = {"n": 0.0, "e": 0.0}
def load_trajectory(path):
"""stratoflights/Tawhiri prediction JSON -> [(sec_from_launch, lat, lon, alt)]."""
d = json.load(open(path))
stages = (d.get("result") or d)["prediction"]
pts = [p for s in stages for p in s["trajectory"]]
t0 = datetime.fromisoformat(pts[0]["datetime"].replace("Z", "+00:00"))
traj = []
for p in pts:
t = (datetime.fromisoformat(p["datetime"].replace("Z", "+00:00")) - t0).total_seconds()
traj.append((t, p["latitude"], p["longitude"], p["altitude"]))
return traj
def traj_at(flight_s):
"""Interpolated (lat, lon, alt, landed) along TRAJ."""
if flight_s <= TRAJ[0][0]:
_, lat, lon, alt = TRAJ[0]
return lat, lon, alt, False
if flight_s >= TRAJ[-1][0]:
_, lat, lon, alt = TRAJ[-1]
return lat, lon, alt, True
i = bisect.bisect_right(_TRAJ_TIMES, flight_s)
t0, la0, lo0, al0 = TRAJ[i - 1]
t1, la1, lo1, al1 = TRAJ[i]
f = (flight_s - t0) / (t1 - t0)
return la0 + f * (la1 - la0), lo0 + f * (lo1 - lo0), al0 + f * (al1 - al0), False
def synth_at(flight_s):
"""Fallback synthetic profile: 5 m/s ascent to 30 km, parachute descent."""
ascent_end = (BURST_ALT - GROUND_ALT) / ASCENT_MS
if flight_s < ascent_end:
alt = GROUND_ALT + ASCENT_MS * flight_s
vs_phase = "ascent"
landed = False
else:
# parachute descent, faster in thin air
t = flight_s - ascent_end
alt = BURST_ALT
dt, step = 0.0, 5.0
@ -44,7 +91,26 @@ def sample(flight_s: float) -> dict:
alt -= rate * step
dt += step
alt = max(alt, GROUND_ALT)
vs_phase = "descent" if alt > GROUND_ALT else "landed"
landed = alt <= GROUND_ALT
lat = BASE_LAT + flight_s * 1.2e-5
lon = BASE_LON + flight_s * 6e-5
return lat, lon, alt, landed
def drift_step():
for k in ("n", "e"):
d = random.uniform(-DRIFT_STEP_M, DRIFT_STEP_M)
if abs(_drift[k]) > DRIFT_MAX_M:
d = -math.copysign(abs(d), _drift[k]) # forced step back
_drift[k] += d
def sample(flight_s: float) -> dict:
lat, lon, alt, landed = traj_at(flight_s) if TRAJ else synth_at(flight_s)
drift_step()
lat += _drift["n"] / 111320.0
lon += _drift["e"] / (111320.0 * math.cos(math.radians(lat)))
vs_phase = "landed" if landed else "flight"
alt_noise = alt + random.uniform(-8, 8)
pressure = 101325.0 * math.exp(-alt / 8400.0)
@ -64,8 +130,8 @@ def sample(flight_s: float) -> dict:
"id": "test",
# flight-time timestamp so derived vertical speed is realistic under --speedup
"timestamp": int(START_WALL + flight_s),
"lat": BASE_LAT + flight_s * 1.2e-5 + random.uniform(-1e-5, 1e-5),
"lon": BASE_LON + flight_s * 6e-5 + random.uniform(-1e-5, 1e-5),
"lat": round(lat, 7),
"lon": round(lon, 7),
"alt": round(alt_noise, 1),
"payload": {
"utc": time.strftime("%H:%M:%S", utc) + ".00",
@ -102,6 +168,33 @@ async def handler(ws, interval: float, speedup: float, start_s: float = 0.0):
print(f"client disconnected after {n} packets")
async def push(base_url, satellite, token, interval, speedup, start_s):
"""Ground-station mode: push the synthetic flight into the Django backend."""
base = base_url.rstrip("/").replace("http://", "ws://").replace("https://", "wss://")
url = f"{base}/api/ws/station/{satellite}/telemetry/?token={token}"
n = 0
while True:
try:
async with websockets.connect(url) as ws:
print(f"pushing to {url.split('?')[0]}")
async def log_errors():
async for msg in ws:
print(f" server: {msg}")
reader = asyncio.ensure_future(log_errors())
while True:
flight_s = start_s + (time.time() - START_WALL) * speedup
await ws.send(json.dumps(sample(flight_s)))
n += 1
if n % 25 == 0:
print(f" sent {n} packets, flight time {flight_s/60:.1f} min")
await asyncio.sleep(interval)
except (OSError, websockets.WebSocketException) as e:
print(f"push link down ({e}); retrying in 3 s")
await asyncio.sleep(3)
async def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=8765)
@ -109,8 +202,27 @@ async def main():
ap.add_argument("--speedup", type=float, default=20.0, help="flight seconds per wall second")
ap.add_argument("--start", type=float, default=0.0,
help="flight seconds to skip at connect (5900 ≈ just before burst)")
ap.add_argument("--push", metavar="URL", help="push to Django backend instead of serving")
ap.add_argument("--satellite", help="satellite UUID (push mode)")
ap.add_argument("--token", help="DRF auth token (push mode)")
ap.add_argument("--trajectory", metavar="JSON",
help="fly along a stratoflights/Tawhiri prediction JSON "
"(с рандомным дрейфом ±5 км) вместо встроенного профиля")
args = ap.parse_args()
if args.trajectory:
global TRAJ, _TRAJ_TIMES
TRAJ = load_trajectory(args.trajectory)
_TRAJ_TIMES = [p[0] for p in TRAJ]
print(f"trajectory: {len(TRAJ)} pts, {_TRAJ_TIMES[-1]/60:.0f} min, "
f"launch {TRAJ[0][1]:.5f},{TRAJ[0][2]:.5f}")
if args.push:
if not args.satellite or not args.token:
ap.error("--push requires --satellite and --token")
await push(args.push, args.satellite, args.token, args.interval, args.speedup, args.start)
return
async with websockets.serve(
lambda ws: handler(ws, args.interval, args.speedup, args.start), "localhost", args.port
):