Godstrument/run.py
monsterrobotparty 60173d44cb Add socio-economic data, performance mode, spells (full-state templates)
- world_bank.py: poverty, food security, fertility, life expectancy (World Bank)
  folded into the "shadow" / "bright" concept groups
- spells: save/load now captures the full live mix (gains/mutes/freezes), not
  just the wiring; TEMPLATES panel renamed SPELLS
- performance mode (press P): clean projection view + rotating "dispatches from
  the world" text readout for live shows

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 02:37:03 +10:00

199 lines
6.4 KiB
Python

"""
run.py — light the whole thing up with one command.
Spawns the hub plus a profile of workers as subprocesses, opens the visualizer
in your browser, and shuts everything down cleanly on Ctrl-C.
python run.py # 'core' profile: hub + sim + all world feeds
python run.py --profile all # + optional mic / webcam / MIDI workers
python run.py --profile world # world data only (no simulated sensors)
python run.py --profile minimal
python run.py --no-browser
Everything a worker emits shows up automatically — add your own worker to
workers/ and to a profile below.
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
import time
import webbrowser
HERE = os.path.dirname(os.path.abspath(__file__))
PY = sys.executable # the venv python when launched via ./.venv/bin/python
WORLD = [
"workers/world_quakes.py",
"workers/world_weather.py",
"workers/world_air.py",
"workers/world_sun.py",
"workers/world_iss.py",
"workers/world_crypto.py",
"workers/world_planes.py",
"workers/world_wiki.py",
"workers/world_sky.py",
"workers/world_clock.py",
"workers/world_ephemeris.py",
"workers/world_almanac.py",
"workers/world_debt.py",
"workers/world_econ.py",
"workers/world_bank.py",
]
# workers that retarget to the venue city
LATLON_WORKERS = {"world_weather.py", "world_air.py", "world_sky.py", "replay_weather.py"}
BBOX_WORKERS = {"world_planes.py"}
def load_location(args):
"""Resolve a venue location from --city (cities.json) or explicit lat/lon/bbox."""
loc = {}
if args.city:
try:
with open(os.path.join(HERE, "cities.json")) as f:
cities = json.load(f)
except OSError:
cities = {}
key = args.city.lower().replace(" ", "")
city = cities.get(key)
if not city:
matches = [k for k in cities if k.startswith("_") is False and key in k]
city = cities.get(matches[0]) if matches else None
if city:
loc = {"lat": city["lat"], "lon": city["lon"],
"bbox": city.get("bbox"), "label": city.get("label", args.city)}
print(f" venue: {loc['label']} ({loc['lat']}, {loc['lon']})")
else:
print(f" ! unknown city '{args.city}' — using worker defaults")
if args.lat is not None and args.lon is not None:
loc["lat"], loc["lon"] = args.lat, args.lon
if args.bbox:
loc["bbox"] = [float(x) for x in args.bbox.split(",")]
return loc
def worker_args(path: str, loc: dict) -> list[str]:
name = os.path.basename(path)
extra: list[str] = []
if loc.get("lat") is not None and name in LATLON_WORKERS:
extra += ["--lat", str(loc["lat"]), "--lon", str(loc["lon"])]
if loc.get("bbox") and name in BBOX_WORKERS:
extra += ["--bbox", ",".join(str(x) for x in loc["bbox"])]
return extra
SIM = ["workers/sim_esp32.py"]
OPTIONAL = [
"workers/audio_worker.py",
"workers/vision_worker.py",
"workers/midi_worker.py",
]
REPLAY = ["workers/replay_quakes.py", "workers/replay_weather.py"]
# warp profile swaps live quakes+weather for their time-warped replay versions
_swapped = [w for w in WORLD
if os.path.basename(w) not in ("world_quakes.py", "world_weather.py")]
PROFILES = {
"minimal": SIM,
"world": WORLD,
"core": SIM + WORLD,
"all": SIM + WORLD + OPTIONAL,
"warp": SIM + REPLAY + _swapped,
}
def launch(path: str, extra: list[str] | None = None) -> subprocess.Popen | None:
full = os.path.join(HERE, path)
if not os.path.exists(full):
print(f" (skip {path} — not built yet)")
return None
return subprocess.Popen([PY, full] + (extra or []), cwd=HERE)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--profile", default="core", choices=list(PROFILES))
ap.add_argument("--config", default="config.json")
ap.add_argument("--city", help="venue city (see cities.json), e.g. melbourne")
ap.add_argument("--lat", type=float, help="override venue latitude")
ap.add_argument("--lon", type=float, help="override venue longitude")
ap.add_argument("--bbox", help='override aircraft bbox "s,w,n,e"')
ap.add_argument("--record", action="store_true",
help="also record everything to godstrument.db")
ap.add_argument("--dealgod", action="store_true",
help="also run the dealgod market worker (reads the warehouse)")
ap.add_argument("--no-browser", action="store_true")
args = ap.parse_args()
procs: list[subprocess.Popen] = []
print("\n waking the godstrument...\n")
loc = load_location(args)
try:
with open(os.path.join(HERE, args.config)) as f:
http_port = json.load(f).get("http_port", 8088)
except OSError:
http_port = 8088
hub = subprocess.Popen([PY, os.path.join(HERE, "hub.py"),
"--config", os.path.join(HERE, args.config)],
cwd=HERE)
procs.append(hub)
time.sleep(2.0) # let the hub bind its ports
for path in PROFILES[args.profile]:
p = launch(path, worker_args(path, loc))
if p:
procs.append(p)
time.sleep(0.15)
if args.dealgod:
d = launch("workers/world_dealgod.py")
if d:
procs.append(d)
print(" dealgod market worker -> reading the warehouse")
if args.record:
r = launch("recorder.py")
if r:
procs.append(r)
print(" recording -> godstrument.db")
url = f"http://localhost:{http_port}"
print(f"\n the console is live -> {url}")
print(" (Ctrl-C to put it back to sleep)\n")
if not args.no_browser:
try:
webbrowser.open(url)
except Exception:
pass
try:
while True:
if hub.poll() is not None:
print(" hub exited; shutting down.")
break
time.sleep(0.5)
except KeyboardInterrupt:
print("\n godstrument sleeps.")
finally:
for p in procs:
try:
p.terminate()
except Exception:
pass
for p in procs:
try:
p.wait(timeout=3)
except Exception:
try:
p.kill()
except Exception:
pass
if __name__ == "__main__":
main()