- 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>
197 lines
6.7 KiB
JavaScript
197 lines
6.7 KiB
JavaScript
/*
|
|
* Binary protocol encoder/decoder for video-node.
|
|
*
|
|
* Frame layout (TCP):
|
|
* [u16 message_type][u32 payload_length][payload...]
|
|
*
|
|
* CONTROL_REQUEST payload:
|
|
* [u16 request_id][u16 command][command-specific...]
|
|
*
|
|
* CONTROL_RESPONSE payload:
|
|
* [u16 request_id][u16 status][command-specific...]
|
|
*
|
|
* str8 encoding: [u8 length][bytes...] — not NUL-terminated on wire.
|
|
*/
|
|
|
|
export const FRAME_HEADER_SIZE = 6;
|
|
export const MSG_CONTROL_REQUEST = 0x0002;
|
|
export const MSG_CONTROL_RESPONSE = 0x0003;
|
|
|
|
export const CMD_ENUM_DEVICES = 0x0003;
|
|
export const CMD_ENUM_CONTROLS = 0x0004;
|
|
export const CMD_GET_CONTROL = 0x0005;
|
|
export const CMD_SET_CONTROL = 0x0006;
|
|
|
|
export const STATUS_OK = 0x0000;
|
|
|
|
/* -------------------------------------------------------------------------
|
|
* Low-level buffer helpers
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
function read_str8(buf, offset) {
|
|
const len = buf.readUInt8(offset);
|
|
const value = buf.toString('utf8', offset + 1, offset + 1 + len);
|
|
return { value, size: 1 + len };
|
|
}
|
|
|
|
function read_i64(buf, offset) {
|
|
/* Returns a JS number — safe for the int32 values V4L2 uses in practice. */
|
|
const hi = buf.readInt32BE(offset);
|
|
const lo = buf.readUInt32BE(offset + 4);
|
|
return hi * 0x100000000 + lo;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------
|
|
* Frame builder
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
export function build_frame(msg_type, payload) {
|
|
const frame = Buffer.allocUnsafe(FRAME_HEADER_SIZE + payload.length);
|
|
frame.writeUInt16BE(msg_type, 0);
|
|
frame.writeUInt32BE(payload.length, 2);
|
|
payload.copy(frame, FRAME_HEADER_SIZE);
|
|
return frame;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------
|
|
* Request encoders — return a complete TCP frame Buffer.
|
|
* request_id must be provided by the caller (u16).
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
export function encode_enum_devices(request_id) {
|
|
const p = Buffer.allocUnsafe(4);
|
|
p.writeUInt16BE(request_id, 0);
|
|
p.writeUInt16BE(CMD_ENUM_DEVICES, 2);
|
|
return build_frame(MSG_CONTROL_REQUEST, p);
|
|
}
|
|
|
|
export function encode_enum_controls(request_id, device_index) {
|
|
const p = Buffer.allocUnsafe(6);
|
|
p.writeUInt16BE(request_id, 0);
|
|
p.writeUInt16BE(CMD_ENUM_CONTROLS, 2);
|
|
p.writeUInt16BE(device_index, 4);
|
|
return build_frame(MSG_CONTROL_REQUEST, p);
|
|
}
|
|
|
|
export function encode_get_control(request_id, device_index, control_id) {
|
|
const p = Buffer.allocUnsafe(10);
|
|
p.writeUInt16BE(request_id, 0);
|
|
p.writeUInt16BE(CMD_GET_CONTROL, 2);
|
|
p.writeUInt16BE(device_index, 4);
|
|
p.writeUInt32BE(control_id, 6);
|
|
return build_frame(MSG_CONTROL_REQUEST, p);
|
|
}
|
|
|
|
export function encode_set_control(request_id, device_index, control_id, value) {
|
|
const p = Buffer.allocUnsafe(14);
|
|
p.writeUInt16BE(request_id, 0);
|
|
p.writeUInt16BE(CMD_SET_CONTROL, 2);
|
|
p.writeUInt16BE(device_index, 4);
|
|
p.writeUInt16BE(0, 6);
|
|
p.writeUInt32BE(control_id, 6);
|
|
p.writeInt32BE(value, 10);
|
|
return build_frame(MSG_CONTROL_REQUEST, p);
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------
|
|
* Response decoders — take a payload Buffer (without frame header).
|
|
* All throw on malformed data.
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
export function decode_response_header(payload) {
|
|
return {
|
|
request_id: payload.readUInt16BE(0),
|
|
status: payload.readUInt16BE(2),
|
|
};
|
|
}
|
|
|
|
export function decode_enum_devices_response(payload) {
|
|
const hdr = decode_response_header(payload);
|
|
if (hdr.status !== STATUS_OK) { return { ...hdr, media: [], standalone: [] }; }
|
|
|
|
let pos = 4;
|
|
const media_count = payload.readUInt16BE(pos); pos += 2;
|
|
const media = [];
|
|
|
|
for (let i = 0; i < media_count; i++) {
|
|
let s;
|
|
s = read_str8(payload, pos); pos += s.size; const path = s.value;
|
|
s = read_str8(payload, pos); pos += s.size; const driver = s.value;
|
|
s = read_str8(payload, pos); pos += s.size; const model = s.value;
|
|
s = read_str8(payload, pos); pos += s.size; const bus_info = s.value;
|
|
const vcount = payload.readUInt8(pos); pos++;
|
|
|
|
const video_nodes = [];
|
|
for (let j = 0; j < vcount; j++) {
|
|
s = read_str8(payload, pos); pos += s.size; const vpath = s.value;
|
|
s = read_str8(payload, pos); pos += s.size; const entity_name = s.value;
|
|
const entity_type = payload.readUInt32BE(pos); pos += 4;
|
|
const entity_flags = payload.readUInt32BE(pos); pos += 4;
|
|
const device_caps = payload.readUInt32BE(pos); pos += 4;
|
|
const pad_flags = payload.readUInt8(pos); pos++;
|
|
const is_capture = payload.readUInt8(pos); pos++;
|
|
video_nodes.push({ path: vpath, entity_name, entity_type,
|
|
entity_flags, device_caps, pad_flags, is_capture: !!is_capture });
|
|
}
|
|
|
|
media.push({ path, driver, model, bus_info, video_nodes });
|
|
}
|
|
|
|
const standalone_count = payload.readUInt16BE(pos); pos += 2;
|
|
const standalone = [];
|
|
for (let i = 0; i < standalone_count; i++) {
|
|
let s;
|
|
s = read_str8(payload, pos); pos += s.size; const path = s.value;
|
|
s = read_str8(payload, pos); pos += s.size; const name = s.value;
|
|
standalone.push({ path, name });
|
|
}
|
|
|
|
return { ...hdr, media, standalone };
|
|
}
|
|
|
|
export function decode_enum_controls_response(payload) {
|
|
const hdr = decode_response_header(payload);
|
|
if (hdr.status !== STATUS_OK) { return { ...hdr, controls: [] }; }
|
|
|
|
let pos = 4;
|
|
const count = payload.readUInt16BE(pos); pos += 2;
|
|
const controls = [];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const id = payload.readUInt32BE(pos); pos += 4;
|
|
const type = payload.readUInt8(pos); pos++;
|
|
const flags = payload.readUInt32BE(pos); pos += 4;
|
|
const s = read_str8(payload, pos); pos += s.size;
|
|
const name = s.value;
|
|
const min = payload.readInt32BE(pos); pos += 4;
|
|
const max = payload.readInt32BE(pos); pos += 4;
|
|
const step = payload.readInt32BE(pos); pos += 4;
|
|
const def = payload.readInt32BE(pos); pos += 4;
|
|
const cur = payload.readInt32BE(pos); pos += 4;
|
|
const menu_count = payload.readUInt8(pos); pos++;
|
|
|
|
const menu_items = [];
|
|
for (let j = 0; j < menu_count; j++) {
|
|
const midx = payload.readUInt32BE(pos); pos += 4;
|
|
const ms = read_str8(payload, pos); pos += ms.size;
|
|
const mval = read_i64(payload, pos); pos += 8;
|
|
menu_items.push({ index: midx, name: ms.value, int_value: mval });
|
|
}
|
|
|
|
controls.push({ id, type, flags, name, min, max, step,
|
|
default_val: def, current_val: cur, menu_items });
|
|
}
|
|
|
|
return { ...hdr, controls };
|
|
}
|
|
|
|
export function decode_get_control_response(payload) {
|
|
const hdr = decode_response_header(payload);
|
|
const value = (hdr.status === STATUS_OK) ? payload.readInt32BE(4) : 0;
|
|
return { ...hdr, value };
|
|
}
|
|
|
|
export function decode_set_control_response(payload) {
|
|
return decode_response_header(payload);
|
|
}
|