Test server
This commit is contained in:
parent
43c120df4c
commit
1e2a276126
3 changed files with 317 additions and 6 deletions
124
test_server.py
124
test_server.py
|
|
@ -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
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue