Fix web inspector: endianness (LE), ECONNRESET, persistent discovery

- protocol.mjs: all reads/writes switched to LE to match serial.h
- node_client.mjs: persistent error handler prevents ECONNRESET crash
- discovery.mjs: remove unnecessary SO_REUSEPORT
- server.mjs: discovery runs at startup (not per SSE open); uses
  EventEmitter + known_peers Map so SSE replays existing peers on connect

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 01:45:33 +00:00
parent 62c25247ef
commit e1151410ad
4 changed files with 112 additions and 86 deletions

View File

@@ -2,14 +2,15 @@
* Express 5 web server — REST bridge to video-node.
*
* Usage:
* node server.mjs [--host IP] [--port PORT] [--discover] [--listen PORT]
* node server.mjs [--host IP] [--port PORT] [--listen PORT]
*
* If --host and --port are given, connects on startup.
* If --discover is given, runs UDP discovery to find the node automatically.
* Otherwise, call POST /api/connect to connect at runtime.
* Discovery runs at startup unconditionally — the server joins the multicast
* group and sends an announcement immediately so nodes respond right away.
* The /api/discover SSE endpoint subscribes to the running peer feed.
*/
import express from 'express';
import { EventEmitter } from 'node:events';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { Node_Client } from './node_client.mjs';
@@ -25,10 +26,29 @@ const arg = name => {
return i >= 0 ? args[i + 1] : null;
};
const opt_host = arg('--host');
const opt_port = arg('--port') ? parseInt(arg('--port')) : null;
const opt_discover = args.includes('--discover');
const listen_port = arg('--listen') ? parseInt(arg('--listen')) : 3000;
const opt_host = arg('--host');
const opt_port = arg('--port') ? parseInt(arg('--port')) : null;
const listen_port = arg('--listen') ? parseInt(arg('--listen')) : 3000;
/* -------------------------------------------------------------------------
* Persistent discovery — runs from startup, feeds the SSE endpoint
* ------------------------------------------------------------------------- */
const peer_bus = new EventEmitter();
const known_peers = new Map(); /* key: addr:name -> peer */
start_discovery(
peer => {
const key = `${peer.addr}:${peer.name}`;
known_peers.set(key, peer);
peer_bus.emit('peer', peer);
console.log(`discovered: ${peer.name} at ${peer.addr}:${peer.tcp_port}`);
},
err => {
console.error('discovery error:', err.message);
peer_bus.emit('discovery_error', err);
},
);
/* -------------------------------------------------------------------------
* Node client (singleton)
@@ -75,9 +95,14 @@ app.post('/api/connect', async (req, res) => {
}
});
app.post('/api/disconnect', (_req, res) => {
client.disconnect();
res.json({ ok: true });
});
/*
* SSE stream: each discovered peer arrives as a JSON event.
* The client closes the connection when done (user picked a node or dismissed).
* SSE stream — immediately replays already-known peers, then streams new ones
* as they arrive. Client closes when done (user picked a node or dismissed).
*/
app.get('/api/discover', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
@@ -85,16 +110,19 @@ app.get('/api/discover', (req, res) => {
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
const stop = start_discovery(peer => {
res.write(`data: ${JSON.stringify(peer)}\n\n`);
const send_peer = peer => res.write(`data: ${JSON.stringify(peer)}\n\n`);
const send_error = err => res.write(`event: discovery_error\ndata: ${JSON.stringify({ error: err.message })}\n\n`);
/* replay peers already seen before this SSE was opened */
for (const peer of known_peers.values()) { send_peer(peer); }
peer_bus.on('peer', send_peer);
peer_bus.on('discovery_error', send_error);
req.on('close', () => {
peer_bus.off('peer', send_peer);
peer_bus.off('discovery_error', send_error);
});
req.on('close', stop);
});
app.post('/api/disconnect', (_req, res) => {
client.disconnect();
res.json({ ok: true });
});
/* -- Devices --------------------------------------------------------------- */
@@ -158,16 +186,7 @@ app.listen(listen_port, () => {
console.log(`listening on http://localhost:${listen_port}`);
});
if (opt_discover) {
console.log('discovering video-nodes...');
const stop = start_discovery(peer => {
console.log(`found ${peer.name} at ${peer.addr}:${peer.tcp_port} — connecting`);
stop();
client.connect(peer.addr, peer.tcp_port)
.then(() => console.log('connected'))
.catch(err => console.error('connect failed:', err.message));
});
} else if (opt_host && opt_port) {
if (opt_host && opt_port) {
client.connect(opt_host, opt_port)
.then(() => console.log(`connected to ${opt_host}:${opt_port}`))
.catch(err => console.error('connect failed:', err.message));