Phone remote: LAN web page + WebSocket, QR pairing, blackout/freeze

remote-server.js: http + ws server (off by default, started from the panel,
port 8787 or next free), every request gated by a secret key embedded in a
QR code; the key persists in userData/remote.json so a phone's home-screen
shortcut survives restarts, 'Nuovo codice' revokes it. Phones send commands
that the panel executes through its own functions (transport, track pick,
effect next/prev/random/family/sequence, Auto VJ, pads, mapping on + visible
zones, blackout, freeze) and the panel pushes a compact live state at ~6 Hz
while a phone is connected (track + progress, effect, sequence, pads,
zones, meters, BPM). New panic controls in output and aux windows: blackout
(fade to black above every layer) and freeze (hold the last frame); also
available as panel buttons. src/remote.{html,css,js} is the touch UI in the
Neon Grid style, installable to the home screen, auto-reconnecting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVMQ2JhsJ77pGKqcvvFHqc
This commit is contained in:
lucianoandClaude Opus 5.5 committed 2026-10-02 13:39:28 +02:00
1 parent f246b8bdb8
commit ab1ab89127
16 files changed
+973 -21

No files matched your search

+36
View File
@@ -3,6 +3,8 @@ const path = require('path');
const fs = require('fs');
const { execFile } = require('child_process');
const { pathToFileURL } = require('url');
const crypto = require('crypto');
const { RemoteServer } = require('./remote-server');
// djvres:// serves the MediaPipe WASM runtime and the bundled pose model to
// the renderer: fetch() does not work on file:// URLs, and the pose tracker
@@ -156,6 +158,7 @@ function createWindows() {
controlWin = null;
for (const win of auxWins.values()) { if (!win.isDestroyed()) win.destroy(); }
auxWins.clear();
remote.stop();
if (outputWin) outputWin.close();
app.quit();
});
@@ -412,6 +415,39 @@ ipcMain.handle('rec:openFolder', () => {
return dir;
});
// --- Phone remote (LAN web page + WebSocket) ---
// Commands from phones go to the panel, which runs them through its own code
// paths; the panel pushes a compact state back for the phones to display.
const remoteCfgFile = () => path.join(app.getPath('userData'), 'remote.json');
function remoteCfg() {
try { return JSON.parse(fs.readFileSync(remoteCfgFile(), 'utf8')); } catch (e) { return {}; }
}
function saveRemoteCfg(c) { try { fs.writeFileSync(remoteCfgFile(), JSON.stringify(c)); } catch (e) {} }
const remote = new RemoteServer({
srcDir: path.join(__dirname, 'src'),
onCommand: (m) => { if (controlWin && !controlWin.isDestroyed()) controlWin.webContents.send('remote', m); }
});
ipcMain.handle('remote:start', async () => {
const c = remoteCfg();
// the key survives restarts so a phone's home-screen shortcut keeps working
if (!c.key) c.key = crypto.randomBytes(6).toString('hex');
c.on = true; saveRemoteCfg(c);
try { return await remote.start(8787, c.key); } catch (e) { return { on: false, error: e.message }; }
});
ipcMain.handle('remote:stop', () => {
remote.stop();
const c = remoteCfg(); c.on = false; saveRemoteCfg(c);
return { on: false };
});
ipcMain.handle('remote:newKey', async () => {
// revoke: every phone that had the old code is disconnected and locked out
remote.stop();
const c = remoteCfg(); c.key = crypto.randomBytes(6).toString('hex'); c.on = true; saveRemoteCfg(c);
try { return await remote.start(8787, c.key); } catch (e) { return { on: false, error: e.message }; }
});
ipcMain.handle('remote:info', async () => (remote.running ? remote.info() : { on: false, wanted: !!remoteCfg().on }));
ipcMain.on('remote:state', (_e, state) => remote.broadcast(state));
// --- Pad bank persistence ---
const padsFile = () => path.join(app.getPath('userData'), 'pads.json');
ipcMain.handle('pads:load', () => {