- Protocol module: framed binary encoding for control requests/responses (ENUM_DEVICES, ENUM_CONTROLS, GET/SET_CONTROL, STREAM_OPEN/CLOSE) - video-node: scans /dev/media* and /dev/video*, serves V4L2 device topology and controls over TCP; uses UDP discovery for peer announce - query_cli: auto-discovers a node, queries devices and controls - protocol_cli: low-level protocol frame decoder for debugging - dev/web: Express 5 ESM web inspector — live SSE discovery picker, REST bridge to video-node, controls UI with sliders/selects/checkboxes - Makefile: sequential module builds before cli/node to fix make -j races - common.mk: add DEPFLAGS (-MMD -MP) for automatic header dependencies - All module Makefiles: split compile/link, generate .d dependency files - discovery: replace 100ms poll loop with pthread_cond_timedwait; respond to all announcements (not just new peers) for instant re-discovery - ENUM_DEVICES response: carry device_caps (V4L2_CAP_*) per video node so clients can distinguish capture nodes from metadata nodes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
87 lines
2.6 KiB
JavaScript
87 lines
2.6 KiB
JavaScript
/*
|
|
* UDP multicast discovery — streaming callback API.
|
|
*
|
|
* Announcement wire format (matches discovery.c):
|
|
* [u16 msg_type=0x0010][u32 payload_len] <- 6-byte frame header
|
|
* [u8 protocol_version]
|
|
* [u16 site_id]
|
|
* [u16 tcp_port]
|
|
* [u16 function_flags]
|
|
* [u8 name_len]
|
|
* [bytes name...]
|
|
*/
|
|
|
|
import dgram from 'node:dgram';
|
|
|
|
const MULTICAST_GROUP = '224.0.0.251';
|
|
const DISCOVERY_PORT = 5353;
|
|
const ANNOUNCE_TYPE = 0x0010;
|
|
const HEADER_SIZE = 6;
|
|
const ANN_FIXED_SIZE = 8;
|
|
|
|
/*
|
|
* Open a discovery socket, send an immediate announce to prompt replies,
|
|
* and call on_peer for each newly-seen peer (deduplicated by addr+name).
|
|
*
|
|
* Returns a stop() function. Call it when done (e.g. user closed the picker
|
|
* or selected a node). Safe to call multiple times.
|
|
*/
|
|
export function start_discovery(on_peer) {
|
|
const sock = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
const seen = new Set();
|
|
let closed = false;
|
|
|
|
sock.on('error', () => stop());
|
|
|
|
sock.on('message', (msg, rinfo) => {
|
|
if (msg.length < HEADER_SIZE) { return; }
|
|
const msg_type = msg.readUInt16BE(0);
|
|
const payload_len = msg.readUInt32BE(2);
|
|
if (msg_type !== ANNOUNCE_TYPE) { return; }
|
|
if (msg.length < HEADER_SIZE + payload_len) { return; }
|
|
if (payload_len < ANN_FIXED_SIZE) { return; }
|
|
|
|
const p = msg.slice(HEADER_SIZE);
|
|
const site_id = p.readUInt16BE(1);
|
|
const tcp_port = p.readUInt16BE(3);
|
|
const func_flags = p.readUInt16BE(5);
|
|
const name_len = p.readUInt8(7);
|
|
if (payload_len < ANN_FIXED_SIZE + name_len) { return; }
|
|
const name = p.toString('utf8', 8, 8 + name_len);
|
|
|
|
const key = `${rinfo.address}:${name}`;
|
|
if (seen.has(key)) { return; }
|
|
seen.add(key);
|
|
|
|
on_peer({ addr: rinfo.address, tcp_port, site_id, function_flags: func_flags, name });
|
|
});
|
|
|
|
sock.bind(DISCOVERY_PORT, () => {
|
|
sock.addMembership(MULTICAST_GROUP);
|
|
send_announce(sock);
|
|
});
|
|
|
|
function stop() {
|
|
if (closed) { return; }
|
|
closed = true;
|
|
try { sock.close(); } catch {}
|
|
}
|
|
|
|
return stop;
|
|
}
|
|
|
|
function send_announce(sock) {
|
|
const name = Buffer.from('web-inspector', 'utf8');
|
|
const payload_len = 8 + name.length;
|
|
const buf = Buffer.allocUnsafe(6 + payload_len);
|
|
buf.writeUInt16BE(ANNOUNCE_TYPE, 0);
|
|
buf.writeUInt32BE(payload_len, 2);
|
|
buf.writeUInt8(1, 6); /* protocol_version */
|
|
buf.writeUInt16BE(0, 7); /* site_id */
|
|
buf.writeUInt16BE(0, 9); /* tcp_port (0 = no server) */
|
|
buf.writeUInt16BE(0, 11); /* function_flags */
|
|
buf.writeUInt8(name.length, 13);
|
|
name.copy(buf, 14);
|
|
sock.send(buf, 0, buf.length, DISCOVERY_PORT, MULTICAST_GROUP);
|
|
}
|