- protocol.yaml: SSoT for the binary framing protocol (12 record types) - codegen/gen.mjs: generates C header/source and Node ESM from protocol.yaml - c-backend: ALSA sequencer with drift-free clock_nanosleep tick thread, pattern store (hierarchical sub-patterns), Unix socket server - node-server: Express 5 web app — REST API, SSE for real-time beat events, step-sequencer frontend with pending-edit / explicit-save flow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
314 lines
11 KiB
JavaScript
314 lines
11 KiB
JavaScript
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname, join, resolve } from 'path';
|
|
import yaml from 'js-yaml';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = resolve(__dirname, '..');
|
|
|
|
const proto = yaml.load(readFileSync(join(ROOT, 'protocol.yaml'), 'utf8'));
|
|
|
|
const RECORDS = Object.entries(proto.records);
|
|
const TYPES = proto.types;
|
|
const C_GEN_DIR = join(ROOT, 'c-backend', 'generated');
|
|
const NODE_GEN_DIR = join(ROOT, 'node-server', 'src', 'generated');
|
|
|
|
mkdirSync(C_GEN_DIR, { recursive: true });
|
|
mkdirSync(NODE_GEN_DIR, { recursive: true });
|
|
|
|
/* ── helpers ──────────────────────────────────────────────────────── */
|
|
|
|
function payload_size(record) {
|
|
let size = 0;
|
|
for (const f of (record.fields || [])) {
|
|
size += TYPES[f.type].size;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
function struct_name(id) {
|
|
return 'Msg_' + id.split('_').map(w => w[0].toUpperCase() + w.slice(1).toLowerCase()).join('_');
|
|
}
|
|
|
|
function rt_const(id) {
|
|
return 'RT_' + id.toUpperCase();
|
|
}
|
|
|
|
function fn_name(id) {
|
|
return id.toLowerCase();
|
|
}
|
|
|
|
/* ── C header ─────────────────────────────────────────────────────── */
|
|
|
|
function gen_c_header() {
|
|
const L = [];
|
|
|
|
L.push(`/* AUTO-GENERATED by codegen/gen.mjs — DO NOT EDIT */`);
|
|
L.push(`/* Source: protocol.yaml version ${proto.version} */`);
|
|
L.push(`#pragma once`);
|
|
L.push(`#include <stdint.h>`);
|
|
L.push(`#include <string.h>`);
|
|
L.push(``);
|
|
L.push(`#define PROTOCOL_VERSION ${proto.version}`);
|
|
L.push(`#define FRAME_HEADER_SIZE 3`);
|
|
L.push(`#define DIRECTION_C_TO_NODE(t) ((t) >= 0x80)`);
|
|
L.push(``);
|
|
|
|
/* enum */
|
|
L.push(`/* Record types */`);
|
|
L.push(`typedef enum {`);
|
|
for (const [id, rec] of RECORDS) {
|
|
L.push(`\t${rt_const(id).padEnd(24)} = 0x${rec.id.toString(16).padStart(2, '0').toUpperCase()},`);
|
|
}
|
|
L.push(`} Record_Type;`);
|
|
L.push(``);
|
|
|
|
/* payload size constants */
|
|
L.push(`/* Payload sizes (bytes, not including 3-byte frame header) */`);
|
|
for (const [id, rec] of RECORDS) {
|
|
const name = `PAYLOAD_SIZE_${id.toUpperCase()}`;
|
|
L.push(`#define ${name.padEnd(32)} ${payload_size(rec)}`);
|
|
}
|
|
L.push(``);
|
|
|
|
/* structs */
|
|
L.push(`/* Record payload structs */`);
|
|
for (const [id, rec] of RECORDS) {
|
|
const sn = struct_name(id);
|
|
L.push(`typedef struct {`);
|
|
if (!rec.fields || rec.fields.length === 0) {
|
|
L.push(`\tchar _empty;`);
|
|
} else {
|
|
for (const f of rec.fields) {
|
|
const ct = TYPES[f.type].c_type;
|
|
const note = f.note ? ` /* ${f.note} */` : '';
|
|
L.push(`\t${ct.padEnd(10)} ${f.name};${note}`);
|
|
}
|
|
}
|
|
L.push(`} ${sn};`);
|
|
}
|
|
L.push(``);
|
|
|
|
/* function declarations */
|
|
L.push(`/* Encode: write complete frame to buf, return total bytes written */`);
|
|
for (const [id, rec] of RECORDS) {
|
|
const sn = struct_name(id);
|
|
if (!rec.fields || rec.fields.length === 0) {
|
|
L.push(`int proto_encode_${fn_name(id)}(uint8_t *buf);`);
|
|
} else {
|
|
L.push(`int proto_encode_${fn_name(id)}(uint8_t *buf, const ${sn} *r);`);
|
|
}
|
|
}
|
|
L.push(``);
|
|
L.push(`/* Decode: parse payload into struct, return 0 on success, -1 on error */`);
|
|
for (const [id, rec] of RECORDS) {
|
|
const sn = struct_name(id);
|
|
L.push(`int proto_decode_${fn_name(id)}(const uint8_t *payload, uint16_t len, ${sn} *r);`);
|
|
}
|
|
|
|
return L.join('\n') + '\n';
|
|
}
|
|
|
|
/* ── C source ─────────────────────────────────────────────────────── */
|
|
|
|
function gen_c_source() {
|
|
const L = [];
|
|
|
|
L.push(`/* AUTO-GENERATED by codegen/gen.mjs — DO NOT EDIT */`);
|
|
L.push(`#include "protocol.h"`);
|
|
L.push(``);
|
|
L.push(`/* ── serialization helpers ───────────────────────────────────── */`);
|
|
L.push(``);
|
|
L.push(`static void put_u8(uint8_t *buf, int *off, uint8_t v) {`);
|
|
L.push(`\tbuf[(*off)++] = v;`);
|
|
L.push(`}`);
|
|
L.push(`static void put_u16(uint8_t *buf, int *off, uint16_t v) {`);
|
|
L.push(`\tbuf[(*off)++] = (uint8_t)(v & 0xFF);`);
|
|
L.push(`\tbuf[(*off)++] = (uint8_t)(v >> 8);`);
|
|
L.push(`}`);
|
|
L.push(`static uint8_t get_u8(const uint8_t *buf, int *off) {`);
|
|
L.push(`\treturn buf[(*off)++];`);
|
|
L.push(`}`);
|
|
L.push(`static uint16_t get_u16(const uint8_t *buf, int *off) {`);
|
|
L.push(`\tuint16_t v = (uint16_t)((uint16_t)buf[*off] | ((uint16_t)buf[*off + 1] << 8));`);
|
|
L.push(`\t*off += 2;`);
|
|
L.push(`\treturn v;`);
|
|
L.push(`}`);
|
|
L.push(`static int write_frame(uint8_t *buf, uint8_t record_type, int payload_len) {`);
|
|
L.push(`\tbuf[0] = record_type;`);
|
|
L.push(`\tbuf[1] = (uint8_t)(payload_len & 0xFF);`);
|
|
L.push(`\tbuf[2] = (uint8_t)(payload_len >> 8);`);
|
|
L.push(`\treturn FRAME_HEADER_SIZE + payload_len;`);
|
|
L.push(`}`);
|
|
L.push(``);
|
|
|
|
/* encode functions */
|
|
L.push(`/* ── encode ──────────────────────────────────────────────────── */`);
|
|
L.push(``);
|
|
for (const [id, rec] of RECORDS) {
|
|
const sn = struct_name(id);
|
|
const ps = payload_size(rec);
|
|
const no_fields = !rec.fields || rec.fields.length === 0;
|
|
|
|
if (no_fields) {
|
|
L.push(`int proto_encode_${fn_name(id)}(uint8_t *buf) {`);
|
|
L.push(`\treturn write_frame(buf, ${rt_const(id)}, 0);`);
|
|
} else {
|
|
L.push(`int proto_encode_${fn_name(id)}(uint8_t *buf, const ${sn} *r) {`);
|
|
L.push(`\tint off = FRAME_HEADER_SIZE;`);
|
|
for (const f of rec.fields) {
|
|
L.push(`\t${TYPES[f.type].c_put}(buf, &off, r->${f.name});`);
|
|
}
|
|
L.push(`\treturn write_frame(buf, ${rt_const(id)}, ${ps});`);
|
|
}
|
|
L.push(`}`);
|
|
L.push(``);
|
|
}
|
|
|
|
/* decode functions */
|
|
L.push(`/* ── decode ──────────────────────────────────────────────────── */`);
|
|
L.push(``);
|
|
for (const [id, rec] of RECORDS) {
|
|
const sn = struct_name(id);
|
|
const ps = payload_size(rec);
|
|
const no_fields = !rec.fields || rec.fields.length === 0;
|
|
|
|
L.push(`int proto_decode_${fn_name(id)}(const uint8_t *p, uint16_t len, ${sn} *r) {`);
|
|
if (no_fields) {
|
|
L.push(`\t(void)p; (void)len; (void)r;`);
|
|
L.push(`\treturn 0;`);
|
|
} else {
|
|
L.push(`\tif (len < ${ps}) return -1;`);
|
|
L.push(`\tint off = 0;`);
|
|
for (const f of rec.fields) {
|
|
L.push(`\tr->${f.name} = ${TYPES[f.type].c_get}(p, &off);`);
|
|
}
|
|
L.push(`\t(void)off;`);
|
|
L.push(`\treturn 0;`);
|
|
}
|
|
L.push(`}`);
|
|
L.push(``);
|
|
}
|
|
|
|
return L.join('\n');
|
|
}
|
|
|
|
/* ── Node ESM module ──────────────────────────────────────────────── */
|
|
|
|
function gen_node_esm() {
|
|
const L = [];
|
|
|
|
L.push(`/* AUTO-GENERATED by codegen/gen.mjs — DO NOT EDIT */`);
|
|
L.push(`/* Source: protocol.yaml version ${proto.version} */`);
|
|
L.push(``);
|
|
L.push(`export const PROTOCOL_VERSION = ${proto.version};`);
|
|
L.push(`export const FRAME_HEADER_SIZE = 3;`);
|
|
L.push(``);
|
|
|
|
/* Record_Type enum */
|
|
L.push(`export const RT = Object.freeze({`);
|
|
for (const [id, rec] of RECORDS) {
|
|
L.push(`\t${id.padEnd(20)}: 0x${rec.id.toString(16).padStart(2, '0').toUpperCase()},`);
|
|
}
|
|
L.push(`});`);
|
|
L.push(``);
|
|
|
|
/* Reverse map */
|
|
L.push(`export const RT_NAME = Object.freeze(`);
|
|
L.push(`\tObject.fromEntries(Object.entries(RT).map(([k, v]) => [v, k]))`);
|
|
L.push(`);`);
|
|
L.push(``);
|
|
|
|
/* Payload sizes */
|
|
L.push(`export const PAYLOAD_SIZE = Object.freeze({`);
|
|
for (const [id, rec] of RECORDS) {
|
|
L.push(`\t${id.padEnd(20)}: ${payload_size(rec)},`);
|
|
}
|
|
L.push(`});`);
|
|
L.push(``);
|
|
|
|
/* write_frame helper */
|
|
L.push(`function write_frame(record_type, payload_buf) {`);
|
|
L.push(`\tconst frame = Buffer.alloc(FRAME_HEADER_SIZE + payload_buf.length);`);
|
|
L.push(`\tframe.writeUInt8(record_type, 0);`);
|
|
L.push(`\tframe.writeUInt16LE(payload_buf.length, 1);`);
|
|
L.push(`\tpayload_buf.copy(frame, FRAME_HEADER_SIZE);`);
|
|
L.push(`\treturn frame;`);
|
|
L.push(`}`);
|
|
L.push(``);
|
|
|
|
/* encode functions */
|
|
L.push(`/* ── encode ──────────────────────────────────────────────────── */`);
|
|
L.push(``);
|
|
for (const [id, rec] of RECORDS) {
|
|
const ps = payload_size(rec);
|
|
const no_fields = !rec.fields || rec.fields.length === 0;
|
|
const params = no_fields ? '' : `{ ${(rec.fields || []).map(f => f.name).join(', ')} }`;
|
|
|
|
L.push(`export function encode_${fn_name(id)}(${params}) {`);
|
|
if (no_fields) {
|
|
L.push(`\treturn write_frame(RT.${id}, Buffer.alloc(0));`);
|
|
} else {
|
|
L.push(`\tconst buf = Buffer.alloc(${ps});`);
|
|
let offset = 0;
|
|
for (const f of rec.fields) {
|
|
const t = TYPES[f.type];
|
|
L.push(`\tbuf.${t.node_write}(${f.name}, ${offset});`);
|
|
offset += t.size;
|
|
}
|
|
L.push(`\treturn write_frame(RT.${id}, buf);`);
|
|
}
|
|
L.push(`}`);
|
|
L.push(``);
|
|
}
|
|
|
|
/* decode functions */
|
|
L.push(`/* ── decode ──────────────────────────────────────────────────── */`);
|
|
L.push(``);
|
|
for (const [id, rec] of RECORDS) {
|
|
const ps = payload_size(rec);
|
|
const no_fields = !rec.fields || rec.fields.length === 0;
|
|
|
|
L.push(`export function decode_${fn_name(id)}(payload) {`);
|
|
if (no_fields) {
|
|
L.push(`\treturn {};`);
|
|
} else {
|
|
L.push(`\tif (payload.length < ${ps}) throw new Error('${id} payload too short');`);
|
|
const obj_fields = [];
|
|
let offset = 0;
|
|
for (const f of rec.fields) {
|
|
const t = TYPES[f.type];
|
|
obj_fields.push(`${f.name}: payload.${t.node_read}(${offset})`);
|
|
offset += t.size;
|
|
}
|
|
L.push(`\treturn { ${obj_fields.join(', ')} };`);
|
|
}
|
|
L.push(`}`);
|
|
L.push(``);
|
|
}
|
|
|
|
/* Generic decode dispatcher */
|
|
L.push(`/* Dispatch: decode any payload by record type */`);
|
|
L.push(`export function decode(record_type, payload) {`);
|
|
L.push(`\tswitch (record_type) {`);
|
|
for (const [id] of RECORDS) {
|
|
L.push(`\t\tcase RT.${id}: return decode_${fn_name(id)}(payload);`);
|
|
}
|
|
L.push(`\t\tdefault: throw new Error(\`Unknown record type 0x\${record_type.toString(16)}\`);`);
|
|
L.push(`\t}`);
|
|
L.push(`}`);
|
|
|
|
return L.join('\n') + '\n';
|
|
}
|
|
|
|
/* ── write outputs ────────────────────────────────────────────────── */
|
|
|
|
writeFileSync(join(C_GEN_DIR, 'protocol.h'), gen_c_header());
|
|
writeFileSync(join(C_GEN_DIR, 'protocol.c'), gen_c_source());
|
|
writeFileSync(join(NODE_GEN_DIR, 'protocol.mjs'), gen_node_esm());
|
|
|
|
console.log('Generated:');
|
|
console.log(' c-backend/generated/protocol.h');
|
|
console.log(' c-backend/generated/protocol.c');
|
|
console.log(' node-server/src/generated/protocol.mjs');
|