/* * 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) { /* Little-endian i64: low 4 bytes first, high 4 bytes second. * Returns a JS number — safe for the int32 values V4L2 uses in practice. */ const lo = buf.readUInt32LE(offset); const hi = buf.readInt32LE(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.writeUInt16LE(msg_type, 0); frame.writeUInt32LE(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.writeUInt16LE(request_id, 0); p.writeUInt16LE(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.writeUInt16LE(request_id, 0); p.writeUInt16LE(CMD_ENUM_CONTROLS, 2); p.writeUInt16LE(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.writeUInt16LE(request_id, 0); p.writeUInt16LE(CMD_GET_CONTROL, 2); p.writeUInt16LE(device_index, 4); p.writeUInt32LE(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.writeUInt16LE(request_id, 0); p.writeUInt16LE(CMD_SET_CONTROL, 2); p.writeUInt16LE(device_index, 4); p.writeUInt32LE(control_id, 6); p.writeInt32LE(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.readUInt16LE(0), status: payload.readUInt16LE(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.readUInt16LE(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.readUInt32LE(pos); pos += 4; const entity_flags = payload.readUInt32LE(pos); pos += 4; const device_caps = payload.readUInt32LE(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.readUInt16LE(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.readUInt16LE(pos); pos += 2; const controls = []; for (let i = 0; i < count; i++) { const id = payload.readUInt32LE(pos); pos += 4; const type = payload.readUInt8(pos); pos++; const flags = payload.readUInt32LE(pos); pos += 4; const s = read_str8(payload, pos); pos += s.size; const name = s.value; const min = payload.readInt32LE(pos); pos += 4; const max = payload.readInt32LE(pos); pos += 4; const step = payload.readInt32LE(pos); pos += 4; const def = payload.readInt32LE(pos); pos += 4; const cur = payload.readInt32LE(pos); pos += 4; const menu_count = payload.readUInt8(pos); pos++; const menu_items = []; for (let j = 0; j < menu_count; j++) { const midx = payload.readUInt32LE(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.readInt32LE(4) : 0; return { ...hdr, value }; } export function decode_set_control_response(payload) { return decode_response_header(payload); }