diff --git a/fedac/native/kernel/config-minimal b/fedac/native/kernel/config-minimal index a151922be0..1c1e13bf4b 100644 --- a/fedac/native/kernel/config-minimal +++ b/fedac/native/kernel/config-minimal @@ -3338,7 +3338,11 @@ CONFIG_USB_DWC3_PCI=y # # USB port drivers # -# CONFIG_USB_SERIAL is not set +CONFIG_USB_SERIAL=y +# CONFIG_USB_SERIAL_CONSOLE is not set +# CONFIG_USB_SERIAL_GENERIC is not set +# CONFIG_USB_SERIAL_SIMPLE is not set +CONFIG_USB_SERIAL_FTDI_SIO=y # # USB Miscellaneous drivers diff --git a/fedac/native/pieces/notepat.mjs b/fedac/native/pieces/notepat.mjs index 9a431a7706..5c6d63263d 100644 --- a/fedac/native/pieces/notepat.mjs +++ b/fedac/native/pieces/notepat.mjs @@ -471,6 +471,13 @@ let dark = true; // AC Native Notepat is an instrument surface: always dark. let bgColor = [0, 0, 0]; let bgTarget = dark ? [20, 20, 25] : [240, 238, 232]; +// DMX light mirrors bgColor via system.dmxSend (USB DMX PRO widget). +// dmxMap turns rgb into universe slots (index 0 = channel 1) — swap this +// per fixture; default is a bare RGB head at address 1. +const dmxMap = (r, g, b) => [r, g, b]; +let dmxLast = [-1, -1, -1]; +let dmxFrame = -60; + // === DRUM BACKGROUND FLASH === // Tones paint the full-screen background from their note colors while held // (see the activeKeys loop in paint()). Drums are one-shots and don't live @@ -6534,6 +6541,17 @@ function paint({ wipe, ink, box, line, write, screen, sound, system, trackpad, p wipe(Math.round(bgColor[0]), Math.round(bgColor[1]), Math.round(bgColor[2])); + // Mirror the backdrop out to the DMX light — on change (capped ~30fps) + // plus a 1s keepalive so a re-plugged widget picks the color back up. + if (system.dmxSend) { + const dr = Math.round(bgColor[0]), dg = Math.round(bgColor[1]), db = Math.round(bgColor[2]); + const changed = dr !== dmxLast[0] || dg !== dmxLast[1] || db !== dmxLast[2]; + if ((changed && frame - dmxFrame >= 2) || frame - dmxFrame >= 60) { + if (system.dmxSend(dmxMap(dr, dg, db))) dmxLast = [dr, dg, db]; + dmxFrame = frame; + } + } + // Tablet mode change detection — beep + log if (system.tabletMode !== lastTabletMode && lastTabletMode !== null) { if (system.tabletMode) { diff --git a/fedac/native/src/js-bindings.c b/fedac/native/src/js-bindings.c index 93cef1c88a..f3256ef7ee 100644 --- a/fedac/native/src/js-bindings.c +++ b/fedac/native/src/js-bindings.c @@ -19,6 +19,7 @@ #include #include #include +#include #include "qrcodegen.h" #include #include "machines.h" @@ -6498,6 +6499,72 @@ static JSValue js_print_raw(JSContext *ctx, JSValueConst this_val, int argc, JSV return written == len ? JS_TRUE : JS_FALSE; } +// system.dmxSend(channels) — push one DMX universe frame to the first USB DMX +// widget (Enttec Pro framing — DMXking USB PRO et al; the widget owns DMX +// timing, we just hand it slots). fd persists across calls; a failed write +// closes it so unplug/replug self-heals on the next send. +static int dmx_fd = -1; + +static int dmx_open(void) { + if (dmx_fd >= 0) return dmx_fd; + for (int i = 0; i < 4; i++) { + char path[24]; + snprintf(path, sizeof(path), "/dev/ttyUSB%d", i); + int fd = open(path, O_WRONLY | O_NOCTTY); + if (fd < 0) continue; + struct termios t; + if (tcgetattr(fd, &t) == 0) { + cfmakeraw(&t); // raw — the frame bytes must pass untouched + cfsetospeed(&t, B115200); + cfsetispeed(&t, B115200); + tcsetattr(fd, TCSANOW, &t); + } + ac_log("💡 [dmx] opened %s\n", path); + dmx_fd = fd; + return fd; + } + return -1; +} + +static JSValue js_dmx_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_FALSE; + int fd = dmx_open(); + if (fd < 0) return JS_FALSE; + + JSValue lenVal = JS_GetPropertyStr(ctx, argv[0], "length"); + uint32_t len = 0; + JS_ToUint32(ctx, &len, lenVal); + JS_FreeValue(ctx, lenVal); + if (len > 512) len = 512; + uint32_t slots = len < 24 ? 24 : len; // widget wants ≥24 slots per frame + + // 7E 06 lenLo lenHi | 00 startcode + slots | E7 + uint8_t pkt[5 + 1 + 512 + 1]; + uint16_t payload = (uint16_t)(slots + 1); + pkt[0] = 0x7E; pkt[1] = 0x06; + pkt[2] = payload & 0xFF; pkt[3] = payload >> 8; + pkt[4] = 0x00; // DMX start code + memset(pkt + 5, 0, slots); + for (uint32_t i = 0; i < len; i++) { + JSValue v = JS_GetPropertyUint32(ctx, argv[0], i); + int32_t b = 0; + JS_ToInt32(ctx, &b, v); + JS_FreeValue(ctx, v); + pkt[5 + i] = (uint8_t)(b & 0xFF); + } + pkt[5 + slots] = 0xE7; + + ssize_t n = write(fd, pkt, 5 + slots + 1); + if (n != (ssize_t)(5 + slots + 1)) { + ac_log("💡 [dmx] write failed (%s) — closing\n", strerror(errno)); + close(dmx_fd); + dmx_fd = -1; + return JS_FALSE; + } + return JS_TRUE; +} + static JSValue js_jump(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 1) return JS_UNDEFINED; @@ -7799,6 +7866,8 @@ static JSValue build_system_obj(JSContext *ctx) { JS_NewCFunction(ctx, js_list_printers, "listPrinters", 0)); JS_SetPropertyStr(ctx, sys, "printRaw", JS_NewCFunction(ctx, js_print_raw, "printRaw", 2)); + JS_SetPropertyStr(ctx, sys, "dmxSend", + JS_NewCFunction(ctx, js_dmx_send, "dmxSend", 1)); // Volume and brightness control from JS JS_SetPropertyStr(ctx, sys, "volumeAdjust",