Godstrument/workers/world_planes.py
type-two e69b7e2ad6 Accounts, socio-economic feeds, and hub fixes (track deployed work)
Captures backend work already running on godstrument.pro but never committed:
- auth.py: invite-only accounts + per-user presets (SQLite, scrypt, signed
  stateless sessions), mounted by hub.py at /api/*
- hub.py: /api static+API handler, readonly public mode, full route spec in hello
- socio-economic / market / fire / debt / crypto workers + normalize/transform
- .gitignore: never commit godstrument_users.db* or auth_secret
- test_fixes.py: framework-free self-check for the review fixes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:18:35 +10:00

137 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""A swarm of real aircraft over a patch of sky, turned into a drone.
OpenSky live states give a count (thickness), mean altitude (pitch/register),
and mean groundspeed (intensity) — the sky's traffic becomes a slow chord.
"""
import argparse
import json
import sys
import time
import urllib.error
import urllib.request
from pythonosc.udp_client import SimpleUDPClient
NAME = "world_planes"
API = "https://opensky-network.org/api/states/all"
UA = "Godstrument/1.0 (live instrument; contact monsterrobotparty@gmail.com)"
# OpenSky anonymous budget is ~400 credits/day; a bbox query costs 2-4 credits,
# so <200 requests/day -> poll no faster than ~1 per 450s or the feed 429s out
# and freezes for the rest of the day. (Add OAuth2 creds for the 4000 tier.)
POLL_OK = 450.0 # normal poll interval (s)
POLL_429 = 600.0 # backoff poll interval after HTTP 429
REEMIT = 10.0 # re-emit last values every 10s
def fetch(bbox, timeout=25):
"""Fetch OpenSky states for the bbox. Returns parsed JSON dict.
Raises urllib.error.HTTPError so the caller can detect 429.
"""
s, w, n, e = bbox
url = f"{API}?lamin={s}&lomin={w}&lamax={n}&lomax={e}"
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def reduce_states(data):
"""Compute (count, mean_alt, mean_speed) from a states payload.
index 7 = geo_altitude (m, may be null), index 9 = velocity (m/s, may be null).
Nulls are ignored in the means. Returns floats; means are 0.0 when no data.
"""
states = (data or {}).get("states") or []
count = float(len(states))
alts = []
speeds = []
for row in states:
if not row:
continue
alt = row[7] if len(row) > 7 else None
spd = row[9] if len(row) > 9 else None
if alt is not None:
try:
alts.append(float(alt))
except (TypeError, ValueError):
pass
if spd is not None:
try:
speeds.append(float(spd))
except (TypeError, ValueError):
pass
avgalt = sum(alts) / len(alts) if alts else 0.0
avgspeed = sum(speeds) / len(speeds) if speeds else 0.0
return count, avgalt, avgspeed
def parse_bbox(text):
parts = [p.strip() for p in text.split(",")]
if len(parts) != 4:
raise argparse.ArgumentTypeError('--bbox must be "s,w,n,e"')
try:
return tuple(float(p) for p in parts)
except ValueError:
raise argparse.ArgumentTypeError("--bbox values must be numbers")
def main():
ap = argparse.ArgumentParser(description="OpenSky aircraft swarm -> OSC drone")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--bbox", type=parse_bbox, default=(45.0, 0.0, 52.0, 10.0),
help='Bounding box "s,w,n,e" (default Western Europe)')
args = ap.parse_args()
client = SimpleUDPClient(args.host, args.port)
poll = POLL_OK
print(f"[{NAME}] emitting /gs/planes/count,/gs/planes/avgalt,/gs/planes/avgspeed every {POLL_OK:.0f}s")
# Last known values, re-emitted every REEMIT seconds between polls.
last = None # (count, avgalt, avgspeed)
next_poll = 0.0
while True:
now = time.monotonic()
if now >= next_poll:
try:
data = fetch(args.bbox)
last = reduce_states(data)
if poll != POLL_OK:
print(f"[{NAME}] recovered; back to {POLL_OK:.0f}s poll")
poll = POLL_OK
except urllib.error.HTTPError as ex:
if ex.code == 429:
retry_after = ex.headers.get("X-Rate-Limit-Retry-After-Seconds")
try:
poll = max(POLL_429, float(retry_after))
except (TypeError, ValueError):
poll = POLL_429
print(f"[{NAME}] HTTP 429 rate limited; backing off to {poll:.0f}s")
else:
print(f"[{NAME}] HTTP {ex.code}; retrying in {poll:.0f}s")
except (urllib.error.URLError, TimeoutError, OSError,
json.JSONDecodeError, ValueError) as ex:
print(f"[{NAME}] fetch error: {ex}; retrying in {poll:.0f}s")
next_poll = time.monotonic() + poll
# Emit (or re-emit) the last known values.
if last is not None:
count, avgalt, avgspeed = last
try:
client.send_message("/gs/planes/count", float(count))
client.send_message("/gs/planes/avgalt", float(avgalt))
client.send_message("/gs/planes/avgspeed", float(avgspeed))
except OSError as ex:
print(f"[{NAME}] OSC send error: {ex}")
time.sleep(REEMIT)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
sys.exit(0)