Files
AV-Live/data_only_viz/web/server.js
T
L'électron rare 4fd37b4faf feat(data-only): GUIs synchronisees + audit
Web data-only fully wired contre les 20 feeds :

dashboard.js : 8 nouveaux handlers (gdelt, wikimedia, tides,
moon, atc, pose, mempool, github, rte_eco2mix). Sparklines +
seuils alert/warn/green coherents.

map.js : marker GDELT (purple) en plus des seismes/foudre/
avions/ISS/volcans, TTL 60s comme les autres ephemeres.

control.html : nouvelle section 'Modes visuels (oF / Metal)'
avec 9 boutons (storm/tunnel/plasma/kaleido/voronoi/metaballs/
stars/bars/hands3d). Le handler scene-btn est splitte par
container (#scenes vs #vizmodes) pour ne pas melanger
l'etat actif.

server.js : whitelist /control/viz* + routing par path. Les
/control/viz* partent vers oF (:57123), tout le reste vers
SC (:57121). Nouveau env OF_PORT_OUT.

Audit fixes :
- LaunchMode displayName Data-only : 'SC + oF + feeds, no web'
  -> 'SC + Metal viz + feeds + web' (l'ancien libelle datait
  d'avant le toggle Web UI dans dataOnly).
- SettingsPanel : toggle Squelette etiquete '(a venir)' tant
  que le rendu n'est pas implemente cote MeshRenderer.
2026-05-13 19:56:17 +02:00

134 lines
4.5 KiB
JavaScript

// =====================================================================
// data_only_viz / web / server.js
//
// Bridge bidirectionnel OSC <-> WebSocket pour le mode data-only.
//
// data_feeds bridge.py ----osc :57124----> server.js
// | WS broadcast
// v
// browser (dashboard / map)
//
// browser (/control) --WS--> server.js --osc :57121--> SuperCollider
//
// SuperCollider --osc :57125--> server.js --WS broadcast--> browser
// =====================================================================
import express from "express";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { createServer } from "node:http";
import { WebSocketServer } from "ws";
import osc from "osc";
const __dirname = dirname(fileURLToPath(import.meta.url));
const HTTP_PORT = parseInt(process.env.HTTP_PORT ?? "3211", 10);
const OSC_DATA_IN = parseInt(process.env.OSC_DATA_IN ?? "57124", 10);
const OSC_SYNC_IN = parseInt(process.env.OSC_SYNC_IN ?? "57125", 10);
const SC_HOST = process.env.SC_HOST ?? "127.0.0.1";
const SC_PORT_OUT = parseInt(process.env.SC_PORT_OUT ?? "57121", 10);
const OF_PORT_OUT = parseInt(process.env.OF_PORT_OUT ?? "57123", 10);
const app = express();
app.use(express.static(join(__dirname, "public")));
app.get("/", (_req, res) => res.redirect("/dashboard.html"));
const httpServer = createServer(app);
const wss = new WebSocketServer({ server: httpServer });
const HISTORY_MAX = 500;
const history = [];
function record(m) {
history.push(m);
if (history.length > HISTORY_MAX) history.shift();
}
function broadcast(msg) {
const payload = JSON.stringify(msg);
for (const c of wss.clients) {
if (c.readyState === 1) {
try { c.send(payload); } catch {}
}
}
}
wss.on("connection", (ws) => {
// Replay last ~100 events to fresh tabs
for (const m of history.slice(-100)) {
try { ws.send(JSON.stringify(m)); } catch {}
}
ws.on("message", (raw) => {
let msg;
try { msg = JSON.parse(String(raw)); } catch { return; }
// From browser : { kind: 'control'|'scene'|'xy', path, args }
// -> dispatch en OSC vers SC.
if (!msg || typeof msg.path !== "string") return;
if (!msg.path.startsWith("/control/")
&& !msg.path.startsWith("/scene/")
&& !msg.path.startsWith("/xy/")) {
return;
}
const args = (msg.args || []).map((v) => {
if (typeof v === "number") return { type: "f", value: v };
return { type: "s", value: String(v) };
});
// Routing par path :
// /control/viz* -> oscope-of (:57123) — Metal viz + skeleton renderer
// tout le reste -> SC (:57121)
const port = msg.path.startsWith("/control/viz")
? OF_PORT_OUT : SC_PORT_OUT;
udpOut.send({ address: msg.path, args }, SC_HOST, port);
});
});
// OSC IN: data feeds + SC sync (2 sockets distincts)
const udpData = new osc.UDPPort({
localAddress: "127.0.0.1", localPort: OSC_DATA_IN, metadata: false,
});
udpData.on("message", (m) => {
const parts = (m.address || "").split("/").filter(Boolean);
if (parts.length < 2 || parts[0] !== "data") return;
const feed = parts[1];
const sub = parts.slice(2).join("/") || "msg";
const msg = {
t: Date.now(), kind: "feed",
feed, sub, args: Array.isArray(m.args) ? m.args : [],
};
record(msg);
broadcast(msg);
});
udpData.on("error", (e) => console.error("OSC data error:", e));
udpData.open();
console.log(`OSC feeds in :${OSC_DATA_IN}`);
const udpSync = new osc.UDPPort({
localAddress: "127.0.0.1", localPort: OSC_SYNC_IN, metadata: false,
});
udpSync.on("message", (m) => {
// SC poussera /sync/bpm <f>, /sync/rms <f>, /sync/amp/<voie> <f>...
const path = m.address || "";
if (!path.startsWith("/sync/")) return;
const sub = path.slice(6) || "msg";
const msg = {
t: Date.now(), kind: "sync", sub,
args: Array.isArray(m.args) ? m.args : [],
};
record(msg);
broadcast(msg);
});
udpSync.on("error", (e) => console.error("OSC sync error:", e));
udpSync.open();
console.log(`OSC sync from SC in :${OSC_SYNC_IN}`);
// OSC OUT vers SuperCollider
const udpOut = new osc.UDPPort({
localAddress: "127.0.0.1", localPort: 0, metadata: false,
});
udpOut.open();
console.log(`OSC out to SC :${SC_HOST}:${SC_PORT_OUT}`);
httpServer.listen(HTTP_PORT, () => {
console.log(`Data-only web on http://127.0.0.1:${HTTP_PORT}/`);
console.log(` Dashboard : /dashboard.html`);
console.log(` Map : /map.html`);
console.log(` Control : /control.html`);
});