Woonsocket's startup list covered: M32, SoundGrid link, MIDI on Macs without python

From the WS Production Startup checklist (check.watersmedia.org):
- M32 (the "X32" there is a Midas M32 at 10.2.1.146): Woonsocket's Companion got the X32/M32
  module (imported from NA's copy through Companion's own API) and an m32 connection; auto-setup
  names a single M32 connection's key M32.
- SuperRack's link to the Waves SoundGrid server: a soundgrid check builder (SuperRack's control
  connection on port 19000) inside every SUPERRACK key.
- MIDI ("Open Audio MIDI"): Macs without the Command Line Tools have their sessions read with
  JavaScript for Automation (coremidi.JXA_SESSIONS) - by the watchdog every 2 s and by discovery. A
  peer with no state (older macOS) counts as configured. A peer joined by name or IPv6 gets the
  wired address of the Mac whose session it is (matched by session name), at that session's port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 16:48:28 -04:00
co-authored by Claude Opus 5.5
parent e1c9b2f974
commit 457335bc96
6 changed files with 98 additions and 4 deletions
+28 -2
View File
@@ -502,7 +502,7 @@ def check_midi_peer(chk, snap, ctx):
# Measured 2026-10-07: a joining peer reads "connected" for ~6 s until
# clock sync, then "running". A peer that dies is removed from the list:
# at once if it says goodbye, after ~60 s if it crashes or its cable is pulled.
if p["state"] in ("running", "connected"):
if p.get("state", "running") in ("running", "connected"): # no state: older macOS, configured
return [("peer", Result(OK))]
return [("peer", Result(ERR, "MIDI " + (p["state"] or "?").upper()[:8]))]
return [("peer", Result(ERR, "MIDI LOST"))]
@@ -2450,7 +2450,9 @@ def stream_remote_midi(snap, cfg, host, stop):
proc.stdin.write(script)
proc.stdin.close()
empty_since = None
lines = 0
for line in proc.stdout:
lines += 1
data = parse_remote_midi(line)
if data is not None:
snap.remote_midi[host], snap.remote_midi_time[host] = data, time.time()
@@ -2466,7 +2468,10 @@ def stream_remote_midi(snap, cfg, host, stop):
empty_since = None
if stop.is_set():
break
except (OSError, ValueError) as e:
if not lines and proc.wait(timeout=10) == 97:
log.info("MIDI on %s: no python there (no Command Line Tools): reading it with osascript every 2 s", host)
return poll_jxa_midi(snap, cfg, host, stop)
except (OSError, ValueError, subprocess.TimeoutExpired) as e:
log.warning("MIDI stream from %s: %s", host, e)
finally:
if proc and proc.poll() is None:
@@ -2475,6 +2480,27 @@ def stream_remote_midi(snap, cfg, host, stop):
stop.wait(3)
def jxa_midi(cfg, host, timeout=15):
"""A Mac's MIDI sessions read with JavaScript for Automation (no python there), or None."""
import coremidi
try:
r = subprocess.run(ssh_cmd(cfg, host) + ["/usr/bin/osascript -l JavaScript -"], input=coremidi.JXA_SESSIONS,
capture_output=True, text=True, timeout=timeout)
except (OSError, subprocess.SubprocessError):
return None
return parse_remote_midi(r.stdout) if r.returncode == 0 else None
def poll_jxa_midi(snap, cfg, host, stop):
"""Every two seconds over the shared ssh connection, for a Mac without python."""
while not stop.is_set():
data = jxa_midi(cfg, host)
if data is not None:
snap.remote_midi[host], snap.remote_midi_time[host] = data, time.time()
remember_peers(snap, host, data)
stop.wait(2)
def start_remote_midi_streams(snap, cfg, stop):
threads = []
for host in remote_midi_hosts(cfg):