Hold Woonsocket's keys from NA; MIDI links moved and fixed on Macs without python; one check per link

- NA's CAMPUS page (99): tap = acknowledge, hold = fix, at the campus shown. NA's settings page
  turns the cell into that campus's key and writes the command into that campus's Companion
  (sp_cmd), exactly as a press on its own deck does. Logged on both sides.
- coremidi.JXA_CONNECT: connect_peer in JavaScript for Automation, used when a Mac has no python
  (no Command Line Tools): a MIDI hold now reconnects Woonsocket's Tracks, FOH and Lights.
- Woonsocket's MIDI links are on wired addresses now (Tracks to Lighting 10.2.2.2, Tracks to FOH
  10.2.1.12; the other ends list Tracks at 10.2.1.219), and the wired-network rule is back on there.
- Auto-setup keeps one check per MIDI link (the end that reports the link's state): two pulled
  links read 4 DOWN before.

Tried live: a hold sent from NA's page 99 reconnected the FOH link at Woonsocket.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 17:00:31 -04:00
co-authored by Claude Opus 5.5
parent 457335bc96
commit b793490d90
5 changed files with 119 additions and 7 deletions
+43
View File
@@ -229,6 +229,49 @@ for (var i = 0; i < n; i++) {
"""
# Connect, for a Mac without python: what connect_peer does, in JavaScript for Automation. Filled in
# with jxa_connect(); prints SPMIDI"already" / "asked" / "kicked" / "moved", like connect_peer.
JXA_CONNECT = r"""
ObjC.import('CoreMIDI'); ObjC.import('Foundation');
var SESSION = __SESSION__, PEER = __PEER__, ADDRESS = __ADDRESS__, REPOINT = __REPOINT__;
function str(o, k) { var r = Ref(); if ($.MIDIObjectGetStringProperty(o, k, r) !== 0) return null; return ObjC.unwrap(ObjC.castRefToObject(r[0])); }
function dict(o, k) { var r = Ref(); if ($.MIDIObjectGetDictionaryProperty(o, k, r) !== 0) return null; return ObjC.deepUnwrap(ObjC.castRefToObject(r[0])); }
function put(ent, s) { var e = $.MIDIObjectSetDictionaryProperty(ent, $('apple.midirtp.session'), $(s)); if (e !== 0) throw new Error('MIDIObjectSetDictionaryProperty failed: ' + e); }
var found = null, n = $.MIDIGetNumberOfDevices();
for (var i = 0; i < n && !found; i++) {
var dev = $.MIDIGetDevice(i), drv = str(dev, $('driver')) || '';
if (drv.indexOf('Network') < 0 && drv.toLowerCase().indexOf('rtp') < 0) continue;
for (var j = 0; j < $.MIDIDeviceGetNumberOfEntities(dev); j++) {
var ent = $.MIDIDeviceGetEntity(dev, j), s = dict(ent, $('apple.midirtp.session'));
if (s && (s['dns-sd-name'] || str(ent, $('name'))) === SESSION) { found = [ent, s]; break; }
}
}
if (!found) throw new Error('no network MIDI session named ' + SESSION + ' on this Mac');
var ent = found[0], s = found[1], peers = s.peers || [], result;
var mine = peers.filter(function (p) { return p.name === PEER; });
var stuck = mine.some(function (p) { return p.state !== undefined && p.state !== 'running' && p.state !== 'connected'; });
var elsewhere = mine.some(function (p) { return p.address !== ADDRESS; });
if (mine.length && (stuck || (REPOINT && elsewhere))) {
s.peers = peers.filter(function (p) { return p.name !== PEER; });
put(ent, s); delay(1);
s.peers = s.peers.concat([{address: ADDRESS, name: PEER}]); put(ent, s);
result = (REPOINT && elsewhere) ? 'moved' : 'kicked';
} else if (mine.length || peers.some(function (p) { return p.address === ADDRESS; })) {
result = 'already';
} else {
s.peers = peers.concat([{address: ADDRESS, name: PEER}]); put(ent, s); result = 'asked';
}
'SPMIDI' + JSON.stringify(result);
"""
def jxa_connect(session_name, peer_name, address, repoint=False):
"""The JXA_CONNECT script for one peer (values go in as JSON, never as code)."""
import json as _json
return (JXA_CONNECT.replace("__SESSION__", _json.dumps(session_name)).replace("__PEER__", _json.dumps(peer_name))
.replace("__ADDRESS__", _json.dumps(address)).replace("__REPOINT__", "true" if repoint else "false"))
def sessions():
"""{session name: {"port": int, "peers": [{"name", "address", "state"}]}}"""
found = {}