- 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>
199 lines
6.4 KiB
Python
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()
|