Monorepo for Aesthetic.Computer aesthetic.computer
at main 7506 lines 322 kB view raw
1#define _GNU_SOURCE 2#include "js-bindings.h" 3#include "usb-midi.h" 4#include "recorder.h" 5#include <pthread.h> 6#include <stdio.h> 7#include <stdlib.h> 8#include <string.h> 9#include <time.h> 10#include <math.h> 11#include <dirent.h> 12#include <unistd.h> 13#include <sys/stat.h> 14#include <sys/statvfs.h> 15#include <sys/wait.h> 16#include <sys/mount.h> 17#include <sys/reboot.h> 18#include <linux/reboot.h> 19#include <fcntl.h> 20#include <errno.h> 21#include "qrcodegen.h" 22#include <alsa/asoundlib.h> 23 24// Defined in ac-native.c — logs to USB mount 25extern void ac_log(const char *fmt, ...); 26extern void ac_log_flush(void); 27extern void ac_log_pause(void); 28extern void ac_log_resume(void); 29extern void perf_flush(void); 30 31// Thread-local reference to current runtime (for C callbacks from JS) 32static __thread ACRuntime *current_rt = NULL; 33static __thread const char *current_phase = "boot"; 34static int config_cache_dirty = 1; // reload /mnt/config.json when set 35 36// Forward declaration (defined later in this file) 37static int resolve_color_name(const char *name, ACColor *out); 38static JSValue js_usb_midi_note_on(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); 39static JSValue js_usb_midi_note_off(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); 40static JSValue js_usb_midi_all_notes_off(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); 41 42// ============================================================ 43// QuickJS Class IDs for 3D objects 44// ============================================================ 45static JSClassID form_class_id = 0; 46static JSClassID painting_class_id = 0; 47 48// Form class finalizer 49static void js_form_finalizer(JSRuntime *rt, JSValue val) { 50 (void)rt; 51 ACForm *form = JS_GetOpaque(val, form_class_id); 52 if (form) { 53 free(form->positions); 54 free(form->colors); 55 free(form->tex_coords); 56 free(form); 57 } 58} 59 60static JSClassDef form_class_def = { 61 "Form", 62 .finalizer = js_form_finalizer, 63}; 64 65// Painting class finalizer 66static void js_painting_finalizer(JSRuntime *rt, JSValue val) { 67 (void)rt; 68 ACFramebuffer *fb = JS_GetOpaque(val, painting_class_id); 69 if (!fb) return; 70 // Don't destroy runtime-owned nopaint buffers 71 if (current_rt && (fb == current_rt->nopaint_painting || fb == current_rt->nopaint_buffer)) 72 return; 73 fb_destroy(fb); 74} 75 76static JSClassDef painting_class_def = { 77 "Painting", 78 .finalizer = js_painting_finalizer, 79}; 80 81// ============================================================ 82// Form constructor: new Form({type, positions, colors, texCoords}, {pos, rot, scale}) 83// ============================================================ 84 85static JSValue js_form_constructor(JSContext *ctx, JSValueConst new_target, 86 int argc, JSValueConst *argv) { 87 (void)new_target; 88 if (argc < 1) return JS_EXCEPTION; 89 90 ACForm *form = calloc(1, sizeof(ACForm)); 91 if (!form) return JS_EXCEPTION; 92 form->scale = 1.0f; 93 94 // Parse geometry descriptor (first argument) 95 JSValue geom = argv[0]; 96 JSValue type_val = JS_GetPropertyStr(ctx, geom, "type"); 97 if (!JS_IsUndefined(type_val)) { 98 const char *type_str = JS_ToCString(ctx, type_val); 99 if (type_str && strcmp(type_str, "line") == 0) 100 form->type = FORM_TYPE_LINE; 101 else 102 form->type = FORM_TYPE_TRIANGLE; 103 if (type_str) JS_FreeCString(ctx, type_str); 104 } 105 JS_FreeValue(ctx, type_val); 106 107 // Parse positions array: [[x,y,z,w], ...] 108 JSValue pos_arr = JS_GetPropertyStr(ctx, geom, "positions"); 109 if (JS_IsArray(ctx, pos_arr)) { 110 JSValue len_val = JS_GetPropertyStr(ctx, pos_arr, "length"); 111 int32_t len = 0; 112 JS_ToInt32(ctx, &len, len_val); 113 JS_FreeValue(ctx, len_val); 114 115 form->vert_count = len; 116 form->positions = calloc(len * 4, sizeof(float)); 117 118 for (int i = 0; i < len; i++) { 119 JSValue vert = JS_GetPropertyUint32(ctx, pos_arr, i); 120 for (int j = 0; j < 4; j++) { 121 JSValue comp = JS_GetPropertyUint32(ctx, vert, j); 122 double v = 0; 123 JS_ToFloat64(ctx, &v, comp); 124 form->positions[i * 4 + j] = (float)v; 125 JS_FreeValue(ctx, comp); 126 } 127 JS_FreeValue(ctx, vert); 128 } 129 } 130 JS_FreeValue(ctx, pos_arr); 131 132 // Parse colors array: [[r,g,b,a], ...] 133 JSValue col_arr = JS_GetPropertyStr(ctx, geom, "colors"); 134 if (JS_IsArray(ctx, col_arr)) { 135 JSValue len_val = JS_GetPropertyStr(ctx, col_arr, "length"); 136 int32_t len = 0; 137 JS_ToInt32(ctx, &len, len_val); 138 JS_FreeValue(ctx, len_val); 139 140 form->colors = calloc(len * 4, sizeof(float)); 141 form->has_colors = 1; 142 143 for (int i = 0; i < len; i++) { 144 JSValue col = JS_GetPropertyUint32(ctx, col_arr, i); 145 for (int j = 0; j < 4; j++) { 146 JSValue comp = JS_GetPropertyUint32(ctx, col, j); 147 double v = 0; 148 JS_ToFloat64(ctx, &v, comp); 149 form->colors[i * 4 + j] = (float)v; 150 JS_FreeValue(ctx, comp); 151 } 152 JS_FreeValue(ctx, col); 153 } 154 } 155 JS_FreeValue(ctx, col_arr); 156 157 // Parse texCoords array: [[u,v], ...] 158 JSValue tc_arr = JS_GetPropertyStr(ctx, geom, "texCoords"); 159 if (JS_IsArray(ctx, tc_arr)) { 160 JSValue len_val = JS_GetPropertyStr(ctx, tc_arr, "length"); 161 int32_t len = 0; 162 JS_ToInt32(ctx, &len, len_val); 163 JS_FreeValue(ctx, len_val); 164 165 form->tex_coords = calloc(len * 2, sizeof(float)); 166 for (int i = 0; i < len; i++) { 167 JSValue uv = JS_GetPropertyUint32(ctx, tc_arr, i); 168 for (int j = 0; j < 2; j++) { 169 JSValue comp = JS_GetPropertyUint32(ctx, uv, j); 170 double v = 0; 171 JS_ToFloat64(ctx, &v, comp); 172 form->tex_coords[i * 2 + j] = (float)v; 173 JS_FreeValue(ctx, comp); 174 } 175 JS_FreeValue(ctx, uv); 176 } 177 } 178 JS_FreeValue(ctx, tc_arr); 179 180 // Parse transform (second argument): {pos, rot, scale} 181 if (argc >= 2 && JS_IsObject(argv[1])) { 182 JSValue opts = argv[1]; 183 184 JSValue pos = JS_GetPropertyStr(ctx, opts, "pos"); 185 if (JS_IsArray(ctx, pos)) { 186 for (int i = 0; i < 3; i++) { 187 JSValue v = JS_GetPropertyUint32(ctx, pos, i); 188 double d = 0; JS_ToFloat64(ctx, &d, v); 189 form->position[i] = (float)d; 190 JS_FreeValue(ctx, v); 191 } 192 } 193 JS_FreeValue(ctx, pos); 194 195 JSValue rot = JS_GetPropertyStr(ctx, opts, "rot"); 196 if (JS_IsArray(ctx, rot)) { 197 for (int i = 0; i < 3; i++) { 198 JSValue v = JS_GetPropertyUint32(ctx, rot, i); 199 double d = 0; JS_ToFloat64(ctx, &d, v); 200 form->rotation[i] = (float)d; 201 JS_FreeValue(ctx, v); 202 } 203 } 204 JS_FreeValue(ctx, rot); 205 206 JSValue sc = JS_GetPropertyStr(ctx, opts, "scale"); 207 if (!JS_IsUndefined(sc)) { 208 double d = 1.0; JS_ToFloat64(ctx, &d, sc); 209 form->scale = (float)d; 210 } 211 JS_FreeValue(ctx, sc); 212 } 213 214 // Create JS object with opaque pointer 215 JSValue obj = JS_NewObjectClass(ctx, form_class_id); 216 JS_SetOpaque(obj, form); 217 218 // Expose position and rotation as JS arrays (live references) 219 JSValue js_pos = JS_NewArray(ctx); 220 for (int i = 0; i < 3; i++) 221 JS_SetPropertyUint32(ctx, js_pos, i, JS_NewFloat64(ctx, form->position[i])); 222 JS_SetPropertyStr(ctx, obj, "position", js_pos); 223 224 JSValue js_rot = JS_NewArray(ctx); 225 for (int i = 0; i < 3; i++) 226 JS_SetPropertyUint32(ctx, js_rot, i, JS_NewFloat64(ctx, form->rotation[i])); 227 JS_SetPropertyStr(ctx, obj, "rotation", js_rot); 228 229 JS_SetPropertyStr(ctx, obj, "noFade", JS_FALSE); 230 231 return obj; 232} 233 234// ============================================================ 235// Chain API: .form(f) renders a 3D form with current ink + camera 236// ============================================================ 237 238static JSValue js_chain_form(JSContext *ctx, JSValueConst this_val, 239 int argc, JSValueConst *argv) { 240 if (argc < 1) return JS_DupValue(ctx, this_val); 241 242 ACForm *form = JS_GetOpaque(argv[0], form_class_id); 243 if (!form) return JS_DupValue(ctx, this_val); 244 245 ACRuntime *rt = current_rt; 246 if (!rt) return JS_DupValue(ctx, this_val); 247 248 // Sync position/rotation from JS arrays back to C struct 249 JSValue js_pos = JS_GetPropertyStr(ctx, argv[0], "position"); 250 if (JS_IsArray(ctx, js_pos)) { 251 for (int i = 0; i < 3; i++) { 252 JSValue v = JS_GetPropertyUint32(ctx, js_pos, i); 253 double d = 0; JS_ToFloat64(ctx, &d, v); 254 form->position[i] = (float)d; 255 JS_FreeValue(ctx, v); 256 } 257 } 258 JS_FreeValue(ctx, js_pos); 259 260 JSValue js_rot = JS_GetPropertyStr(ctx, argv[0], "rotation"); 261 if (JS_IsArray(ctx, js_rot)) { 262 for (int i = 0; i < 3; i++) { 263 JSValue v = JS_GetPropertyUint32(ctx, js_rot, i); 264 double d = 0; JS_ToFloat64(ctx, &d, v); 265 form->rotation[i] = (float)d; 266 JS_FreeValue(ctx, v); 267 } 268 } 269 JS_FreeValue(ctx, js_rot); 270 271 // Sync noFade 272 JSValue nf = JS_GetPropertyStr(ctx, argv[0], "noFade"); 273 form->no_fade = JS_ToBool(ctx, nf); 274 JS_FreeValue(ctx, nf); 275 276 // Sync texture (painting with opaque ACFramebuffer*) 277 JSValue tex = JS_GetPropertyStr(ctx, argv[0], "texture"); 278 if (!JS_IsUndefined(tex) && !JS_IsNull(tex)) { 279 ACFramebuffer *tex_fb = JS_GetOpaque(tex, painting_class_id); 280 form->texture = tex_fb; 281 } else { 282 form->texture = NULL; 283 } 284 JS_FreeValue(ctx, tex); 285 286 // Ensure depth buffer exists 287 if (!rt->depth_buf) { 288 rt->depth_buf = depth_create(rt->graph->fb->width, rt->graph->fb->height, 289 rt->graph->fb->stride); 290 } 291 292 // Build view and projection matrices 293 mat4 view, proj; 294 camera3d_view_matrix(view, &rt->camera3d); 295 float aspect = (float)rt->graph->fb->width / (float)rt->graph->fb->height; 296 mat4_perspective(proj, 70.0f * (float)M_PI / 180.0f, aspect, 0.01f, 100.0f); 297 298 // Render 299 graph3d_render_form(rt->graph->fb, rt->depth_buf, form, 300 view, proj, rt->graph->ink, &rt->render_stats); 301 302 return JS_DupValue(ctx, this_val); 303} 304 305// Chain .ink() — sets ink color, returns chain for further chaining 306static JSValue js_chain_ink(JSContext *ctx, JSValueConst this_val, 307 int argc, JSValueConst *argv) { 308 ACGraph *g = current_rt->graph; 309 ACColor c = {255, 255, 255, 255}; 310 if (argc >= 3) { 311 int r, gr, b; 312 JS_ToInt32(ctx, &r, argv[0]); 313 JS_ToInt32(ctx, &gr, argv[1]); 314 JS_ToInt32(ctx, &b, argv[2]); 315 c.r = (uint8_t)r; c.g = (uint8_t)gr; c.b = (uint8_t)b; 316 if (argc >= 4) { int a; JS_ToInt32(ctx, &a, argv[3]); c.a = (uint8_t)a; } 317 } else if (argc == 1) { 318 if (JS_IsString(argv[0])) { 319 const char *name = JS_ToCString(ctx, argv[0]); 320 if (!resolve_color_name(name, &c)) 321 c = (ACColor){255, 255, 255, 255}; 322 JS_FreeCString(ctx, name); 323 } else { 324 int v; if (JS_ToInt32(ctx, &v, argv[0]) == 0) 325 c = (ACColor){(uint8_t)v, (uint8_t)v, (uint8_t)v, 255}; 326 } 327 } 328 graph_ink(g, c); 329 return JS_DupValue(ctx, this_val); 330} 331 332// Chain .write() — forward to existing write, return chain 333static JSValue js_write(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); 334static JSValue js_chain_write(JSContext *ctx, JSValueConst this_val, 335 int argc, JSValueConst *argv) { 336 js_write(ctx, JS_UNDEFINED, argc, argv); 337 return JS_DupValue(ctx, this_val); 338} 339 340// ============================================================ 341// penLock() — enables FPS camera mode 342// ============================================================ 343 344static JSValue js_pen_lock(JSContext *ctx, JSValueConst this_val, 345 int argc, JSValueConst *argv) { 346 (void)this_val; (void)argc; (void)argv; 347 if (current_rt) { 348 current_rt->pen_locked = 1; 349 current_rt->fps_system_active = 1; 350 camera3d_init(&current_rt->camera3d); 351 } 352 return JS_UNDEFINED; 353} 354 355// ============================================================ 356// CUBEL geometry constant (wireframe cube, 12 lines = 24 vertices) 357// ============================================================ 358 359static JSValue build_cubel(JSContext *ctx) { 360 // 12 edges of a unit cube from -1 to 1 361 static const float cube_lines[24][4] = { 362 // Bottom face 363 {-1,-1,-1,1}, { 1,-1,-1,1}, 364 { 1,-1,-1,1}, { 1,-1, 1,1}, 365 { 1,-1, 1,1}, {-1,-1, 1,1}, 366 {-1,-1, 1,1}, {-1,-1,-1,1}, 367 // Top face 368 {-1, 1,-1,1}, { 1, 1,-1,1}, 369 { 1, 1,-1,1}, { 1, 1, 1,1}, 370 { 1, 1, 1,1}, {-1, 1, 1,1}, 371 {-1, 1, 1,1}, {-1, 1,-1,1}, 372 // Vertical edges 373 {-1,-1,-1,1}, {-1, 1,-1,1}, 374 { 1,-1,-1,1}, { 1, 1,-1,1}, 375 { 1,-1, 1,1}, { 1, 1, 1,1}, 376 {-1,-1, 1,1}, {-1, 1, 1,1}, 377 }; 378 379 JSValue geom = JS_NewObject(ctx); 380 JS_SetPropertyStr(ctx, geom, "type", JS_NewString(ctx, "line")); 381 382 JSValue positions = JS_NewArray(ctx); 383 for (int i = 0; i < 24; i++) { 384 JSValue vert = JS_NewArray(ctx); 385 for (int j = 0; j < 4; j++) 386 JS_SetPropertyUint32(ctx, vert, j, JS_NewFloat64(ctx, cube_lines[i][j])); 387 JS_SetPropertyUint32(ctx, positions, i, vert); 388 } 389 JS_SetPropertyStr(ctx, geom, "positions", positions); 390 391 return geom; 392} 393 394// QUAD geometry constant (2 triangles = 6 vertices with gradient colors) 395static JSValue build_quad(JSContext *ctx) { 396 static const float quad_pos[6][4] = { 397 {-1,-1,0,1}, {-1,1,0,1}, {1,1,0,1}, // tri 1 398 {-1,-1,0,1}, {1,1,0,1}, {1,-1,0,1}, // tri 2 399 }; 400 static const float quad_col[6][4] = { 401 {1,0,0,1}, {0,1,0,1}, {0,0,1,1}, 402 {1,0,0,1}, {0,0,1,1}, {1,1,0,1}, 403 }; 404 405 JSValue geom = JS_NewObject(ctx); 406 JS_SetPropertyStr(ctx, geom, "type", JS_NewString(ctx, "triangle")); 407 408 JSValue positions = JS_NewArray(ctx); 409 JSValue colors = JS_NewArray(ctx); 410 for (int i = 0; i < 6; i++) { 411 JSValue vert = JS_NewArray(ctx); 412 JSValue col = JS_NewArray(ctx); 413 for (int j = 0; j < 4; j++) { 414 JS_SetPropertyUint32(ctx, vert, j, JS_NewFloat64(ctx, quad_pos[i][j])); 415 JS_SetPropertyUint32(ctx, col, j, JS_NewFloat64(ctx, quad_col[i][j])); 416 } 417 JS_SetPropertyUint32(ctx, positions, i, vert); 418 JS_SetPropertyUint32(ctx, colors, i, col); 419 } 420 JS_SetPropertyStr(ctx, geom, "positions", positions); 421 JS_SetPropertyStr(ctx, geom, "colors", colors); 422 423 return geom; 424} 425 426// ============================================================ 427// JS Native Functions — Graphics 428// ============================================================ 429 430// Resolve a CSS color name string to an ACColor. Returns 1 on match, 0 if unknown. 431static int resolve_color_name(const char *name, ACColor *out) { 432 if (!name) return 0; 433 if (strcmp(name, "black") == 0) { *out = (ACColor){0, 0, 0, 255}; return 1; } 434 if (strcmp(name, "white") == 0) { *out = (ACColor){255, 255, 255, 255}; return 1; } 435 if (strcmp(name, "red") == 0) { *out = (ACColor){255, 0, 0, 255}; return 1; } 436 if (strcmp(name, "green") == 0) { *out = (ACColor){0, 128, 0, 255}; return 1; } 437 if (strcmp(name, "blue") == 0) { *out = (ACColor){0, 0, 255, 255}; return 1; } 438 if (strcmp(name, "yellow") == 0) { *out = (ACColor){255, 255, 0, 255}; return 1; } 439 if (strcmp(name, "cyan") == 0) { *out = (ACColor){0, 255, 255, 255}; return 1; } 440 if (strcmp(name, "magenta") == 0) { *out = (ACColor){255, 0, 255, 255}; return 1; } 441 if (strcmp(name, "gray") == 0) { *out = (ACColor){128, 128, 128, 255}; return 1; } 442 if (strcmp(name, "grey") == 0) { *out = (ACColor){128, 128, 128, 255}; return 1; } 443 if (strcmp(name, "orange") == 0) { *out = (ACColor){255, 165, 0, 255}; return 1; } 444 if (strcmp(name, "purple") == 0) { *out = (ACColor){128, 0, 128, 255}; return 1; } 445 if (strcmp(name, "pink") == 0) { *out = (ACColor){255, 192, 203, 255}; return 1; } 446 if (strcmp(name, "lime") == 0) { *out = (ACColor){0, 255, 0, 255}; return 1; } 447 if (strcmp(name, "brown") == 0) { *out = (ACColor){139, 69, 19, 255}; return 1; } 448 return 0; 449} 450 451static JSValue js_wipe(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 452 (void)this_val; 453 ACGraph *g = current_rt->graph; 454 ACColor c = {0, 0, 0, 255}; 455 if (argc == 0) { 456 c = (ACColor){255, 255, 255, 255}; 457 } else if (argc >= 3) { 458 int r, gr, b; 459 JS_ToInt32(ctx, &r, argv[0]); 460 JS_ToInt32(ctx, &gr, argv[1]); 461 JS_ToInt32(ctx, &b, argv[2]); 462 c = (ACColor){(uint8_t)r, (uint8_t)gr, (uint8_t)b, 255}; 463 } else if (argc == 1) { 464 if (JS_IsString(argv[0])) { 465 const char *name = JS_ToCString(ctx, argv[0]); 466 if (!resolve_color_name(name, &c)) 467 c = (ACColor){0, 0, 0, 255}; // unknown name → black 468 JS_FreeCString(ctx, name); 469 } else { 470 int v; 471 if (JS_ToInt32(ctx, &v, argv[0]) == 0) { 472 c = (ACColor){(uint8_t)v, (uint8_t)v, (uint8_t)v, 255}; 473 } 474 } 475 } 476 graph_wipe(g, c); 477 478 // Clear depth buffer for new frame 479 if (current_rt && current_rt->depth_buf) 480 depth_clear(current_rt->depth_buf); 481 // Reset render stats 482 if (current_rt) 483 memset(&current_rt->render_stats, 0, sizeof(ACRenderStats)); 484 485 // Return chain object for .form()/.ink() chaining 486 JSValue global = JS_GetGlobalObject(ctx); 487 JSValue chain = JS_GetPropertyStr(ctx, global, "__paintApi"); 488 JS_FreeValue(ctx, global); 489 return chain; 490} 491 492static JSValue js_ink(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 493 (void)this_val; 494 ACGraph *g = current_rt->graph; 495 ACColor c = {255, 255, 255, 255}; 496 if (argc >= 3) { 497 int r, gr, b; 498 JS_ToInt32(ctx, &r, argv[0]); 499 JS_ToInt32(ctx, &gr, argv[1]); 500 JS_ToInt32(ctx, &b, argv[2]); 501 c.r = (uint8_t)r; c.g = (uint8_t)gr; c.b = (uint8_t)b; 502 if (argc >= 4) { 503 int a; JS_ToInt32(ctx, &a, argv[3]); 504 c.a = (uint8_t)a; 505 } 506 } else if (argc == 1) { 507 if (JS_IsString(argv[0])) { 508 const char *name = JS_ToCString(ctx, argv[0]); 509 if (!resolve_color_name(name, &c)) 510 c = (ACColor){255, 255, 255, 255}; // unknown name → white 511 JS_FreeCString(ctx, name); 512 } else { 513 int v; 514 if (JS_ToInt32(ctx, &v, argv[0]) == 0) { 515 c = (ACColor){(uint8_t)v, (uint8_t)v, (uint8_t)v, 255}; 516 } 517 } 518 } 519 graph_ink(g, c); 520 521 JSValue global = JS_GetGlobalObject(ctx); 522 JSValue paint_api = JS_GetPropertyStr(ctx, global, "__paintApi"); 523 JS_FreeValue(ctx, global); 524 return paint_api; 525} 526 527static JSValue js_plot(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 528 (void)this_val; 529 if (argc < 2) return JS_UNDEFINED; 530 int x, y; 531 JS_ToInt32(ctx, &x, argv[0]); 532 JS_ToInt32(ctx, &y, argv[1]); 533 graph_plot(current_rt->graph, x, y); 534 return JS_UNDEFINED; 535} 536 537static JSValue js_line(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 538 (void)this_val; 539 if (argc < 4) return JS_UNDEFINED; 540 int x0, y0, x1, y1; 541 JS_ToInt32(ctx, &x0, argv[0]); 542 JS_ToInt32(ctx, &y0, argv[1]); 543 JS_ToInt32(ctx, &x1, argv[2]); 544 JS_ToInt32(ctx, &y1, argv[3]); 545 if (argc >= 5) { 546 int thickness; 547 JS_ToInt32(ctx, &thickness, argv[4]); 548 graph_line_thick(current_rt->graph, x0, y0, x1, y1, thickness); 549 } else { 550 graph_line(current_rt->graph, x0, y0, x1, y1); 551 } 552 return JS_UNDEFINED; 553} 554 555static JSValue js_box(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 556 (void)this_val; 557 if (argc < 4) return JS_UNDEFINED; 558 int x, y, w, h; 559 JS_ToInt32(ctx, &x, argv[0]); 560 JS_ToInt32(ctx, &y, argv[1]); 561 JS_ToInt32(ctx, &w, argv[2]); 562 JS_ToInt32(ctx, &h, argv[3]); 563 564 int filled = 1; 565 if (argc >= 5 && JS_IsString(argv[4])) { 566 const char *mode = JS_ToCString(ctx, argv[4]); 567 if (mode && strcmp(mode, "outline") == 0) filled = 0; 568 JS_FreeCString(ctx, mode); 569 } 570 graph_box(current_rt->graph, x, y, w, h, filled); 571 572 JSValue global = JS_GetGlobalObject(ctx); 573 JSValue paint_api = JS_GetPropertyStr(ctx, global, "__paintApi"); 574 JS_FreeValue(ctx, global); 575 return paint_api; 576} 577 578static JSValue js_circle(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 579 (void)this_val; 580 if (argc < 3) return JS_UNDEFINED; 581 int cx, cy, r; 582 JS_ToInt32(ctx, &cx, argv[0]); 583 JS_ToInt32(ctx, &cy, argv[1]); 584 JS_ToInt32(ctx, &r, argv[2]); 585 int filled = (argc >= 4) ? JS_ToBool(ctx, argv[3]) : 0; 586 graph_circle(current_rt->graph, cx, cy, r, filled); 587 return JS_UNDEFINED; 588} 589 590static JSValue js_write(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 591 (void)this_val; 592 if (argc < 1) return JS_UNDEFINED; 593 const char *text = JS_ToCString(ctx, argv[0]); 594 if (!text) return JS_UNDEFINED; 595 596 int x = 0, y = 0, scale = 1; 597 598 if (argc >= 2) { 599 if (JS_IsObject(argv[1])) { 600 JSValue vx = JS_GetPropertyStr(ctx, argv[1], "x"); 601 JSValue vy = JS_GetPropertyStr(ctx, argv[1], "y"); 602 if (!JS_IsUndefined(vx)) JS_ToInt32(ctx, &x, vx); 603 if (!JS_IsUndefined(vy)) JS_ToInt32(ctx, &y, vy); 604 605 JSValue center = JS_GetPropertyStr(ctx, argv[1], "center"); 606 if (JS_IsString(center)) { 607 const char *cv = JS_ToCString(ctx, center); 608 if (cv) { 609 int tw = font_measure(text, scale); 610 if (strchr(cv, 'x')) x = (current_rt->graph->fb->width - tw) / 2; 611 if (strchr(cv, 'y')) y = (current_rt->graph->fb->height - FONT_CHAR_H * scale) / 2; 612 JS_FreeCString(ctx, cv); 613 } 614 } 615 JS_FreeValue(ctx, center); 616 617 JSValue size = JS_GetPropertyStr(ctx, argv[1], "size"); 618 if (!JS_IsUndefined(size)) JS_ToInt32(ctx, &scale, size); 619 JS_FreeValue(ctx, size); 620 621 JS_FreeValue(ctx, vx); 622 JS_FreeValue(ctx, vy); 623 } 624 } 625 626 // Check for font option: "matrix", "font_1"/"6x10", default is font_1 (6x10) 627 int font_id = FONT_6X10; 628 if (argc >= 2 && JS_IsObject(argv[1])) { 629 JSValue font_v = JS_GetPropertyStr(ctx, argv[1], "font"); 630 if (JS_IsString(font_v)) { 631 const char *fname = JS_ToCString(ctx, font_v); 632 if (fname) { 633 if (strcmp(fname, "matrix") == 0) font_id = FONT_MATRIX; 634 else if (strcmp(fname, "font_1") == 0 || strcmp(fname, "6x10") == 0) 635 font_id = FONT_6X10; 636 JS_FreeCString(ctx, fname); 637 } 638 } 639 JS_FreeValue(ctx, font_v); 640 641 // Re-check center with correct font metrics for non-8x8 fonts 642 if (font_id != FONT_8X8) { 643 JSValue center2 = JS_GetPropertyStr(ctx, argv[1], "center"); 644 if (JS_IsString(center2)) { 645 const char *cv = JS_ToCString(ctx, center2); 646 if (cv) { 647 int tw = (font_id == FONT_MATRIX) 648 ? font_measure_matrix(text, scale) 649 : font_measure_6x10(text, scale); 650 int th = (font_id == FONT_MATRIX) 651 ? 8 * scale // MatrixChunky8 ascent 652 : FONT_6X10_CHAR_H * scale; 653 if (strchr(cv, 'x')) x = (current_rt->graph->fb->width - tw) / 2; 654 if (strchr(cv, 'y')) y = (current_rt->graph->fb->height - th) / 2; 655 JS_FreeCString(ctx, cv); 656 } 657 } 658 JS_FreeValue(ctx, center2); 659 } 660 } 661 662 if (font_id == FONT_MATRIX) 663 font_draw_matrix(current_rt->graph, text, x, y, scale); 664 else if (font_id == FONT_6X10) 665 font_draw_6x10(current_rt->graph, text, x, y, scale); 666 else 667 font_draw(current_rt->graph, text, x, y, scale); 668 JS_FreeCString(ctx, text); 669 return JS_UNDEFINED; 670} 671 672static JSValue js_scroll(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 673 (void)this_val; 674 int dx = 0, dy = 0; 675 if (argc >= 1) JS_ToInt32(ctx, &dx, argv[0]); 676 if (argc >= 2) JS_ToInt32(ctx, &dy, argv[1]); 677 graph_scroll(current_rt->graph, dx, dy); 678 return JS_UNDEFINED; 679} 680 681static JSValue js_blur(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 682 (void)this_val; 683 int s = 1; 684 if (argc >= 1) JS_ToInt32(ctx, &s, argv[0]); 685 graph_blur(current_rt->graph, s); 686 return JS_UNDEFINED; 687} 688 689static JSValue js_zoom(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 690 (void)this_val; 691 double level = 1.0; 692 if (argc >= 1) JS_ToFloat64(ctx, &level, argv[0]); 693 graph_zoom(current_rt->graph, level); 694 return JS_UNDEFINED; 695} 696 697static JSValue js_contrast(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 698 (void)this_val; 699 double level = 1.0; 700 if (argc >= 1) JS_ToFloat64(ctx, &level, argv[0]); 701 graph_contrast(current_rt->graph, level); 702 return JS_UNDEFINED; 703} 704 705static JSValue js_spin(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 706 (void)this_val; 707 double angle = 0.0; 708 if (argc >= 1) JS_ToFloat64(ctx, &angle, argv[0]); 709 graph_spin(current_rt->graph, angle); 710 return JS_UNDEFINED; 711} 712 713// qr(text, x, y, scale) — render QR code at position 714static JSValue js_qr(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 715 (void)this_val; 716 if (!current_rt || argc < 3) return JS_UNDEFINED; 717 const char *text = JS_ToCString(ctx, argv[0]); 718 if (!text) return JS_UNDEFINED; 719 int x = 0, y = 0, scale = 2; 720 JS_ToInt32(ctx, &x, argv[1]); 721 JS_ToInt32(ctx, &y, argv[2]); 722 if (argc >= 4) JS_ToInt32(ctx, &scale, argv[3]); 723 graph_qr(current_rt->graph, text, x, y, scale); 724 JS_FreeCString(ctx, text); 725 return JS_UNDEFINED; 726} 727 728// ============================================================ 729// JS Native Functions — System 730// ============================================================ 731 732static JSValue js_console_log(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 733 (void)this_val; 734 // Build message string, then log via ac_log (writes to both stderr + log file) 735 char buf[512]; 736 int pos = 0; 737 for (int i = 0; i < argc && pos < (int)sizeof(buf) - 2; i++) { 738 const char *s = JS_ToCString(ctx, argv[i]); 739 if (s) { 740 int n = snprintf(buf + pos, sizeof(buf) - pos, "%s%s", i ? " " : "", s); 741 if (n > 0) pos += n; 742 JS_FreeCString(ctx, s); 743 } 744 } 745 buf[pos] = '\0'; 746 ac_log("[js] %s\n", buf); 747 return JS_UNDEFINED; 748} 749 750static JSValue js_performance_now(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 751 (void)this_val; (void)argc; (void)argv; 752 struct timespec ts; 753 clock_gettime(CLOCK_MONOTONIC, &ts); 754 double ms = ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; 755 return JS_NewFloat64(ctx, ms); 756} 757 758// No-op function for stubs 759static JSValue js_noop(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 760 (void)ctx; (void)this_val; (void)argc; (void)argv; 761 return JS_UNDEFINED; 762} 763 764// clock.time() — returns a JS Date object with the current system time 765static JSValue js_clock_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 766 (void)this_val; (void)argc; (void)argv; 767 struct timespec ts; 768 clock_gettime(CLOCK_REALTIME, &ts); 769 double ms = (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1000000.0; 770 JSValue global = JS_GetGlobalObject(ctx); 771 JSValue date_ctor = JS_GetPropertyStr(ctx, global, "Date"); 772 JSValue ms_val = JS_NewFloat64(ctx, ms); 773 JSValue date = JS_CallConstructor(ctx, date_ctor, 1, &ms_val); 774 JS_FreeValue(ctx, ms_val); 775 JS_FreeValue(ctx, date_ctor); 776 JS_FreeValue(ctx, global); 777 return date; 778} 779 780// Returns a resolved promise with null 781static JSValue js_promise_null(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 782 (void)this_val; (void)argc; (void)argv; 783 // Use Promise.resolve(null) 784 JSValue global = JS_GetGlobalObject(ctx); 785 JSValue promise = JS_GetPropertyStr(ctx, global, "Promise"); 786 JSValue resolve = JS_GetPropertyStr(ctx, promise, "resolve"); 787 JSValue null_val = JS_NULL; 788 JSValue result = JS_Call(ctx, resolve, promise, 1, &null_val); 789 JS_FreeValue(ctx, resolve); 790 JS_FreeValue(ctx, promise); 791 JS_FreeValue(ctx, global); 792 return result; 793} 794 795// Returns a function that returns a no-op object with send() 796static JSValue js_net_udp(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 797 (void)this_val; (void)argc; (void)argv; 798 JSValue obj = JS_NewObject(ctx); 799 JS_SetPropertyStr(ctx, obj, "send", JS_NewCFunction(ctx, js_noop, "send", 2)); 800 return obj; 801} 802 803// ============================================================ 804// Sound Bindings 805// ============================================================ 806 807static WaveType parse_wave_type(const char *type) { 808 if (!type) return WAVE_SINE; 809 if (strcmp(type, "sine") == 0) return WAVE_SINE; 810 if (strcmp(type, "triangle") == 0) return WAVE_TRIANGLE; 811 if (strcmp(type, "sawtooth") == 0) return WAVE_SAWTOOTH; 812 if (strcmp(type, "square") == 0) return WAVE_SQUARE; 813 if (strcmp(type, "noise-white") == 0 || strcmp(type, "noise") == 0) return WAVE_NOISE; 814 if (strcmp(type, "whistle") == 0 || strcmp(type, "ocarina") == 0 || 815 strcmp(type, "flute") == 0 || strcmp(type, "skullwhistle") == 0 || 816 strcmp(type, "skull-whistle") == 0) return WAVE_WHISTLE; 817 if (strncmp(type, "gun", 3) == 0) return WAVE_GUN; 818 // composite → treat as sine for now 819 return WAVE_SINE; 820} 821 822// Parse the preset suffix for a gun-* wave type string. Optional model 823// suffix `/classic` or `/physical` overrides the preset's default model. 824// Examples: "gun-pistol", "gun-pistol/classic", "gun-pistol/physical". 825// Returns GUN_PISTOL on parse failure. Pass `out_force_model` to receive 826// the override (-1 = use preset default, 0 = CLASSIC, 1 = PHYSICAL). 827static GunPreset parse_gun_preset(const char *type, int *out_force_model) { 828 if (out_force_model) *out_force_model = -1; 829 if (!type) return GUN_PISTOL; 830 if (strncmp(type, "gun", 3) != 0) return GUN_PISTOL; 831 const char *p = type + 3; 832 if (*p == '-' || *p == '_') p++; 833 if (!*p) return GUN_PISTOL; 834 835 // Split off /classic or /physical suffix into a temp buffer. 836 char name_buf[32]; 837 const char *slash = strchr(p, '/'); 838 if (slash && out_force_model) { 839 size_t n = (size_t)(slash - p); 840 if (n >= sizeof(name_buf)) n = sizeof(name_buf) - 1; 841 memcpy(name_buf, p, n); 842 name_buf[n] = '\0'; 843 p = name_buf; 844 const char *m = slash + 1; 845 if (strcmp(m, "classic") == 0) *out_force_model = 0; 846 else if (strcmp(m, "physical") == 0 || strcmp(m, "phys") == 0) *out_force_model = 1; 847 } 848 849 if (strcmp(p, "pistol") == 0) return GUN_PISTOL; 850 if (strcmp(p, "rifle") == 0) return GUN_RIFLE; 851 if (strcmp(p, "shotgun") == 0) return GUN_SHOTGUN; 852 if (strcmp(p, "smg") == 0) return GUN_SMG; 853 if (strcmp(p, "suppressed") == 0 || strcmp(p, "silenced") == 0) return GUN_SUPPRESSED; 854 if (strcmp(p, "lmg") == 0 || strcmp(p, "mg") == 0) return GUN_LMG; 855 if (strcmp(p, "sniper") == 0) return GUN_SNIPER; 856 if (strcmp(p, "grenade") == 0) return GUN_GRENADE; 857 if (strcmp(p, "rpg") == 0 || strcmp(p, "rocket") == 0) return GUN_RPG; 858 if (strcmp(p, "reload") == 0) return GUN_RELOAD; 859 if (strcmp(p, "cock") == 0 || strcmp(p, "bolt") == 0) return GUN_COCK; 860 if (strcmp(p, "ricochet") == 0 || strcmp(p, "pew") == 0) return GUN_RICOCHET; 861 return GUN_PISTOL; 862} 863 864// synthObj.kill(fade) — method on synth return object 865static JSValue js_synth_obj_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 866 ACAudio *audio = current_rt->audio; 867 if (!audio) return JS_UNDEFINED; 868 869 // Get id from 'this' object 870 JSValue id_v = JS_GetPropertyStr(ctx, this_val, "id"); 871 double id_d = 0; 872 if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); 873 JS_FreeValue(ctx, id_v); 874 uint64_t id = (uint64_t)id_d; 875 if (id == 0) return JS_UNDEFINED; 876 877 double fade = 0.025; 878 if (argc >= 1 && JS_IsNumber(argv[0])) { 879 JS_ToFloat64(ctx, &fade, argv[0]); 880 } 881 audio_kill(audio, id, fade); 882 return JS_UNDEFINED; 883} 884 885// synthObj.update({volume, tone, pan}) — method on synth return object 886static JSValue js_synth_obj_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 887 ACAudio *audio = current_rt->audio; 888 if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED; 889 890 // Get id from 'this' object 891 JSValue id_v = JS_GetPropertyStr(ctx, this_val, "id"); 892 double id_d = 0; 893 if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); 894 JS_FreeValue(ctx, id_v); 895 uint64_t id = (uint64_t)id_d; 896 if (id == 0) return JS_UNDEFINED; 897 898 double freq = -1, vol = -1, pan = -3; 899 JSValue v; 900 v = JS_GetPropertyStr(ctx, argv[0], "tone"); 901 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); 902 JS_FreeValue(ctx, v); 903 v = JS_GetPropertyStr(ctx, argv[0], "volume"); 904 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &vol, v); 905 JS_FreeValue(ctx, v); 906 v = JS_GetPropertyStr(ctx, argv[0], "pan"); 907 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); 908 JS_FreeValue(ctx, v); 909 910 audio_update(audio, id, freq, vol, pan); 911 return JS_UNDEFINED; 912} 913 914// sound.synth({type, tone, duration, volume, attack, decay, pan}) 915static JSValue js_synth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 916 (void)this_val; 917 ACAudio *audio = current_rt->audio; 918 if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED; 919 920 JSValue opts = argv[0]; 921 922 // Parse type. For "gun-*" strings we also extract the preset so the 923 // DWG synth can branch to audio_synth_gun below. Optional "/classic" 924 // or "/physical" suffix overrides the preset's default model. 925 WaveType wt = WAVE_SINE; 926 GunPreset gun_preset = GUN_PISTOL; 927 int gun_force_model = -1; 928 int is_gun = 0; 929 JSValue type_v = JS_GetPropertyStr(ctx, opts, "type"); 930 if (JS_IsString(type_v)) { 931 const char *ts = JS_ToCString(ctx, type_v); 932 wt = parse_wave_type(ts); 933 if (wt == WAVE_GUN) { 934 gun_preset = parse_gun_preset(ts, &gun_force_model); 935 is_gun = 1; 936 } 937 JS_FreeCString(ctx, ts); 938 } 939 JS_FreeValue(ctx, type_v); 940 941 // Parse tone (can be number or string) 942 double freq = 440.0; 943 JSValue tone_v = JS_GetPropertyStr(ctx, opts, "tone"); 944 if (JS_IsNumber(tone_v)) { 945 JS_ToFloat64(ctx, &freq, tone_v); 946 } else if (JS_IsString(tone_v)) { 947 const char *ts = JS_ToCString(ctx, tone_v); 948 freq = audio_note_to_freq(ts); 949 JS_FreeCString(ctx, ts); 950 } 951 JS_FreeValue(ctx, tone_v); 952 953 // Parse duration (number or "🔁" for infinity) 954 double duration = 0.1; 955 JSValue dur_v = JS_GetPropertyStr(ctx, opts, "duration"); 956 if (JS_IsNumber(dur_v)) { 957 JS_ToFloat64(ctx, &duration, dur_v); 958 } else if (JS_IsString(dur_v)) { 959 duration = INFINITY; 960 } 961 JS_FreeValue(ctx, dur_v); 962 963 // Parse volume, attack, decay, pan 964 double volume = 1.0, attack = 0.005, decay = 0.1, pan = 0.0; 965 JSValue v; 966 967 v = JS_GetPropertyStr(ctx, opts, "volume"); 968 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &volume, v); 969 JS_FreeValue(ctx, v); 970 971 v = JS_GetPropertyStr(ctx, opts, "attack"); 972 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &attack, v); 973 JS_FreeValue(ctx, v); 974 975 v = JS_GetPropertyStr(ctx, opts, "decay"); 976 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &decay, v); 977 JS_FreeValue(ctx, v); 978 979 v = JS_GetPropertyStr(ctx, opts, "pan"); 980 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); 981 JS_FreeValue(ctx, v); 982 983 // Create voice. Guns take a separate path so we can seed per-preset 984 // DWG parameters (bore length, body modes, etc). 985 uint64_t id; 986 if (is_gun) { 987 // Let `tone` (1.0 default) scale the combustion pressure so pads 988 // can express accent via velocity. A JS caller passing tone=1.0 989 // = unity pressure from the preset. 990 double pressure_scale = freq > 0 && freq < 5.0 ? freq : 1.0; 991 id = audio_synth_gun(audio, gun_preset, duration, volume, attack, 992 decay, pan, pressure_scale, gun_force_model); 993 fprintf(stderr, "[synth] gun preset=%d model=%d vol=%.2f dur=%.1f id=%lu\n", 994 gun_preset, gun_force_model, volume, duration, (unsigned long)id); 995 ac_log("[synth] gun preset=%d model=%d vol=%.2f dur=%.1f id=%lu\n", 996 gun_preset, gun_force_model, volume, duration, (unsigned long)id); 997 } else { 998 id = audio_synth(audio, wt, freq, duration, volume, attack, decay, pan); 999 fprintf(stderr, "[synth] type=%d freq=%.1f vol=%.2f dur=%.1f id=%lu\n", 1000 wt, freq, volume, duration, (unsigned long)id); 1001 ac_log("[synth] type=%d freq=%.1f vol=%.2f dur=%.1f id=%lu\n", 1002 wt, freq, volume, duration, (unsigned long)id); 1003 } 1004 1005 // For gun voices, an optional `params` object on opts passes per-shot 1006 // overrides into the C-side synth state (drag-to-edit on inspector 1007 // cards). Each numeric property maps to a gun_presets[] field key. 1008 // Applied immediately after synth init so the next audio thread tick 1009 // sees the patched values. 1010 if (is_gun && id) { 1011 JSValue params_v = JS_GetPropertyStr(ctx, opts, "params"); 1012 if (JS_IsObject(params_v)) { 1013 JSPropertyEnum *props = NULL; 1014 uint32_t plen = 0; 1015 if (JS_GetOwnPropertyNames(ctx, &props, &plen, params_v, 1016 JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { 1017 for (uint32_t i = 0; i < plen; i++) { 1018 const char *k = JS_AtomToCString(ctx, props[i].atom); 1019 if (k) { 1020 JSValue pv = JS_GetProperty(ctx, params_v, props[i].atom); 1021 double pd; 1022 if (JS_IsNumber(pv) && JS_ToFloat64(ctx, &pd, pv) == 0) { 1023 audio_gun_voice_set_param(audio, id, k, pd); 1024 } 1025 JS_FreeValue(ctx, pv); 1026 JS_FreeCString(ctx, k); 1027 } 1028 JS_FreeAtom(ctx, props[i].atom); 1029 } 1030 js_free(ctx, props); 1031 } 1032 } 1033 JS_FreeValue(ctx, params_v); 1034 } 1035 1036 // Return sound object with kill(), update(), startedAt 1037 JSValue snd = JS_NewObject(ctx); 1038 JS_SetPropertyStr(ctx, snd, "id", JS_NewFloat64(ctx, (double)id)); 1039 JS_SetPropertyStr(ctx, snd, "startedAt", JS_NewFloat64(ctx, audio->time)); 1040 1041 // kill(fade) and update({volume,tone,pan}) methods 1042 JS_SetPropertyStr(ctx, snd, "kill", JS_NewCFunction(ctx, js_synth_obj_kill, "kill", 1)); 1043 JS_SetPropertyStr(ctx, snd, "update", JS_NewCFunction(ctx, js_synth_obj_update, "update", 1)); 1044 1045 return snd; 1046} 1047 1048// sound.kill(idOrObj, fade) — accepts raw id or synth/sample/replay object 1049static JSValue js_sound_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1050 (void)this_val; 1051 ACAudio *audio = current_rt->audio; 1052 if (!audio || argc < 1) return JS_UNDEFINED; 1053 1054 double id_d = 0; 1055 // Accept either a number (raw id) or an object with .id property 1056 if (JS_IsNumber(argv[0])) { 1057 JS_ToFloat64(ctx, &id_d, argv[0]); 1058 } else if (JS_IsObject(argv[0])) { 1059 JSValue id_v = JS_GetPropertyStr(ctx, argv[0], "id"); 1060 if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); 1061 JS_FreeValue(ctx, id_v); 1062 } 1063 uint64_t id = (uint64_t)id_d; 1064 1065 double fade = 0.025; 1066 if (argc >= 2 && JS_IsNumber(argv[1])) { 1067 JS_ToFloat64(ctx, &fade, argv[1]); 1068 } 1069 1070 // Check if it's a sample voice 1071 int is_sample = 0; 1072 int is_replay = 0; 1073 if (JS_IsObject(argv[0])) { 1074 JSValue is_v = JS_GetPropertyStr(ctx, argv[0], "isSample"); 1075 is_sample = JS_ToBool(ctx, is_v); 1076 JS_FreeValue(ctx, is_v); 1077 is_v = JS_GetPropertyStr(ctx, argv[0], "isReplay"); 1078 is_replay = JS_ToBool(ctx, is_v); 1079 JS_FreeValue(ctx, is_v); 1080 } 1081 1082 if (is_replay) { 1083 audio_replay_kill(audio, id, fade); 1084 } else if (is_sample) { 1085 audio_sample_kill(audio, id, fade); 1086 } else { 1087 audio_kill(audio, id, fade); 1088 } 1089 return JS_UNDEFINED; 1090} 1091 1092// sound.update(id, {tone, volume, pan}) 1093static JSValue js_sound_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1094 (void)this_val; 1095 ACAudio *audio = current_rt->audio; 1096 if (!audio || argc < 2) return JS_UNDEFINED; 1097 1098 double id_d; 1099 JS_ToFloat64(ctx, &id_d, argv[0]); 1100 uint64_t id = (uint64_t)id_d; 1101 1102 double freq = -1, vol = -1, pan = -3; 1103 if (JS_IsObject(argv[1])) { 1104 JSValue v; 1105 v = JS_GetPropertyStr(ctx, argv[1], "tone"); 1106 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); 1107 JS_FreeValue(ctx, v); 1108 1109 v = JS_GetPropertyStr(ctx, argv[1], "volume"); 1110 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &vol, v); 1111 JS_FreeValue(ctx, v); 1112 1113 v = JS_GetPropertyStr(ctx, argv[1], "pan"); 1114 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); 1115 JS_FreeValue(ctx, v); 1116 } 1117 1118 audio_update(audio, id, freq, vol, pan); 1119 return JS_UNDEFINED; 1120} 1121 1122// sound.freq(note) — convert note to Hz 1123static JSValue js_sound_freq(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1124 (void)this_val; 1125 if (argc < 1) return JS_NewFloat64(ctx, 440.0); 1126 1127 double f; 1128 if (JS_IsNumber(argv[0])) { 1129 JS_ToFloat64(ctx, &f, argv[0]); 1130 return JS_NewFloat64(ctx, f); 1131 } 1132 1133 const char *note = JS_ToCString(ctx, argv[0]); 1134 f = audio_note_to_freq(note); 1135 JS_FreeCString(ctx, note); 1136 return JS_NewFloat64(ctx, f); 1137} 1138 1139// sound.room.toggle() 1140static JSValue js_room_toggle(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1141 (void)this_val; (void)argc; (void)argv; 1142 if (current_rt->audio) audio_room_toggle(current_rt->audio); 1143 return JS_UNDEFINED; 1144} 1145 1146// sound.room.setMix(value) 1147static JSValue js_set_room_mix(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1148 (void)this_val; 1149 if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; 1150 double v; 1151 JS_ToFloat64(ctx, &v, argv[0]); 1152 audio_set_room_mix(current_rt->audio, (float)v); 1153 return JS_UNDEFINED; 1154} 1155 1156// sound.fx.setMix(value) — dry/wet for entire FX chain 1157static JSValue js_set_fx_mix(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1158 (void)this_val; 1159 if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; 1160 double v; 1161 JS_ToFloat64(ctx, &v, argv[0]); 1162 audio_set_fx_mix(current_rt->audio, (float)v); 1163 return JS_UNDEFINED; 1164} 1165 1166// sound.glitch.toggle() 1167static JSValue js_glitch_toggle(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1168 (void)this_val; (void)argc; (void)argv; 1169 if (current_rt->audio) audio_glitch_toggle(current_rt->audio); 1170 return JS_UNDEFINED; 1171} 1172 1173// sound.glitch.setMix(value) 1174static JSValue js_set_glitch_mix(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1175 (void)this_val; 1176 if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; 1177 double v; 1178 JS_ToFloat64(ctx, &v, argv[0]); 1179 audio_set_glitch_mix(current_rt->audio, (float)v); 1180 return JS_UNDEFINED; 1181} 1182 1183// sound.microphone.open() — open device + start hot-mic thread 1184static JSValue js_mic_open(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1185 (void)this_val; (void)argc; (void)argv; 1186 if (!current_rt->audio) return JS_FALSE; 1187 int ok = audio_mic_open(current_rt->audio); 1188 ac_log("[js][mic] open -> %s\n", ok == 0 ? "ok" : "fail"); 1189 return JS_NewBool(ctx, ok == 0); 1190} 1191 1192// sound.microphone.close() — stop hot-mic thread + close device 1193static JSValue js_mic_close(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1194 (void)this_val; (void)argc; (void)argv; 1195 if (!current_rt->audio) return JS_UNDEFINED; 1196 audio_mic_close(current_rt->audio); 1197 ac_log("[js][mic] close\n"); 1198 return JS_UNDEFINED; 1199} 1200 1201// sound.microphone.rec() — start buffering (instant, device already open) 1202static JSValue js_mic_rec(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1203 (void)this_val; (void)argc; (void)argv; 1204 if (!current_rt->audio) return JS_FALSE; 1205 int ok = audio_mic_start(current_rt->audio); 1206 ac_log("[js][mic] rec -> %s\n", ok == 0 ? "ok" : "fail"); 1207 return JS_NewBool(ctx, ok == 0); 1208} 1209 1210// sound.microphone.cut() — stop buffering, return sample length 1211static JSValue js_mic_cut(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1212 (void)this_val; (void)argc; (void)argv; 1213 if (!current_rt->audio) return JS_NewInt32(ctx, 0); 1214 int len = audio_mic_stop(current_rt->audio); 1215 ac_log("[js][mic] cut -> len=%d rate=%u err=%s\n", 1216 len, 1217 current_rt->audio->sample_rate, 1218 current_rt->audio->mic_last_error[0] ? current_rt->audio->mic_last_error : "(none)"); 1219 // Persist sample to disk for next boot 1220 if (len > 0) { 1221 int saved = audio_sample_save(current_rt->audio, "/mnt/ac-sample.raw"); 1222 ac_log("[js][mic] sample saved to /mnt/ac-sample.raw (%d samples)\n", saved); 1223 } 1224 return JS_NewInt32(ctx, len); 1225} 1226 1227// sound.microphone.recording — check if currently recording 1228static JSValue js_mic_recording(JSContext *ctx, JSValueConst this_val) { 1229 if (!current_rt->audio) return JS_FALSE; 1230 return JS_NewBool(ctx, current_rt->audio->recording); 1231} 1232 1233// sound.microphone.sampleLength — length of recorded sample 1234static JSValue js_mic_sample_length(JSContext *ctx, JSValueConst this_val) { 1235 if (!current_rt->audio) return JS_NewInt32(ctx, 0); 1236 return JS_NewInt32(ctx, current_rt->audio->sample_len); 1237} 1238 1239// sound.microphone.sampleRate — capture rate of recorded sample 1240static JSValue js_mic_sample_rate(JSContext *ctx, JSValueConst this_val) { 1241 if (!current_rt->audio) return JS_NewInt32(ctx, 0); 1242 return JS_NewInt32(ctx, current_rt->audio->sample_rate); 1243} 1244 1245// sampleObj.update({tone, base, volume, pan}) — update a playing sample voice 1246static JSValue js_sample_obj_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1247 ACAudio *audio = current_rt->audio; 1248 if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED; 1249 1250 JSValue id_v = JS_GetPropertyStr(ctx, this_val, "id"); 1251 double id_d = 0; 1252 if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); 1253 JS_FreeValue(ctx, id_v); 1254 uint64_t id = (uint64_t)id_d; 1255 if (id == 0) return JS_UNDEFINED; 1256 1257 double freq = -1, base_freq = -1, vol = -1, pan = -3; 1258 JSValue v; 1259 v = JS_GetPropertyStr(ctx, argv[0], "tone"); 1260 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); 1261 JS_FreeValue(ctx, v); 1262 v = JS_GetPropertyStr(ctx, argv[0], "base"); 1263 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &base_freq, v); 1264 JS_FreeValue(ctx, v); 1265 v = JS_GetPropertyStr(ctx, argv[0], "volume"); 1266 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &vol, v); 1267 JS_FreeValue(ctx, v); 1268 v = JS_GetPropertyStr(ctx, argv[0], "pan"); 1269 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); 1270 JS_FreeValue(ctx, v); 1271 1272 if (freq > 0 && base_freq < 0) base_freq = 261.63; // default C4 1273 audio_sample_update(audio, id, freq, base_freq, vol, pan); 1274 return JS_UNDEFINED; 1275} 1276 1277// replayObj.update({tone, base, volume, pan}) — update the dedicated replay voice 1278static JSValue js_replay_obj_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1279 ACAudio *audio = current_rt->audio; 1280 if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED; 1281 1282 JSValue id_v = JS_GetPropertyStr(ctx, this_val, "id"); 1283 double id_d = 0; 1284 if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); 1285 JS_FreeValue(ctx, id_v); 1286 uint64_t id = (uint64_t)id_d; 1287 if (id == 0) return JS_UNDEFINED; 1288 1289 double freq = -1, base_freq = -1, vol = -1, pan = -3; 1290 JSValue v; 1291 v = JS_GetPropertyStr(ctx, argv[0], "tone"); 1292 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); 1293 JS_FreeValue(ctx, v); 1294 v = JS_GetPropertyStr(ctx, argv[0], "base"); 1295 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &base_freq, v); 1296 JS_FreeValue(ctx, v); 1297 v = JS_GetPropertyStr(ctx, argv[0], "volume"); 1298 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &vol, v); 1299 JS_FreeValue(ctx, v); 1300 v = JS_GetPropertyStr(ctx, argv[0], "pan"); 1301 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); 1302 JS_FreeValue(ctx, v); 1303 1304 if (freq > 0 && base_freq < 0) base_freq = 261.63; 1305 audio_replay_update(audio, id, freq, base_freq, vol, pan); 1306 return JS_UNDEFINED; 1307} 1308 1309// sound.sample.play({tone, base, volume, pan}) — play recorded sample at pitch 1310static JSValue js_sample_play(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1311 (void)this_val; 1312 ACAudio *audio = current_rt->audio; 1313 if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED; 1314 1315 JSValue opts = argv[0]; 1316 double freq = 261.63, base_freq = 261.63, volume = 0.7, pan = 0.0; 1317 JSValue v; 1318 1319 v = JS_GetPropertyStr(ctx, opts, "tone"); 1320 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); 1321 JS_FreeValue(ctx, v); 1322 1323 v = JS_GetPropertyStr(ctx, opts, "base"); 1324 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &base_freq, v); 1325 JS_FreeValue(ctx, v); 1326 1327 v = JS_GetPropertyStr(ctx, opts, "volume"); 1328 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &volume, v); 1329 JS_FreeValue(ctx, v); 1330 1331 v = JS_GetPropertyStr(ctx, opts, "pan"); 1332 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); 1333 JS_FreeValue(ctx, v); 1334 1335 int loop = 0; 1336 v = JS_GetPropertyStr(ctx, opts, "loop"); 1337 if (JS_IsBool(v)) loop = JS_ToBool(ctx, v); 1338 JS_FreeValue(ctx, v); 1339 1340 uint64_t id = audio_sample_play(audio, freq, base_freq, volume, pan, loop); 1341 if (id == 0) { 1342 ac_log("[sample] play FAILED (sample_len=%d)\n", audio->sample_len); 1343 return JS_UNDEFINED; 1344 } 1345 ac_log("[sample] play OK id=%llu freq=%.1f vol=%.2f\n", (unsigned long long)id, freq, volume); 1346 1347 // Return object with id, update(), isSample 1348 JSValue snd = JS_NewObject(ctx); 1349 JS_SetPropertyStr(ctx, snd, "id", JS_NewFloat64(ctx, (double)id)); 1350 JS_SetPropertyStr(ctx, snd, "isSample", JS_TRUE); 1351 JS_SetPropertyStr(ctx, snd, "update", JS_NewCFunction(ctx, js_sample_obj_update, "update", 1)); 1352 return snd; 1353} 1354 1355// sound.replay.play({tone, base, volume, pan}) — play dedicated global replay buffer 1356static JSValue js_replay_play(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1357 (void)this_val; 1358 ACAudio *audio = current_rt->audio; 1359 if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED; 1360 1361 JSValue opts = argv[0]; 1362 double freq = 261.63, base_freq = 261.63, volume = 0.7, pan = 0.0; 1363 JSValue v; 1364 1365 v = JS_GetPropertyStr(ctx, opts, "tone"); 1366 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); 1367 JS_FreeValue(ctx, v); 1368 1369 v = JS_GetPropertyStr(ctx, opts, "base"); 1370 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &base_freq, v); 1371 JS_FreeValue(ctx, v); 1372 1373 v = JS_GetPropertyStr(ctx, opts, "volume"); 1374 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &volume, v); 1375 JS_FreeValue(ctx, v); 1376 1377 v = JS_GetPropertyStr(ctx, opts, "pan"); 1378 if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); 1379 JS_FreeValue(ctx, v); 1380 1381 int loop = 0; 1382 v = JS_GetPropertyStr(ctx, opts, "loop"); 1383 if (JS_IsBool(v)) loop = JS_ToBool(ctx, v); 1384 JS_FreeValue(ctx, v); 1385 1386 uint64_t id = audio_replay_play(audio, freq, base_freq, volume, pan, loop); 1387 if (id == 0) return JS_UNDEFINED; 1388 1389 JSValue snd = JS_NewObject(ctx); 1390 JS_SetPropertyStr(ctx, snd, "id", JS_NewFloat64(ctx, (double)id)); 1391 JS_SetPropertyStr(ctx, snd, "isReplay", JS_TRUE); 1392 JS_SetPropertyStr(ctx, snd, "update", JS_NewCFunction(ctx, js_replay_obj_update, "update", 1)); 1393 return snd; 1394} 1395 1396// sound.sample.kill(idOrObj, fade) 1397static JSValue js_sample_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1398 (void)this_val; 1399 ACAudio *audio = current_rt->audio; 1400 if (!audio || argc < 1) return JS_UNDEFINED; 1401 1402 double id_d = 0; 1403 if (JS_IsNumber(argv[0])) { 1404 JS_ToFloat64(ctx, &id_d, argv[0]); 1405 } else if (JS_IsObject(argv[0])) { 1406 JSValue id_v = JS_GetPropertyStr(ctx, argv[0], "id"); 1407 if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); 1408 JS_FreeValue(ctx, id_v); 1409 } 1410 1411 double fade = 0.02; 1412 if (argc >= 2 && JS_IsNumber(argv[1])) JS_ToFloat64(ctx, &fade, argv[1]); 1413 1414 audio_sample_kill(audio, (uint64_t)id_d, fade); 1415 return JS_UNDEFINED; 1416} 1417 1418// sound.replay.kill(idOrObj, fade) 1419static JSValue js_replay_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1420 (void)this_val; 1421 ACAudio *audio = current_rt->audio; 1422 if (!audio || argc < 1) return JS_UNDEFINED; 1423 1424 double id_d = 0; 1425 if (JS_IsNumber(argv[0])) { 1426 JS_ToFloat64(ctx, &id_d, argv[0]); 1427 } else if (JS_IsObject(argv[0])) { 1428 JSValue id_v = JS_GetPropertyStr(ctx, argv[0], "id"); 1429 if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); 1430 JS_FreeValue(ctx, id_v); 1431 } 1432 1433 double fade = 0.02; 1434 if (argc >= 2 && JS_IsNumber(argv[1])) JS_ToFloat64(ctx, &fade, argv[1]); 1435 1436 audio_replay_kill(audio, (uint64_t)id_d, fade); 1437 return JS_UNDEFINED; 1438} 1439 1440// sound.sample.getData() — returns Float32Array of current sample buffer 1441// Proper free callback for JS_NewArrayBuffer (3-arg signature, not plain free) 1442static void js_free_array_buffer(JSRuntime *rt, void *opaque, void *ptr) { 1443 (void)rt; (void)opaque; 1444 free(ptr); 1445} 1446 1447static JSValue js_sample_get_data(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1448 (void)this_val; (void)argc; (void)argv; 1449 ACAudio *audio = current_rt ? current_rt->audio : NULL; 1450 if (!audio || !audio->sample_buf || audio->sample_len <= 0) return JS_UNDEFINED; 1451 1452 // Lock to prevent sample_buf pointer swap during copy 1453 pthread_mutex_lock(&audio->lock); 1454 int len = audio->sample_len; 1455 if (len > audio->sample_max_len) len = audio->sample_max_len; 1456 if (len <= 0) { pthread_mutex_unlock(&audio->lock); return JS_UNDEFINED; } 1457 1458 size_t byte_len = (size_t)len * sizeof(float); 1459 float *copy = malloc(byte_len); 1460 if (!copy) { pthread_mutex_unlock(&audio->lock); return JS_UNDEFINED; } 1461 memcpy(copy, audio->sample_buf, byte_len); 1462 pthread_mutex_unlock(&audio->lock); 1463 1464 // Create ArrayBuffer from our copy 1465 JSValue ab = JS_NewArrayBuffer(ctx, (uint8_t *)copy, byte_len, 1466 js_free_array_buffer, NULL, 0); 1467 if (JS_IsException(ab)) { free(copy); return JS_UNDEFINED; } 1468 1469 // Create Float32Array view 1470 JSValue global = JS_GetGlobalObject(ctx); 1471 JSValue ctor = JS_GetPropertyStr(ctx, global, "Float32Array"); 1472 JSValue f32 = JS_CallConstructor(ctx, ctor, 1, &ab); 1473 JS_FreeValue(ctx, ctor); 1474 JS_FreeValue(ctx, global); 1475 JS_FreeValue(ctx, ab); 1476 return f32; 1477} 1478 1479// sound.sample.saveTo(path) — save current sample to a file, returns sample count or -1 1480static JSValue js_sample_save_to(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1481 (void)this_val; 1482 if (!current_rt || !current_rt->audio || argc < 1) return JS_NewInt32(ctx, -1); 1483 const char *path = JS_ToCString(ctx, argv[0]); 1484 if (!path) return JS_NewInt32(ctx, -1); 1485 int result = audio_sample_save(current_rt->audio, path); 1486 ac_log("[sample] saveTo(%s) -> %d samples\n", path, result); 1487 JS_FreeCString(ctx, path); 1488 return JS_NewInt32(ctx, result); 1489} 1490 1491// sound.sample.loadFrom(path) — load sample from a file, returns sample count or -1 1492static JSValue js_sample_load_from(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1493 (void)this_val; 1494 if (!current_rt || !current_rt->audio || argc < 1) return JS_NewInt32(ctx, -1); 1495 const char *path = JS_ToCString(ctx, argv[0]); 1496 if (!path) return JS_NewInt32(ctx, -1); 1497 int result = audio_sample_load(current_rt->audio, path); 1498 ac_log("[sample] loadFrom(%s) -> %d samples\n", path, result); 1499 JS_FreeCString(ctx, path); 1500 return JS_NewInt32(ctx, result); 1501} 1502 1503// sound.sample.loadData(float32array, rate) — load sample data from JS array 1504static JSValue js_sample_load_data(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1505 (void)this_val; 1506 if (!current_rt) return JS_FALSE; 1507 ACAudio *audio = current_rt->audio; 1508 if (!audio || argc < 1) return JS_FALSE; 1509 1510 size_t byte_len = 0; 1511 size_t byte_off = 0; 1512 size_t bytes_per = 0; 1513 JSValue ab = JS_GetTypedArrayBuffer(ctx, argv[0], &byte_off, &byte_len, &bytes_per); 1514 if (JS_IsException(ab)) return JS_FALSE; 1515 1516 size_t ab_len = 0; 1517 uint8_t *ptr = JS_GetArrayBuffer(ctx, &ab_len, ab); 1518 if (!ptr) { JS_FreeValue(ctx, ab); return JS_FALSE; } 1519 1520 float *data = (float *)(ptr + byte_off); 1521 int len = (int)(byte_len / sizeof(float)); 1522 ac_log("[sample] loadData: byte_len=%zu byte_off=%zu bytes_per=%zu ab_len=%zu len=%d\n", 1523 byte_len, byte_off, bytes_per, ab_len, len); 1524 1525 unsigned int rate = 48000; 1526 if (argc >= 2 && JS_IsNumber(argv[1])) { 1527 double r; JS_ToFloat64(ctx, &r, argv[1]); 1528 if (r > 0) rate = (unsigned int)r; 1529 } 1530 1531 audio_sample_load_data(audio, data, len, rate); 1532 JS_FreeValue(ctx, ab); // Free AFTER memcpy — ptr is into this buffer 1533 return JS_TRUE; 1534} 1535 1536// sound.replay.loadData(float32array, rate) — load data into dedicated replay buffer 1537static JSValue js_replay_load_data(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1538 (void)this_val; 1539 if (!current_rt) return JS_FALSE; 1540 ACAudio *audio = current_rt->audio; 1541 if (!audio || argc < 1) return JS_FALSE; 1542 1543 size_t byte_len = 0; 1544 size_t byte_off = 0; 1545 size_t bytes_per = 0; 1546 JSValue ab = JS_GetTypedArrayBuffer(ctx, argv[0], &byte_off, &byte_len, &bytes_per); 1547 if (JS_IsException(ab)) return JS_FALSE; 1548 1549 size_t ab_len = 0; 1550 uint8_t *ptr = JS_GetArrayBuffer(ctx, &ab_len, ab); 1551 if (!ptr) { JS_FreeValue(ctx, ab); return JS_FALSE; } 1552 1553 float *data = (float *)(ptr + byte_off); 1554 int len = (int)(byte_len / sizeof(float)); 1555 1556 unsigned int rate = 48000; 1557 if (argc >= 2 && JS_IsNumber(argv[1])) { 1558 double r; JS_ToFloat64(ctx, &r, argv[1]); 1559 if (r > 0) rate = (unsigned int)r; 1560 } 1561 1562 audio_replay_load_data(audio, data, len, rate); 1563 JS_FreeValue(ctx, ab); 1564 return JS_TRUE; 1565} 1566 1567// sound.speaker.getRecentBuffer(seconds) -> { data: Float32Array, rate: number } 1568static JSValue js_speaker_get_recent_buffer(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1569 (void)this_val; 1570 ACAudio *audio = current_rt ? current_rt->audio : NULL; 1571 if (!audio || !audio->output_history_buf || audio->output_history_size <= 0) return JS_NULL; 1572 1573 double seconds = 1.0; 1574 if (argc >= 1 && JS_IsNumber(argv[0])) JS_ToFloat64(ctx, &seconds, argv[0]); 1575 if (seconds <= 0.0) return JS_NULL; 1576 1577 unsigned int rate_guess = audio->output_history_rate ? audio->output_history_rate : AUDIO_OUTPUT_HISTORY_RATE; 1578 int want_len = (int)(seconds * (double)rate_guess + 0.5); 1579 if (want_len < 1) want_len = 1; 1580 if (want_len > audio->output_history_size) want_len = audio->output_history_size; 1581 1582 float *copy = malloc((size_t)want_len * sizeof(float)); 1583 if (!copy) return JS_NULL; 1584 1585 unsigned int actual_rate = 0; 1586 int len = audio_output_get_recent(audio, copy, want_len, &actual_rate); 1587 if (len <= 0 || actual_rate == 0) { 1588 free(copy); 1589 return JS_NULL; 1590 } 1591 1592 size_t byte_len = (size_t)len * sizeof(float); 1593 JSValue ab = JS_NewArrayBuffer(ctx, (uint8_t *)copy, byte_len, 1594 js_free_array_buffer, NULL, 0); 1595 if (JS_IsException(ab)) { free(copy); return JS_NULL; } 1596 1597 JSValue global = JS_GetGlobalObject(ctx); 1598 JSValue ctor = JS_GetPropertyStr(ctx, global, "Float32Array"); 1599 JSValue f32 = JS_CallConstructor(ctx, ctor, 1, &ab); 1600 JS_FreeValue(ctx, ctor); 1601 JS_FreeValue(ctx, global); 1602 JS_FreeValue(ctx, ab); 1603 if (JS_IsException(f32)) return JS_NULL; 1604 1605 JSValue out = JS_NewObject(ctx); 1606 JS_SetPropertyStr(ctx, out, "data", f32); 1607 JS_SetPropertyStr(ctx, out, "rate", JS_NewInt32(ctx, (int)actual_rate)); 1608 return out; 1609} 1610 1611// sound.tape.recording() -> bool 1612// Returns true while the tape recorder (recorder.c) is capturing. 1613// Pure read-only state getter for notepat UI. 1614static JSValue js_tape_recording(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1615 (void)this_val; (void)argc; (void)argv; 1616#ifdef HAVE_AVCODEC 1617 if (current_rt && current_rt->recorder) { 1618 return JS_NewBool(ctx, recorder_is_recording((ACRecorder *)current_rt->recorder)); 1619 } 1620#endif 1621 return JS_NewBool(ctx, 0); 1622} 1623 1624// sound.tape.elapsed() -> number (seconds, 0 if not recording) 1625static JSValue js_tape_elapsed(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1626 (void)this_val; (void)argc; (void)argv; 1627#ifdef HAVE_AVCODEC 1628 if (current_rt && current_rt->recorder) { 1629 return JS_NewFloat64(ctx, recorder_elapsed((ACRecorder *)current_rt->recorder)); 1630 } 1631#endif 1632 return JS_NewFloat64(ctx, 0.0); 1633} 1634 1635// sound.speak(text) 1636static JSValue js_speak(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1637 (void)this_val; 1638 if (argc < 1 || !current_rt->tts) return JS_UNDEFINED; 1639 const char *text = JS_ToCString(ctx, argv[0]); 1640 if (!text) return JS_UNDEFINED; 1641 tts_speak(current_rt->tts, text); 1642 JS_FreeCString(ctx, text); 1643 return JS_UNDEFINED; 1644} 1645 1646// sound.speakVoice(text, male) — speak with voice selection (0=female, 1=male) 1647static JSValue js_speak_voice(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1648 (void)this_val; 1649 if (argc < 2 || !current_rt->tts) return JS_UNDEFINED; 1650 const char *text = JS_ToCString(ctx, argv[0]); 1651 if (!text) return JS_UNDEFINED; 1652 int male = 0; 1653 JS_ToInt32(ctx, &male, argv[1]); 1654 tts_speak_voice(current_rt->tts, text, male); 1655 JS_FreeCString(ctx, text); 1656 return JS_UNDEFINED; 1657} 1658 1659// sound.speakCached(key) — instant playback of pre-rendered letter/key 1660static JSValue js_speak_cached(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1661 (void)this_val; 1662 if (argc < 1 || !current_rt->tts) return JS_UNDEFINED; 1663 const char *key = JS_ToCString(ctx, argv[0]); 1664 if (!key) return JS_UNDEFINED; 1665 tts_speak_cached(current_rt->tts, key); 1666 JS_FreeCString(ctx, key); 1667 return JS_UNDEFINED; 1668} 1669 1670// ============================================================ 1671// Event System 1672// ============================================================ 1673 1674static JSValue js_event_is(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1675 if (argc < 1) return JS_FALSE; 1676 const char *pattern = JS_ToCString(ctx, argv[0]); 1677 if (!pattern) return JS_FALSE; 1678 1679 JSValue type_val = JS_GetPropertyStr(ctx, this_val, "_type"); 1680 JSValue key_val = JS_GetPropertyStr(ctx, this_val, "_key"); 1681 1682 int type; 1683 JS_ToInt32(ctx, &type, type_val); 1684 const char *key = JS_ToCString(ctx, key_val); 1685 1686 int match = 0; 1687 1688 if (strcmp(pattern, "touch") == 0) 1689 match = (type == AC_EVENT_TOUCH); 1690 else if (strcmp(pattern, "lift") == 0) 1691 match = (type == AC_EVENT_LIFT); 1692 else if (strcmp(pattern, "draw") == 0) 1693 match = (type == AC_EVENT_DRAW); 1694 else if (strcmp(pattern, "keyboard:down") == 0) 1695 match = (type == AC_EVENT_KEYBOARD_DOWN); 1696 else if (strcmp(pattern, "keyboard:up") == 0) 1697 match = (type == AC_EVENT_KEYBOARD_UP); 1698 else if (strncmp(pattern, "keyboard:down:", 14) == 0) 1699 match = (type == AC_EVENT_KEYBOARD_DOWN && key && strcmp(key, pattern + 14) == 0); 1700 else if (strncmp(pattern, "keyboard:up:", 12) == 0) 1701 match = (type == AC_EVENT_KEYBOARD_UP && key && strcmp(key, pattern + 12) == 0); 1702 else if (strcmp(pattern, "reframed") == 0) 1703 match = 0; // no resize on bare metal 1704 else if (strncmp(pattern, "gamepad", 7) == 0) 1705 match = 0; // stub 1706 else if (strncmp(pattern, "midi:", 5) == 0) 1707 match = 0; // stub 1708 else if (strcmp(pattern, "compose") == 0) 1709 match = 0; // stub 1710 1711 JS_FreeCString(ctx, pattern); 1712 JS_FreeCString(ctx, key); 1713 JS_FreeValue(ctx, type_val); 1714 JS_FreeValue(ctx, key_val); 1715 1716 return JS_NewBool(ctx, match); 1717} 1718 1719static JSValue make_event_object(JSContext *ctx, ACEvent *ev) { 1720 JSValue obj = JS_NewObject(ctx); 1721 JS_SetPropertyStr(ctx, obj, "_type", JS_NewInt32(ctx, ev->type)); 1722 JS_SetPropertyStr(ctx, obj, "_key", JS_NewString(ctx, ev->key_name)); 1723 JS_SetPropertyStr(ctx, obj, "x", JS_NewInt32(ctx, ev->x)); 1724 JS_SetPropertyStr(ctx, obj, "y", JS_NewInt32(ctx, ev->y)); 1725 JS_SetPropertyStr(ctx, obj, "repeat", JS_NewBool(ctx, ev->repeat)); 1726 JS_SetPropertyStr(ctx, obj, "key", JS_NewString(ctx, ev->key_name)); 1727 JS_SetPropertyStr(ctx, obj, "code", JS_NewString(ctx, ev->key_name)); 1728 // Velocity: 0-127 from analog pressure (0.0-1.0), default 127 for digital keys 1729 int velocity = ev->pressure > 0.001f ? (int)(ev->pressure * 127.0f) : 127; 1730 if (velocity < 1 && ev->type == AC_EVENT_KEYBOARD_DOWN) velocity = 1; 1731 JS_SetPropertyStr(ctx, obj, "velocity", JS_NewInt32(ctx, velocity)); 1732 JS_SetPropertyStr(ctx, obj, "pressure", JS_NewFloat64(ctx, (double)ev->pressure)); 1733 JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, ev->key_name)); 1734 1735 // pointer sub-object 1736 JSValue pointer = JS_NewObject(ctx); 1737 JS_SetPropertyStr(ctx, pointer, "x", JS_NewInt32(ctx, ev->x)); 1738 JS_SetPropertyStr(ctx, pointer, "y", JS_NewInt32(ctx, ev->y)); 1739 JS_SetPropertyStr(ctx, obj, "pointer", pointer); 1740 1741 JS_SetPropertyStr(ctx, obj, "is", JS_NewCFunction(ctx, js_event_is, "is", 1)); 1742 1743 return obj; 1744} 1745 1746// ============================================================ 1747// num utilities 1748// ============================================================ 1749 1750static JSValue js_num_clamp(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1751 (void)this_val; 1752 if (argc < 3) return JS_UNDEFINED; 1753 double v, mn, mx; 1754 JS_ToFloat64(ctx, &v, argv[0]); 1755 JS_ToFloat64(ctx, &mn, argv[1]); 1756 JS_ToFloat64(ctx, &mx, argv[2]); 1757 if (v < mn) v = mn; 1758 if (v > mx) v = mx; 1759 return JS_NewFloat64(ctx, v); 1760} 1761 1762static JSValue js_num_rand(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1763 (void)this_val; (void)argc; (void)argv; 1764 return JS_NewFloat64(ctx, (double)rand() / RAND_MAX); 1765} 1766 1767static JSValue js_num_randint(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1768 (void)this_val; 1769 if (argc < 2) return JS_NewInt32(ctx, 0); 1770 int a, b; 1771 JS_ToInt32(ctx, &a, argv[0]); 1772 JS_ToInt32(ctx, &b, argv[1]); 1773 if (b <= a) return JS_NewInt32(ctx, a); 1774 return JS_NewInt32(ctx, a + rand() % (b - a + 1)); 1775} 1776 1777// ============================================================ 1778// Module loader for ES module imports 1779// ============================================================ 1780 1781static JSModuleDef *js_module_loader(JSContext *ctx, const char *module_name, void *opaque) { 1782 (void)opaque; 1783 1784 // Check if it's a stub module 1785 const char *stub_src = NULL; 1786 1787 if (strstr(module_name, "chord-detection")) { 1788 stub_src = "export function detectChord() { return null; }"; 1789 } else if (strstr(module_name, "gamepad-diagram")) { 1790 stub_src = "export function drawMiniControllerDiagram() {}"; 1791 } else if (strstr(module_name, "pixel-sample")) { 1792 stub_src = "export function decodeBitmapToSample() { return null; }\n" 1793 "export function loadPaintingAsAudio() { return null; }"; 1794 } else if (strstr(module_name, "nopaint")) { 1795 stub_src = "export function nopaint_generateColoredLabel() {}\n" 1796 "export function nopaint_boot() {}\n" 1797 "export function nopaint_act() {}\n" 1798 "export function nopaint_paint() {}\n" 1799 "export function nopaint_is(s) { return false; }\n" 1800 "export function nopaint_cancelStroke() {}\n" 1801 "export function nopaint_adjust() {}\n" 1802 "export function nopaint_handleColor(c, ink) { return ink(c); }\n" 1803 "export function nopaint_cleanupColor() {}\n" 1804 "export function nopaint_parseBrushParams(o) { return {color:[], mode:'fill', thickness:1}; }\n" 1805 "export function nopaint_renderPerfHUD() {}\n" 1806 "export function nopaint_triggerBakeFlash() {}"; 1807 } else if (strstr(module_name, "color-highlighting")) { 1808 stub_src = "export function generateNopaintHUDLabel() { return ''; }\n" 1809 "export function colorizeColorName() { return ''; }"; 1810 } 1811 1812 // Try to load the module from the filesystem 1813 char resolved[512] = {0}; 1814 1815 if (!stub_src) { 1816 // Try multiple paths 1817 const char *search_paths[] = { 1818 module_name, 1819 NULL 1820 }; 1821 1822 // If it's a relative path like "../lib/note-colors.mjs", resolve from / 1823 if (module_name[0] == '.' && module_name[1] == '.') { 1824 // "../lib/X" → "/lib/X" 1825 const char *p = module_name + 2; 1826 while (*p == '/') p++; 1827 if (*p == '.') { // "../../lib/X" 1828 p++; 1829 while (*p == '.') p++; 1830 while (*p == '/') p++; 1831 } 1832 snprintf(resolved, sizeof(resolved), "/%s", p); 1833 } else { 1834 snprintf(resolved, sizeof(resolved), "%s", module_name); 1835 } 1836 1837 FILE *f = fopen(resolved, "r"); 1838 if (!f) { 1839 // Try /lib/ prefix 1840 snprintf(resolved, sizeof(resolved), "/lib/%s", 1841 strrchr(module_name, '/') ? strrchr(module_name, '/') + 1 : module_name); 1842 f = fopen(resolved, "r"); 1843 } 1844 if (f) { 1845 fseek(f, 0, SEEK_END); 1846 long len = ftell(f); 1847 fseek(f, 0, SEEK_SET); 1848 char *src = malloc(len + 1); 1849 fread(src, 1, len, f); 1850 src[len] = '\0'; 1851 fclose(f); 1852 1853 JSValue val = JS_Eval(ctx, src, len, module_name, 1854 JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); 1855 free(src); 1856 1857 if (JS_IsException(val)) { 1858 JSValue exc = JS_GetException(ctx); 1859 const char *str = JS_ToCString(ctx, exc); 1860 fprintf(stderr, "[js] Module load error (%s): %s\n", module_name, str); 1861 JS_FreeCString(ctx, str); 1862 JS_FreeValue(ctx, exc); 1863 return NULL; 1864 } 1865 1866 JSModuleDef *m = (JSModuleDef *)JS_VALUE_GET_PTR(val); 1867 return m; 1868 } 1869 1870 // Module not found — create empty stub 1871 fprintf(stderr, "[js] Module not found, stubbing: %s\n", module_name); 1872 stub_src = "// stub"; 1873 } 1874 1875 // Compile stub module 1876 JSValue val = JS_Eval(ctx, stub_src, strlen(stub_src), module_name, 1877 JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); 1878 if (JS_IsException(val)) { 1879 JSValue exc = JS_GetException(ctx); 1880 const char *str = JS_ToCString(ctx, exc); 1881 fprintf(stderr, "[js] Stub module error (%s): %s\n", module_name, str); 1882 JS_FreeCString(ctx, str); 1883 JS_FreeValue(ctx, exc); 1884 return NULL; 1885 } 1886 1887 JSModuleDef *m = (JSModuleDef *)JS_VALUE_GET_PTR(val); 1888 return m; 1889} 1890 1891// ============================================================ 1892// Runtime Init / Lifecycle 1893// ============================================================ 1894 1895// JS init code evaluated before pieces — defines Button, Box, etc. 1896static const char *js_init_code = 1897 "globalThis.__Button = class Button {\n" 1898 " constructor(x, y, w, h) {\n" 1899 " if (typeof x === 'object' && x !== null) {\n" 1900 " this.box = { x: x.x || 0, y: x.y || 0, w: x.w || x.width || 0, h: x.h || x.height || 0 };\n" 1901 " } else {\n" 1902 " this.box = { x: x || 0, y: y || 0, w: w || 0, h: h || 0 };\n" 1903 " }\n" 1904 " this.down = false;\n" 1905 " this.over = false;\n" 1906 " this.disabled = false;\n" 1907 " this.multitouch = true;\n" 1908 " }\n" 1909 " get up() { return !this.down; }\n" 1910 " set up(v) { this.down = !v; }\n" 1911 " paint(fn) { if (!this.disabled && fn) fn(this); }\n" 1912 " act(e, callbacks, pens) {\n" 1913 " if (this.disabled) return;\n" 1914 " if (typeof callbacks === 'function') callbacks = { push: callbacks };\n" 1915 " this.actions = callbacks;\n" 1916 " }\n" 1917 "};\n" 1918 "globalThis.__Box = class Box {\n" 1919 " constructor(x, y, w, h) {\n" 1920 " if (typeof x === 'object') { this.x = x.x; this.y = x.y; this.w = x.w; this.h = x.h; }\n" 1921 " else { this.x = x || 0; this.y = y || 0; this.w = w || 0; this.h = h || 0; }\n" 1922 " }\n" 1923 " static from(obj) { return new __Box(obj); }\n" 1924 "};\n" 1925 "globalThis.Number.isFinite = globalThis.Number.isFinite || function(v) { return typeof v === 'number' && isFinite(v); };\n" 1926 // num.shiftRGB — lerp between two RGB arrays 1927 "globalThis.__shiftRGB = function(from, to, t, mode) {\n" 1928 " if (!from || !to) return from || [0,0,0];\n" 1929 " t = Math.max(0, Math.min(1, t || 0));\n" 1930 " return [\n" 1931 " Math.round(from[0] + (to[0] - from[0]) * t),\n" 1932 " Math.round(from[1] + (to[1] - from[1]) * t),\n" 1933 " Math.round(from[2] + (to[2] - from[2]) * t)\n" 1934 " ];\n" 1935 "};\n" 1936 // num.dist — Euclidean distance 1937 "globalThis.__dist = function(x1, y1, x2, y2) {\n" 1938 " const dx = x2 - x1, dy = y2 - y1;\n" 1939 " return Math.sqrt(dx*dx + dy*dy);\n" 1940 "};\n" 1941 // num.map — map value from one range to another 1942 "globalThis.__map = function(v, inMin, inMax, outMin, outMax) {\n" 1943 " return outMin + (v - inMin) * (outMax - outMin) / (inMax - inMin);\n" 1944 "};\n" 1945 // num.lerp 1946 "globalThis.__lerp = function(a, b, t) { return a + (b - a) * t; };\n" 1947 // num.parseColor — parse color from params array (e.g. ["purple"], ["255","0","0"], ["128"]) 1948 "globalThis.__parseColor = function(params) {\n" 1949 " if (!params || params.length === 0) return [];\n" 1950 " var names = {red:[255,0,0],orange:[255,165,0],yellow:[255,255,0],green:[0,128,0],\n" 1951 " cyan:[0,255,255],blue:[0,0,255],purple:[128,0,128],magenta:[255,0,255],\n" 1952 " pink:[255,192,203],white:[255,255,255],gray:[128,128,128],grey:[128,128,128],\n" 1953 " black:[0,0,0],brown:[139,69,19]};\n" 1954 " var first = params[0];\n" 1955 " if (typeof first === 'string' && names[first.toLowerCase()]) {\n" 1956 " var c = names[first.toLowerCase()].slice();\n" 1957 " if (params.length >= 2) c.push(parseInt(params[1]) || 255);\n" 1958 " return c;\n" 1959 " }\n" 1960 " var nums = params.map(function(p){return parseInt(p)}).filter(function(n){return !isNaN(n)});\n" 1961 " return nums;\n" 1962 "};\n" 1963 // num.randIntArr — array of N random ints in [0, max] 1964 "globalThis.__randIntArr = function(max, len) {\n" 1965 " var a = []; for (var i = 0; i < len; i++) a.push(Math.floor(Math.random() * (max + 1)));\n" 1966 " return a;\n" 1967 "};\n" 1968 // num.timestamp — returns a timestamp string 1969 "globalThis.__timestamp = function() { return Date.now().toString(36); };\n" 1970 // nopaint_generateColoredLabel stub (native doesn't have HUD color highlighting) 1971 "globalThis.__nopaint_generateColoredLabel = function() {};\n" 1972 // Typeface constructor stub 1973 "globalThis.__Typeface = function Typeface(name) {\n" 1974 " this.name = name;\n" 1975 " this.loaded = false;\n" 1976 " this.load = function(preloadFn) { this.loaded = true; return this; };\n" 1977 " this.measure = function(text) { return { width: (text||'').length * 8, height: 8 }; };\n" 1978 "};\n" 1979 // ── Global theme system ── 1980 // Auto-switches dark/light based on LA time. All pieces use theme.fg, theme.bg, etc. 1981 // Supports named presets via __theme.apply(id) that persist via config.json. 1982 "globalThis.__theme = (function() {\n" 1983 " function getLAOffset() {\n" 1984 " var d = new Date(), m = d.getUTCMonth(), y = d.getUTCFullYear();\n" 1985 " if (m > 2 && m < 10) return 7;\n" 1986 " if (m < 2 || m > 10) return 8;\n" 1987 " if (m === 2) {\n" 1988 " var mar1 = new Date(y, 2, 1), ss = 8 + (7 - mar1.getDay()) % 7;\n" 1989 " return d.getUTCDate() > ss || (d.getUTCDate() === ss && d.getUTCHours() >= 10) ? 7 : 8;\n" 1990 " }\n" 1991 " var nov1 = new Date(y, 10, 1), fs = 1 + (7 - nov1.getDay()) % 7;\n" 1992 " return d.getUTCDate() < fs || (d.getUTCDate() === fs && d.getUTCHours() < 9) ? 7 : 8;\n" 1993 " }\n" 1994 " function getLAHour() {\n" 1995 " return (new Date().getUTCHours() - getLAOffset() + 24) % 24;\n" 1996 " }\n" 1997 " var t = { dark: true, _lastCheck: 0, _overrideId: null, _override: null };\n" 1998 " // Theme presets: each has dark and light color overrides\n" 1999 " t.presets = {\n" 2000 " serious: {\n" 2001 " label: 'serious', desc: 'black & white',\n" 2002 " dark: { bg:[0,0,0], bgAlt:[10,10,10], bgDim:[0,0,0],\n" 2003 " fg:255, fgDim:160, fgMute:90,\n" 2004 " bar:[15,15,15], border:[60,60,60],\n" 2005 " accent:[128,128,128], ok:[200,200,200], err:[255,100,100],\n" 2006 " warn:[200,200,100], link:[180,180,255],\n" 2007 " pad:[10,10,10], padSharp:[5,5,5], padLine:[40,40,40],\n" 2008 " cursor:[255,255,255] },\n" 2009 " light: { bg:[255,255,255], bgAlt:[245,245,245], bgDim:[235,235,235],\n" 2010 " fg:0, fgDim:80, fgMute:160,\n" 2011 " bar:[240,240,240], border:[180,180,180],\n" 2012 " accent:[100,100,100], ok:[40,40,40], err:[180,40,40],\n" 2013 " warn:[120,100,20], link:[40,40,180],\n" 2014 " pad:[245,245,245], padSharp:[230,230,230], padLine:[200,200,200],\n" 2015 " cursor:[0,0,0] }\n" 2016 " },\n" 2017 " neo: {\n" 2018 " label: 'neo', desc: 'lime & black',\n" 2019 " dark: { bg:[0,0,0], bgAlt:[5,10,5], bgDim:[0,0,0],\n" 2020 " fg:200, fgDim:120, fgMute:60,\n" 2021 " bar:[5,15,5], border:[0,80,0],\n" 2022 " accent:[0,200,80], ok:[0,255,0], err:[255,50,50],\n" 2023 " warn:[200,255,0], link:[0,180,255],\n" 2024 " pad:[5,10,5], padSharp:[0,5,0], padLine:[0,50,0],\n" 2025 " cursor:[0,255,80] },\n" 2026 " light: { bg:[220,255,220], bgAlt:[230,255,230], bgDim:[200,240,200],\n" 2027 " fg:10, fgDim:60, fgMute:120,\n" 2028 " bar:[200,240,200], border:[100,180,100],\n" 2029 " accent:[0,140,60], ok:[0,120,40], err:[180,30,30],\n" 2030 " warn:[120,140,0], link:[0,80,180],\n" 2031 " pad:[210,245,210], padSharp:[190,230,190], padLine:[140,200,140],\n" 2032 " cursor:[0,120,40] }\n" 2033 " },\n" 2034 " ember: {\n" 2035 " label: 'ember', desc: 'warm amber',\n" 2036 " dark: { bg:[20,12,8], bgAlt:[28,18,12], bgDim:[14,8,5],\n" 2037 " fg:220, fgDim:150, fgMute:90,\n" 2038 " bar:[35,20,12], border:[60,35,20],\n" 2039 " accent:[255,140,40], ok:[120,220,80], err:[255,70,50],\n" 2040 " warn:[255,200,60], link:[255,180,100],\n" 2041 " pad:[28,18,12], padSharp:[18,10,6], padLine:[55,35,22],\n" 2042 " cursor:[255,120,30] },\n" 2043 " light: { bg:[255,245,230], bgAlt:[255,250,240], bgDim:[245,235,218],\n" 2044 " fg:40, fgDim:90, fgMute:140,\n" 2045 " bar:[245,232,215], border:[210,190,160],\n" 2046 " accent:[200,100,20], ok:[40,140,50], err:[190,40,30],\n" 2047 " warn:[180,120,20], link:[180,90,20],\n" 2048 " pad:[250,240,225], padSharp:[238,225,208], padLine:[220,200,175],\n" 2049 " cursor:[200,90,15] }\n" 2050 " }\n" 2051 " };\n" 2052 " // Apply a named preset (or 'default' to clear override)\n" 2053 " t.apply = function(id) {\n" 2054 " if (!id || id === 'default') {\n" 2055 " t._overrideId = null;\n" 2056 " t._override = null;\n" 2057 " } else if (t.presets[id]) {\n" 2058 " t._overrideId = id;\n" 2059 " t._override = t.presets[id];\n" 2060 " }\n" 2061 " t._lastCheck = 0;\n" 2062 " t.update();\n" 2063 " };\n" 2064 " t.update = function() {\n" 2065 " var now = Date.now();\n" 2066 " if (now - t._lastCheck < 5000) return t;\n" 2067 " t._lastCheck = now;\n" 2068 " var h = getLAHour();\n" 2069 " t.dark = (t._forceDark !== undefined) ? !!t._forceDark : (h >= 20 || h < 7);\n" 2070 " t.hour = h;\n" 2071 " // Backgrounds\n" 2072 " t.bg = t.dark ? [20, 20, 25] : [240, 238, 232];\n" 2073 " t.bgAlt = t.dark ? [28, 28, 30] : [250, 248, 244];\n" 2074 " t.bgDim = t.dark ? [15, 15, 18] : [230, 228, 222];\n" 2075 " // Foregrounds\n" 2076 " t.fg = t.dark ? 220 : 40;\n" 2077 " t.fgDim = t.dark ? 140 : 100;\n" 2078 " t.fgMute = t.dark ? 80 : 150;\n" 2079 " // UI elements\n" 2080 " t.bar = t.dark ? [35, 20, 30] : [225, 220, 215];\n" 2081 " t.border = t.dark ? [55, 35, 45] : [200, 195, 190];\n" 2082 " t.accent = t.dark ? [200, 100, 140] : [180, 60, 120];\n" 2083 " t.ok = t.dark ? [80, 255, 120] : [30, 160, 60];\n" 2084 " t.err = t.dark ? [255, 85, 85] : [200, 40, 40];\n" 2085 " t.warn = t.dark ? [255, 200, 60] : [180, 120, 20];\n" 2086 " t.link = t.dark ? [120, 200, 255] : [40, 100, 200];\n" 2087 " // Pad colors (for notepat-style grids)\n" 2088 " t.pad = t.dark ? [28, 28, 30] : [250, 248, 244];\n" 2089 " t.padSharp = t.dark ? [18, 18, 20] : [235, 232, 228];\n" 2090 " t.padLine = t.dark ? [50, 50, 55] : [210, 205, 200];\n" 2091 " // Cursor\n" 2092 " t.cursor = t.dark ? [220, 80, 140] : [180, 50, 110];\n" 2093 " // Apply preset override if active\n" 2094 " if (t._override) {\n" 2095 " var m = t.dark ? t._override.dark : t._override.light;\n" 2096 " if (m) { for (var k in m) { t[k] = m[k]; } }\n" 2097 " }\n" 2098 " return t;\n" 2099 " };\n" 2100 " t.update();\n" 2101 " return t;\n" 2102 "})();\n" 2103 ; 2104 2105ACRuntime *js_init(ACGraph *graph, ACInput *input, ACAudio *audio, ACWifi *wifi, ACTts *tts) { 2106 ACRuntime *rt = calloc(1, sizeof(ACRuntime)); 2107 if (!rt) return NULL; 2108 2109 rt->graph = graph; 2110 rt->input = input; 2111 rt->audio = audio; 2112 rt->wifi = wifi; 2113 rt->tts = tts; 2114 rt->ws = ws_create(); 2115 rt->udp = udp_create(); 2116 rt->boot_fn = JS_UNDEFINED; 2117 rt->paint_fn = JS_UNDEFINED; 2118 rt->act_fn = JS_UNDEFINED; 2119 rt->sim_fn = JS_UNDEFINED; 2120 rt->leave_fn = JS_UNDEFINED; 2121 rt->beat_fn = JS_UNDEFINED; 2122 2123 // Initialize 3D camera 2124 camera3d_init(&rt->camera3d); 2125 2126 rt->rt = JS_NewRuntime(); 2127 rt->ctx = JS_NewContext(rt->rt); 2128 2129 // Register Form and Painting classes 2130 JS_NewClassID(&form_class_id); 2131 JS_NewClass(rt->rt, form_class_id, &form_class_def); 2132 JS_NewClassID(&painting_class_id); 2133 JS_NewClass(rt->rt, painting_class_id, &painting_class_def); 2134 2135 // Set module loader 2136 JS_SetModuleLoaderFunc(rt->rt, NULL, js_module_loader, NULL); 2137 2138 current_rt = rt; 2139 2140 JSContext *ctx = rt->ctx; 2141 JSValue global = JS_GetGlobalObject(ctx); 2142 2143 // Create the chainable paint API object (with 3D .form() and .ink() support) 2144 JSValue paint_api = JS_NewObject(ctx); 2145 JS_SetPropertyStr(ctx, paint_api, "box", JS_NewCFunction(ctx, js_box, "box", 5)); 2146 JS_SetPropertyStr(ctx, paint_api, "line", JS_NewCFunction(ctx, js_line, "line", 4)); 2147 JS_SetPropertyStr(ctx, paint_api, "circle", JS_NewCFunction(ctx, js_circle, "circle", 4)); 2148 JS_SetPropertyStr(ctx, paint_api, "plot", JS_NewCFunction(ctx, js_plot, "plot", 2)); 2149 JS_SetPropertyStr(ctx, paint_api, "write", JS_NewCFunction(ctx, js_chain_write, "write", 6)); 2150 JS_SetPropertyStr(ctx, paint_api, "scroll", JS_NewCFunction(ctx, js_scroll, "scroll", 2)); 2151 JS_SetPropertyStr(ctx, paint_api, "blur", JS_NewCFunction(ctx, js_blur, "blur", 1)); 2152 JS_SetPropertyStr(ctx, paint_api, "zoom", JS_NewCFunction(ctx, js_zoom, "zoom", 1)); 2153 JS_SetPropertyStr(ctx, paint_api, "contrast", JS_NewCFunction(ctx, js_contrast, "contrast", 1)); 2154 JS_SetPropertyStr(ctx, paint_api, "spin", JS_NewCFunction(ctx, js_spin, "spin", 1)); 2155 JS_SetPropertyStr(ctx, paint_api, "qr", JS_NewCFunction(ctx, js_qr, "qr", 4)); 2156 // 3D chain methods 2157 JS_SetPropertyStr(ctx, paint_api, "form", JS_NewCFunction(ctx, js_chain_form, "form", 1)); 2158 JS_SetPropertyStr(ctx, paint_api, "ink", JS_NewCFunction(ctx, js_chain_ink, "ink", 4)); 2159 // pppline — polyline through array of {x,y} points (used by line.mjs for 1px strokes) 2160 { 2161 const char *pppline_src = 2162 "(function(pts) {\n" 2163 " if (!pts || pts.length < 2) return;\n" 2164 " for (var i = 0; i < pts.length - 1; i++)\n" 2165 " line(pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y);\n" 2166 "})"; 2167 JSValue pppline_fn = JS_Eval(ctx, pppline_src, strlen(pppline_src), "<pppline>", JS_EVAL_TYPE_GLOBAL); 2168 JS_SetPropertyStr(ctx, paint_api, "pppline", pppline_fn); 2169 } 2170 JS_SetPropertyStr(ctx, global, "__paintApi", JS_DupValue(ctx, paint_api)); 2171 JS_FreeValue(ctx, paint_api); 2172 2173 // Register top-level graphics functions 2174 JS_SetPropertyStr(ctx, global, "wipe", JS_NewCFunction(ctx, js_wipe, "wipe", 3)); 2175 JS_SetPropertyStr(ctx, global, "ink", JS_NewCFunction(ctx, js_ink, "ink", 4)); 2176 JS_SetPropertyStr(ctx, global, "line", JS_NewCFunction(ctx, js_line, "line", 5)); 2177 JS_SetPropertyStr(ctx, global, "box", JS_NewCFunction(ctx, js_box, "box", 5)); 2178 JS_SetPropertyStr(ctx, global, "circle", JS_NewCFunction(ctx, js_circle, "circle", 4)); 2179 JS_SetPropertyStr(ctx, global, "qr", JS_NewCFunction(ctx, js_qr, "qr", 4)); 2180 JS_SetPropertyStr(ctx, global, "plot", JS_NewCFunction(ctx, js_plot, "plot", 2)); 2181 JS_SetPropertyStr(ctx, global, "write", JS_NewCFunction(ctx, js_write, "write", 6)); 2182 JS_SetPropertyStr(ctx, global, "scroll", JS_NewCFunction(ctx, js_scroll, "scroll", 2)); 2183 JS_SetPropertyStr(ctx, global, "blur", JS_NewCFunction(ctx, js_blur, "blur", 1)); 2184 JS_SetPropertyStr(ctx, global, "zoom", JS_NewCFunction(ctx, js_zoom, "zoom", 1)); 2185 JS_SetPropertyStr(ctx, global, "contrast", JS_NewCFunction(ctx, js_contrast, "contrast", 1)); 2186 JS_SetPropertyStr(ctx, global, "spin", JS_NewCFunction(ctx, js_spin, "spin", 1)); 2187 2188 // console.log 2189 JSValue console_obj = JS_NewObject(ctx); 2190 JS_SetPropertyStr(ctx, console_obj, "log", JS_NewCFunction(ctx, js_console_log, "log", 1)); 2191 JS_SetPropertyStr(ctx, console_obj, "warn", JS_NewCFunction(ctx, js_console_log, "warn", 1)); 2192 JS_SetPropertyStr(ctx, console_obj, "error", JS_NewCFunction(ctx, js_console_log, "error", 1)); 2193 JS_SetPropertyStr(ctx, global, "console", console_obj); 2194 2195 // performance.now() 2196 { 2197 JSValue perf = JS_NewObject(ctx); 2198 JS_SetPropertyStr(ctx, perf, "now", JS_NewCFunction(ctx, js_performance_now, "now", 0)); 2199 JS_SetPropertyStr(ctx, global, "performance", perf); 2200 } 2201 2202 // Run init JS code (Button, Box classes) 2203 JSValue init_result = JS_Eval(ctx, js_init_code, strlen(js_init_code), "<init>", JS_EVAL_TYPE_GLOBAL); 2204 if (JS_IsException(init_result)) { 2205 JSValue exc = JS_GetException(ctx); 2206 const char *str = JS_ToCString(ctx, exc); 2207 fprintf(stderr, "[js] Init code error: %s\n", str); 2208 JS_FreeCString(ctx, str); 2209 JS_FreeValue(ctx, exc); 2210 } 2211 JS_FreeValue(ctx, init_result); 2212 2213 // Browser API stubs for KidLisp evaluator compatibility 2214 static const char *browser_stubs = 2215 "if(typeof window==='undefined'){" 2216 " globalThis.window={" 2217 " location:{href:'',origin:'',hostname:'localhost',pathname:'/',search:'',hash:''}," 2218 " history:{pushState:function(){},replaceState:function(){}}," 2219 " matchMedia:function(){return{matches:false,addEventListener:function(){}}}," 2220 " addEventListener:function(){}," 2221 " postMessage:function(){}," 2222 " navigator:{userAgent:'ac-native'}," 2223 " document:{createElement:function(){return{style:{},getContext:function(){return{}},setAttribute:function(){}}}}," 2224 " origin:''" 2225 " };" 2226 " globalThis.document=window.document;" 2227 " globalThis.navigator=window.navigator;" 2228 " globalThis.localStorage={_s:{},getItem:function(k){return this._s[k]||null},setItem:function(k,v){this._s[k]=v},removeItem:function(k){delete this._s[k]}};" 2229 " globalThis.fetch=function(){return Promise.resolve({ok:false,json:function(){return Promise.resolve({})}})};" 2230 " globalThis.indexedDB=null;" 2231 " globalThis.requestAnimationFrame=function(cb){cb(performance.now());return 0};" 2232 " globalThis.cancelAnimationFrame=function(){};" 2233 " globalThis.Image=function(){this.src='';this.onload=null};" 2234 "}"; 2235 JSValue stubs_result = JS_Eval(ctx, browser_stubs, strlen(browser_stubs), "<browser-stubs>", JS_EVAL_TYPE_GLOBAL); 2236 if (JS_IsException(stubs_result)) { 2237 JSValue exc = JS_GetException(ctx); 2238 const char *str = JS_ToCString(ctx, exc); 2239 fprintf(stderr, "[js] Browser stubs error: %s\n", str); 2240 JS_FreeCString(ctx, str); 2241 JS_FreeValue(ctx, exc); 2242 } 2243 JS_FreeValue(ctx, stubs_result); 2244 2245 // Load KidLisp evaluator bundle (IIFE → globalThis.KidLispModule) 2246 FILE *kl_f = fopen("/jslib/kidlisp-bundle.js", "r"); 2247 if (kl_f) { 2248 fseek(kl_f, 0, SEEK_END); 2249 long kl_len = ftell(kl_f); 2250 fseek(kl_f, 0, SEEK_SET); 2251 char *kl_src = malloc(kl_len + 1); 2252 fread(kl_src, 1, kl_len, kl_f); 2253 kl_src[kl_len] = '\0'; 2254 fclose(kl_f); 2255 JSValue kl_result = JS_Eval(ctx, kl_src, kl_len, "<kidlisp-bundle>", JS_EVAL_TYPE_GLOBAL); 2256 free(kl_src); 2257 if (JS_IsException(kl_result)) { 2258 JSValue exc = JS_GetException(ctx); 2259 const char *str = JS_ToCString(ctx, exc); 2260 fprintf(stderr, "[js] KidLisp bundle error: %s\n", str); 2261 JS_FreeCString(ctx, str); 2262 JS_FreeValue(ctx, exc); 2263 } else { 2264 fprintf(stderr, "[js] KidLisp evaluator loaded (%ld bytes)\n", kl_len); 2265 } 2266 JS_FreeValue(ctx, kl_result); 2267 } else { 2268 fprintf(stderr, "[js] KidLisp bundle not found at /jslib/kidlisp-bundle.js (optional)\n"); 2269 } 2270 2271 // Load FPS system bundle (Camera + Dolly + CamDoll as globalThis.__FpsSystem) 2272 // so pieces that export `system = "fps"` (arena.mjs etc.) can be wrapped 2273 // synchronously in the piece-load shim without async module resolution. 2274 FILE *fps_f = fopen("/jslib/fps-system-bundle.js", "r"); 2275 if (fps_f) { 2276 fseek(fps_f, 0, SEEK_END); 2277 long fps_len = ftell(fps_f); 2278 fseek(fps_f, 0, SEEK_SET); 2279 char *fps_src = malloc(fps_len + 1); 2280 fread(fps_src, 1, fps_len, fps_f); 2281 fps_src[fps_len] = '\0'; 2282 fclose(fps_f); 2283 JSValue fps_result = JS_Eval(ctx, fps_src, fps_len, "<fps-system-bundle>", JS_EVAL_TYPE_GLOBAL); 2284 free(fps_src); 2285 if (JS_IsException(fps_result)) { 2286 JSValue exc = JS_GetException(ctx); 2287 const char *str = JS_ToCString(ctx, exc); 2288 fprintf(stderr, "[js] FPS bundle error: %s\n", str); 2289 JS_FreeCString(ctx, str); 2290 JS_FreeValue(ctx, exc); 2291 } else { 2292 fprintf(stderr, "[js] FPS system loaded (%ld bytes)\n", fps_len); 2293 } 2294 JS_FreeValue(ctx, fps_result); 2295 } else { 2296 fprintf(stderr, "[js] FPS bundle not found at /jslib/fps-system-bundle.js (optional)\n"); 2297 } 2298 2299 JS_FreeValue(ctx, global); 2300 return rt; 2301} 2302 2303// painting(w, h, callback) — create a real off-screen framebuffer for textures 2304// Calls the callback with a paint API that draws into the off-screen buffer 2305static JSValue js_painting(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2306 (void)this_val; 2307 int w = 100, h = 100; 2308 if (argc >= 1) JS_ToInt32(ctx, &w, argv[0]); 2309 if (argc >= 2) JS_ToInt32(ctx, &h, argv[1]); 2310 2311 // Create a real off-screen framebuffer 2312 ACFramebuffer *tex_fb = fb_create(w, h); 2313 if (!tex_fb) return JS_EXCEPTION; 2314 2315 // Create JS painting object with opaque ACFramebuffer* 2316 JSValue painting = JS_NewObjectClass(ctx, painting_class_id); 2317 JS_SetOpaque(painting, tex_fb); 2318 JS_SetPropertyStr(ctx, painting, "width", JS_NewInt32(ctx, w)); 2319 JS_SetPropertyStr(ctx, painting, "height", JS_NewInt32(ctx, h)); 2320 2321 // Expose pixels as a Uint8Array view into the framebuffer memory. 2322 // Nopaint uses buffer.pixels[i] to adjust alpha during bake. 2323 // Note: pixel format is ARGB32 (native byte order). 2324 { 2325 int byte_len = tex_fb->stride * h * 4; 2326 JSValue ab = JS_NewArrayBuffer(ctx, (uint8_t *)tex_fb->pixels, byte_len, 2327 NULL, NULL, 0); // No free — painting finalizer handles the framebuffer 2328 // Construct Uint8Array from the ArrayBuffer via JS eval 2329 JSValue global = JS_GetGlobalObject(ctx); 2330 JSValue uint8_ctor = JS_GetPropertyStr(ctx, global, "Uint8Array"); 2331 JSValue pixels = JS_CallConstructor(ctx, uint8_ctor, 1, &ab); 2332 JS_SetPropertyStr(ctx, painting, "pixels", pixels); 2333 JS_FreeValue(ctx, uint8_ctor); 2334 JS_FreeValue(ctx, global); 2335 JS_FreeValue(ctx, ab); 2336 } 2337 2338 // If callback provided, render into the off-screen buffer 2339 if (argc >= 3 && JS_IsFunction(ctx, argv[2])) { 2340 // Save current render target and switch to off-screen 2341 ACFramebuffer *saved_fb = current_rt->graph->fb; 2342 graph_page(current_rt->graph, tex_fb); 2343 2344 JSValue global = JS_GetGlobalObject(ctx); 2345 JSValue paint_api = JS_NewObject(ctx); 2346 JS_SetPropertyStr(ctx, paint_api, "wipe", JS_GetPropertyStr(ctx, global, "wipe")); 2347 JS_SetPropertyStr(ctx, paint_api, "ink", JS_GetPropertyStr(ctx, global, "ink")); 2348 JS_SetPropertyStr(ctx, paint_api, "box", JS_GetPropertyStr(ctx, global, "box")); 2349 JS_SetPropertyStr(ctx, paint_api, "line", JS_GetPropertyStr(ctx, global, "line")); 2350 JS_SetPropertyStr(ctx, paint_api, "circle", JS_GetPropertyStr(ctx, global, "circle")); 2351 JS_SetPropertyStr(ctx, paint_api, "plot", JS_GetPropertyStr(ctx, global, "plot")); 2352 JS_SetPropertyStr(ctx, paint_api, "write", JS_GetPropertyStr(ctx, global, "write")); 2353 JS_SetPropertyStr(ctx, paint_api, "kidlisp", JS_NewCFunction(ctx, js_noop, "kidlisp", 5)); 2354 JS_FreeValue(ctx, global); 2355 2356 JSValue result = JS_Call(ctx, argv[2], JS_UNDEFINED, 1, &paint_api); 2357 JS_FreeValue(ctx, result); 2358 JS_FreeValue(ctx, paint_api); 2359 2360 // Restore previous render target 2361 graph_page(current_rt->graph, saved_fb); 2362 } 2363 2364 return painting; 2365} 2366 2367// page(painting) — switch render target to a painting buffer (or back to screen) 2368// page(painting) returns an object with wipe/ink/box/line/etc that draw into that buffer. 2369// In AC web, page() returns a chainable proxy. Here we just switch the graph target. 2370static JSValue js_page(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2371 (void)this_val; 2372 if (!current_rt || !current_rt->graph) return JS_UNDEFINED; 2373 2374 if (argc < 1 || JS_IsUndefined(argv[0]) || JS_IsNull(argv[0])) { 2375 // page() with no args or page(screen) — restore screen target 2376 graph_page(current_rt->graph, current_rt->graph->screen); 2377 } else { 2378 // page(painting) — switch to painting buffer 2379 ACFramebuffer *fb = JS_GetOpaque(argv[0], painting_class_id); 2380 if (fb) graph_page(current_rt->graph, fb); 2381 } 2382 2383 // Return a page proxy object with wipe() so callers can do page(buf).wipe(...) 2384 JSValue proxy = JS_NewObject(ctx); 2385 JSValue global = JS_GetGlobalObject(ctx); 2386 JS_SetPropertyStr(ctx, proxy, "wipe", JS_GetPropertyStr(ctx, global, "wipe")); 2387 JS_SetPropertyStr(ctx, proxy, "ink", JS_GetPropertyStr(ctx, global, "ink")); 2388 JS_SetPropertyStr(ctx, proxy, "box", JS_GetPropertyStr(ctx, global, "box")); 2389 JS_SetPropertyStr(ctx, proxy, "line", JS_GetPropertyStr(ctx, global, "line")); 2390 JS_SetPropertyStr(ctx, proxy, "circle", JS_GetPropertyStr(ctx, global, "circle")); 2391 JS_SetPropertyStr(ctx, proxy, "plot", JS_GetPropertyStr(ctx, global, "plot")); 2392 JS_SetPropertyStr(ctx, proxy, "write", JS_GetPropertyStr(ctx, global, "write")); 2393 JS_FreeValue(ctx, global); 2394 return proxy; 2395} 2396 2397// paste(painting, dx?, dy?) — alpha-composite a painting onto the current render target 2398static JSValue js_paste(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2399 (void)this_val; 2400 if (!current_rt || !current_rt->graph) return JS_UNDEFINED; 2401 if (argc < 1) return JS_UNDEFINED; 2402 2403 ACFramebuffer *src = JS_GetOpaque(argv[0], painting_class_id); 2404 if (!src) return JS_UNDEFINED; 2405 2406 int dx = 0, dy = 0; 2407 if (argc >= 2) JS_ToInt32(ctx, &dx, argv[1]); 2408 if (argc >= 3) JS_ToInt32(ctx, &dy, argv[2]); 2409 2410 graph_paste(current_rt->graph, src, dx, dy); 2411 return JS_UNDEFINED; 2412} 2413 2414// sound.bpm(val?) — get or set BPM, returns current value 2415static JSValue js_sound_bpm(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2416 (void)this_val; 2417 ACAudio *audio = current_rt->audio; 2418 if (!audio) return JS_NewFloat64(ctx, 120.0); 2419 if (argc >= 1 && JS_IsNumber(argv[0])) { 2420 double val; 2421 JS_ToFloat64(ctx, &val, argv[0]); 2422 if (val > 0) audio->bpm = val; 2423 } 2424 return JS_NewFloat64(ctx, audio->bpm); 2425} 2426 2427// --- DJ deck bindings --- 2428 2429static void blit_argb_nearest(ACFramebuffer *fb, const uint32_t *src, 2430 int sw, int sh, int dx, int dy, int dw, int dh) { 2431 if (!fb || !src || sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0) return; 2432 2433 int x0 = dx < 0 ? 0 : dx; 2434 int y0 = dy < 0 ? 0 : dy; 2435 int x1 = dx + dw; 2436 int y1 = dy + dh; 2437 if (x1 > fb->width) x1 = fb->width; 2438 if (y1 > fb->height) y1 = fb->height; 2439 if (x0 >= x1 || y0 >= y1) return; 2440 2441 for (int y = y0; y < y1; y++) { 2442 int sy = (int)(((int64_t)(y - dy) * sh) / dh); 2443 if (sy < 0) sy = 0; 2444 if (sy >= sh) sy = sh - 1; 2445 const uint32_t *src_row = src + sy * sw; 2446 uint32_t *dst_row = fb->pixels + y * fb->stride; 2447 for (int x = x0; x < x1; x++) { 2448 int sx = (int)(((int64_t)(x - dx) * sw) / dw); 2449 if (sx < 0) sx = 0; 2450 if (sx >= sw) sx = sw - 1; 2451 uint32_t px = src_row[sx]; 2452 if ((px >> 24) == 0) continue; 2453 dst_row[x] = px; 2454 } 2455 } 2456} 2457 2458// sound.deck.load(deck, path) -> true/false 2459static JSValue js_deck_load(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2460 (void)this_val; 2461 if (argc < 2 || !current_rt || !current_rt->audio) return JS_FALSE; 2462 int deck; 2463 JS_ToInt32(ctx, &deck, argv[0]); 2464 const char *path = JS_ToCString(ctx, argv[1]); 2465 if (!path) return JS_FALSE; 2466 int ret = audio_deck_load(current_rt->audio, deck, path); 2467 JS_FreeCString(ctx, path); 2468 return JS_NewBool(ctx, ret == 0); 2469} 2470 2471// sound.deck.play(deck) 2472static JSValue js_deck_play(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2473 (void)this_val; 2474 if (argc < 1 || !current_rt || !current_rt->audio) return JS_UNDEFINED; 2475 int deck; JS_ToInt32(ctx, &deck, argv[0]); 2476 audio_deck_play(current_rt->audio, deck); 2477 return JS_UNDEFINED; 2478} 2479 2480// sound.deck.pause(deck) 2481static JSValue js_deck_pause(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2482 (void)this_val; 2483 if (argc < 1 || !current_rt || !current_rt->audio) return JS_UNDEFINED; 2484 int deck; JS_ToInt32(ctx, &deck, argv[0]); 2485 audio_deck_pause(current_rt->audio, deck); 2486 return JS_UNDEFINED; 2487} 2488 2489// sound.deck.seek(deck, seconds) 2490static JSValue js_deck_seek(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2491 (void)this_val; 2492 if (argc < 2 || !current_rt || !current_rt->audio) return JS_UNDEFINED; 2493 int deck; JS_ToInt32(ctx, &deck, argv[0]); 2494 double sec; JS_ToFloat64(ctx, &sec, argv[1]); 2495 audio_deck_seek(current_rt->audio, deck, sec); 2496 return JS_UNDEFINED; 2497} 2498 2499// sound.deck.setSpeed(deck, speed) 2500static JSValue js_deck_set_speed(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2501 (void)this_val; 2502 if (argc < 2 || !current_rt || !current_rt->audio) return JS_UNDEFINED; 2503 int deck; JS_ToInt32(ctx, &deck, argv[0]); 2504 double spd; JS_ToFloat64(ctx, &spd, argv[1]); 2505 audio_deck_set_speed(current_rt->audio, deck, spd); 2506 return JS_UNDEFINED; 2507} 2508 2509// sound.deck.getPeaks(deck) — returns Float32Array of normalized peak amplitudes 2510static JSValue js_deck_get_peaks(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2511 (void)this_val; 2512 if (argc < 1 || !current_rt || !current_rt->audio) return JS_NULL; 2513 int deck; JS_ToInt32(ctx, &deck, argv[0]); 2514 if (deck < 0 || deck >= AUDIO_MAX_DECKS) return JS_NULL; 2515 ACDeck *dk = &current_rt->audio->decks[deck]; 2516 if (!dk->decoder || !dk->decoder->peaks || dk->decoder->peak_count <= 0) return JS_NULL; 2517 2518 // Build a JS array from the peak data 2519 JSValue arr = JS_NewArray(ctx); 2520 for (int i = 0; i < dk->decoder->peak_count; i++) { 2521 JS_SetPropertyUint32(ctx, arr, i, JS_NewFloat64(ctx, dk->decoder->peaks[i])); 2522 } 2523 return arr; 2524} 2525 2526// sound.deck.prepareVideo(deck[, width, height, fps]) — decode a low-res preview strip 2527static JSValue js_deck_prepare_video(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2528 (void)this_val; 2529 if (argc < 1 || !current_rt || !current_rt->audio) return JS_FALSE; 2530 int deck = 0, width = 96, height = 54, fps = 12; 2531 JS_ToInt32(ctx, &deck, argv[0]); 2532 if (argc > 1) JS_ToInt32(ctx, &width, argv[1]); 2533 if (argc > 2) JS_ToInt32(ctx, &height, argv[2]); 2534 if (argc > 3) JS_ToInt32(ctx, &fps, argv[3]); 2535 if (deck < 0 || deck >= AUDIO_MAX_DECKS) return JS_FALSE; 2536 ACDeck *dk = &current_rt->audio->decks[deck]; 2537 if (!dk->decoder) return JS_FALSE; 2538 int ret = deck_decoder_generate_video_preview(dk->decoder, width, height, fps); 2539 return JS_NewBool(ctx, ret > 0); 2540} 2541 2542// sound.deck.videoBlit(deck, x, y, w, h) — draw current preview frame into the framebuffer 2543static JSValue js_deck_video_blit(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2544 (void)this_val; 2545 if (argc < 5 || !current_rt || !current_rt->audio || !current_rt->graph || !current_rt->graph->fb) 2546 return JS_FALSE; 2547 2548 int deck = 0, x = 0, y = 0, w = 0, h = 0; 2549 JS_ToInt32(ctx, &deck, argv[0]); 2550 JS_ToInt32(ctx, &x, argv[1]); 2551 JS_ToInt32(ctx, &y, argv[2]); 2552 JS_ToInt32(ctx, &w, argv[3]); 2553 JS_ToInt32(ctx, &h, argv[4]); 2554 if (deck < 0 || deck >= AUDIO_MAX_DECKS) return JS_FALSE; 2555 2556 ACDeck *dk = &current_rt->audio->decks[deck]; 2557 if (!dk->decoder || !dk->decoder->video_ready || !dk->decoder->video_frames || 2558 dk->decoder->video_frame_count <= 0 || dk->decoder->video_width <= 0 || 2559 dk->decoder->video_height <= 0 || dk->decoder->video_fps <= 0.0) { 2560 return JS_FALSE; 2561 } 2562 2563 if (w <= 0) w = dk->decoder->video_width; 2564 if (h <= 0) h = dk->decoder->video_height; 2565 2566 int idx = (int)floor(dk->decoder->position * dk->decoder->video_fps + 0.0001); 2567 if (idx < 0) idx = 0; 2568 if (idx >= dk->decoder->video_frame_count) idx = dk->decoder->video_frame_count - 1; 2569 2570 size_t pixels_per_frame = (size_t)dk->decoder->video_width * (size_t)dk->decoder->video_height; 2571 const uint32_t *src = dk->decoder->video_frames + ((size_t)idx * pixels_per_frame); 2572 blit_argb_nearest(current_rt->graph->fb, src, 2573 dk->decoder->video_width, dk->decoder->video_height, 2574 x, y, w, h); 2575 return JS_TRUE; 2576} 2577 2578// sound.deck.setVolume(deck, vol) 2579static JSValue js_deck_set_volume(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2580 (void)this_val; 2581 if (argc < 2 || !current_rt || !current_rt->audio) return JS_UNDEFINED; 2582 int deck; JS_ToInt32(ctx, &deck, argv[0]); 2583 double vol; JS_ToFloat64(ctx, &vol, argv[1]); 2584 audio_deck_set_volume(current_rt->audio, deck, (float)vol); 2585 return JS_UNDEFINED; 2586} 2587 2588// sound.deck.setCrossfader(value) 2589static JSValue js_deck_set_crossfader(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2590 (void)this_val; 2591 if (argc < 1 || !current_rt || !current_rt->audio) return JS_UNDEFINED; 2592 double val; JS_ToFloat64(ctx, &val, argv[0]); 2593 audio_deck_set_crossfader(current_rt->audio, (float)val); 2594 return JS_UNDEFINED; 2595} 2596 2597// sound.deck.setMasterVolume(value) 2598static JSValue js_deck_set_master_vol(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2599 (void)this_val; 2600 if (argc < 1 || !current_rt || !current_rt->audio) return JS_UNDEFINED; 2601 double val; JS_ToFloat64(ctx, &val, argv[0]); 2602 audio_deck_set_master_volume(current_rt->audio, (float)val); 2603 return JS_UNDEFINED; 2604} 2605 2606// Build the sound object for the API 2607static JSValue build_sound_obj(JSContext *ctx, ACRuntime *rt) { 2608 JSValue sound = JS_NewObject(ctx); 2609 2610 // Core synthesis functions 2611 JS_SetPropertyStr(ctx, sound, "synth", JS_NewCFunction(ctx, js_synth, "synth", 1)); 2612 JS_SetPropertyStr(ctx, sound, "freq", JS_NewCFunction(ctx, js_sound_freq, "freq", 1)); 2613 JS_SetPropertyStr(ctx, sound, "play", JS_NewCFunction(ctx, js_noop, "play", 2)); 2614 JS_SetPropertyStr(ctx, sound, "kill", JS_NewCFunction(ctx, js_sound_kill, "kill", 2)); 2615 JS_SetPropertyStr(ctx, sound, "update", JS_NewCFunction(ctx, js_sound_update, "update", 2)); 2616 JS_SetPropertyStr(ctx, sound, "bpm", JS_NewCFunction(ctx, js_sound_bpm, "bpm", 1)); 2617 JS_SetPropertyStr(ctx, sound, "time", JS_NewFloat64(ctx, rt->audio ? rt->audio->time : 0.0)); 2618 JS_SetPropertyStr(ctx, sound, "registerSample", JS_NewCFunction(ctx, js_noop, "registerSample", 3)); 2619 2620 // speaker sub-object 2621 JSValue speaker = JS_NewObject(ctx); 2622 JS_SetPropertyStr(ctx, speaker, "poll", JS_NewCFunction(ctx, js_noop, "poll", 0)); 2623 JS_SetPropertyStr(ctx, speaker, "getRecentBuffer", 2624 JS_NewCFunction(ctx, js_speaker_get_recent_buffer, "getRecentBuffer", 1)); 2625 JS_SetPropertyStr(ctx, speaker, "sampleRate", 2626 JS_NewInt32(ctx, rt->audio ? (int)rt->audio->actual_rate : AUDIO_SAMPLE_RATE)); 2627 2628 // sound.tape — read-only tape recorder state. The actual start/stop 2629 // is driven by the PrintScreen key handler in ac-native.c (which 2630 // owns the MP4 file lifecycle, TTS announce, on-screen overlay, and 2631 // cloud upload). JS just observes. 2632 JSValue tape = JS_NewObject(ctx); 2633 JS_SetPropertyStr(ctx, tape, "recording", 2634 JS_NewCFunction(ctx, js_tape_recording, "recording", 0)); 2635 JS_SetPropertyStr(ctx, tape, "elapsed", 2636 JS_NewCFunction(ctx, js_tape_elapsed, "elapsed", 0)); 2637 JS_SetPropertyStr(ctx, sound, "tape", tape); 2638 2639 // waveforms (32 samples, left channel only — sufficient for visualizer) 2640 JSValue waveforms = JS_NewObject(ctx); 2641 JSValue wf_left = JS_NewArray(ctx); 2642 if (rt->audio) { 2643 for (int i = 0; i < 32; i++) { 2644 int idx = (rt->audio->waveform_pos - 32 + i + AUDIO_WAVEFORM_SIZE) % AUDIO_WAVEFORM_SIZE; 2645 JS_SetPropertyUint32(ctx, wf_left, i, JS_NewFloat64(ctx, rt->audio->waveform_left[idx])); 2646 } 2647 } 2648 JS_SetPropertyStr(ctx, waveforms, "left", wf_left); 2649 JS_SetPropertyStr(ctx, speaker, "waveforms", waveforms); 2650 2651 // amplitudes 2652 JSValue amplitudes = JS_NewObject(ctx); 2653 JS_SetPropertyStr(ctx, amplitudes, "left", JS_NewFloat64(ctx, rt->audio ? rt->audio->amplitude_left : 0.0)); 2654 JS_SetPropertyStr(ctx, amplitudes, "right", JS_NewFloat64(ctx, rt->audio ? rt->audio->amplitude_right : 0.0)); 2655 JS_SetPropertyStr(ctx, speaker, "amplitudes", amplitudes); 2656 2657 // frequencies (stub) 2658 JSValue frequencies = JS_NewObject(ctx); 2659 JS_SetPropertyStr(ctx, frequencies, "left", JS_NewArray(ctx)); 2660 JS_SetPropertyStr(ctx, speaker, "frequencies", frequencies); 2661 2662 // beat detection (stub) 2663 JSValue beat = JS_NewObject(ctx); 2664 JS_SetPropertyStr(ctx, beat, "detected", JS_FALSE); 2665 JS_SetPropertyStr(ctx, speaker, "beat", beat); 2666 2667 // System volume (0-100) 2668 JS_SetPropertyStr(ctx, speaker, "systemVolume", 2669 JS_NewInt32(ctx, rt->audio ? rt->audio->system_volume : 100)); 2670 2671 JS_SetPropertyStr(ctx, sound, "speaker", speaker); 2672 2673 // room 2674 JSValue room = JS_NewObject(ctx); 2675 JS_SetPropertyStr(ctx, room, "toggle", JS_NewCFunction(ctx, js_room_toggle, "toggle", 0)); 2676 JS_SetPropertyStr(ctx, room, "setMix", JS_NewCFunction(ctx, js_set_room_mix, "setMix", 1)); 2677 JS_SetPropertyStr(ctx, room, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->room_mix : 0.0)); 2678 JS_SetPropertyStr(ctx, room, "set", JS_NewCFunction(ctx, js_noop, "set", 1)); 2679 JS_SetPropertyStr(ctx, room, "get", JS_NewCFunction(ctx, js_noop, "get", 0)); 2680 JS_SetPropertyStr(ctx, sound, "room", room); 2681 2682 // glitch 2683 JSValue glitch = JS_NewObject(ctx); 2684 JS_SetPropertyStr(ctx, glitch, "toggle", JS_NewCFunction(ctx, js_glitch_toggle, "toggle", 0)); 2685 JS_SetPropertyStr(ctx, glitch, "setMix", JS_NewCFunction(ctx, js_set_glitch_mix, "setMix", 1)); 2686 JS_SetPropertyStr(ctx, glitch, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->glitch_mix : 0.0)); 2687 JS_SetPropertyStr(ctx, glitch, "set", JS_NewCFunction(ctx, js_set_glitch_mix, "set", 1)); 2688 JS_SetPropertyStr(ctx, glitch, "get", JS_NewCFunction(ctx, js_noop, "get", 0)); 2689 JS_SetPropertyStr(ctx, sound, "glitch", glitch); 2690 2691 // fx (dry/wet for entire FX chain) 2692 JSValue fx = JS_NewObject(ctx); 2693 JS_SetPropertyStr(ctx, fx, "setMix", JS_NewCFunction(ctx, js_set_fx_mix, "setMix", 1)); 2694 JS_SetPropertyStr(ctx, fx, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->fx_mix : 1.0)); 2695 JS_SetPropertyStr(ctx, sound, "fx", fx); 2696 2697 // microphone 2698 JSValue mic = JS_NewObject(ctx); 2699 JS_SetPropertyStr(ctx, mic, "open", JS_NewCFunction(ctx, js_mic_open, "open", 0)); 2700 JS_SetPropertyStr(ctx, mic, "close", JS_NewCFunction(ctx, js_mic_close, "close", 0)); 2701 JS_SetPropertyStr(ctx, mic, "rec", JS_NewCFunction(ctx, js_mic_rec, "rec", 0)); 2702 JS_SetPropertyStr(ctx, mic, "cut", JS_NewCFunction(ctx, js_mic_cut, "cut", 0)); 2703 JS_SetPropertyStr(ctx, mic, "recording", 2704 JS_NewBool(ctx, rt->audio ? rt->audio->recording : 0)); 2705 JS_SetPropertyStr(ctx, mic, "connected", 2706 JS_NewBool(ctx, rt->audio ? rt->audio->mic_connected : 0)); 2707 JS_SetPropertyStr(ctx, mic, "hot", 2708 JS_NewBool(ctx, rt->audio ? rt->audio->mic_hot : 0)); 2709 JS_SetPropertyStr(ctx, mic, "sampleLength", 2710 JS_NewInt32(ctx, rt->audio ? rt->audio->sample_len : 0)); 2711 JS_SetPropertyStr(ctx, mic, "sampleRate", 2712 JS_NewInt32(ctx, rt->audio ? (int)rt->audio->sample_rate : 0)); 2713 JS_SetPropertyStr(ctx, mic, "level", 2714 JS_NewFloat64(ctx, rt->audio ? rt->audio->mic_level : 0.0)); 2715 JS_SetPropertyStr(ctx, mic, "lastChunk", 2716 JS_NewInt32(ctx, rt->audio ? rt->audio->mic_last_chunk : 0)); 2717 JS_SetPropertyStr(ctx, mic, "device", 2718 JS_NewString(ctx, (rt->audio && rt->audio->mic_device[0]) ? rt->audio->mic_device : "none")); 2719 JS_SetPropertyStr(ctx, mic, "lastError", 2720 JS_NewString(ctx, (rt->audio && rt->audio->mic_last_error[0]) ? rt->audio->mic_last_error : "")); 2721 // Mic level is already exposed as mic.level (single float, no array overhead) 2722 JS_SetPropertyStr(ctx, sound, "microphone", mic); 2723 2724 // sample playback 2725 JSValue samp = JS_NewObject(ctx); 2726 JS_SetPropertyStr(ctx, samp, "play", JS_NewCFunction(ctx, js_sample_play, "play", 1)); 2727 JS_SetPropertyStr(ctx, samp, "kill", JS_NewCFunction(ctx, js_sample_kill, "kill", 2)); 2728 JS_SetPropertyStr(ctx, samp, "getData", JS_NewCFunction(ctx, js_sample_get_data, "getData", 0)); 2729 JS_SetPropertyStr(ctx, samp, "loadData", JS_NewCFunction(ctx, js_sample_load_data, "loadData", 2)); 2730 JS_SetPropertyStr(ctx, samp, "saveTo", JS_NewCFunction(ctx, js_sample_save_to, "saveTo", 1)); 2731 JS_SetPropertyStr(ctx, samp, "loadFrom", JS_NewCFunction(ctx, js_sample_load_from, "loadFrom", 1)); 2732 JS_SetPropertyStr(ctx, sound, "sample", samp); 2733 2734 // dedicated global replay voice/buffer 2735 JSValue replay = JS_NewObject(ctx); 2736 JS_SetPropertyStr(ctx, replay, "play", JS_NewCFunction(ctx, js_replay_play, "play", 1)); 2737 JS_SetPropertyStr(ctx, replay, "kill", JS_NewCFunction(ctx, js_replay_kill, "kill", 2)); 2738 JS_SetPropertyStr(ctx, replay, "loadData", JS_NewCFunction(ctx, js_replay_load_data, "loadData", 2)); 2739 JS_SetPropertyStr(ctx, sound, "replay", replay); 2740 2741 // DJ deck 2742 JSValue deck_obj = JS_NewObject(ctx); 2743 JS_SetPropertyStr(ctx, deck_obj, "load", JS_NewCFunction(ctx, js_deck_load, "load", 2)); 2744 JS_SetPropertyStr(ctx, deck_obj, "play", JS_NewCFunction(ctx, js_deck_play, "play", 1)); 2745 JS_SetPropertyStr(ctx, deck_obj, "pause", JS_NewCFunction(ctx, js_deck_pause, "pause", 1)); 2746 JS_SetPropertyStr(ctx, deck_obj, "seek", JS_NewCFunction(ctx, js_deck_seek, "seek", 2)); 2747 JS_SetPropertyStr(ctx, deck_obj, "setSpeed", JS_NewCFunction(ctx, js_deck_set_speed, "setSpeed", 2)); 2748 JS_SetPropertyStr(ctx, deck_obj, "setVolume", JS_NewCFunction(ctx, js_deck_set_volume, "setVolume", 2)); 2749 JS_SetPropertyStr(ctx, deck_obj, "setCrossfader", JS_NewCFunction(ctx, js_deck_set_crossfader, "setCrossfader", 1)); 2750 JS_SetPropertyStr(ctx, deck_obj, "setMasterVolume", JS_NewCFunction(ctx, js_deck_set_master_vol, "setMasterVolume", 1)); 2751 JS_SetPropertyStr(ctx, deck_obj, "getPeaks", JS_NewCFunction(ctx, js_deck_get_peaks, "getPeaks", 1)); 2752 JS_SetPropertyStr(ctx, deck_obj, "prepareVideo", JS_NewCFunction(ctx, js_deck_prepare_video, "prepareVideo", 4)); 2753 JS_SetPropertyStr(ctx, deck_obj, "videoBlit", JS_NewCFunction(ctx, js_deck_video_blit, "videoBlit", 5)); 2754 2755 // Deck state (read-only, rebuilt each frame) 2756 JSValue decks_arr = JS_NewArray(ctx); 2757 ACAudio *aud = rt->audio; 2758 for (int d = 0; d < AUDIO_MAX_DECKS; d++) { 2759 JSValue di = JS_NewObject(ctx); 2760 if (aud) { 2761 ACDeck *dk = &aud->decks[d]; 2762 JS_SetPropertyStr(ctx, di, "loaded", JS_NewBool(ctx, dk->active)); 2763 JS_SetPropertyStr(ctx, di, "playing", JS_NewBool(ctx, dk->playing)); 2764 JS_SetPropertyStr(ctx, di, "volume", JS_NewFloat64(ctx, dk->volume)); 2765 if (dk->decoder) { 2766 JS_SetPropertyStr(ctx, di, "position", JS_NewFloat64(ctx, dk->decoder->position)); 2767 JS_SetPropertyStr(ctx, di, "duration", JS_NewFloat64(ctx, dk->decoder->duration)); 2768 JS_SetPropertyStr(ctx, di, "speed", JS_NewFloat64(ctx, dk->decoder->speed)); 2769 JS_SetPropertyStr(ctx, di, "title", JS_NewString(ctx, dk->decoder->title)); 2770 JS_SetPropertyStr(ctx, di, "artist", JS_NewString(ctx, dk->decoder->artist)); 2771 JS_SetPropertyStr(ctx, di, "finished", JS_NewBool(ctx, dk->decoder->finished)); 2772 JS_SetPropertyStr(ctx, di, "error", JS_NewString(ctx, dk->decoder->error ? dk->decoder->error_msg : "")); 2773 JS_SetPropertyStr(ctx, di, "videoReady", JS_NewBool(ctx, dk->decoder->video_ready)); 2774 JS_SetPropertyStr(ctx, di, "videoWidth", JS_NewInt32(ctx, dk->decoder->video_width)); 2775 JS_SetPropertyStr(ctx, di, "videoHeight", JS_NewInt32(ctx, dk->decoder->video_height)); 2776 JS_SetPropertyStr(ctx, di, "videoFps", JS_NewFloat64(ctx, dk->decoder->video_fps)); 2777 JS_SetPropertyStr(ctx, di, "videoFrames", JS_NewInt32(ctx, dk->decoder->video_frame_count)); 2778 } else { 2779 JS_SetPropertyStr(ctx, di, "position", JS_NewFloat64(ctx, 0)); 2780 JS_SetPropertyStr(ctx, di, "duration", JS_NewFloat64(ctx, 0)); 2781 JS_SetPropertyStr(ctx, di, "speed", JS_NewFloat64(ctx, 1.0)); 2782 JS_SetPropertyStr(ctx, di, "title", JS_NewString(ctx, "")); 2783 JS_SetPropertyStr(ctx, di, "artist", JS_NewString(ctx, "")); 2784 JS_SetPropertyStr(ctx, di, "finished", JS_FALSE); 2785 JS_SetPropertyStr(ctx, di, "error", JS_NewString(ctx, "")); 2786 JS_SetPropertyStr(ctx, di, "videoReady", JS_FALSE); 2787 JS_SetPropertyStr(ctx, di, "videoWidth", JS_NewInt32(ctx, 0)); 2788 JS_SetPropertyStr(ctx, di, "videoHeight", JS_NewInt32(ctx, 0)); 2789 JS_SetPropertyStr(ctx, di, "videoFps", JS_NewFloat64(ctx, 0)); 2790 JS_SetPropertyStr(ctx, di, "videoFrames", JS_NewInt32(ctx, 0)); 2791 } 2792 } 2793 JS_SetPropertyUint32(ctx, decks_arr, d, di); 2794 } 2795 JS_SetPropertyStr(ctx, deck_obj, "decks", decks_arr); 2796 JS_SetPropertyStr(ctx, deck_obj, "crossfaderValue", 2797 JS_NewFloat64(ctx, aud ? aud->crossfader : 0.5)); 2798 JS_SetPropertyStr(ctx, deck_obj, "masterVolume", 2799 JS_NewFloat64(ctx, aud ? aud->deck_master_volume : 0.8)); 2800 JS_SetPropertyStr(ctx, sound, "deck", deck_obj); 2801 2802 // TTS 2803 JS_SetPropertyStr(ctx, sound, "speak", JS_NewCFunction(ctx, js_speak, "speak", 1)); 2804 JS_SetPropertyStr(ctx, sound, "speakVoice", JS_NewCFunction(ctx, js_speak_voice, "speakVoice", 2)); 2805 JS_SetPropertyStr(ctx, sound, "speakCached", JS_NewCFunction(ctx, js_speak_cached, "speakCached", 1)); 2806 2807 // sound.paint (visualization helpers) 2808 JSValue sound_paint = JS_NewObject(ctx); 2809 JS_SetPropertyStr(ctx, sound_paint, "bars", JS_NewCFunction(ctx, js_noop, "bars", 10)); 2810 JS_SetPropertyStr(ctx, sound_paint, "audioEngineBadge", JS_NewCFunction(ctx, js_noop, "audioEngineBadge", 5)); 2811 JS_SetPropertyStr(ctx, sound, "paint", sound_paint); 2812 2813 // midi (stub) 2814 JSValue midi = JS_NewObject(ctx); 2815 JS_SetPropertyStr(ctx, midi, "connect", JS_NewCFunction(ctx, js_noop, "connect", 0)); 2816 JS_SetPropertyStr(ctx, midi, "noteOn", JS_NewCFunction(ctx, js_usb_midi_note_on, "noteOn", 3)); 2817 JS_SetPropertyStr(ctx, midi, "noteOff", JS_NewCFunction(ctx, js_usb_midi_note_off, "noteOff", 3)); 2818 JS_SetPropertyStr(ctx, midi, "allNotesOff", JS_NewCFunction(ctx, js_usb_midi_all_notes_off, "allNotesOff", 1)); 2819 JS_SetPropertyStr(ctx, sound, "midi", midi); 2820 2821 // daw (stub) 2822 JSValue daw = JS_NewObject(ctx); 2823 JS_SetPropertyStr(ctx, daw, "bpm", JS_UNDEFINED); 2824 JS_SetPropertyStr(ctx, sound, "daw", daw); 2825 2826 return sound; 2827} 2828 2829// Read a small sysfs file into a buffer, return length or -1 2830static int read_sysfs(const char *path, char *buf, int bufsize) { 2831 FILE *f = fopen(path, "r"); 2832 if (!f) return -1; 2833 int len = (int)fread(buf, 1, bufsize - 1, f); 2834 fclose(f); 2835 if (len > 0 && buf[len-1] == '\n') len--; 2836 buf[len] = 0; 2837 return len; 2838} 2839 2840// ============================================================ 2841// WiFi JS bindings 2842// ============================================================ 2843 2844static JSValue js_wifi_scan(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2845 (void)this_val; (void)argc; (void)argv; 2846 if (current_rt->wifi) wifi_scan(current_rt->wifi); 2847 return JS_UNDEFINED; 2848} 2849 2850static JSValue js_wifi_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2851 (void)this_val; 2852 if (!current_rt->wifi || argc < 1) return JS_UNDEFINED; 2853 const char *ssid = JS_ToCString(ctx, argv[0]); 2854 const char *pass = (argc >= 2 && JS_IsString(argv[1])) ? JS_ToCString(ctx, argv[1]) : NULL; 2855 if (ssid) wifi_connect(current_rt->wifi, ssid, pass); 2856 if (ssid) JS_FreeCString(ctx, ssid); 2857 if (pass) JS_FreeCString(ctx, pass); 2858 return JS_UNDEFINED; 2859} 2860 2861static JSValue js_wifi_disconnect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2862 (void)this_val; (void)argc; (void)argv; 2863 if (current_rt->wifi) wifi_disconnect(current_rt->wifi); 2864 return JS_UNDEFINED; 2865} 2866 2867static JSValue build_wifi_obj(JSContext *ctx, ACWifi *wifi) { 2868 JSValue obj = JS_NewObject(ctx); 2869 2870 JS_SetPropertyStr(ctx, obj, "scan", JS_NewCFunction(ctx, js_wifi_scan, "scan", 0)); 2871 JS_SetPropertyStr(ctx, obj, "connect", JS_NewCFunction(ctx, js_wifi_connect, "connect", 2)); 2872 JS_SetPropertyStr(ctx, obj, "disconnect", JS_NewCFunction(ctx, js_wifi_disconnect, "disconnect", 0)); 2873 2874 if (wifi) { 2875 // State is updated by the wifi worker thread — just read it here 2876 JS_SetPropertyStr(ctx, obj, "state", JS_NewInt32(ctx, wifi->state)); 2877 JS_SetPropertyStr(ctx, obj, "status", JS_NewString(ctx, wifi->status_msg)); 2878 JS_SetPropertyStr(ctx, obj, "connected", JS_NewBool(ctx, wifi->state == WIFI_STATE_CONNECTED)); 2879 JS_SetPropertyStr(ctx, obj, "ssid", JS_NewString(ctx, wifi->connected_ssid)); 2880 JS_SetPropertyStr(ctx, obj, "ip", JS_NewString(ctx, wifi->ip_address)); 2881 JS_SetPropertyStr(ctx, obj, "iface", JS_NewString(ctx, wifi->iface)); 2882 JS_SetPropertyStr(ctx, obj, "signal", JS_NewInt32(ctx, wifi->signal_strength)); 2883 2884 // Networks array 2885 JSValue networks = JS_NewArray(ctx); 2886 for (int i = 0; i < wifi->network_count; i++) { 2887 JSValue net = JS_NewObject(ctx); 2888 JS_SetPropertyStr(ctx, net, "ssid", JS_NewString(ctx, wifi->networks[i].ssid)); 2889 JS_SetPropertyStr(ctx, net, "signal", JS_NewInt32(ctx, wifi->networks[i].signal)); 2890 JS_SetPropertyStr(ctx, net, "encrypted", JS_NewBool(ctx, wifi->networks[i].encrypted)); 2891 JS_SetPropertyUint32(ctx, networks, i, net); 2892 } 2893 JS_SetPropertyStr(ctx, obj, "networks", networks); 2894 2895 // Log ring buffer (last 32 messages from wifi thread) 2896 JSValue logs = JS_NewArray(ctx); 2897 int total = wifi->log_count; 2898 int count = total < 32 ? total : 32; 2899 for (int i = 0; i < count; i++) { 2900 // Read in chronological order (oldest first) 2901 int idx = (total >= 32) ? ((total - 32 + i) % 32) : i; 2902 JS_SetPropertyUint32(ctx, logs, i, 2903 JS_NewString(ctx, wifi->log[idx])); 2904 } 2905 JS_SetPropertyStr(ctx, obj, "logs", logs); 2906 } else { 2907 extern int wifi_disabled; 2908 JS_SetPropertyStr(ctx, obj, "state", JS_NewInt32(ctx, WIFI_STATE_OFF)); 2909 JS_SetPropertyStr(ctx, obj, "status", 2910 JS_NewString(ctx, wifi_disabled ? "disabled" : "no wifi")); 2911 JS_SetPropertyStr(ctx, obj, "disabled", JS_NewBool(ctx, wifi_disabled)); 2912 JS_SetPropertyStr(ctx, obj, "connected", JS_FALSE); 2913 JS_SetPropertyStr(ctx, obj, "networks", JS_NewArray(ctx)); 2914 } 2915 2916 return obj; 2917} 2918 2919// system.hdmi(r, g, b) — fill secondary display with solid color 2920static JSValue js_hdmi_fill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2921 (void)this_val; 2922 if (!current_rt || !current_rt->hdmi || !current_rt->hdmi->active) return JS_UNDEFINED; 2923 if (argc < 3) return JS_UNDEFINED; 2924 int r, g, b; 2925 JS_ToInt32(ctx, &r, argv[0]); 2926 JS_ToInt32(ctx, &g, argv[1]); 2927 JS_ToInt32(ctx, &b, argv[2]); 2928 drm_secondary_fill(current_rt->hdmi, (uint8_t)r, (uint8_t)g, (uint8_t)b); 2929 return JS_UNDEFINED; 2930} 2931 2932// system.readFile(path, [lastNLines]) — read a file from disk, returns string or null 2933// If lastNLines is provided, returns only the last N lines. 2934static JSValue js_read_file(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2935 (void)this_val; 2936 if (argc < 1) return JS_NULL; 2937 const char *path = JS_ToCString(ctx, argv[0]); 2938 if (!path) return JS_NULL; 2939 int lastN = 0; 2940 if (argc >= 2) JS_ToInt32(ctx, &lastN, argv[1]); 2941 FILE *fp = fopen(path, "r"); 2942 if (!fp) { JS_FreeCString(ctx, path); return JS_NULL; } 2943 fseek(fp, 0, SEEK_END); 2944 long sz = ftell(fp); 2945 if (sz <= 0) { fclose(fp); JS_FreeCString(ctx, path); return JS_NULL; } 2946 if (sz > 65536) { 2947 fseek(fp, -65536, SEEK_END); 2948 sz = 65536; 2949 } else { 2950 rewind(fp); 2951 } 2952 char *buf = malloc(sz + 1); 2953 if (!buf) { fclose(fp); JS_FreeCString(ctx, path); return JS_NULL; } 2954 long rd = (long)fread(buf, 1, sz, fp); 2955 buf[rd] = 0; 2956 fclose(fp); 2957 JS_FreeCString(ctx, path); 2958 if (lastN > 0 && rd > 0) { 2959 int nl_count = 0; 2960 char *p = buf + rd - 1; 2961 if (*p == '\n') p--; 2962 while (p > buf) { 2963 if (*p == '\n') { nl_count++; if (nl_count >= lastN) { p++; break; } } 2964 p--; 2965 } 2966 JSValue result = JS_NewString(ctx, p); 2967 free(buf); 2968 return result; 2969 } 2970 JSValue result = JS_NewStringLen(ctx, buf, rd); 2971 free(buf); 2972 return result; 2973} 2974 2975// system.writeFile(path, data) — write a string to disk, returns true/false 2976static JSValue js_write_file(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2977 (void)this_val; 2978 if (argc < 2) return JS_FALSE; 2979 const char *path = JS_ToCString(ctx, argv[0]); 2980 const char *data = JS_ToCString(ctx, argv[1]); 2981 if (!path || !data) { 2982 if (path) JS_FreeCString(ctx, path); 2983 if (data) JS_FreeCString(ctx, data); 2984 return JS_FALSE; 2985 } 2986 FILE *fp = fopen(path, "w"); 2987 int ok = 0; 2988 if (fp) { 2989 fputs(data, fp); 2990 fclose(fp); 2991 sync(); // flush to USB 2992 ok = 1; 2993 } 2994 JS_FreeCString(ctx, path); 2995 JS_FreeCString(ctx, data); 2996 return JS_NewBool(ctx, ok); 2997} 2998 2999// system.deleteFile(path) — unlink a file, returns true on success 3000static JSValue js_delete_file(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3001 (void)this_val; 3002 if (argc < 1) return JS_FALSE; 3003 const char *path = JS_ToCString(ctx, argv[0]); 3004 if (!path) return JS_FALSE; 3005 int ok = (unlink(path) == 0); 3006 if (ok) sync(); 3007 JS_FreeCString(ctx, path); 3008 return JS_NewBool(ctx, ok); 3009} 3010 3011// system.diskInfo(path) — returns {total, free, available, blockSize, fstype} 3012// for the filesystem containing `path`, or null on error. 3013static JSValue js_disk_info(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3014 (void)this_val; 3015 if (argc < 1) return JS_NULL; 3016 const char *path = JS_ToCString(ctx, argv[0]); 3017 if (!path) return JS_NULL; 3018 struct statvfs vfs; 3019 int rc = statvfs(path, &vfs); 3020 JS_FreeCString(ctx, path); 3021 if (rc != 0) return JS_NULL; 3022 JSValue obj = JS_NewObject(ctx); 3023 uint64_t bs = (uint64_t)vfs.f_frsize ? (uint64_t)vfs.f_frsize : (uint64_t)vfs.f_bsize; 3024 JS_SetPropertyStr(ctx, obj, "total", JS_NewInt64(ctx, (int64_t)(vfs.f_blocks * bs))); 3025 JS_SetPropertyStr(ctx, obj, "free", JS_NewInt64(ctx, (int64_t)(vfs.f_bfree * bs))); 3026 JS_SetPropertyStr(ctx, obj, "available", JS_NewInt64(ctx, (int64_t)(vfs.f_bavail * bs))); 3027 JS_SetPropertyStr(ctx, obj, "blockSize", JS_NewInt64(ctx, (int64_t)bs)); 3028 return obj; 3029} 3030 3031// system.blockDevices() — list /sys/block entries with size + removable + model. 3032// Returns array of {name, sizeBytes, removable, model, vendor} or null. 3033static JSValue js_block_devices(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3034 (void)this_val; (void)argc; (void)argv; 3035 DIR *dir = opendir("/sys/block"); 3036 if (!dir) return JS_NULL; 3037 JSValue arr = JS_NewArray(ctx); 3038 struct dirent *ent; 3039 int idx = 0; 3040 while ((ent = readdir(dir)) != NULL) { 3041 if (ent->d_name[0] == '.') continue; 3042 // Skip loop/ram/dm pseudo devs 3043 if (strncmp(ent->d_name, "loop", 4) == 0) continue; 3044 if (strncmp(ent->d_name, "ram", 3) == 0) continue; 3045 if (strncmp(ent->d_name, "dm-", 3) == 0) continue; 3046 char p[256]; char buf[128]; 3047 JSValue obj = JS_NewObject(ctx); 3048 JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, ent->d_name)); 3049 // size (in 512-byte sectors) 3050 snprintf(p, sizeof(p), "/sys/block/%s/size", ent->d_name); 3051 FILE *f = fopen(p, "r"); long long sectors = 0; 3052 if (f) { fscanf(f, "%lld", &sectors); fclose(f); } 3053 JS_SetPropertyStr(ctx, obj, "sizeBytes", JS_NewInt64(ctx, sectors * 512LL)); 3054 // removable flag 3055 snprintf(p, sizeof(p), "/sys/block/%s/removable", ent->d_name); 3056 int removable = 0; 3057 f = fopen(p, "r"); 3058 if (f) { fscanf(f, "%d", &removable); fclose(f); } 3059 JS_SetPropertyStr(ctx, obj, "removable", JS_NewBool(ctx, removable != 0)); 3060 // vendor + model 3061 const char *fields[][2] = { {"vendor", "vendor"}, {"model", "model"}, {NULL, NULL} }; 3062 for (int i = 0; fields[i][0]; i++) { 3063 snprintf(p, sizeof(p), "/sys/block/%s/device/%s", ent->d_name, fields[i][0]); 3064 f = fopen(p, "r"); 3065 if (f) { 3066 if (fgets(buf, sizeof(buf), f)) { 3067 // trim trailing whitespace 3068 size_t L = strlen(buf); 3069 while (L > 0 && (buf[L-1] == '\n' || buf[L-1] == ' ' || buf[L-1] == '\t')) buf[--L] = 0; 3070 JS_SetPropertyStr(ctx, obj, fields[i][1], JS_NewString(ctx, buf)); 3071 } 3072 fclose(f); 3073 } 3074 } 3075 JS_SetPropertyUint32(ctx, arr, idx++, obj); 3076 } 3077 closedir(dir); 3078 return arr; 3079} 3080 3081// system.listDir(path) — returns [{name, isDir, size}, ...] or null 3082static JSValue js_list_dir(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3083 (void)this_val; 3084 if (argc < 1) return JS_NULL; 3085 const char *path = JS_ToCString(ctx, argv[0]); 3086 if (!path) return JS_NULL; 3087 3088 DIR *dir = opendir(path); 3089 if (!dir) { 3090 JS_FreeCString(ctx, path); 3091 return JS_NULL; 3092 } 3093 3094 JSValue arr = JS_NewArray(ctx); 3095 struct dirent *ent; 3096 int idx = 0; 3097 while ((ent = readdir(dir)) != NULL) { 3098 // Skip . and .. 3099 if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || 3100 (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) continue; 3101 3102 JSValue obj = JS_NewObject(ctx); 3103 JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, ent->d_name)); 3104 3105 // stat for size and type 3106 char full[1024]; 3107 snprintf(full, sizeof(full), "%s/%s", path, ent->d_name); 3108 struct stat st; 3109 int is_dir = 0; 3110 int64_t size = 0; 3111 if (stat(full, &st) == 0) { 3112 is_dir = S_ISDIR(st.st_mode); 3113 size = st.st_size; 3114 } 3115 JS_SetPropertyStr(ctx, obj, "isDir", JS_NewBool(ctx, is_dir)); 3116 JS_SetPropertyStr(ctx, obj, "size", JS_NewInt64(ctx, size)); 3117 3118 JS_SetPropertyUint32(ctx, arr, idx++, obj); 3119 } 3120 closedir(dir); 3121 JS_FreeCString(ctx, path); 3122 return arr; 3123} 3124 3125// system.mountMusic() — non-blocking secondary USB probe. 3126// Schedules a background mount check for /media and immediately returns the 3127// last known mounted state. JS reads system.mountMusicMounted / 3128// system.mountMusicPending for cached state. 3129static pthread_mutex_t music_mount_mu = PTHREAD_MUTEX_INITIALIZER; 3130static volatile int music_mount_pending = 0; 3131static volatile int music_mount_state = 0; 3132static int music_mount_last_result = -1; 3133static char music_mount_last_skip_base[64] = ""; 3134 3135static int probe_mount_music_once(void) { 3136 3137 mkdir("/media", 0755); 3138 3139 // Check /proc/mounts for any existing mount at /media, and capture the device. 3140 char media_dev[128] = {0}; 3141 FILE *mf = fopen("/proc/mounts", "r"); 3142 if (mf) { 3143 char line[512]; 3144 while (fgets(line, sizeof(line), mf)) { 3145 char dev[128], target[128]; 3146 if (sscanf(line, "%127s %127s", dev, target) == 2 && strcmp(target, "/media") == 0) { 3147 strncpy(media_dev, dev, sizeof(media_dev) - 1); 3148 break; 3149 } 3150 } 3151 fclose(mf); 3152 } 3153 3154 if (media_dev[0]) { 3155 // There's something mounted at /media. Verify the backing device is still 3156 // physically present (USB stick didn't get yanked). 3157 struct stat ds; 3158 int dev_alive = (stat(media_dev, &ds) == 0 && S_ISBLK(ds.st_mode)); 3159 // Extra check: opendir succeeds on a live mount but may EIO on a stale one. 3160 int dir_ok = 0; 3161 if (dev_alive) { 3162 DIR *d = opendir("/media"); 3163 if (d) { dir_ok = 1; closedir(d); } 3164 } 3165 if (dev_alive && dir_ok) { 3166 music_mount_last_result = 1; 3167 return 1; // legitimate, live mount 3168 } 3169 // Stale: device is gone or dir unreadable. Lazy-unmount so we can remount. 3170 ac_log("[dj] stale /media mount (dev=%s, alive=%d, dir=%d) — detaching\n", 3171 media_dev, dev_alive, dir_ok); 3172 if (umount2("/media", MNT_DETACH) != 0) { 3173 ac_log("[dj] umount /media failed: %s\n", strerror(errno)); 3174 } 3175 } 3176 3177 // Find ALL mounted devices to skip (boot USB may have multiple partitions) 3178 char skip_bases[8][64]; 3179 int skip_count = 0; 3180 FILE *fp = fopen("/proc/mounts", "r"); 3181 if (fp) { 3182 char line[256]; 3183 while (fgets(line, sizeof(line), fp) && skip_count < 8) { 3184 char mdev[64]; 3185 if (sscanf(line, "%63s", mdev) == 1 && strncmp(mdev, "/dev/sd", 7) == 0) { 3186 // Strip partition number to get base 3187 char base[64]; 3188 snprintf(base, sizeof(base), "%s", mdev); 3189 int len = strlen(base); 3190 while (len > 0 && base[len-1] >= '0' && base[len-1] <= '9') base[--len] = '\0'; 3191 // Add if not already in skip list 3192 int found = 0; 3193 for (int i = 0; i < skip_count; i++) 3194 if (strcmp(skip_bases[i], base) == 0) { found = 1; break; } 3195 if (!found) { 3196 strncpy(skip_bases[skip_count++], base, 63); 3197 if (strcmp(music_mount_last_skip_base, base) != 0) { 3198 snprintf(music_mount_last_skip_base, sizeof(music_mount_last_skip_base), "%s", base); 3199 ac_log("[dj] skip boot device: %s\n", base); 3200 } 3201 } 3202 } 3203 } 3204 fclose(fp); 3205 } 3206 3207 // Try mounting partitions on non-boot USB devices 3208 const char *bases[] = { "/dev/sda", "/dev/sdb", "/dev/sdc", "/dev/sdd", NULL }; 3209 for (int b = 0; bases[b]; b++) { 3210 int skip = 0; 3211 for (int i = 0; i < skip_count; i++) 3212 if (strcmp(bases[b], skip_bases[i]) == 0) { skip = 1; break; } 3213 if (skip) continue; 3214 3215 for (int p = 1; p <= 8; p++) { 3216 char dev[64]; 3217 snprintf(dev, sizeof(dev), "%s%d", bases[b], p); 3218 struct stat ds; 3219 if (stat(dev, &ds) != 0) continue; 3220 3221 if (mount(dev, "/media", "vfat", MS_RDONLY, "iocharset=utf8") == 0) { 3222 ac_log("[dj] mounted %s at /media (vfat)\n", dev); 3223 music_mount_last_result = 1; 3224 return 1; 3225 } 3226 if (mount(dev, "/media", "exfat", MS_RDONLY, NULL) == 0) { 3227 ac_log("[dj] mounted %s at /media (exfat)\n", dev); 3228 music_mount_last_result = 1; 3229 return 1; 3230 } 3231 if (mount(dev, "/media", "ext4", MS_RDONLY, NULL) == 0) { 3232 ac_log("[dj] mounted %s at /media (ext4)\n", dev); 3233 music_mount_last_result = 1; 3234 return 1; 3235 } 3236 if (mount(dev, "/media", "ntfs3", MS_RDONLY, NULL) == 0) { 3237 ac_log("[dj] mounted %s at /media (ntfs)\n", dev); 3238 music_mount_last_result = 1; 3239 return 1; 3240 } 3241 } 3242 } 3243 if (music_mount_last_result != 0) { 3244 ac_log("[dj] no music USB found\n"); 3245 music_mount_last_result = 0; 3246 } 3247 return 0; 3248} 3249 3250static void *music_mount_thread_fn(void *arg) { 3251 (void)arg; 3252 int mounted = probe_mount_music_once(); 3253 pthread_mutex_lock(&music_mount_mu); 3254 music_mount_state = mounted ? 1 : 0; 3255 music_mount_pending = 0; 3256 pthread_mutex_unlock(&music_mount_mu); 3257 return NULL; 3258} 3259 3260static void request_music_mount_probe(void) { 3261 int should_spawn = 0; 3262 pthread_t tid; 3263 3264 pthread_mutex_lock(&music_mount_mu); 3265 if (!music_mount_pending) { 3266 music_mount_pending = 1; 3267 should_spawn = 1; 3268 } 3269 pthread_mutex_unlock(&music_mount_mu); 3270 3271 if (!should_spawn) return; 3272 3273 if (pthread_create(&tid, NULL, music_mount_thread_fn, NULL) != 0) { 3274 pthread_mutex_lock(&music_mount_mu); 3275 music_mount_pending = 0; 3276 pthread_mutex_unlock(&music_mount_mu); 3277 ac_log("[dj] mountMusic thread create failed\n"); 3278 return; 3279 } 3280 pthread_detach(tid); 3281} 3282 3283static JSValue js_mount_music(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3284 (void)this_val; (void)argc; (void)argv; 3285 request_music_mount_probe(); 3286 return JS_NewBool(ctx, music_mount_state); 3287} 3288 3289// --------------------------------------------------------------------------- 3290// system.ws — WebSocket client 3291// --------------------------------------------------------------------------- 3292 3293static JSValue js_ws_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3294 (void)this_val; 3295 if (argc < 1 || !current_rt) return JS_UNDEFINED; 3296 const char *url = JS_ToCString(ctx, argv[0]); 3297 if (!url) return JS_UNDEFINED; 3298 ws_connect(current_rt->ws, url); // non-blocking: signals background thread 3299 JS_FreeCString(ctx, url); 3300 return JS_UNDEFINED; 3301} 3302 3303static JSValue js_ws_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3304 (void)this_val; 3305 if (argc < 1 || !current_rt || !current_rt->ws) return JS_UNDEFINED; 3306 const char *text = JS_ToCString(ctx, argv[0]); 3307 if (!text) return JS_UNDEFINED; 3308 ws_send(current_rt->ws, text); 3309 JS_FreeCString(ctx, text); 3310 return JS_UNDEFINED; 3311} 3312 3313static JSValue js_ws_close(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3314 (void)this_val; (void)argc; (void)argv; 3315 if (current_rt && current_rt->ws) ws_close(current_rt->ws); 3316 return JS_UNDEFINED; 3317} 3318 3319static JSValue build_ws_obj(JSContext *ctx, const char *phase) { 3320 JSValue ws_obj = JS_NewObject(ctx); 3321 JS_SetPropertyStr(ctx, ws_obj, "connect", JS_NewCFunction(ctx, js_ws_connect, "connect", 1)); 3322 JS_SetPropertyStr(ctx, ws_obj, "send", JS_NewCFunction(ctx, js_ws_send, "send", 1)); 3323 JS_SetPropertyStr(ctx, ws_obj, "close", JS_NewCFunction(ctx, js_ws_close, "close", 0)); 3324 3325 ACWs *ws = current_rt ? current_rt->ws : NULL; 3326 if (ws) { 3327 // Only drain messages during "paint" phase — that's where JS processes them. 3328 // Other phases (act, sim) see connected/connecting but empty messages array, 3329 // preventing the queue from being consumed before paint() can read it. 3330 int drain = (strcmp(phase, "paint") == 0); 3331 pthread_mutex_lock(&ws->mu); 3332 int count = drain ? ws->msg_count : 0; 3333 if (drain) ws->msg_count = 0; 3334 int connected = ws->connected; 3335 int connecting = ws->connecting; 3336 // Copy only actual message bytes (not full 256KB buffer each) — heap alloc 3337 if (count > WS_MAX_MESSAGES) count = WS_MAX_MESSAGES; 3338 char *msgs[WS_MAX_MESSAGES]; 3339 int msg_lens[WS_MAX_MESSAGES]; 3340 for (int i = 0; i < count; i++) { 3341 int len = strnlen(ws->messages[i], WS_MAX_MSG_LEN - 1); 3342 msgs[i] = malloc(len + 1); 3343 if (msgs[i]) { memcpy(msgs[i], ws->messages[i], len); msgs[i][len] = 0; } 3344 msg_lens[i] = len; 3345 } 3346 pthread_mutex_unlock(&ws->mu); 3347 3348 JS_SetPropertyStr(ctx, ws_obj, "connected", JS_NewBool(ctx, connected)); 3349 JS_SetPropertyStr(ctx, ws_obj, "connecting", JS_NewBool(ctx, connecting)); 3350 3351 JSValue arr = JS_NewArray(ctx); 3352 for (int i = 0; i < count; i++) { 3353 if (msgs[i]) { 3354 JS_SetPropertyUint32(ctx, arr, (uint32_t)i, JS_NewStringLen(ctx, msgs[i], msg_lens[i])); 3355 free(msgs[i]); 3356 } 3357 } 3358 JS_SetPropertyStr(ctx, ws_obj, "messages", arr); 3359 } else { 3360 JS_SetPropertyStr(ctx, ws_obj, "connected", JS_FALSE); 3361 JS_SetPropertyStr(ctx, ws_obj, "connecting", JS_FALSE); 3362 JS_SetPropertyStr(ctx, ws_obj, "messages", JS_NewArray(ctx)); 3363 } 3364 return ws_obj; 3365} 3366 3367// system.fetch(url) — async HTTP GET via curl, result polled via system.fetchResult 3368static JSValue js_fetch(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3369 (void)this_val; 3370 if (!current_rt || argc < 1) return JS_UNDEFINED; 3371 // Reject if a fetch is already in flight (single-slot) 3372 if (current_rt->fetch_pending) return JS_FALSE; 3373 const char *url = JS_ToCString(ctx, argv[0]); 3374 if (!url) return JS_UNDEFINED; 3375 unlink("/tmp/ac_fetch.json"); 3376 unlink("/tmp/ac_fetch_rc"); 3377 unlink("/tmp/ac_fetch_err"); 3378 char cmd[2048]; 3379 ac_log("[fetch] start: %s\n", url); 3380 snprintf(cmd, sizeof(cmd), 3381 "sh -c 'curl -fsSL --retry 2 --retry-delay 1 --connect-timeout 5 --max-time 12 " 3382 "--cacert /etc/pki/tls/certs/ca-bundle.crt " 3383 "--output /tmp/ac_fetch.json \"%s\" 2>/tmp/ac_fetch_err;" 3384 " echo $? > /tmp/ac_fetch_rc' &", url); 3385 system(cmd); 3386 current_rt->fetch_pending = 1; 3387 current_rt->fetch_result[0] = 0; 3388 current_rt->fetch_error[0] = 0; 3389 JS_FreeCString(ctx, url); 3390 return JS_TRUE; 3391} 3392 3393// system.fetchPost(url, body, headersJSON) — POST request with custom headers 3394// Uses the same single fetch slot as system.fetch. 3395// headersJSON is a JSON string like '{"Authorization":"Bearer ...","Content-Type":"application/json"}' 3396static JSValue js_fetch_post(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3397 (void)this_val; 3398 if (!current_rt || argc < 2) return JS_UNDEFINED; 3399 if (current_rt->fetch_pending) return JS_FALSE; 3400 const char *url = JS_ToCString(ctx, argv[0]); 3401 const char *body = JS_ToCString(ctx, argv[1]); 3402 const char *headers_json = (argc >= 3) ? JS_ToCString(ctx, argv[2]) : NULL; 3403 if (!url || !body) { 3404 JS_FreeCString(ctx, url); 3405 JS_FreeCString(ctx, body); 3406 if (headers_json) JS_FreeCString(ctx, headers_json); 3407 return JS_UNDEFINED; 3408 } 3409 unlink("/tmp/ac_fetch.json"); 3410 unlink("/tmp/ac_fetch_rc"); 3411 unlink("/tmp/ac_fetch_err"); 3412 3413 // Write body to temp file to avoid shell escaping issues 3414 FILE *bf = fopen("/tmp/ac_fetch_body.json", "w"); 3415 if (bf) { fputs(body, bf); fclose(bf); } 3416 3417 // Build header flags by parsing simple JSON {"key":"value",...} 3418 // We build a file of -H flags to avoid shell escaping nightmares 3419 FILE *hf = fopen("/tmp/ac_fetch_headers.txt", "w"); 3420 if (hf) { 3421 // Always include Content-Type 3422 fprintf(hf, "-H\nContent-Type: application/json\n"); 3423 if (headers_json) { 3424 // Simple JSON parser: find "key":"value" pairs 3425 const char *p = headers_json; 3426 while (*p) { 3427 const char *kq = strchr(p, '"'); 3428 if (!kq) break; 3429 const char *ke = strchr(kq + 1, '"'); 3430 if (!ke) break; 3431 const char *vq = strchr(ke + 1, '"'); 3432 if (!vq) break; 3433 const char *ve = strchr(vq + 1, '"'); 3434 if (!ve) break; 3435 int klen = (int)(ke - kq - 1); 3436 int vlen = (int)(ve - vq - 1); 3437 if (klen > 0 && klen < 256 && vlen > 0 && vlen < 2048) { 3438 fprintf(hf, "-H\n%.*s: %.*s\n", klen, kq + 1, vlen, vq + 1); 3439 } 3440 p = ve + 1; 3441 } 3442 } 3443 fclose(hf); 3444 } 3445 3446 ac_log("[fetchPost] start: %s (body %ld bytes)\n", url, (long)strlen(body)); 3447 char cmd[2048]; 3448 snprintf(cmd, sizeof(cmd), 3449 "sh -c 'curl -fsSL -X POST --retry 1 --connect-timeout 10 --max-time 120 " 3450 "--cacert /etc/pki/tls/certs/ca-bundle.crt " 3451 "-K /tmp/ac_fetch_headers.txt " 3452 "-d @/tmp/ac_fetch_body.json " 3453 "--output /tmp/ac_fetch.json \"%s\" 2>/tmp/ac_fetch_err;" 3454 " echo $? > /tmp/ac_fetch_rc' &", url); 3455 system(cmd); 3456 current_rt->fetch_pending = 1; 3457 current_rt->fetch_result[0] = 0; 3458 current_rt->fetch_error[0] = 0; 3459 JS_FreeCString(ctx, url); 3460 JS_FreeCString(ctx, body); 3461 if (headers_json) JS_FreeCString(ctx, headers_json); 3462 return JS_TRUE; 3463} 3464 3465// system.fetchCancel() — kill in-flight curl and free the fetch slot 3466static JSValue js_fetch_cancel(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3467 (void)this_val; (void)argc; (void)argv; 3468 if (!current_rt) return JS_UNDEFINED; 3469 if (current_rt->fetch_pending) { 3470 ac_log("[fetch] cancel: killing in-flight curl\n"); 3471 // Kill any background curl for ac_fetch 3472 system("pkill -f 'curl.*ac_fetch' 2>/dev/null"); 3473 unlink("/tmp/ac_fetch.json"); 3474 unlink("/tmp/ac_fetch_rc"); 3475 unlink("/tmp/ac_fetch_err"); 3476 current_rt->fetch_pending = 0; 3477 current_rt->fetch_result[0] = 0; 3478 current_rt->fetch_error[0] = 0; 3479 } 3480 return JS_UNDEFINED; 3481} 3482 3483// --------------------------------------------------------------------------- 3484// system.scanQR() — Open camera and scan for QR codes in a background thread 3485// system.scanQRStop() — Stop scanning and close camera 3486// Poll system.qrPending / system.qrResult each frame. 3487// --------------------------------------------------------------------------- 3488 3489static void *qr_scan_thread(void *arg) { 3490 ACRuntime *rt = (ACRuntime *)arg; 3491 ACCamera *cam = &rt->camera; 3492 3493 if (camera_open(cam) < 0) { 3494 cam->scan_done = 1; 3495 rt->qr_thread_running = 0; 3496 return NULL; 3497 } 3498 3499 // Grab frames and scan until we find a QR code or are told to stop 3500 int max_attempts = 300; // ~10 seconds at 30fps 3501 for (int i = 0; i < max_attempts && rt->qr_scan_active; i++) { 3502 if (camera_grab(cam) == 0) { 3503 if (camera_scan_qr(cam) > 0) { 3504 cam->scan_done = 1; 3505 break; 3506 } 3507 } 3508 usleep(33000); // ~30fps 3509 } 3510 3511 if (!cam->scan_done) { 3512 cam->scan_done = 1; 3513 if (!cam->scan_result[0] && !cam->scan_error[0]) { 3514 snprintf(cam->scan_error, sizeof(cam->scan_error), "no QR code found"); 3515 } 3516 } 3517 3518 camera_close(cam); 3519 rt->qr_scan_active = 0; 3520 rt->qr_thread_running = 0; 3521 return NULL; 3522} 3523 3524static JSValue js_scan_qr(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3525 (void)this_val; (void)argc; (void)argv; 3526 if (!current_rt) return JS_UNDEFINED; 3527 3528 // Already scanning? 3529 if (current_rt->qr_scan_active) return JS_FALSE; 3530 3531 // Reset state 3532 current_rt->camera.scan_result[0] = 0; 3533 current_rt->camera.scan_error[0] = 0; 3534 current_rt->camera.scan_done = 0; 3535 current_rt->qr_scan_active = 1; 3536 current_rt->qr_thread_running = 1; 3537 3538 if (pthread_create(&current_rt->qr_thread, NULL, qr_scan_thread, current_rt) != 0) { 3539 current_rt->qr_scan_active = 0; 3540 current_rt->qr_thread_running = 0; 3541 snprintf(current_rt->camera.scan_error, sizeof(current_rt->camera.scan_error), 3542 "thread create failed"); 3543 current_rt->camera.scan_done = 1; 3544 return JS_FALSE; 3545 } 3546 pthread_detach(current_rt->qr_thread); 3547 3548 ac_log("[qr] scan started\n"); 3549 return JS_TRUE; 3550} 3551 3552static JSValue js_scan_qr_stop(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3553 (void)this_val; (void)argc; (void)argv; (void)ctx; 3554 if (!current_rt) return JS_UNDEFINED; 3555 current_rt->qr_scan_active = 0; 3556 ac_log("[qr] scan stopped\n"); 3557 return JS_UNDEFINED; 3558} 3559 3560// cameraBlit(x, y, w, h) — render camera display buffer to graph framebuffer 3561static JSValue js_camera_blit(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3562 (void)this_val; 3563 if (!current_rt || !current_rt->graph) return JS_FALSE; 3564 ACCamera *cam = &current_rt->camera; 3565 if (!cam->display || !cam->display_ready) return JS_FALSE; 3566 3567 int dx = 0, dy = 0, dw = 0, dh = 0; 3568 if (argc >= 4) { 3569 JS_ToInt32(ctx, &dx, argv[0]); 3570 JS_ToInt32(ctx, &dy, argv[1]); 3571 JS_ToInt32(ctx, &dw, argv[2]); 3572 JS_ToInt32(ctx, &dh, argv[3]); 3573 } else { 3574 // Default: fit to screen 3575 dw = current_rt->graph->fb->width; 3576 dh = current_rt->graph->fb->height; 3577 } 3578 if (dw <= 0 || dh <= 0) return JS_FALSE; 3579 3580 // Lock and copy the display buffer to a local copy 3581 int cw = cam->width, ch = cam->height; 3582 int pixels = cw * ch; 3583 uint8_t *local = malloc(pixels); 3584 if (!local) return JS_FALSE; 3585 3586 pthread_mutex_lock(&cam->display_mu); 3587 memcpy(local, cam->display, pixels); 3588 pthread_mutex_unlock(&cam->display_mu); 3589 3590 // Blit grayscale to framebuffer with nearest-neighbor scaling 3591 ACFramebuffer *fb = current_rt->graph->fb; 3592 for (int py = 0; py < dh; py++) { 3593 int fy = dy + py; 3594 if (fy < 0 || fy >= fb->height) continue; 3595 int sy = py * ch / dh; 3596 if (sy >= ch) sy = ch - 1; 3597 for (int px = 0; px < dw; px++) { 3598 int fx = dx + px; 3599 if (fx < 0 || fx >= fb->width) continue; 3600 int sx = px * cw / dw; 3601 if (sx >= cw) sx = cw - 1; 3602 uint8_t g = local[sy * cw + sx]; 3603 fb->pixels[fy * fb->stride + fx] = 0xFF000000u | ((uint32_t)g << 16) | ((uint32_t)g << 8) | g; 3604 } 3605 } 3606 3607 free(local); 3608 return JS_TRUE; 3609} 3610 3611// --------------------------------------------------------------------------- 3612// system.udp — Raw UDP fairy point co-presence 3613// --------------------------------------------------------------------------- 3614 3615static void sync_udp_identity(void) { 3616 extern char g_machine_id[64]; 3617 3618 if (!current_rt || !current_rt->udp) return; 3619 udp_set_identity( 3620 current_rt->udp, 3621 current_rt->handle[0] ? current_rt->handle : "", 3622 g_machine_id[0] ? g_machine_id : "unknown" 3623 ); 3624} 3625 3626static JSValue js_udp_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3627 (void)this_val; 3628 if (!current_rt || !current_rt->udp) return JS_UNDEFINED; 3629 const char *host = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; 3630 if (!host) host = "session-server.aesthetic.computer"; 3631 int port = UDP_FAIRY_PORT; 3632 if (argc > 1) JS_ToInt32(ctx, &port, argv[1]); 3633 sync_udp_identity(); 3634 udp_connect(current_rt->udp, host, port); 3635 if (argc > 0) JS_FreeCString(ctx, host); 3636 return JS_UNDEFINED; 3637} 3638 3639static JSValue js_udp_send_fairy(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3640 (void)this_val; 3641 if (!current_rt || !current_rt->udp || argc < 2) return JS_UNDEFINED; 3642 double x, y; 3643 JS_ToFloat64(ctx, &x, argv[0]); 3644 JS_ToFloat64(ctx, &y, argv[1]); 3645 udp_send_fairy(current_rt->udp, (float)x, (float)y); 3646 return JS_UNDEFINED; 3647} 3648 3649static JSValue js_udp_send_midi(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3650 (void)this_val; 3651 if (!current_rt || !current_rt->udp || argc < 4) return JS_UNDEFINED; 3652 3653 const char *event = JS_ToCString(ctx, argv[0]); 3654 int32_t note = 0; 3655 int32_t velocity = 0; 3656 int32_t channel = 0; 3657 const char *piece = argc > 4 ? JS_ToCString(ctx, argv[4]) : NULL; 3658 3659 JS_ToInt32(ctx, &note, argv[1]); 3660 JS_ToInt32(ctx, &velocity, argv[2]); 3661 JS_ToInt32(ctx, &channel, argv[3]); 3662 3663 if (!event) { 3664 if (piece) JS_FreeCString(ctx, piece); 3665 return JS_UNDEFINED; 3666 } 3667 3668 sync_udp_identity(); 3669 udp_send_midi( 3670 current_rt->udp, 3671 event, 3672 (int)note, 3673 (int)velocity, 3674 (int)channel, 3675 piece ? piece : "notepat" 3676 ); 3677 3678 JS_FreeCString(ctx, event); 3679 if (piece) JS_FreeCString(ctx, piece); 3680 return JS_UNDEFINED; 3681} 3682 3683static JSValue js_udp_send_midi_heartbeat(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3684 (void)this_val; 3685 if (!current_rt || !current_rt->udp) return JS_UNDEFINED; 3686 3687 const char *piece = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; 3688 sync_udp_identity(); 3689 udp_send_midi_heartbeat(current_rt->udp, piece ? piece : "notepat"); 3690 if (piece) JS_FreeCString(ctx, piece); 3691 return JS_UNDEFINED; 3692} 3693 3694static JSValue build_udp_obj(JSContext *ctx, const char *phase) { 3695 JSValue obj = JS_NewObject(ctx); 3696 JS_SetPropertyStr(ctx, obj, "connect", JS_NewCFunction(ctx, js_udp_connect, "connect", 2)); 3697 JS_SetPropertyStr(ctx, obj, "sendFairy", JS_NewCFunction(ctx, js_udp_send_fairy, "sendFairy", 2)); 3698 JS_SetPropertyStr(ctx, obj, "sendMidi", JS_NewCFunction(ctx, js_udp_send_midi, "sendMidi", 5)); 3699 JS_SetPropertyStr(ctx, obj, "sendMidiHeartbeat", JS_NewCFunction(ctx, js_udp_send_midi_heartbeat, "sendMidiHeartbeat", 1)); 3700 3701 ACUdp *udp = current_rt ? current_rt->udp : NULL; 3702 if (udp) { 3703 sync_udp_identity(); 3704 JS_SetPropertyStr(ctx, obj, "connected", JS_NewBool(ctx, udp->connected)); 3705 JS_SetPropertyStr(ctx, obj, "handle", 3706 JS_NewString(ctx, current_rt && current_rt->handle[0] ? current_rt->handle : "")); 3707 3708 // Only deliver fairies during paint phase 3709 if (strcmp(phase, "paint") == 0) { 3710 UDPFairy fairies[UDP_MAX_FAIRIES]; 3711 int count = udp_poll_fairies(udp, fairies, UDP_MAX_FAIRIES); 3712 JSValue arr = JS_NewArray(ctx); 3713 for (int i = 0; i < count; i++) { 3714 JSValue f = JS_NewObject(ctx); 3715 JS_SetPropertyStr(ctx, f, "x", JS_NewFloat64(ctx, fairies[i].x)); 3716 JS_SetPropertyStr(ctx, f, "y", JS_NewFloat64(ctx, fairies[i].y)); 3717 JS_SetPropertyUint32(ctx, arr, i, f); 3718 } 3719 JS_SetPropertyStr(ctx, obj, "fairies", arr); 3720 } 3721 } 3722 return obj; 3723} 3724 3725// system.fetchBinary(url, destPath[, expectedBytes]) 3726// Non-blocking: runs curl in background; poll system.fetchBinaryProgress/Done/Ok each frame. 3727static JSValue js_fetch_binary(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 3728 (void)this_val; 3729 if (!current_rt || argc < 2) return JS_UNDEFINED; 3730 const char *url = JS_ToCString(ctx, argv[0]); 3731 const char *dest = JS_ToCString(ctx, argv[1]); 3732 if (!url || !dest) { 3733 JS_FreeCString(ctx, url); 3734 JS_FreeCString(ctx, dest); 3735 return JS_UNDEFINED; 3736 } 3737 long expected = 0; 3738 if (argc >= 3) { 3739 double v; 3740 JS_ToFloat64(ctx, &v, argv[2]); 3741 expected = (long)v; 3742 } 3743 ac_log("[fetchBinary] start: url=%s dest=%s expected=%ld\n", url, dest, expected); 3744 // Reset state 3745 current_rt->fetch_binary_pending = 1; 3746 current_rt->fetch_binary_done = 0; 3747 current_rt->fetch_binary_ok = 0; 3748 current_rt->fetch_binary_progress = 0.0f; 3749 current_rt->fetch_binary_expected = expected; 3750 strncpy(current_rt->fetch_binary_dest, dest, sizeof(current_rt->fetch_binary_dest) - 1); 3751 current_rt->fetch_binary_dest[sizeof(current_rt->fetch_binary_dest) - 1] = 0; 3752 // Remove stale files 3753 unlink("/tmp/ac_fb_rc"); 3754 unlink("/tmp/ac_fb_err"); 3755 unlink(dest); 3756 // Start curl in background 3757 char cmd[1024]; 3758 snprintf(cmd, sizeof(cmd), 3759 "sh -c 'curl -fSL --retry 3 --retry-delay 1 --connect-timeout 8 --max-time 600 " 3760 "--cacert /etc/pki/tls/certs/ca-bundle.crt " 3761 "--output \"%s\" \"%s\" 2>/tmp/ac_fb_err;" 3762 " echo $? > /tmp/ac_fb_rc' &", dest, url); 3763 ac_log("[fetchBinary] cmd: curl -> %s\n", dest); 3764 system(cmd); 3765 JS_FreeCString(ctx, url); 3766 JS_FreeCString(ctx, dest); 3767 return JS_UNDEFINED; 3768} 3769 3770// Detect the EFI partition we booted from: 3771// - If /sys/block/sda/removable == 1 → USB → /dev/sda1 3772// - Else if /dev/nvme0n1p1 exists → NVMe internal → /dev/nvme0n1p1 3773// - Else if /dev/mmcblk0p1 exists → eMMC internal (Chromebooks) → /dev/mmcblk0p1 3774// - Fallback: /dev/sda1 3775static void detect_boot_device(char *out, size_t len) { 3776 char removable[8] = {0}; 3777 if (read_sysfs("/sys/block/sda/removable", removable, sizeof(removable)) > 0 3778 && removable[0] == '1') { 3779 snprintf(out, len, "/dev/sda1"); 3780 ac_log("[flash] boot device: USB (/dev/sda1, removable)"); 3781 return; 3782 } 3783 if (access("/dev/nvme0n1p1", F_OK) == 0) { 3784 snprintf(out, len, "/dev/nvme0n1p1"); 3785 ac_log("[flash] boot device: NVMe (/dev/nvme0n1p1)"); 3786 return; 3787 } 3788 if (access("/dev/mmcblk0p1", F_OK) == 0) { 3789 snprintf(out, len, "/dev/mmcblk0p1"); 3790 ac_log("[flash] boot device: eMMC (/dev/mmcblk0p1)"); 3791 return; 3792 } 3793 // Fallback — try sda1 anyway 3794 snprintf(out, len, "/dev/sda1"); 3795 ac_log("[flash] boot device: fallback (/dev/sda1)"); 3796} 3797 3798// Pure C file copy (no shell needed) 3799static long flash_copy_file(const char *src, const char *dst) { 3800 FILE *in = fopen(src, "rb"); 3801 if (!in) { 3802 ac_log("[flash] fopen src failed: %s errno=%d (%s)", src, errno, strerror(errno)); 3803 return -1; 3804 } 3805 // Delete destination first, then sync to flush vfat metadata 3806 unlink(dst); 3807 sync(); 3808 FILE *out = fopen(dst, "wb"); 3809 if (!out) { 3810 ac_log("[flash] fopen dst failed: %s errno=%d (%s)", dst, errno, strerror(errno)); 3811 fclose(in); 3812 return -1; 3813 } 3814 char buf[65536]; 3815 long total = 0; 3816 size_t n; 3817 while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { 3818 if (fwrite(buf, 1, n, out) != n) { 3819 ac_log("[flash] fwrite failed at offset %ld errno=%d (%s)", total, errno, strerror(errno)); 3820 total = -1; 3821 break; 3822 } 3823 total += n; 3824 } 3825 // Flush stdio buffer, then fsync to force data+metadata to physical disk 3826 // (critical for vfat — without this, data stays in page cache and reboot loses it) 3827 if (total > 0) { 3828 fflush(out); 3829 if (fsync(fileno(out)) != 0) { 3830 ac_log("[flash] fsync failed: errno=%d (%s)", errno, strerror(errno)); 3831 total = -1; 3832 } 3833 } 3834 fclose(out); 3835 fclose(in); 3836 return total; 3837} 3838 3839// Byte-for-byte verification: evict destination page cache, re-read from disk, 3840// compare against source (which lives in tmpfs and must NOT be evicted). 3841// Returns number of verified bytes, or -1 on mismatch/error. 3842static void flash_trace(const char *fmt, ...); 3843static void flash_trace_open(void); 3844static void flash_trace_close_and_archive(void); 3845static long flash_verify(const char *src, const char *dst) { 3846 // SIZE CHECK FIRST — cheap, diagnostic, catches the common failure 3847 // where the copy was short (disk full, sync incomplete, device removed 3848 // mid-write, etc.) without needing to read 272 MB twice. 3849 struct stat src_st, dst_st; 3850 if (stat(src, &src_st) != 0) { 3851 ac_log("[verify] stat src=%s failed errno=%d", src, errno); 3852 flash_trace("verify: stat src FAILED errno=%d", errno); 3853 return -1; 3854 } 3855 if (stat(dst, &dst_st) != 0) { 3856 ac_log("[verify] stat dst=%s failed errno=%d", dst, errno); 3857 flash_trace("verify: stat dst FAILED errno=%d", errno); 3858 return -1; 3859 } 3860 flash_trace("verify: src=%ld dst=%ld", (long)src_st.st_size, (long)dst_st.st_size); 3861 if (src_st.st_size != dst_st.st_size) { 3862 ac_log("[verify] SIZE MISMATCH: src=%ld dst=%ld (diff=%ld)", 3863 (long)src_st.st_size, (long)dst_st.st_size, 3864 (long)(src_st.st_size - dst_st.st_size)); 3865 flash_trace("verify: SIZE MISMATCH src=%ld dst=%ld", 3866 (long)src_st.st_size, (long)dst_st.st_size); 3867 return -1; 3868 } 3869 3870 // Evict ONLY the destination file's page cache using posix_fadvise. 3871 // DO NOT use drop_caches — that nukes tmpfs pages and destroys the source! 3872 int dst_fd = open(dst, O_RDONLY); 3873 if (dst_fd >= 0) { 3874 posix_fadvise(dst_fd, 0, dst_st.st_size, POSIX_FADV_DONTNEED); 3875 ac_log("[verify] evicted dst page cache (%ld bytes)", (long)dst_st.st_size); 3876 flash_trace("verify: evicted page cache for dst"); 3877 close(dst_fd); 3878 } 3879 3880 FILE *fa = fopen(src, "rb"); 3881 FILE *fb = fopen(dst, "rb"); 3882 if (!fa || !fb) { 3883 ac_log("[verify] fopen failed: src=%s dst=%s", fa ? "ok" : "FAIL", fb ? "ok" : "FAIL"); 3884 flash_trace("verify: fopen src=%s dst=%s", fa ? "ok" : "FAIL", fb ? "ok" : "FAIL"); 3885 if (fa) fclose(fa); 3886 if (fb) fclose(fb); 3887 return -1; 3888 } 3889 3890 char bufa[65536], bufb[65536]; 3891 long verified = 0; 3892 size_t na, nb; 3893 while ((na = fread(bufa, 1, sizeof(bufa), fa)) > 0) { 3894 nb = fread(bufb, 1, sizeof(bufb), fb); 3895 if (na != nb || memcmp(bufa, bufb, na) != 0) { 3896 ac_log("[verify] MISMATCH at offset %ld (read %zu vs %zu)", verified, na, nb); 3897 flash_trace("verify: MISMATCH at offset=%ld src_read=%zu dst_read=%zu", 3898 verified, na, nb); 3899 fclose(fa); 3900 fclose(fb); 3901 return -1; 3902 } 3903 verified += (long)na; 3904 } 3905 // Make sure dst doesn't have extra bytes 3906 nb = fread(bufb, 1, 1, fb); 3907 if (nb != 0) { 3908 ac_log("[verify] dst has extra bytes after %ld", verified); 3909 flash_trace("verify: dst has extra bytes after %ld", verified); 3910 fclose(fa); 3911 fclose(fb); 3912 return -1; 3913 } 3914 fclose(fa); 3915 fclose(fb); 3916 ac_log("[verify] OK: %ld bytes match", verified); 3917 flash_trace("verify: OK %ld bytes match", verified); 3918 return verified; 3919} 3920 3921// Write a line to the flash telemetry ring buffer (thread-safe enough for single writer) 3922static void flash_tlog(ACRuntime *rt, const char *fmt, ...) { 3923 va_list ap; 3924 va_start(ap, fmt); 3925 int idx = rt->flash_log_count % 16; 3926 vsnprintf(rt->flash_log[idx], 128, fmt, ap); 3927 va_end(ap); 3928 __sync_fetch_and_add(&rt->flash_log_count, 1); 3929} 3930 3931// Append a timestamped line to /tmp/flash-trace.log, which lives in tmpfs so 3932// it can't be affected by the vfat partition we're flashing. On flash 3933// completion (success OR failure) this file is copied to /mnt/flash-last.log 3934// for post-mortem. This is INDEPENDENT of ac_log — the previous design 3935// paused ac_log across the entire flash and lost all diagnostic output when 3936// the flash thread hung or crashed. flash_trace NEVER pauses. 3937static FILE *flash_trace_fp = NULL; 3938static void flash_trace_open(void) { 3939 if (flash_trace_fp) { fclose(flash_trace_fp); flash_trace_fp = NULL; } 3940 // Truncate per-flash so we get a fresh log 3941 flash_trace_fp = fopen("/tmp/flash-trace.log", "w"); 3942 if (flash_trace_fp) { 3943 fprintf(flash_trace_fp, "=== flash-trace %ld ===\n", (long)time(NULL)); 3944 fflush(flash_trace_fp); 3945 } 3946} 3947static void flash_trace(const char *fmt, ...) { 3948 if (!flash_trace_fp) return; 3949 struct timespec ts; 3950 clock_gettime(CLOCK_MONOTONIC, &ts); 3951 fprintf(flash_trace_fp, "[%ld.%03ld] ", (long)ts.tv_sec, ts.tv_nsec / 1000000); 3952 va_list ap; 3953 va_start(ap, fmt); 3954 vfprintf(flash_trace_fp, fmt, ap); 3955 va_end(ap); 3956 if (fmt[0] && fmt[strlen(fmt) - 1] != '\n') fputc('\n', flash_trace_fp); 3957 fflush(flash_trace_fp); 3958 // No fsync — tmpfs doesn't need it and we want speed 3959} 3960static void flash_trace_close_and_archive(void) { 3961 if (flash_trace_fp) { fclose(flash_trace_fp); flash_trace_fp = NULL; } 3962 // Copy to /mnt/flash-last.log for post-mortem after flash completes. 3963 // If /mnt is mid-remount or busy, this might fail silently — that's OK, 3964 // the tmpfs copy is still available while the process lives. 3965 FILE *src = fopen("/tmp/flash-trace.log", "rb"); 3966 if (!src) return; 3967 FILE *dst = fopen("/mnt/flash-last.log", "wb"); 3968 if (dst) { 3969 char buf[4096]; 3970 size_t n; 3971 while ((n = fread(buf, 1, sizeof(buf), src)) > 0) fwrite(buf, 1, n, dst); 3972 fflush(dst); 3973 fsync(fileno(dst)); 3974 fclose(dst); 3975 } 3976 fclose(src); 3977} 3978 3979// Flash update background thread: mount EFI, copy vmlinuz, sync, umount 3980// All operations use C syscalls — no shell commands (cp/mkdir/mount not in initramfs PATH) 3981// NOTE: current_rt is __thread (thread-local); pass ACRuntime * via arg instead 3982static void *flash_thread_fn(void *arg) { 3983 ACRuntime *rt = (ACRuntime *)arg; 3984 if (!rt) return NULL; 3985 rt->flash_log_count = 0; 3986 rt->flash_dst[0] = '\0'; 3987 rt->flash_same_device = 0; 3988 3989 // Open a dedicated trace log in tmpfs. This survives even if the main 3990 // log gets suspended / the vfat fs we're flashing goes read-only / the 3991 // flash thread hangs. At the end of the thread (success or failure) we 3992 // copy it to /mnt/flash-last.log for post-mortem on the next boot. 3993 flash_trace_open(); 3994 flash_trace("flash_thread_fn start"); 3995 flash_trace("src=%s device=%s", rt->flash_src, rt->flash_device); 3996 3997 ac_log("[flash] starting: src=%s device=%s", rt->flash_src, rt->flash_device); 3998 flash_tlog(rt, "src=%s", rt->flash_src); 3999 flash_tlog(rt, "device=%s", rt->flash_device); 4000 4001 // Check if device node exists 4002 { 4003 struct stat dev_st; 4004 if (stat(rt->flash_device, &dev_st) != 0) { 4005 ac_log("[flash] device %s does not exist (errno=%d %s)", 4006 rt->flash_device, errno, strerror(errno)); 4007 flash_trace("device %s missing errno=%d", rt->flash_device, errno); 4008 flash_trace_close_and_archive(); 4009 rt->flash_phase = 4; 4010 rt->flash_ok = 0; 4011 rt->flash_pending = 0; 4012 rt->flash_done = 1; 4013 return NULL; 4014 } 4015 ac_log("[flash] device %s exists (mode=0%o)", rt->flash_device, dev_st.st_mode); 4016 flash_trace("device exists mode=0%o", dev_st.st_mode); 4017 } 4018 4019 // Mount the EFI partition for flashing. 4020 // IMPORTANT: If the flash target is the same device already mounted at /mnt 4021 // (e.g. NVMe-to-NVMe OTA), we MUST use /mnt directly. Mounting the same 4022 // vfat block device twice causes FAT table corruption — each mount has its 4023 // own in-memory FAT, and umount of one will overwrite the other's changes. 4024 const char *efi_mount = NULL; 4025 int did_mount = 0; 4026 4027 // Check if flash target is already mounted at /mnt 4028 extern char log_dev[]; // set during setup_logging() 4029 int same_as_mnt = (log_dev[0] && strcmp(log_dev, rt->flash_device) == 0); 4030 rt->flash_same_device = same_as_mnt; 4031 flash_tlog(rt, "log_dev=%s same=%d", log_dev, same_as_mnt); 4032 4033 if (same_as_mnt) { 4034 // Same device as /mnt — use it directly, never double-mount 4035 if (access("/mnt/EFI/BOOT", F_OK) == 0 || access("/mnt/EFI", F_OK) == 0) { 4036 efi_mount = "/mnt"; 4037 ac_log("[flash] flash target = boot device (%s), using /mnt directly", rt->flash_device); 4038 } else { 4039 ac_log("[flash] flash target = boot device but /mnt has no EFI dir"); 4040 // Create EFI structure on /mnt 4041 mkdir("/mnt/EFI", 0755); 4042 mkdir("/mnt/EFI/BOOT", 0755); 4043 if (access("/mnt/EFI/BOOT", F_OK) == 0) { 4044 efi_mount = "/mnt"; 4045 ac_log("[flash] created EFI/BOOT on /mnt"); 4046 } 4047 } 4048 } 4049 4050 if (!efi_mount) { 4051 // Different device — mount fresh at /tmp/efi 4052 mkdir("/tmp/efi", 0755); 4053 umount("/tmp/efi"); // clean up any stale mount 4054 4055 const char *fstypes[] = {"vfat", "ext4", "ext2", NULL}; 4056 for (int fi = 0; fstypes[fi] && !did_mount; fi++) { 4057 int mr = mount(rt->flash_device, "/tmp/efi", fstypes[fi], 0, NULL); 4058 if (mr == 0) { 4059 efi_mount = "/tmp/efi"; 4060 did_mount = 1; 4061 ac_log("[flash] mounted %s at /tmp/efi (type=%s)", rt->flash_device, fstypes[fi]); 4062 } else { 4063 ac_log("[flash] mount %s as %s failed: errno=%d (%s)", 4064 rt->flash_device, fstypes[fi], errno, strerror(errno)); 4065 } 4066 } 4067 4068 // If all mounts failed and this is NVMe, try formatting as vfat (fresh drive) 4069 if (!did_mount && strstr(rt->flash_device, "nvme")) { 4070 ac_log("[flash] NVMe mount failed — attempting mkfs.vfat on %s", rt->flash_device); 4071 char cmd[256]; 4072 snprintf(cmd, sizeof(cmd), "mkfs.vfat -F 32 -n ACBOOT %s 2>&1", rt->flash_device); 4073 FILE *p = popen(cmd, "r"); 4074 if (p) { 4075 char buf[256]; 4076 while (fgets(buf, sizeof(buf), p)) ac_log("[flash] mkfs: %s", buf); 4077 int rc = pclose(p); 4078 ac_log("[flash] mkfs exit=%d", rc); 4079 if (rc == 0) { 4080 if (mount(rt->flash_device, "/tmp/efi", "vfat", 0, NULL) == 0) { 4081 efi_mount = "/tmp/efi"; 4082 did_mount = 1; 4083 ac_log("[flash] mounted fresh vfat on %s", rt->flash_device); 4084 } 4085 } 4086 } 4087 } 4088 4089 // Last resort: check /mnt 4090 if (!did_mount) { 4091 if (access("/mnt/EFI/BOOT", F_OK) == 0) { 4092 efi_mount = "/mnt"; 4093 ac_log("[flash] using existing /mnt mount (fresh mount failed)"); 4094 flash_trace("falling back to existing /mnt mount"); 4095 } else { 4096 ac_log("[flash] ABORT: mount %s failed and /mnt has no EFI/BOOT", 4097 rt->flash_device); 4098 flash_trace("ABORT mount %s failed and no /mnt/EFI/BOOT", rt->flash_device); 4099 flash_trace_close_and_archive(); 4100 rt->flash_phase = 4; 4101 rt->flash_ok = 0; 4102 rt->flash_pending = 0; 4103 rt->flash_done = 1; 4104 return NULL; 4105 } 4106 } 4107 } 4108 flash_trace("efi_mount=%s did_mount=%d", efi_mount, did_mount); 4109 // Create EFI directory structure if it doesn't exist (fresh NVMe install) 4110 char efi_dir[512]; 4111 snprintf(efi_dir, sizeof(efi_dir), "%s/EFI", efi_mount); 4112 mkdir(efi_dir, 0755); 4113 snprintf(efi_dir, sizeof(efi_dir), "%s/EFI/BOOT", efi_mount); 4114 mkdir(efi_dir, 0755); 4115 if (access(efi_dir, F_OK) != 0) { 4116 ac_log("[flash] ABORT: %s could not be created", efi_dir); 4117 flash_trace("ABORT could not create %s", efi_dir); 4118 flash_trace_close_and_archive(); 4119 if (did_mount) umount("/tmp/efi"); 4120 rt->flash_phase = 4; 4121 rt->flash_ok = 0; 4122 rt->flash_pending = 0; 4123 rt->flash_done = 1; 4124 return NULL; 4125 } 4126 4127 // If splash chainloader is present, kernel lives at KERNEL.EFI; else BOOTX64.EFI 4128 char dst[512]; 4129 char kernel_path[512]; 4130 snprintf(kernel_path, sizeof(kernel_path), "%s/EFI/BOOT/KERNEL.EFI", efi_mount); 4131 if (access(kernel_path, F_OK) == 0) { 4132 snprintf(dst, sizeof(dst), "%s", kernel_path); 4133 ac_log("[flash] chainloader detected, updating KERNEL.EFI"); 4134 } else { 4135 snprintf(dst, sizeof(dst), "%s/EFI/BOOT/BOOTX64.EFI", efi_mount); 4136 ac_log("[flash] no chainloader, updating BOOTX64.EFI"); 4137 } 4138 ac_log("[flash] destination: %s (efi_mount=%s, same_device=%d, did_mount=%d)", 4139 dst, efi_mount, same_as_mnt, did_mount); 4140 strncpy(rt->flash_dst, dst, sizeof(rt->flash_dst) - 1); 4141 rt->flash_dst[sizeof(rt->flash_dst) - 1] = '\0'; 4142 flash_tlog(rt, "dst=%s mount=%s", dst, efi_mount); 4143 4144 // NOTE: we intentionally no longer ac_log_pause() the main log during 4145 // the flash. The old design closed /mnt/ac-native.log while writing to 4146 // the same vfat partition, which caused every flash failure to leave 4147 // zero diagnostic trail — if the thread hung or the mount went 4148 // read-only mid-write, ac_log_resume() never fired and the entire 4149 // session's logs after the pause were silently dropped. Instead we 4150 // write flash progress to flash_trace (tmpfs, can't be affected by the 4151 // partition we're flashing) AND best-effort to the main log. At the 4152 // end we archive the trace to /mnt/flash-last.log. 4153 int same_mount = same_as_mnt || !did_mount; 4154 flash_trace("same_mount=%d same_as_mnt=%d did_mount=%d", same_mount, same_as_mnt, did_mount); 4155 4156 // Pre-flight: validate source file exists and has reasonable size 4157 { 4158 struct stat src_st; 4159 if (stat(rt->flash_src, &src_st) != 0 || src_st.st_size < 1048576) { 4160 ac_log("[flash] ABORT: source %s invalid (size=%ld, errno=%d)", 4161 rt->flash_src, (long)(src_st.st_size), errno); 4162 flash_trace("ABORT source invalid size=%ld errno=%d", 4163 (long)(src_st.st_size), errno); 4164 flash_trace_close_and_archive(); 4165 if (did_mount) umount("/tmp/efi"); 4166 rt->flash_phase = 4; 4167 rt->flash_ok = 0; 4168 rt->flash_pending = 0; 4169 rt->flash_done = 1; 4170 return NULL; 4171 } 4172 ac_log("[flash] source validated: %ld bytes", (long)src_st.st_size); 4173 flash_trace("source validated size=%ld", (long)src_st.st_size); 4174 } 4175 4176 // Phase 1: Backup previous kernel, then write new one 4177 rt->flash_phase = 1; 4178 4179 // Keep previous version as .prev for rollback 4180 { 4181 char prev[512]; 4182 snprintf(prev, sizeof(prev), "%s.prev", dst); 4183 if (access(dst, F_OK) == 0) { 4184 // Remove old .prev, rename current to .prev 4185 unlink(prev); 4186 if (rename(dst, prev) == 0) { 4187 ac_log("[flash] backed up previous kernel to %s", prev); 4188 flash_tlog(rt, "backup=%s", prev); 4189 } else { 4190 ac_log("[flash] backup rename failed (errno=%d), continuing anyway", errno); 4191 } 4192 } 4193 } 4194 4195 flash_tlog(rt, "writing %s -> %s", rt->flash_src, dst); 4196 long copied = flash_copy_file(rt->flash_src, dst); 4197 flash_tlog(rt, "wrote %ld bytes", copied); 4198 4199 // Also update Microsoft Boot Manager path (ThinkPad BIOS often boots this first) 4200 // Check available space first — don't write second copy if it won't fit. 4201 // FAT32 can't hardlink, so we need actual space for both copies. 4202 { 4203 char ms_dir[512], ms_dst[512]; 4204 snprintf(ms_dir, sizeof(ms_dir), "%s/EFI/Microsoft", efi_mount); 4205 mkdir(ms_dir, 0755); 4206 snprintf(ms_dir, sizeof(ms_dir), "%s/EFI/Microsoft/Boot", efi_mount); 4207 mkdir(ms_dir, 0755); 4208 snprintf(ms_dst, sizeof(ms_dst), "%s/EFI/Microsoft/Boot/bootmgfw.efi", efi_mount); 4209 4210 // Check free space on partition before writing second copy 4211 struct statvfs vfs; 4212 long free_bytes = 0; 4213 if (statvfs(efi_mount, &vfs) == 0) { 4214 free_bytes = (long)vfs.f_bavail * (long)vfs.f_bsize; 4215 } 4216 long need_bytes = copied + (1024 * 1024); // need kernel size + 1MB margin 4217 4218 if (free_bytes >= need_bytes) { 4219 // Backup old MS boot path too 4220 char ms_prev[512]; 4221 snprintf(ms_prev, sizeof(ms_prev), "%s.prev", ms_dst); 4222 unlink(ms_prev); 4223 if (access(ms_dst, F_OK) == 0) { 4224 rename(ms_dst, ms_prev); // safe: old version preserved 4225 } 4226 long ms_copied = flash_copy_file(rt->flash_src, ms_dst); 4227 if (ms_copied > 0) { 4228 unlink(ms_prev); // success: remove backup 4229 ac_log("[flash] also wrote Microsoft boot path: %ld bytes -> %s", ms_copied, ms_dst); 4230 flash_tlog(rt, "ms_boot=%ld", ms_copied); 4231 } else { 4232 // Write failed — restore backup 4233 if (access(ms_prev, F_OK) == 0) { 4234 rename(ms_prev, ms_dst); 4235 ac_log("[flash] MS boot write failed, restored previous"); 4236 } 4237 } 4238 } else { 4239 ac_log("[flash] skipping MS boot path: %ldMB free < %ldMB needed", 4240 free_bytes / 1048576, need_bytes / 1048576); 4241 flash_tlog(rt, "ms_boot=skipped (space)"); 4242 } 4243 } 4244 4245 // Optional initramfs copy — for OTA updates where the kernel is now 4246 // slim (Phase 2 de-embed) and must load `\initramfs.cpio.gz` from the 4247 // ESP root via its baked-in `initrd=` cmdline. Without this step, an 4248 // os.mjs OTA would leave the new kernel paired with the PREVIOUS 4249 // initramfs, which is a latent source of boot breakage whenever any 4250 // file inside initramfs changes. 4251 if (rt->flash_initramfs_src[0]) { 4252 char initramfs_dst[512]; 4253 snprintf(initramfs_dst, sizeof(initramfs_dst), "%s/initramfs.cpio.gz", efi_mount); 4254 ac_log("[flash] writing initramfs: %s -> %s", rt->flash_initramfs_src, initramfs_dst); 4255 flash_tlog(rt, "initramfs: %s -> %s", rt->flash_initramfs_src, initramfs_dst); 4256 // Keep previous as .prev for rollback 4257 char initramfs_prev[512]; 4258 snprintf(initramfs_prev, sizeof(initramfs_prev), "%s.prev", initramfs_dst); 4259 if (access(initramfs_dst, F_OK) == 0) { 4260 unlink(initramfs_prev); 4261 rename(initramfs_dst, initramfs_prev); 4262 } 4263 long initramfs_copied = flash_copy_file(rt->flash_initramfs_src, initramfs_dst); 4264 flash_tlog(rt, "initramfs wrote %ld bytes", initramfs_copied); 4265 if (initramfs_copied <= 0) { 4266 ac_log("[flash] initramfs copy FAILED (copied=%ld) — restoring .prev", 4267 initramfs_copied); 4268 if (access(initramfs_prev, F_OK) == 0) { 4269 unlink(initramfs_dst); 4270 rename(initramfs_prev, initramfs_dst); 4271 } 4272 // Non-fatal to the kernel write — but flag as failure since the 4273 // new kernel won't boot without a matching initramfs. 4274 flash_trace_close_and_archive(); 4275 rt->flash_phase = 4; 4276 rt->flash_ok = 0; 4277 rt->flash_pending = 0; 4278 rt->flash_done = 1; 4279 return NULL; 4280 } 4281 } 4282 4283 // Phase 2: Syncing to disk — belt-and-suspenders for vfat 4284 rt->flash_phase = 2; 4285 4286 // Force page cache eviction of the written file so subsequent reads hit disk 4287 { 4288 int dst_fd = open(dst, O_RDONLY); 4289 if (dst_fd >= 0) { 4290 struct stat st; 4291 if (fstat(dst_fd, &st) == 0) { 4292 posix_fadvise(dst_fd, 0, st.st_size, POSIX_FADV_DONTNEED); 4293 ac_log("[flash] evicted written file from page cache (%ld bytes)", (long)st.st_size); 4294 } 4295 close(dst_fd); 4296 } 4297 } 4298 4299 // syncfs + multi-round sync with generous waits. USB flash controllers 4300 // can take SECONDS to flush 272 MB through vfat metadata, and the old 4301 // 500 ms window was frequently insufficient — we'd then try to verify 4302 // against data that still existed only in the kernel's dirty page 4303 // cache, so the eviction step exposed a mismatch with uncommitted 4304 // backing store bytes. 4305 int mnt_fd = open(efi_mount, O_RDONLY); 4306 if (mnt_fd >= 0) { 4307 int sr = syncfs(mnt_fd); 4308 if (sr != 0) { 4309 ac_log("[flash] syncfs failed: errno=%d (%s)", errno, strerror(errno)); 4310 flash_trace("syncfs FAILED errno=%d", errno); 4311 } else { 4312 flash_trace("syncfs ok"); 4313 } 4314 close(mnt_fd); 4315 } else { 4316 ac_log("[flash] WARNING: open(%s) for syncfs failed errno=%d", efi_mount, errno); 4317 flash_trace("open(efi_mount) for syncfs FAILED errno=%d", errno); 4318 } 4319 sync(); 4320 flash_trace("sync #1 returned"); 4321 // First wait — let the block layer start flushing 4322 usleep(1500000); // 1.5 s 4323 sync(); 4324 flash_trace("sync #2 after 1.5s wait"); 4325 // Second wait — USB flash write can be very slow for 272 MB 4326 usleep(1500000); // another 1.5 s = 3 s total 4327 sync(); 4328 flash_trace("sync #3 after 3s total wait"); 4329 4330 // Previously we remounted /mnt ro+rw to force vfat metadata flush. That 4331 // was fragile: it bypassed the VFS safely-unmount path, briefly made 4332 // /mnt read-only so any concurrent log write would fail, and on systems 4333 // with mount stacking (init.sh leaves nvme0n1p1 stacked under sda1) 4334 // the remount hit the wrong layer. syncfs + sync + 3 s of settle time 4335 // is more reliable. 4336 ac_log("[flash] sync complete"); 4337 flash_trace("sync complete (3 rounds, 3s total wait)"); 4338 4339 if (copied <= 0) { 4340 ac_log("[flash] copy failed (copied=%ld)", copied); 4341 flash_trace("copy FAILED copied=%ld", copied); 4342 flash_trace_close_and_archive(); 4343 rt->flash_phase = 4; 4344 rt->flash_ok = 0; 4345 rt->flash_pending = 0; 4346 rt->flash_done = 1; 4347 return NULL; 4348 } 4349 4350 // Phase 3: Verify — read back from physical disk and compare byte-for-byte. 4351 // flash_verify now does a size-check first (fast, diagnostic) before 4352 // paying the cost of a 272 MB read+compare. 4353 rt->flash_phase = 3; 4354 flash_trace("verify starting (expecting %ld bytes)", copied); 4355 long verified = flash_verify(rt->flash_src, dst); 4356 flash_tlog(rt, "verify=%ld (expected=%ld)", verified, copied); 4357 flash_trace("verify returned: %ld", verified); 4358 4359 if (did_mount) { 4360 umount("/tmp/efi"); 4361 ac_log("[flash] unmounted /tmp/efi"); 4362 flash_trace("unmounted /tmp/efi"); 4363 } 4364 4365 rt->flash_verified_bytes = verified; 4366 if (verified != copied) { 4367 ac_log("[flash] VERIFY FAILED: wrote %ld, verified %ld", copied, verified); 4368 flash_trace("VERIFY FAILED wrote=%ld verified=%ld", copied, verified); 4369 // CRITICAL: restore previous kernel so device remains bootable 4370 { 4371 char prev[512]; 4372 snprintf(prev, sizeof(prev), "%s.prev", dst); 4373 if (access(prev, F_OK) == 0) { 4374 unlink(dst); 4375 if (rename(prev, dst) == 0) { 4376 ac_log("[flash] RESTORED previous kernel from %s — device still bootable", prev); 4377 flash_trace("RESTORED previous kernel from %s", prev); 4378 sync(); 4379 } else { 4380 ac_log("[flash] CRITICAL: restore failed errno=%d", errno); 4381 flash_trace("CRITICAL restore FAILED errno=%d", errno); 4382 } 4383 } else { 4384 flash_trace("no .prev backup to restore (device may be unbootable)"); 4385 } 4386 } 4387 flash_trace_close_and_archive(); 4388 rt->flash_phase = 4; 4389 rt->flash_ok = 0; 4390 rt->flash_pending = 0; 4391 rt->flash_done = 1; 4392 return NULL; 4393 } 4394 4395 // Set UEFI boot order so firmware boots our kernel (not a stale vendor entry). 4396 // Many OEM firmwares (HP, Dell, Lenovo) have hardcoded boot entries pointing at 4397 // vendor-specific EFI paths. Without updating the boot order, the firmware may 4398 // boot an old kernel (e.g. from a previous OS install) instead of ours. 4399 { 4400 // Mount efivarfs if not already mounted (needed to write UEFI variables) 4401 struct stat evs; 4402 if (stat("/sys/firmware/efi/efivars", &evs) == 0) { 4403 // Try mounting (harmless if already mounted) 4404 mount("efivarfs", "/sys/firmware/efi/efivars", "efivarfs", 0, NULL); 4405 4406 // Extract disk device from partition path for efibootmgr --disk 4407 // e.g. /dev/sda1 -> /dev/sda, /dev/nvme0n1p1 -> /dev/nvme0n1 4408 char disk[64] = "", partnum[8] = ""; 4409 strncpy(disk, rt->flash_device, sizeof(disk) - 1); 4410 char *p = disk + strlen(disk) - 1; 4411 // Walk back past the partition number 4412 while (p > disk && *p >= '0' && *p <= '9') p--; 4413 if (p > disk) { 4414 strncpy(partnum, p + 1, sizeof(partnum) - 1); 4415 // For NVMe: /dev/nvme0n1p1 -> strip 'p' before part number 4416 if (*p == 'p' && p > disk && *(p-1) >= '0' && *(p-1) <= '9') 4417 *p = '\0'; 4418 else 4419 *(p + 1) = '\0'; 4420 } 4421 4422 // Use efibootmgr if available (cleanest approach) 4423 char cmd[512]; 4424 snprintf(cmd, sizeof(cmd), 4425 "efibootmgr -c -d %s -p %s -L 'AC Native OS' " 4426 "-l '\\EFI\\BOOT\\BOOTX64.EFI' 2>&1", 4427 disk, partnum[0] ? partnum : "1"); 4428 ac_log("[flash] setting UEFI boot entry: %s", cmd); 4429 flash_tlog(rt, "efibootmgr: %s %s part=%s", disk, partnum, partnum[0] ? partnum : "1"); 4430 4431 FILE *efip = popen(cmd, "r"); 4432 if (efip) { 4433 char buf[256]; 4434 while (fgets(buf, sizeof(buf), efip)) { 4435 // Trim newline 4436 char *nl = strchr(buf, '\n'); 4437 if (nl) *nl = '\0'; 4438 ac_log("[flash] efibootmgr: %s", buf); 4439 flash_tlog(rt, "efi: %s", buf); 4440 } 4441 int rc = pclose(efip); 4442 if (rc == 0) { 4443 ac_log("[flash] UEFI boot entry created successfully"); 4444 flash_tlog(rt, "efi_boot=ok"); 4445 } else { 4446 ac_log("[flash] efibootmgr exit=%d (boot entry may not be set)", rc); 4447 flash_tlog(rt, "efi_boot=fail(%d)", rc); 4448 } 4449 } else { 4450 ac_log("[flash] efibootmgr not available — UEFI boot order unchanged"); 4451 flash_tlog(rt, "efi_boot=no_efibootmgr"); 4452 } 4453 } else { 4454 ac_log("[flash] no EFI variables support — skipping boot order update"); 4455 } 4456 } 4457 4458 // Remove downloaded file to free /tmp space 4459 unlink(rt->flash_src); 4460 ac_log("[flash] done: %ld bytes written, verified OK", copied); 4461 flash_trace("done: %ld bytes verified OK", copied); 4462 flash_trace_close_and_archive(); 4463 rt->flash_phase = 4; 4464 rt->flash_ok = 1; 4465 rt->flash_pending = 0; 4466 rt->flash_done = 1; 4467 return NULL; 4468} 4469 4470// ───────────────────────────────────────────────────────────────── 4471// Audio diagnostic — list PCMs + play a short test tone on an arbitrary 4472// device. Used by the speaker.mjs piece to figure out which ALSA PCM 4473// actually produces sound on a given machine (vs wiring-dependent guesses 4474// like hw:0,0). The tone plays in a detached thread so the JS caller 4475// returns immediately; the piece UI stays responsive. 4476// ───────────────────────────────────────────────────────────────── 4477 4478static JSValue js_audio_list_pcms(JSContext *ctx, JSValueConst this_val, 4479 int argc, JSValueConst *argv) { 4480 (void)this_val; (void)argc; (void)argv; 4481 JSValue arr = JS_NewArray(ctx); 4482 int idx = 0; 4483 for (int c = 0; c < 4; c++) { 4484 for (int d = 0; d < 8; d++) { 4485 char path[80]; 4486 snprintf(path, sizeof(path), 4487 "/proc/asound/card%d/pcm%dp/info", c, d); 4488 FILE *fp = fopen(path, "r"); 4489 if (!fp) continue; 4490 char line[256], id_str[96] = "", name_str[96] = ""; 4491 while (fgets(line, sizeof(line), fp)) { 4492 char *nl = strchr(line, '\n'); if (nl) *nl = 0; 4493 if (!strncmp(line, "id: ", 4)) 4494 snprintf(id_str, sizeof(id_str), "%s", line + 4); 4495 else if (!strncmp(line, "name: ", 6)) 4496 snprintf(name_str, sizeof(name_str), "%s", line + 6); 4497 } 4498 fclose(fp); 4499 char dev[16]; 4500 snprintf(dev, sizeof(dev), "hw:%d,%d", c, d); 4501 JSValue obj = JS_NewObject(ctx); 4502 JS_SetPropertyStr(ctx, obj, "device", JS_NewString(ctx, dev)); 4503 JS_SetPropertyStr(ctx, obj, "card", JS_NewInt32(ctx, c)); 4504 JS_SetPropertyStr(ctx, obj, "num", JS_NewInt32(ctx, d)); 4505 JS_SetPropertyStr(ctx, obj, "id", JS_NewString(ctx, id_str)); 4506 JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, name_str)); 4507 /* Tag the PCM that ac-native's main audio thread picked so the 4508 * UI can highlight it. */ 4509 if (current_rt && current_rt->audio && 4510 strcmp(current_rt->audio->audio_device, dev) == 0) 4511 JS_SetPropertyStr(ctx, obj, "active", JS_NewBool(ctx, 1)); 4512 JS_SetPropertyUint32(ctx, arr, idx++, obj); 4513 } 4514 } 4515 return arr; 4516} 4517 4518struct audio_probe_args { 4519 char device[32]; 4520 int freq_hz; 4521 int duration_ms; 4522 float volume; 4523}; 4524 4525static void *audio_probe_thread(void *arg) { 4526 struct audio_probe_args *a = (struct audio_probe_args *)arg; 4527 snd_pcm_t *pcm = NULL; 4528 int err = snd_pcm_open(&pcm, a->device, SND_PCM_STREAM_PLAYBACK, 0); 4529 if (err < 0) { 4530 ac_log("[audio-probe] open %s failed: %s", 4531 a->device, snd_strerror(err)); 4532 free(a); 4533 return NULL; 4534 } 4535 unsigned int rate = 48000; 4536 snd_pcm_uframes_t period = 480, buffer = 1920; 4537 err = snd_pcm_set_params(pcm, 4538 SND_PCM_FORMAT_S16_LE, 4539 SND_PCM_ACCESS_RW_INTERLEAVED, 4540 2, /* stereo */ 4541 rate, 4542 1, /* soft_resample */ 4543 100 * 1000); /* 100ms latency */ 4544 if (err < 0) { 4545 ac_log("[audio-probe] set_params %s failed: %s", 4546 a->device, snd_strerror(err)); 4547 snd_pcm_close(pcm); 4548 free(a); 4549 return NULL; 4550 } 4551 /* Synthesize + play a sine wave of a->freq_hz at a->volume for 4552 * a->duration_ms milliseconds. Stereo S16 LE. */ 4553 int total_frames = (long long)rate * a->duration_ms / 1000; 4554 int16_t *buf = (int16_t *)malloc(period * 2 * sizeof(int16_t)); 4555 if (!buf) { snd_pcm_close(pcm); free(a); return NULL; } 4556 double phase = 0.0; 4557 double step = 2.0 * 3.14159265358979 * (double)a->freq_hz / (double)rate; 4558 int32_t amp = (int32_t)(a->volume * 32760.0); 4559 if (amp < 0) amp = 0; if (amp > 32760) amp = 32760; 4560 int written = 0; 4561 ac_log("[audio-probe] %s %dHz %dms vol=%.2f", a->device, a->freq_hz, 4562 a->duration_ms, a->volume); 4563 while (written < total_frames) { 4564 int chunk = total_frames - written; 4565 if (chunk > (int)period) chunk = (int)period; 4566 for (int i = 0; i < chunk; i++) { 4567 int16_t s = (int16_t)(sin(phase) * amp); 4568 buf[i * 2] = s; 4569 buf[i * 2 + 1] = s; 4570 phase += step; 4571 if (phase > 6.283185307179586) phase -= 6.283185307179586; 4572 } 4573 snd_pcm_sframes_t w = snd_pcm_writei(pcm, buf, chunk); 4574 if (w < 0) w = snd_pcm_recover(pcm, w, 1); 4575 if (w < 0) { ac_log("[audio-probe] writei failed: %s", 4576 snd_strerror((int)w)); break; } 4577 written += (int)w; 4578 } 4579 snd_pcm_drain(pcm); 4580 snd_pcm_close(pcm); 4581 free(buf); 4582 ac_log("[audio-probe] %s done (%d frames)", a->device, written); 4583 free(a); 4584 return NULL; 4585} 4586 4587static JSValue js_audio_test_pcm(JSContext *ctx, JSValueConst this_val, 4588 int argc, JSValueConst *argv) { 4589 (void)this_val; 4590 if (argc < 1) return JS_FALSE; 4591 struct audio_probe_args *a = (struct audio_probe_args *)calloc(1, sizeof(*a)); 4592 if (!a) return JS_FALSE; 4593 const char *dev = JS_ToCString(ctx, argv[0]); 4594 if (!dev) { free(a); return JS_FALSE; } 4595 strncpy(a->device, dev, sizeof(a->device) - 1); 4596 JS_FreeCString(ctx, dev); 4597 a->freq_hz = 440; 4598 a->duration_ms = 500; 4599 a->volume = 0.3f; 4600 if (argc >= 2) JS_ToInt32(ctx, &a->freq_hz, argv[1]); 4601 if (argc >= 3) JS_ToInt32(ctx, &a->duration_ms, argv[2]); 4602 if (argc >= 4) { 4603 double vol; JS_ToFloat64(ctx, &vol, argv[3]); 4604 a->volume = (float)vol; 4605 } 4606 pthread_t th; 4607 pthread_attr_t attr; 4608 pthread_attr_init(&attr); 4609 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 4610 int pr = pthread_create(&th, &attr, audio_probe_thread, a); 4611 pthread_attr_destroy(&attr); 4612 if (pr != 0) { free(a); return JS_FALSE; } 4613 return JS_TRUE; 4614} 4615 4616// ───────────────────────────────────────────────────────────────── 4617// Firmware update thread — runs /bin/ac-firmware-install (bundled copy of 4618// system/public/install-firmware.sh) and streams its stdout into 4619// rt->fw_log. The script handles all the dangerous bits (backup via 4620// flashrom -r, CBFS swap via cbfstool, flashrom -w) — we just orchestrate 4621// + surface progress. Availability is gated by os.mjs before this runs. 4622// ───────────────────────────────────────────────────────────────── 4623static void fw_log_line(ACRuntime *rt, const char *line) { 4624 int idx = rt->fw_log_count % 32; 4625 strncpy(rt->fw_log[idx], line, sizeof(rt->fw_log[idx]) - 1); 4626 rt->fw_log[idx][sizeof(rt->fw_log[idx]) - 1] = 0; 4627 __sync_fetch_and_add(&rt->fw_log_count, 1); 4628 ac_log("[fw] %s", line); 4629} 4630 4631static void *fw_thread_fn(void *arg) { 4632 ACRuntime *rt = (ACRuntime *)arg; 4633 if (!rt) return NULL; 4634 rt->fw_log_count = 0; 4635 rt->fw_backup_path[0] = 0; 4636 if (access("/bin/ac-firmware-install", X_OK) != 0) { 4637 fw_log_line(rt, "ac-firmware-install not bundled in initramfs"); 4638 rt->fw_ok = 0; 4639 rt->fw_pending = 0; 4640 rt->fw_done = 1; 4641 return NULL; 4642 } 4643 char cmd[512]; 4644 snprintf(cmd, sizeof(cmd), 4645 "AC_CDN=https://aesthetic.computer " 4646 "SPLASH_URL=https://oven.aesthetic.computer/firmware/splash.bmp " 4647 "/bin/ac-firmware-install %s 2>&1", 4648 rt->fw_args[0] ? rt->fw_args : ""); 4649 fw_log_line(rt, "starting ac-firmware-install"); 4650 FILE *p = popen(cmd, "r"); 4651 if (!p) { 4652 fw_log_line(rt, "popen failed"); 4653 rt->fw_ok = 0; 4654 rt->fw_pending = 0; 4655 rt->fw_done = 1; 4656 return NULL; 4657 } 4658 char line[256]; 4659 while (fgets(line, sizeof(line), p)) { 4660 char *nl = strchr(line, '\n'); 4661 if (nl) *nl = 0; 4662 // Capture the backup path line so JS can show it for "copy to USB" 4663 const char *bk = strstr(line, "/tmp/firmware-backup-"); 4664 if (bk) { 4665 int i = 0; 4666 while (bk[i] && bk[i] != ' ' && i < (int)sizeof(rt->fw_backup_path) - 1) { 4667 rt->fw_backup_path[i] = bk[i]; i++; 4668 } 4669 rt->fw_backup_path[i] = 0; 4670 } 4671 fw_log_line(rt, line); 4672 } 4673 int rc = pclose(p); 4674 rt->fw_ok = (WIFEXITED(rc) && WEXITSTATUS(rc) == 0) ? 1 : 0; 4675 char finish[64]; 4676 snprintf(finish, sizeof(finish), "ac-firmware-install exit=%d", 4677 WIFEXITED(rc) ? WEXITSTATUS(rc) : -1); 4678 fw_log_line(rt, finish); 4679 rt->fw_pending = 0; 4680 rt->fw_done = 1; 4681 return NULL; 4682} 4683 4684// system.firmwareInstall([mode]) — mode: "install" (default), "dry-run", 4685// "restore". Returns immediately; poll system.firmware.{pending,done,ok,log}. 4686static JSValue js_firmware_install(JSContext *ctx, JSValueConst this_val, 4687 int argc, JSValueConst *argv) { 4688 (void)this_val; 4689 if (!current_rt) return JS_UNDEFINED; 4690 if (current_rt->fw_pending) { 4691 ac_log("[fw] refused: already running"); 4692 return JS_NewBool(ctx, 0); 4693 } 4694 current_rt->fw_args[0] = 0; 4695 if (argc >= 1 && JS_IsString(argv[0])) { 4696 const char *mode = JS_ToCString(ctx, argv[0]); 4697 if (mode) { 4698 if (strcmp(mode, "dry-run") == 0) { 4699 strncpy(current_rt->fw_args, "--dry-run", 4700 sizeof(current_rt->fw_args) - 1); 4701 } else if (strcmp(mode, "restore") == 0) { 4702 strncpy(current_rt->fw_args, "--restore", 4703 sizeof(current_rt->fw_args) - 1); 4704 } 4705 JS_FreeCString(ctx, mode); 4706 } 4707 } 4708 current_rt->fw_pending = 1; 4709 current_rt->fw_done = 0; 4710 current_rt->fw_ok = 0; 4711 pthread_attr_t attr; 4712 pthread_attr_init(&attr); 4713 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 4714 int pr = pthread_create(&current_rt->fw_thread, &attr, 4715 fw_thread_fn, current_rt); 4716 pthread_attr_destroy(&attr); 4717 if (pr != 0) { 4718 current_rt->fw_pending = 0; 4719 current_rt->fw_done = 1; 4720 return JS_NewBool(ctx, 0); 4721 } 4722 return JS_NewBool(ctx, 1); 4723} 4724 4725// system.flashUpdate(srcPath[, devicePath[, initramfsSrcPath]]) 4726// devicePath defaults to auto-detected boot device if omitted. 4727// initramfsSrcPath is optional — when provided, the flash thread also copies 4728// it to `<ESP>/initramfs.cpio.gz` (matching the kernel's baked-in 4729// `initrd=\initramfs.cpio.gz` cmdline). Required for OTA updates since the 4730// kernel no longer embeds initramfs (Phase 2 de-embed). 4731static JSValue js_flash_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4732 (void)this_val; 4733 if (!current_rt || argc < 1) { ac_log("[flash] flashUpdate called with no runtime/args\n"); return JS_UNDEFINED; } 4734 const char *src = JS_ToCString(ctx, argv[0]); 4735 if (!src) { ac_log("[flash] flashUpdate: null src arg\n"); return JS_UNDEFINED; } 4736 ac_log("[flash] flashUpdate called: src=%s\n", src); 4737 current_rt->flash_pending = 1; 4738 current_rt->flash_done = 0; 4739 current_rt->flash_ok = 0; 4740 strncpy(current_rt->flash_src, src, sizeof(current_rt->flash_src) - 1); 4741 current_rt->flash_src[sizeof(current_rt->flash_src) - 1] = 0; 4742 JS_FreeCString(ctx, src); 4743 current_rt->flash_initramfs_src[0] = '\0'; 4744 // Device: use arg[1] if provided, else auto-detect 4745 if (argc >= 2 && !JS_IsUndefined(argv[1]) && !JS_IsNull(argv[1])) { 4746 const char *dev = JS_ToCString(ctx, argv[1]); 4747 if (dev) { 4748 strncpy(current_rt->flash_device, dev, sizeof(current_rt->flash_device) - 1); 4749 current_rt->flash_device[sizeof(current_rt->flash_device) - 1] = 0; 4750 JS_FreeCString(ctx, dev); 4751 } 4752 } else { 4753 detect_boot_device(current_rt->flash_device, sizeof(current_rt->flash_device)); 4754 } 4755 // Optional initramfs source — arg[2] 4756 if (argc >= 3 && !JS_IsUndefined(argv[2]) && !JS_IsNull(argv[2])) { 4757 const char *init_src = JS_ToCString(ctx, argv[2]); 4758 if (init_src) { 4759 strncpy(current_rt->flash_initramfs_src, init_src, 4760 sizeof(current_rt->flash_initramfs_src) - 1); 4761 current_rt->flash_initramfs_src[sizeof(current_rt->flash_initramfs_src) - 1] = 0; 4762 ac_log("[flash] initramfs src=%s", init_src); 4763 JS_FreeCString(ctx, init_src); 4764 } 4765 } 4766 // Pass runtime as arg (current_rt is thread-local, unavailable in new thread) 4767 pthread_attr_t attr; 4768 pthread_attr_init(&attr); 4769 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 4770 int pr = pthread_create(&current_rt->flash_thread, &attr, flash_thread_fn, current_rt); 4771 pthread_attr_destroy(&attr); 4772 if (pr != 0) { 4773 ac_log("[flash] pthread_create failed (%d)", pr); 4774 current_rt->flash_ok = 0; 4775 current_rt->flash_pending = 0; 4776 current_rt->flash_done = 1; 4777 } 4778 return JS_UNDEFINED; 4779} 4780 4781// system.reboot() — triggers system reboot (direct syscall, no external binary needed) 4782// system.setPowerRole("port0", "source"|"sink") — swap USB-C power role 4783static JSValue js_set_power_role(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4784 (void)this_val; 4785 if (argc < 2) return JS_NewBool(ctx, 0); 4786 const char *port = JS_ToCString(ctx, argv[0]); 4787 const char *role = JS_ToCString(ctx, argv[1]); 4788 if (!port || !role) { 4789 if (port) JS_FreeCString(ctx, port); 4790 if (role) JS_FreeCString(ctx, role); 4791 return JS_NewBool(ctx, 0); 4792 } 4793 // Validate role 4794 if (strcmp(role, "source") != 0 && strcmp(role, "sink") != 0) { 4795 JS_FreeCString(ctx, port); JS_FreeCString(ctx, role); 4796 return JS_NewBool(ctx, 0); 4797 } 4798 // Validate port name (must be "portN") 4799 if (strncmp(port, "port", 4) != 0 || !port[4]) { 4800 JS_FreeCString(ctx, port); JS_FreeCString(ctx, role); 4801 return JS_NewBool(ctx, 0); 4802 } 4803 char path[256]; 4804 snprintf(path, sizeof(path), "/sys/class/typec/%s/power_role", port); 4805 FILE *f = fopen(path, "w"); 4806 int ok = 0; 4807 if (f) { 4808 ok = fprintf(f, "%s\n", role) > 0; 4809 fclose(f); 4810 ac_log("[typec] setPowerRole %s -> %s: %s", port, role, ok ? "ok" : "write failed"); 4811 } else { 4812 ac_log("[typec] setPowerRole: cannot open %s: %s", path, strerror(errno)); 4813 } 4814 JS_FreeCString(ctx, port); 4815 JS_FreeCString(ctx, role); 4816 return JS_NewBool(ctx, ok); 4817} 4818 4819static JSValue build_usb_midi_state_obj(JSContext *ctx) { 4820 ACUsbMidiState state; 4821 int has_state = usb_midi_read_state(&state); 4822 if (!has_state) { 4823 memset(&state, 0, sizeof(state)); 4824 snprintf(state.reason, sizeof(state.reason), "uninitialized"); 4825 } 4826 4827 JSValue obj = JS_NewObject(ctx); 4828 JS_SetPropertyStr(ctx, obj, "enabled", JS_NewBool(ctx, state.enabled)); 4829 JS_SetPropertyStr(ctx, obj, "active", JS_NewBool(ctx, state.active)); 4830 JS_SetPropertyStr(ctx, obj, "reason", JS_NewString(ctx, state.reason[0] ? state.reason : "unknown")); 4831 JS_SetPropertyStr(ctx, obj, "udc", JS_NewString(ctx, state.udc)); 4832 JS_SetPropertyStr(ctx, obj, "port", JS_NewString(ctx, state.port)); 4833 JS_SetPropertyStr(ctx, obj, "powerRole", JS_NewString(ctx, state.power_role)); 4834 JS_SetPropertyStr(ctx, obj, "dataRole", JS_NewString(ctx, state.data_role)); 4835 JS_SetPropertyStr(ctx, obj, "alsaDevice", JS_NewString(ctx, state.alsa_device)); 4836 JS_SetPropertyStr(ctx, obj, "serial", JS_NewString(ctx, state.serial)); 4837 return obj; 4838} 4839 4840static JSValue js_usb_midi_status(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4841 (void)this_val; (void)argc; (void)argv; 4842 return build_usb_midi_state_obj(ctx); 4843} 4844 4845static JSValue js_usb_midi_control(JSContext *ctx, const char *action) { 4846 usb_midi_close(); 4847 if (access("/scripts/usb-midi-gadget.sh", X_OK) == 0) { 4848 char cmd[256]; 4849 snprintf(cmd, sizeof(cmd), "/scripts/usb-midi-gadget.sh %s >/tmp/usb-midi-gadget.log 2>&1", action); 4850 int rc = system(cmd); 4851 ac_log("[usb-midi] control %s rc=%d", action, rc); 4852 } else { 4853 FILE *fp = fopen("/run/usb-midi.state", "w"); 4854 if (fp) { 4855 fputs("enabled=0\nactive=0\nreason=missing-script\n", fp); 4856 fclose(fp); 4857 } 4858 ac_log("[usb-midi] control %s failed: missing /scripts/usb-midi-gadget.sh", action); 4859 } 4860 usb_midi_close(); 4861 return build_usb_midi_state_obj(ctx); 4862} 4863 4864static JSValue js_usb_midi_enable(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4865 (void)this_val; (void)argc; (void)argv; 4866 return js_usb_midi_control(ctx, "up"); 4867} 4868 4869static JSValue js_usb_midi_disable(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4870 (void)this_val; (void)argc; (void)argv; 4871 return js_usb_midi_control(ctx, "down"); 4872} 4873 4874static JSValue js_usb_midi_refresh(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4875 (void)this_val; (void)argc; (void)argv; 4876 return js_usb_midi_control(ctx, "refresh"); 4877} 4878 4879static JSValue js_usb_midi_note_on(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4880 (void)this_val; 4881 int32_t note = 60; 4882 int32_t velocity = 100; 4883 int32_t channel = 0; 4884 if (argc < 1 || JS_ToInt32(ctx, &note, argv[0])) return JS_FALSE; 4885 if (argc >= 2) JS_ToInt32(ctx, &velocity, argv[1]); 4886 if (argc >= 3) JS_ToInt32(ctx, &channel, argv[2]); 4887 return JS_NewBool(ctx, usb_midi_note_on(note, velocity, channel)); 4888} 4889 4890static JSValue js_usb_midi_note_off(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4891 (void)this_val; 4892 int32_t note = 60; 4893 int32_t velocity = 0; 4894 int32_t channel = 0; 4895 if (argc < 1 || JS_ToInt32(ctx, &note, argv[0])) return JS_FALSE; 4896 if (argc >= 2) JS_ToInt32(ctx, &velocity, argv[1]); 4897 if (argc >= 3) JS_ToInt32(ctx, &channel, argv[2]); 4898 return JS_NewBool(ctx, usb_midi_note_off(note, velocity, channel)); 4899} 4900 4901static JSValue js_usb_midi_all_notes_off(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4902 (void)this_val; 4903 int32_t channel = 0; 4904 if (argc >= 1) JS_ToInt32(ctx, &channel, argv[0]); 4905 return JS_NewBool(ctx, usb_midi_all_notes_off(channel)); 4906} 4907 4908// Hardened: triple sync with delays, pre-reboot EFI file size sanity check, 4909// perf flush, log flush before rebooting. 4910static JSValue js_reboot(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4911 (void)ctx; (void)this_val; (void)argc; (void)argv; 4912 ac_log("[system] reboot requested"); 4913 4914 // Pre-reboot sanity: verify EFI file exists and is >1MB (catch corrupted flash) 4915 { 4916 const char *efi_paths[] = { 4917 "/mnt/EFI/BOOT/BOOTX64.EFI", 4918 "/tmp/efi/EFI/BOOT/BOOTX64.EFI", 4919 NULL 4920 }; 4921 int efi_ok = 0; 4922 for (int i = 0; efi_paths[i]; i++) { 4923 struct stat st; 4924 if (stat(efi_paths[i], &st) == 0 && st.st_size > 1048576) { 4925 ac_log("[reboot] EFI verified: %s (%ld bytes)", efi_paths[i], (long)st.st_size); 4926 efi_ok = 1; 4927 break; 4928 } 4929 } 4930 if (!efi_ok) { 4931 ac_log("[reboot] WARNING: no valid EFI file found on mount — rebooting anyway"); 4932 } 4933 } 4934 4935 // Flush perf data before reboot 4936 perf_flush(); 4937 4938 // Play shutdown chime and let audio drain 4939 if (current_rt && current_rt->audio) { 4940 audio_shutdown_sound(current_rt->audio); 4941 usleep(500000); // 500ms for chime to play 4942 } 4943 4944 // Flush log file 4945 ac_log("[reboot] syncing filesystems..."); 4946 ac_log_flush(); 4947 4948 // Triple sync with delays — vfat write-back needs time 4949 sync(); 4950 usleep(500000); 4951 sync(); 4952 usleep(500000); 4953 sync(); 4954 4955 ac_log("[reboot] executing reboot syscall"); 4956 ac_log_flush(); 4957 4958 // Exit with code 2 — init script sees this as reboot request 4959 _exit(2); 4960 return JS_UNDEFINED; 4961} 4962 4963// system.poweroff() — signal main loop to run shutdown animation then exit 4964extern volatile int poweroff_requested; 4965static JSValue js_poweroff(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4966 (void)ctx; (void)this_val; (void)argc; (void)argv; 4967 ac_log("[system] poweroff requested via JS"); 4968 poweroff_requested = 1; // main loop will run bye animation + exit(0) 4969 return JS_UNDEFINED; 4970} 4971 4972// ============================================================ 4973// JS Native Functions — PTY Terminal Emulator 4974// ============================================================ 4975 4976// system.pty.spawn(cmd, [args...], cols, rows) — spawn a process in a PTY 4977static JSValue js_pty_spawn(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4978 (void)this_val; 4979 if (!current_rt || argc < 1) return JS_FALSE; 4980 if (current_rt->pty_active) { 4981 pty_destroy(&current_rt->pty); 4982 current_rt->pty_active = 0; 4983 } 4984 4985 const char *cmd = JS_ToCString(ctx, argv[0]); 4986 if (!cmd) return JS_FALSE; 4987 4988 // Collect args from JS array (argv[1]) or use cmd as argv[0] 4989 char *child_argv[32] = {0}; 4990 int nargs = 0; 4991 child_argv[nargs++] = (char *)cmd; 4992 4993 if (argc > 1 && JS_IsArray(ctx, argv[1])) { 4994 int len = 0; 4995 JSValue jlen = JS_GetPropertyStr(ctx, argv[1], "length"); 4996 JS_ToInt32(ctx, &len, jlen); 4997 JS_FreeValue(ctx, jlen); 4998 for (int i = 0; i < len && nargs < 30; i++) { 4999 JSValue el = JS_GetPropertyUint32(ctx, argv[1], i); 5000 child_argv[nargs++] = (char *)JS_ToCString(ctx, el); 5001 JS_FreeValue(ctx, el); 5002 } 5003 } 5004 child_argv[nargs] = NULL; 5005 5006 int cols = 80, rows = 24; 5007 if (argc > 2) JS_ToInt32(ctx, &cols, argv[2]); 5008 if (argc > 3) JS_ToInt32(ctx, &rows, argv[3]); 5009 5010 int ok = pty_spawn(&current_rt->pty, cols, rows, cmd, child_argv); 5011 5012 // Free ToCString results for args 5013 for (int i = 1; i < nargs; i++) { 5014 JS_FreeCString(ctx, child_argv[i]); 5015 } 5016 JS_FreeCString(ctx, cmd); 5017 5018 if (ok == 0) { 5019 current_rt->pty_active = 1; 5020 return JS_TRUE; 5021 } 5022 return JS_FALSE; 5023} 5024 5025// system.pty.write(str) — send input to PTY 5026static JSValue js_pty_write(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5027 (void)this_val; 5028 if (!current_rt || !current_rt->pty_active || argc < 1) return JS_UNDEFINED; 5029 size_t len; 5030 const char *data = JS_ToCStringLen(ctx, &len, argv[0]); 5031 if (!data) return JS_UNDEFINED; 5032 pty_write(&current_rt->pty, data, (int)len); 5033 JS_FreeCString(ctx, data); 5034 return JS_UNDEFINED; 5035} 5036 5037// system.pty.resize(cols, rows) — resize PTY 5038static JSValue js_pty_resize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5039 (void)this_val; 5040 if (!current_rt || !current_rt->pty_active || argc < 2) return JS_UNDEFINED; 5041 int cols, rows; 5042 JS_ToInt32(ctx, &cols, argv[0]); 5043 JS_ToInt32(ctx, &rows, argv[1]); 5044 pty_resize(&current_rt->pty, cols, rows); 5045 return JS_UNDEFINED; 5046} 5047 5048// system.pty.kill() — kill the PTY child 5049static JSValue js_pty_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5050 (void)this_val; (void)argc; (void)argv; 5051 if (!current_rt || !current_rt->pty_active) return JS_UNDEFINED; 5052 pty_destroy(&current_rt->pty); 5053 current_rt->pty_active = 0; 5054 return JS_UNDEFINED; 5055} 5056 5057// JS Native Functions — PTY2 (second terminal for split-screen) 5058// ============================================================ 5059 5060static JSValue js_pty2_spawn(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5061 (void)this_val; 5062 if (!current_rt || argc < 1) return JS_FALSE; 5063 if (current_rt->pty2_active) { 5064 pty_destroy(&current_rt->pty2); 5065 current_rt->pty2_active = 0; 5066 } 5067 const char *cmd = JS_ToCString(ctx, argv[0]); 5068 if (!cmd) return JS_FALSE; 5069 char *child_argv[32] = {0}; 5070 int nargs = 0; 5071 child_argv[nargs++] = (char *)cmd; 5072 if (argc > 1 && JS_IsArray(ctx, argv[1])) { 5073 int len = 0; 5074 JSValue jlen = JS_GetPropertyStr(ctx, argv[1], "length"); 5075 JS_ToInt32(ctx, &len, jlen); 5076 JS_FreeValue(ctx, jlen); 5077 for (int i = 0; i < len && nargs < 30; i++) { 5078 JSValue el = JS_GetPropertyUint32(ctx, argv[1], i); 5079 child_argv[nargs++] = (char *)JS_ToCString(ctx, el); 5080 JS_FreeValue(ctx, el); 5081 } 5082 } 5083 child_argv[nargs] = NULL; 5084 int cols = 80, rows = 24; 5085 if (argc > 2) JS_ToInt32(ctx, &cols, argv[2]); 5086 if (argc > 3) JS_ToInt32(ctx, &rows, argv[3]); 5087 int ok = pty_spawn(&current_rt->pty2, cols, rows, cmd, child_argv); 5088 for (int i = 1; i < nargs; i++) JS_FreeCString(ctx, child_argv[i]); 5089 JS_FreeCString(ctx, cmd); 5090 if (ok == 0) { current_rt->pty2_active = 1; return JS_TRUE; } 5091 return JS_FALSE; 5092} 5093 5094static JSValue js_pty2_write(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5095 (void)this_val; 5096 if (!current_rt || !current_rt->pty2_active || argc < 1) return JS_UNDEFINED; 5097 size_t len; 5098 const char *data = JS_ToCStringLen(ctx, &len, argv[0]); 5099 if (!data) return JS_UNDEFINED; 5100 pty_write(&current_rt->pty2, data, (int)len); 5101 JS_FreeCString(ctx, data); 5102 return JS_UNDEFINED; 5103} 5104 5105static JSValue js_pty2_resize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5106 (void)this_val; 5107 if (!current_rt || !current_rt->pty2_active || argc < 2) return JS_UNDEFINED; 5108 int cols, rows; 5109 JS_ToInt32(ctx, &cols, argv[0]); 5110 JS_ToInt32(ctx, &rows, argv[1]); 5111 pty_resize(&current_rt->pty2, cols, rows); 5112 return JS_UNDEFINED; 5113} 5114 5115static JSValue js_pty2_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5116 (void)this_val; (void)argc; (void)argv; 5117 if (!current_rt || !current_rt->pty2_active) return JS_UNDEFINED; 5118 pty_destroy(&current_rt->pty2); 5119 current_rt->pty2_active = 0; 5120 return JS_UNDEFINED; 5121} 5122 5123// system.jump(pieceName) — request piece switch (handled in main loop) 5124// system.saveConfig(key, value) — merge a key-value pair into /mnt/config.json 5125static JSValue js_save_config(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5126 (void)this_val; 5127 if (argc < 2) return JS_FALSE; 5128 const char *key = JS_ToCString(ctx, argv[0]); 5129 const char *val = JS_ToCString(ctx, argv[1]); 5130 if (!key || !val) { 5131 JS_FreeCString(ctx, key); 5132 JS_FreeCString(ctx, val); 5133 return JS_FALSE; 5134 } 5135 5136 // Read existing config 5137 char buf[8192] = {0}; 5138 FILE *f = fopen("/mnt/config.json", "r"); 5139 if (f) { 5140 size_t n = fread(buf, 1, sizeof(buf) - 1, f); 5141 buf[n] = '\0'; 5142 fclose(f); 5143 } 5144 5145 // Simple JSON merge: if key exists, replace its value; otherwise append 5146 // We build a new JSON string manually since we don't have a JSON library 5147 char out[8192] = {0}; 5148 char needle[256]; 5149 snprintf(needle, sizeof(needle), "\"%s\"", key); 5150 5151 // Detect if value is a bare literal (boolean/number) — don't quote it 5152 int is_bare = (strcmp(val, "true") == 0 || strcmp(val, "false") == 0 || 5153 (val[0] >= '0' && val[0] <= '9') || val[0] == '-'); 5154 const char *qL = is_bare ? "" : "\""; 5155 const char *qR = is_bare ? "" : "\""; 5156 5157 char *existing = strstr(buf, needle); 5158 if (existing && buf[0] == '{') { 5159 // Key exists — find value start (after ":") and end (next , or }) 5160 char *colon = strchr(existing, ':'); 5161 if (colon) { 5162 char *vstart = colon + 1; 5163 while (*vstart == ' ') vstart++; 5164 char *vend; 5165 if (*vstart == '"') { 5166 vend = strchr(vstart + 1, '"'); 5167 if (vend) vend++; // past closing quote 5168 } else { 5169 vend = vstart; 5170 while (*vend && *vend != ',' && *vend != '}') vend++; 5171 } 5172 if (vend) { 5173 int prefix_len = (int)(vstart - buf); 5174 snprintf(out, sizeof(out), "%.*s%s%s%s%s", 5175 prefix_len, buf, qL, val, qR, vend); 5176 } 5177 } 5178 } else if (buf[0] == '{') { 5179 // Append new key before closing } 5180 char *closing = strrchr(buf, '}'); 5181 if (closing) { 5182 int prefix_len = (int)(closing - buf); 5183 // Check if there's existing content (non-empty object) 5184 int has_content = 0; 5185 for (char *p = buf + 1; p < closing; p++) { 5186 if (*p != ' ' && *p != '\n' && *p != '\r' && *p != '\t') { 5187 has_content = 1; break; 5188 } 5189 } 5190 snprintf(out, sizeof(out), "%.*s%s\"%s\":%s%s%s}", 5191 prefix_len, buf, has_content ? "," : "", key, qL, val, qR); 5192 } 5193 } else { 5194 // No valid JSON — create new 5195 snprintf(out, sizeof(out), "{\"%s\":%s%s%s}", key, qL, val, qR); 5196 } 5197 5198 f = fopen("/mnt/config.json", "w"); 5199 if (f) { 5200 fputs(out[0] ? out : buf, f); 5201 fflush(f); 5202 fsync(fileno(f)); 5203 fclose(f); 5204 config_cache_dirty = 1; 5205 ac_log("[config] saved %s to /mnt/config.json\n", key); 5206 // Update runtime config if it's a known field 5207 if (current_rt && strcmp(key, "handle") == 0) { 5208 strncpy(current_rt->handle, val, sizeof(current_rt->handle) - 1); 5209 } else if (current_rt && strcmp(key, "piece") == 0) { 5210 strncpy(current_rt->piece, val, sizeof(current_rt->piece) - 1); 5211 } else if (strcmp(key, "voice") == 0) { 5212 extern int voice_off; 5213 voice_off = (strcmp(val, "off") == 0); 5214 ac_log("[config] voice_off = %d\n", voice_off); 5215 } 5216 } else { 5217 ac_log("[config] ERROR: cannot write /mnt/config.json\n"); 5218 } 5219 5220 JS_FreeCString(ctx, key); 5221 JS_FreeCString(ctx, val); 5222 return JS_TRUE; 5223} 5224 5225// system.startSSH() — start dropbear SSH daemon (generates host key if needed) 5226static int ssh_started = 0; 5227static JSValue js_start_ssh(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5228 (void)this_val; (void)argc; (void)argv; 5229 if (ssh_started) return JS_NewBool(ctx, 1); 5230 5231 // Check if dropbear exists 5232 if (access("/usr/sbin/dropbear", X_OK) != 0 && access("/bin/dropbear", X_OK) != 0) { 5233 ac_log("[ssh] dropbear not found in initramfs\n"); 5234 return JS_NewBool(ctx, 0); 5235 } 5236 5237 // Generate host key if needed 5238 if (access("/etc/dropbear/dropbear_ed25519_host_key", F_OK) != 0) { 5239 ac_log("[ssh] generating host key...\n"); 5240 system("mkdir -p /etc/dropbear && " 5241 "dropbearkey -t ed25519 -f /etc/dropbear/dropbear_ed25519_host_key 2>/dev/null"); 5242 } 5243 5244 // Start dropbear (no password auth, only key-based or allow all for now) 5245 // -R = generate keys if missing, -B = allow blank passwords (for root w/o passwd) 5246 // -p 22 = listen on port 22, -F = foreground (& to background) 5247 ac_log("[ssh] starting dropbear on port 22...\n"); 5248 system("dropbear -R -B -p 22 -P /tmp/dropbear.pid 2>/tmp/dropbear.log &"); 5249 ssh_started = 1; 5250 ac_log("[ssh] dropbear started\n"); 5251 return JS_NewBool(ctx, 1); 5252} 5253 5254// system.execNode(script) — run Node.js with a script string, async 5255static JSValue js_exec_node(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5256 (void)this_val; 5257 if (!current_rt || argc < 1) return JS_UNDEFINED; 5258 if (current_rt->fetch_pending) return JS_FALSE; // reuse fetch slot for output 5259 5260 const char *script = JS_ToCString(ctx, argv[0]); 5261 if (!script) return JS_UNDEFINED; 5262 5263 // Write script to temp file 5264 FILE *sf = fopen("/tmp/ac_node_script.mjs", "w"); 5265 if (sf) { fputs(script, sf); fclose(sf); } 5266 5267 ac_log("[node] executing script (%ld bytes)\n", (long)strlen(script)); 5268 // Run node and capture output to /tmp/ac_fetch.json (reusing fetch result slot) 5269 unlink("/tmp/ac_fetch.json"); 5270 unlink("/tmp/ac_fetch_rc"); 5271 unlink("/tmp/ac_fetch_err"); 5272 system("sh -c 'node /tmp/ac_node_script.mjs > /tmp/ac_fetch.json 2>/tmp/ac_fetch_err;" 5273 " echo $? > /tmp/ac_fetch_rc' &"); 5274 current_rt->fetch_pending = 1; 5275 current_rt->fetch_result[0] = 0; 5276 current_rt->fetch_error[0] = 0; 5277 5278 JS_FreeCString(ctx, script); 5279 return JS_TRUE; 5280} 5281 5282// system.listPieces() — scan /pieces/*.mjs and /pieces/*.lisp, return array of piece names 5283static JSValue js_list_pieces(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5284 (void)this_val; (void)argc; (void)argv; 5285 JSValue arr = JS_NewArray(ctx); 5286 DIR *d = opendir("/pieces"); 5287 if (d) { 5288 struct dirent *ent; 5289 uint32_t idx = 0; 5290 while ((ent = readdir(d)) != NULL) { 5291 if (ent->d_name[0] == '.') continue; 5292 // Check for .mjs 5293 char *dot = strstr(ent->d_name, ".mjs"); 5294 if (dot && dot[4] == '\0') { 5295 char name[64]; 5296 int len = (int)(dot - ent->d_name); 5297 if (len > 63) len = 63; 5298 memcpy(name, ent->d_name, len); 5299 name[len] = '\0'; 5300 JS_SetPropertyUint32(ctx, arr, idx++, JS_NewString(ctx, name)); 5301 continue; 5302 } 5303 // Check for .lisp — include extension so list.mjs can distinguish 5304 dot = strstr(ent->d_name, ".lisp"); 5305 if (dot && dot[5] == '\0') { 5306 JS_SetPropertyUint32(ctx, arr, idx++, JS_NewString(ctx, ent->d_name)); 5307 } 5308 } 5309 closedir(d); 5310 } 5311 return arr; 5312} 5313 5314// system.listPrinters() — detect USB printers via /dev/usb/lp* and sysfs 5315static JSValue js_list_printers(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5316 (void)this_val; (void)argc; (void)argv; 5317 JSValue arr = JS_NewArray(ctx); 5318 uint32_t idx = 0; 5319 5320 // Scan /dev/usb/lp* devices 5321 DIR *d = opendir("/dev/usb"); 5322 if (d) { 5323 struct dirent *ent; 5324 while ((ent = readdir(d)) != NULL) { 5325 if (strncmp(ent->d_name, "lp", 2) != 0) continue; 5326 char devpath[64]; 5327 snprintf(devpath, sizeof(devpath), "/dev/usb/%s", ent->d_name); 5328 5329 JSValue printer = JS_NewObject(ctx); 5330 JS_SetPropertyStr(ctx, printer, "device", JS_NewString(ctx, devpath)); 5331 JS_SetPropertyStr(ctx, printer, "id", JS_NewString(ctx, ent->d_name)); 5332 5333 // Try to find printer name via sysfs usbmisc 5334 char syspath[256], name[128] = {0}, vendor[128] = {0}; 5335 snprintf(syspath, sizeof(syspath), 5336 "/sys/class/usbmisc/%s/device/../product", ent->d_name); 5337 FILE *fp = fopen(syspath, "r"); 5338 if (fp) { 5339 if (fgets(name, sizeof(name), fp)) { 5340 char *nl = strchr(name, '\n'); 5341 if (nl) *nl = 0; 5342 } 5343 fclose(fp); 5344 } 5345 snprintf(syspath, sizeof(syspath), 5346 "/sys/class/usbmisc/%s/device/../manufacturer", ent->d_name); 5347 fp = fopen(syspath, "r"); 5348 if (fp) { 5349 if (fgets(vendor, sizeof(vendor), fp)) { 5350 char *nl = strchr(vendor, '\n'); 5351 if (nl) *nl = 0; 5352 } 5353 fclose(fp); 5354 } 5355 5356 if (name[0]) 5357 JS_SetPropertyStr(ctx, printer, "name", JS_NewString(ctx, name)); 5358 else 5359 JS_SetPropertyStr(ctx, printer, "name", 5360 JS_NewString(ctx, ent->d_name)); 5361 if (vendor[0]) 5362 JS_SetPropertyStr(ctx, printer, "vendor", 5363 JS_NewString(ctx, vendor)); 5364 5365 JS_SetPropertyUint32(ctx, arr, idx++, printer); 5366 } 5367 closedir(d); 5368 } 5369 return arr; 5370} 5371 5372// system.printRaw(devicePath, byteArray) — write raw bytes to printer device 5373// byteArray is a JS array of integers (0-255) 5374static JSValue js_print_raw(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5375 (void)this_val; 5376 if (argc < 2) return JS_FALSE; 5377 const char *devpath = JS_ToCString(ctx, argv[0]); 5378 if (!devpath) return JS_FALSE; 5379 5380 // Validate device path starts with /dev/usb/lp 5381 if (strncmp(devpath, "/dev/usb/lp", 11) != 0) { 5382 ac_log("[print] rejected non-printer path: %s\n", devpath); 5383 JS_FreeCString(ctx, devpath); 5384 return JS_FALSE; 5385 } 5386 5387 // Get array length 5388 JSValue lenVal = JS_GetPropertyStr(ctx, argv[1], "length"); 5389 uint32_t len = 0; 5390 JS_ToUint32(ctx, &len, lenVal); 5391 JS_FreeValue(ctx, lenVal); 5392 5393 if (len == 0 || len > 65536) { 5394 ac_log("[print] invalid data length: %u\n", len); 5395 JS_FreeCString(ctx, devpath); 5396 return JS_FALSE; 5397 } 5398 5399 // Build byte buffer from JS array 5400 uint8_t *buf = malloc(len); 5401 if (!buf) { 5402 JS_FreeCString(ctx, devpath); 5403 return JS_FALSE; 5404 } 5405 for (uint32_t i = 0; i < len; i++) { 5406 JSValue v = JS_GetPropertyUint32(ctx, argv[1], i); 5407 int32_t b = 0; 5408 JS_ToInt32(ctx, &b, v); 5409 JS_FreeValue(ctx, v); 5410 buf[i] = (uint8_t)(b & 0xFF); 5411 } 5412 5413 // Open device and write 5414 FILE *fp = fopen(devpath, "wb"); 5415 if (!fp) { 5416 ac_log("[print] failed to open %s: %s\n", devpath, strerror(errno)); 5417 free(buf); 5418 JS_FreeCString(ctx, devpath); 5419 return JS_FALSE; 5420 } 5421 5422 size_t written = fwrite(buf, 1, len, fp); 5423 fflush(fp); 5424 fclose(fp); 5425 free(buf); 5426 5427 ac_log("[print] wrote %zu/%u bytes to %s\n", written, len, devpath); 5428 JS_FreeCString(ctx, devpath); 5429 return written == len ? JS_TRUE : JS_FALSE; 5430} 5431 5432static JSValue js_jump(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5433 (void)this_val; 5434 if (!current_rt || argc < 1) return JS_UNDEFINED; 5435 const char *name = JS_ToCString(ctx, argv[0]); 5436 if (!name) return JS_UNDEFINED; 5437 strncpy(current_rt->jump_target, name, sizeof(current_rt->jump_target) - 1); 5438 current_rt->jump_target[sizeof(current_rt->jump_target) - 1] = 0; 5439 current_rt->jump_requested = 1; 5440 ac_log("[system] jump requested: %s", name); 5441 JS_FreeCString(ctx, name); 5442 return JS_UNDEFINED; 5443} 5444 5445// system.volumeAdjust(delta) — +1 up, -1 down, 0 mute toggle 5446static JSValue js_volume_adjust(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5447 (void)this_val; 5448 if (!current_rt || !current_rt->audio || argc < 1) return JS_UNDEFINED; 5449 int delta = 0; 5450 JS_ToInt32(ctx, &delta, argv[0]); 5451 audio_volume_adjust(current_rt->audio, delta); 5452 return JS_UNDEFINED; 5453} 5454 5455// system.brightnessAdjust(delta) — +1 up, -1 down 5456static JSValue js_brightness_adjust(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5457 (void)this_val; 5458 if (argc < 1) return JS_UNDEFINED; 5459 int delta = 0; 5460 JS_ToInt32(ctx, &delta, argv[0]); 5461 // Read current backlight from sysfs 5462 DIR *bldir = opendir("/sys/class/backlight"); 5463 if (!bldir) return JS_UNDEFINED; 5464 struct dirent *ent; 5465 while ((ent = readdir(bldir))) { 5466 if (ent->d_name[0] == '.') continue; 5467 char tmp[160]; 5468 snprintf(tmp, sizeof(tmp), "/sys/class/backlight/%s/max_brightness", ent->d_name); 5469 FILE *f = fopen(tmp, "r"); 5470 if (!f) continue; 5471 int bl_max = 100; 5472 fscanf(f, "%d", &bl_max); 5473 fclose(f); 5474 snprintf(tmp, sizeof(tmp), "/sys/class/backlight/%s/brightness", ent->d_name); 5475 f = fopen(tmp, "r"); 5476 int cur = bl_max / 2; 5477 if (f) { fscanf(f, "%d", &cur); fclose(f); } 5478 int step = bl_max / 20; // 5% 5479 if (step < 1) step = 1; 5480 cur += delta * step; 5481 if (cur < 1) cur = 1; 5482 if (cur > bl_max) cur = bl_max; 5483 f = fopen(tmp, "w"); 5484 if (f) { fprintf(f, "%d", cur); fclose(f); } 5485 break; 5486 } 5487 closedir(bldir); 5488 return JS_UNDEFINED; 5489} 5490 5491// system.openBrowser(url) — launch Firefox for OAuth login 5492// Under Wayland: fork+exec firefox as sibling Wayland client (cage composites it on top) 5493// Under DRM: releases DRM master, runs cage+firefox, reclaims on exit 5494// Returns true if browser exited normally. 5495static JSValue js_open_browser(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5496 (void)this_val; 5497 if (argc < 1 || !current_rt) return JS_FALSE; 5498 const char *url = JS_ToCString(ctx, argv[0]); 5499 if (!url) return JS_FALSE; 5500 5501 ac_log("[browser] Opening: %s", url); 5502 int rc = -1; 5503 5504#ifdef USE_WAYLAND 5505 if (getenv("WAYLAND_DISPLAY")) { 5506 // Under cage: just fork+exec Firefox as a sibling Wayland client 5507 // cage handles window stacking — Firefox appears on top, ac-native resumes when it exits 5508 pid_t pid = fork(); 5509 if (pid == 0) { 5510 // Child process — exec firefox 5511 setenv("MOZ_ENABLE_WAYLAND", "1", 1); 5512 setenv("GDK_BACKEND", "wayland", 1); 5513 setenv("MOZ_DISABLE_CONTENT_SANDBOX", "1", 1); 5514 setenv("DBUS_SESSION_BUS_ADDRESS", "disabled:", 1); 5515 setenv("MOZ_DBUS_REMOTE", "0", 1); 5516 setenv("HOME", "/tmp", 1); 5517 setenv("LD_LIBRARY_PATH", "/lib64:/opt/firefox", 1); 5518 setenv("GRE_HOME", "/opt/firefox", 1); 5519 mkdir("/tmp/.mozilla", 0700); 5520 execlp("/opt/firefox/firefox-bin", "firefox-bin", 5521 "--kiosk", "--no-remote", "--new-instance", url, NULL); 5522 _exit(127); 5523 } else if (pid > 0) { 5524 // Parent — wait for Firefox to exit 5525 int status; 5526 waitpid(pid, &status, 0); 5527 rc = WIFEXITED(status) ? WEXITSTATUS(status) : -1; 5528 ac_log("[browser] Firefox exited: %d", rc); 5529 } else { 5530 ac_log("[browser] fork failed: %s", strerror(errno)); 5531 } 5532 } else 5533#endif 5534 { 5535 // Legacy DRM handoff path 5536 extern int drm_release_master(void *display); 5537 extern int drm_acquire_master(void *display); 5538 extern void *g_display; 5539 if (g_display) { 5540 drm_release_master(g_display); 5541 ac_log("[browser] Released DRM master"); 5542 } 5543 5544 char cmd[4096]; 5545 mkdir("/tmp/xdg", 0700); 5546 mkdir("/tmp/.mozilla", 0700); 5547 snprintf(cmd, sizeof(cmd), 5548 "export HOME=/tmp && " 5549 "export XDG_RUNTIME_DIR=/tmp/xdg && " 5550 "export WLR_BACKENDS=drm && " 5551 "export WLR_SESSION=direct && " 5552 "export WLR_RENDERER=pixman && " 5553 "export LIBSEAT_BACKEND=noop && " 5554 "export LD_LIBRARY_PATH=/lib64:/opt/firefox && " 5555 "export LIBGL_ALWAYS_SOFTWARE=1 && " 5556 "export MOZ_ENABLE_WAYLAND=1 && " 5557 "export GDK_BACKEND=wayland && " 5558 "cage -s -- /opt/firefox/firefox-bin " 5559 "--kiosk --no-remote --new-instance '%s' " 5560 ">/mnt/cage.log 2>&1; echo \"exit=$?\" >>/mnt/cage.log", 5561 url); 5562 rc = system(cmd); 5563 ac_log("[browser] cage exited: %d", rc); 5564 5565 if (g_display) { 5566 drm_acquire_master(g_display); 5567 ac_log("[browser] Reclaimed DRM master"); 5568 } 5569 } 5570 5571 JS_FreeCString(ctx, url); 5572 return JS_NewBool(ctx, rc == 0); 5573} 5574 5575// system.qrEncode(text) → { size: N, modules: [bool, bool, ...] } 5576// Encodes text as QR code using nayuki qrcodegen, returns module grid. 5577static JSValue js_qr_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5578 (void)this_val; 5579 if (argc < 1) return JS_UNDEFINED; 5580 const char *text = JS_ToCString(ctx, argv[0]); 5581 if (!text) return JS_UNDEFINED; 5582 5583 // Buffers for qrcodegen (version 1-40, up to ~4296 chars at ECC LOW) 5584 uint8_t qrcode[qrcodegen_BUFFER_LEN_MAX]; 5585 uint8_t tempBuf[qrcodegen_BUFFER_LEN_MAX]; 5586 5587 bool ok = qrcodegen_encodeText(text, tempBuf, qrcode, 5588 qrcodegen_Ecc_LOW, qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, 5589 qrcodegen_Mask_AUTO, true); 5590 JS_FreeCString(ctx, text); 5591 5592 if (!ok) return JS_UNDEFINED; 5593 5594 int size = qrcodegen_getSize(qrcode); 5595 JSValue result = JS_NewObject(ctx); 5596 JS_SetPropertyStr(ctx, result, "size", JS_NewInt32(ctx, size)); 5597 5598 JSValue modules = JS_NewArray(ctx); 5599 for (int y = 0; y < size; y++) { 5600 for (int x = 0; x < size; x++) { 5601 JS_SetPropertyUint32(ctx, modules, (uint32_t)(y * size + x), 5602 JS_NewBool(ctx, qrcodegen_getModule(qrcode, x, y))); 5603 } 5604 } 5605 JS_SetPropertyStr(ctx, result, "modules", modules); 5606 return result; 5607} 5608 5609// nopaint.is(stateStr) — returns true if current nopaint state matches 5610static JSValue js_nopaint_is(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5611 (void)this_val; 5612 if (argc < 1 || !current_rt) return JS_FALSE; 5613 const char *query = JS_ToCString(ctx, argv[0]); 5614 if (!query) return JS_FALSE; 5615 int match = 0; 5616 if (strcmp(query, "painting") == 0) match = (current_rt->nopaint_state == 1); 5617 else if (strcmp(query, "idle") == 0) match = (current_rt->nopaint_state == 0); 5618 JS_FreeCString(ctx, query); 5619 return JS_NewBool(ctx, match); 5620} 5621 5622// nopaint.cancelStroke() — abort current stroke 5623static JSValue js_nopaint_cancel(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 5624 (void)this_val; (void)argc; (void)argv; 5625 if (current_rt) { current_rt->nopaint_state = 0; current_rt->nopaint_needs_bake = 0; } 5626 return JS_UNDEFINED; 5627} 5628 5629static JSValue build_system_obj(JSContext *ctx) { 5630 JSValue sys = JS_NewObject(ctx); 5631 5632 // Battery info — scan /sys/class/power_supply/ for any battery 5633 char buf[64]; 5634 int capacity = -1, power_now = 0, energy_now = 0; 5635 const char *status = "Unknown"; 5636 char bat_path[128] = ""; 5637 5638 // Try BAT0, BAT1, or scan directory 5639 const char *bat_names[] = {"BAT0", "BAT1", NULL}; 5640 for (int i = 0; bat_names[i] && !bat_path[0]; i++) { 5641 char tmp[128]; 5642 snprintf(tmp, sizeof(tmp), "/sys/class/power_supply/%s/capacity", bat_names[i]); 5643 if (read_sysfs(tmp, buf, sizeof(buf)) > 0) { 5644 snprintf(bat_path, sizeof(bat_path), "/sys/class/power_supply/%s", bat_names[i]); 5645 } 5646 } 5647 // Fallback: scan directory 5648 if (!bat_path[0]) { 5649 DIR *dir = opendir("/sys/class/power_supply"); 5650 if (dir) { 5651 struct dirent *ent; 5652 while ((ent = readdir(dir)) && !bat_path[0]) { 5653 if (ent->d_name[0] == '.') continue; 5654 char tmp[160]; 5655 snprintf(tmp, sizeof(tmp), "/sys/class/power_supply/%s/type", ent->d_name); 5656 if (read_sysfs(tmp, buf, sizeof(buf)) > 0 && strcmp(buf, "Battery") == 0) { 5657 snprintf(bat_path, sizeof(bat_path), "/sys/class/power_supply/%s", ent->d_name); 5658 } 5659 } 5660 closedir(dir); 5661 } 5662 } 5663 5664 if (bat_path[0]) { 5665 char tmp[160]; 5666 snprintf(tmp, sizeof(tmp), "%s/capacity", bat_path); 5667 if (read_sysfs(tmp, buf, sizeof(buf)) > 0) capacity = atoi(buf); 5668 snprintf(tmp, sizeof(tmp), "%s/status", bat_path); 5669 if (read_sysfs(tmp, buf, sizeof(buf)) > 0) 5670 status = (strcmp(buf, "Charging") == 0) ? "Charging" : 5671 (strcmp(buf, "Full") == 0) ? "Full" : 5672 (strcmp(buf, "Discharging") == 0) ? "Discharging" : "Unknown"; 5673 snprintf(tmp, sizeof(tmp), "%s/power_now", bat_path); 5674 if (read_sysfs(tmp, buf, sizeof(buf)) > 0) power_now = atoi(buf); 5675 snprintf(tmp, sizeof(tmp), "%s/energy_now", bat_path); 5676 if (read_sysfs(tmp, buf, sizeof(buf)) > 0) energy_now = atoi(buf); 5677 } 5678 5679 JSValue battery = JS_NewObject(ctx); 5680 JS_SetPropertyStr(ctx, battery, "percent", JS_NewInt32(ctx, capacity)); 5681 JS_SetPropertyStr(ctx, battery, "charging", JS_NewBool(ctx, strcmp(status, "Charging") == 0)); 5682 JS_SetPropertyStr(ctx, battery, "status", JS_NewString(ctx, status)); 5683 5684 if (power_now > 0 && energy_now > 0 && strcmp(status, "Discharging") == 0) { 5685 double hours = (double)energy_now / (double)power_now; 5686 JS_SetPropertyStr(ctx, battery, "minutesLeft", JS_NewInt32(ctx, (int)(hours * 60))); 5687 } else { 5688 JS_SetPropertyStr(ctx, battery, "minutesLeft", JS_NewInt32(ctx, -1)); 5689 } 5690 5691 JS_SetPropertyStr(ctx, sys, "battery", battery); 5692 5693 // Hardware info — system.hw (model, cpu, ram) 5694 { 5695 JSValue hw = JS_NewObject(ctx); 5696 char hbuf[256]; 5697 5698 // Product name: /sys/class/dmi/id/product_name 5699 if (read_sysfs("/sys/class/dmi/id/product_name", hbuf, sizeof(hbuf)) > 0) 5700 JS_SetPropertyStr(ctx, hw, "model", JS_NewString(ctx, hbuf)); 5701 else 5702 JS_SetPropertyStr(ctx, hw, "model", JS_NewString(ctx, "unknown")); 5703 5704 // Vendor: /sys/class/dmi/id/sys_vendor 5705 if (read_sysfs("/sys/class/dmi/id/sys_vendor", hbuf, sizeof(hbuf)) > 0) 5706 JS_SetPropertyStr(ctx, hw, "vendor", JS_NewString(ctx, hbuf)); 5707 else 5708 JS_SetPropertyStr(ctx, hw, "vendor", JS_NewString(ctx, "unknown")); 5709 5710 // CPU model: first "model name" line from /proc/cpuinfo 5711 { 5712 FILE *cpuinfo = fopen("/proc/cpuinfo", "r"); 5713 char cpuline[256] = {0}; 5714 if (cpuinfo) { 5715 char line[512]; 5716 while (fgets(line, sizeof(line), cpuinfo)) { 5717 if (strncmp(line, "model name", 10) == 0) { 5718 char *colon = strchr(line, ':'); 5719 if (colon) { 5720 colon++; 5721 while (*colon == ' ') colon++; 5722 // Trim newline 5723 char *nl = strchr(colon, '\n'); 5724 if (nl) *nl = 0; 5725 strncpy(cpuline, colon, sizeof(cpuline) - 1); 5726 } 5727 break; 5728 } 5729 } 5730 fclose(cpuinfo); 5731 } 5732 JS_SetPropertyStr(ctx, hw, "cpu", JS_NewString(ctx, cpuline[0] ? cpuline : "unknown")); 5733 } 5734 5735 // CPU cores: count "processor" lines in /proc/cpuinfo 5736 { 5737 FILE *cpuinfo = fopen("/proc/cpuinfo", "r"); 5738 int cores = 0; 5739 if (cpuinfo) { 5740 char line[256]; 5741 while (fgets(line, sizeof(line), cpuinfo)) { 5742 if (strncmp(line, "processor", 9) == 0) cores++; 5743 } 5744 fclose(cpuinfo); 5745 } 5746 JS_SetPropertyStr(ctx, hw, "cores", JS_NewInt32(ctx, cores)); 5747 } 5748 5749 // RAM: total from /proc/meminfo (in MB) 5750 { 5751 FILE *meminfo = fopen("/proc/meminfo", "r"); 5752 long total_kb = 0, avail_kb = 0; 5753 if (meminfo) { 5754 char line[256]; 5755 while (fgets(line, sizeof(line), meminfo)) { 5756 if (strncmp(line, "MemTotal:", 9) == 0) { 5757 sscanf(line + 9, " %ld", &total_kb); 5758 } else if (strncmp(line, "MemAvailable:", 13) == 0) { 5759 sscanf(line + 13, " %ld", &avail_kb); 5760 } 5761 if (total_kb && avail_kb) break; 5762 } 5763 fclose(meminfo); 5764 } 5765 JS_SetPropertyStr(ctx, hw, "ramTotalMB", JS_NewInt32(ctx, (int)(total_kb / 1024))); 5766 JS_SetPropertyStr(ctx, hw, "ramAvailMB", JS_NewInt32(ctx, (int)(avail_kb / 1024))); 5767 } 5768 5769 // Process count from /proc (count numeric dirs) 5770 { 5771 int procs = 0; 5772 DIR *pd = opendir("/proc"); 5773 if (pd) { 5774 struct dirent *pe; 5775 while ((pe = readdir(pd))) { 5776 if (pe->d_name[0] >= '1' && pe->d_name[0] <= '9') procs++; 5777 } 5778 closedir(pd); 5779 } 5780 JS_SetPropertyStr(ctx, hw, "processes", JS_NewInt32(ctx, procs)); 5781 } 5782 5783 // Load average from /proc/loadavg 5784 { 5785 char labuf[128] = {0}; 5786 FILE *la = fopen("/proc/loadavg", "r"); 5787 if (la) { 5788 if (fgets(labuf, sizeof(labuf), la)) { 5789 // "0.12 0.34 0.56 1/42 1234" 5790 double l1 = 0, l5 = 0, l15 = 0; 5791 sscanf(labuf, "%lf %lf %lf", &l1, &l5, &l15); 5792 JS_SetPropertyStr(ctx, hw, "load1", JS_NewFloat64(ctx, l1)); 5793 JS_SetPropertyStr(ctx, hw, "load5", JS_NewFloat64(ctx, l5)); 5794 JS_SetPropertyStr(ctx, hw, "load15", JS_NewFloat64(ctx, l15)); 5795 } 5796 fclose(la); 5797 } 5798 } 5799 5800 // CPU governor (power mode) 5801 if (read_sysfs("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor", hbuf, sizeof(hbuf)) > 0) { 5802 JS_SetPropertyStr(ctx, hw, "governor", JS_NewString(ctx, hbuf)); 5803 } 5804 5805 // Connected devices: scan /sys/class and /sys/bus for peripherals 5806 { 5807 JSValue devs = JS_NewArray(ctx); 5808 int di = 0; 5809 5810 // USB devices: scan /sys/bus/usb/devices/*/product 5811 { 5812 DIR *ud = opendir("/sys/bus/usb/devices"); 5813 if (ud) { 5814 struct dirent *ue; 5815 while ((ue = readdir(ud))) { 5816 if (ue->d_name[0] == '.') continue; 5817 char pp[256], prod[128] = {0}, mfr[128] = {0}; 5818 snprintf(pp, sizeof(pp), "/sys/bus/usb/devices/%s/product", ue->d_name); 5819 int got_prod = read_sysfs(pp, prod, sizeof(prod)) > 0; 5820 if (!got_prod) continue; 5821 snprintf(pp, sizeof(pp), "/sys/bus/usb/devices/%s/manufacturer", ue->d_name); 5822 read_sysfs(pp, mfr, sizeof(mfr)); 5823 JSValue d = JS_NewObject(ctx); 5824 JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "usb")); 5825 JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, prod)); 5826 if (mfr[0]) JS_SetPropertyStr(ctx, d, "vendor", JS_NewString(ctx, mfr)); 5827 JS_SetPropertyStr(ctx, d, "id", JS_NewString(ctx, ue->d_name)); 5828 JS_SetPropertyUint32(ctx, devs, di++, d); 5829 } 5830 closedir(ud); 5831 } 5832 } 5833 5834 // Input (HID) devices: scan /sys/class/input/*/name 5835 { 5836 DIR *id = opendir("/sys/class/input"); 5837 if (id) { 5838 struct dirent *ie; 5839 while ((ie = readdir(id))) { 5840 if (strncmp(ie->d_name, "event", 5) != 0) continue; 5841 char np[256], name[128] = {0}; 5842 snprintf(np, sizeof(np), "/sys/class/input/%s/device/name", ie->d_name); 5843 if (read_sysfs(np, name, sizeof(name)) <= 0) continue; 5844 JSValue d = JS_NewObject(ctx); 5845 JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "input")); 5846 JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, name)); 5847 JS_SetPropertyStr(ctx, d, "id", JS_NewString(ctx, ie->d_name)); 5848 JS_SetPropertyUint32(ctx, devs, di++, d); 5849 } 5850 closedir(id); 5851 } 5852 } 5853 5854 // Video devices (cameras): /sys/class/video4linux/*/name 5855 { 5856 DIR *vd = opendir("/sys/class/video4linux"); 5857 if (vd) { 5858 struct dirent *ve; 5859 while ((ve = readdir(vd))) { 5860 if (ve->d_name[0] == '.') continue; 5861 char vp[256], vname[128] = {0}; 5862 snprintf(vp, sizeof(vp), "/sys/class/video4linux/%s/name", ve->d_name); 5863 if (read_sysfs(vp, vname, sizeof(vname)) <= 0) continue; 5864 JSValue d = JS_NewObject(ctx); 5865 JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "camera")); 5866 JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, vname)); 5867 JS_SetPropertyStr(ctx, d, "id", JS_NewString(ctx, ve->d_name)); 5868 JS_SetPropertyUint32(ctx, devs, di++, d); 5869 } 5870 closedir(vd); 5871 } 5872 } 5873 5874 // Sound cards: /proc/asound/cards 5875 { 5876 FILE *sc = fopen("/proc/asound/cards", "r"); 5877 if (sc) { 5878 char sline[256]; 5879 while (fgets(sline, sizeof(sline), sc)) { 5880 // Lines like " 0 [PCH ]: HDA-Intel - HDA Intel PCH" 5881 char *dash = strstr(sline, " - "); 5882 if (dash) { 5883 dash += 3; 5884 char *nl = strchr(dash, '\n'); 5885 if (nl) *nl = 0; 5886 JSValue d = JS_NewObject(ctx); 5887 JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "audio")); 5888 JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, dash)); 5889 JS_SetPropertyUint32(ctx, devs, di++, d); 5890 } 5891 } 5892 fclose(sc); 5893 } 5894 } 5895 5896 // Block devices: /sys/block/*/device/model (drives, USB sticks) 5897 { 5898 DIR *bd = opendir("/sys/block"); 5899 if (bd) { 5900 struct dirent *be; 5901 while ((be = readdir(bd))) { 5902 if (be->d_name[0] == '.') continue; 5903 if (strncmp(be->d_name, "loop", 4) == 0) continue; 5904 if (strncmp(be->d_name, "ram", 3) == 0) continue; 5905 char bp[256], model[128] = {0}; 5906 snprintf(bp, sizeof(bp), "/sys/block/%s/device/model", be->d_name); 5907 read_sysfs(bp, model, sizeof(model)); 5908 // Get size 5909 char sz[32] = {0}; 5910 snprintf(bp, sizeof(bp), "/sys/block/%s/size", be->d_name); 5911 read_sysfs(bp, sz, sizeof(sz)); 5912 long sectors = sz[0] ? atol(sz) : 0; 5913 int gb = (int)(sectors / 2 / 1024 / 1024); // 512-byte sectors -> GB 5914 // Removable? 5915 char rem[8] = {0}; 5916 snprintf(bp, sizeof(bp), "/sys/block/%s/removable", be->d_name); 5917 read_sysfs(bp, rem, sizeof(rem)); 5918 JSValue d = JS_NewObject(ctx); 5919 JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "disk")); 5920 JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, model[0] ? model : be->d_name)); 5921 JS_SetPropertyStr(ctx, d, "id", JS_NewString(ctx, be->d_name)); 5922 JS_SetPropertyStr(ctx, d, "sizeGB", JS_NewInt32(ctx, gb)); 5923 JS_SetPropertyStr(ctx, d, "removable", JS_NewBool(ctx, rem[0] == '1')); 5924 JS_SetPropertyUint32(ctx, devs, di++, d); 5925 } 5926 closedir(bd); 5927 } 5928 } 5929 5930 // DRM connectors (HDMI, DP, etc.): /sys/class/drm/card*-*/status 5931 { 5932 DIR *dd = opendir("/sys/class/drm"); 5933 if (dd) { 5934 struct dirent *de; 5935 while ((de = readdir(dd))) { 5936 if (strncmp(de->d_name, "card", 4) != 0) continue; 5937 if (!strchr(de->d_name, '-')) continue; // skip "card0" itself 5938 char sp[256], st[32] = {0}; 5939 snprintf(sp, sizeof(sp), "/sys/class/drm/%s/status", de->d_name); 5940 if (read_sysfs(sp, st, sizeof(st)) <= 0) continue; 5941 JSValue d = JS_NewObject(ctx); 5942 JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "display")); 5943 JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, de->d_name)); 5944 JS_SetPropertyStr(ctx, d, "connected", JS_NewBool(ctx, strcmp(st, "connected") == 0)); 5945 JS_SetPropertyUint32(ctx, devs, di++, d); 5946 } 5947 closedir(dd); 5948 } 5949 } 5950 5951 JS_SetPropertyStr(ctx, hw, "devices", devs); 5952 } 5953 5954 // Build name (baked in at compile time) 5955#ifdef AC_BUILD_NAME 5956 JS_SetPropertyStr(ctx, hw, "buildName", JS_NewString(ctx, AC_BUILD_NAME)); 5957#endif 5958#ifdef AC_GIT_HASH 5959 JS_SetPropertyStr(ctx, hw, "gitHash", JS_NewString(ctx, AC_GIT_HASH)); 5960#endif 5961#ifdef AC_BUILD_TS 5962 JS_SetPropertyStr(ctx, hw, "buildTs", JS_NewString(ctx, AC_BUILD_TS)); 5963#endif 5964 5965 // Display driver and GPU info 5966 { 5967 extern void *g_display; 5968 if (g_display) { 5969 const char *drv = drm_display_driver((ACDisplay *)g_display); 5970 JS_SetPropertyStr(ctx, hw, "displayDriver", JS_NewString(ctx, drv)); 5971 } else { 5972 JS_SetPropertyStr(ctx, hw, "displayDriver", JS_NewString(ctx, "wayland")); 5973 } 5974 // Read GPU renderer from sysfs (Mesa exposes via DRI) 5975 char gpu_name[128] = "unknown"; 5976 FILE *fp = popen("cat /sys/kernel/debug/dri/0/name 2>/dev/null || " 5977 "cat /sys/class/drm/card0/device/label 2>/dev/null || " 5978 "echo unknown", "r"); 5979 if (fp) { 5980 if (fgets(gpu_name, sizeof(gpu_name), fp)) { 5981 // Strip trailing newline 5982 char *nl = strchr(gpu_name, '\n'); 5983 if (nl) *nl = '\0'; 5984 } 5985 pclose(fp); 5986 } 5987 JS_SetPropertyStr(ctx, hw, "gpu", JS_NewString(ctx, gpu_name)); 5988 } 5989 5990 // Audio diagnostics 5991 if (current_rt->audio) { 5992 JS_SetPropertyStr(ctx, hw, "audioDevice", 5993 JS_NewString(ctx, current_rt->audio->audio_device)); 5994 JS_SetPropertyStr(ctx, hw, "audioStatus", 5995 JS_NewString(ctx, current_rt->audio->audio_status)); 5996 JS_SetPropertyStr(ctx, hw, "audioRetries", 5997 JS_NewInt32(ctx, current_rt->audio->audio_init_retries)); 5998 JS_SetPropertyStr(ctx, hw, "audioRate", 5999 JS_NewInt32(ctx, (int)current_rt->audio->actual_rate)); 6000 } else { 6001 JS_SetPropertyStr(ctx, hw, "audioStatus", 6002 JS_NewString(ctx, "no audio subsystem")); 6003 } 6004 6005 JS_SetPropertyStr(ctx, sys, "hw", hw); 6006 } 6007 6008 // Backlight brightness — scan /sys/class/backlight/ 6009 int bl_cur = -1, bl_max = -1; 6010 { 6011 DIR *bldir = opendir("/sys/class/backlight"); 6012 if (bldir) { 6013 struct dirent *ent; 6014 while ((ent = readdir(bldir))) { 6015 if (ent->d_name[0] == '.') continue; 6016 char tmp[160]; 6017 snprintf(tmp, sizeof(tmp), "/sys/class/backlight/%s/max_brightness", ent->d_name); 6018 if (read_sysfs(tmp, buf, sizeof(buf)) > 0) { 6019 bl_max = atoi(buf); 6020 snprintf(tmp, sizeof(tmp), "/sys/class/backlight/%s/brightness", ent->d_name); 6021 if (read_sysfs(tmp, buf, sizeof(buf)) > 0) bl_cur = atoi(buf); 6022 break; 6023 } 6024 } 6025 closedir(bldir); 6026 } 6027 } 6028 if (bl_max > 0 && bl_cur >= 0) { 6029 JS_SetPropertyStr(ctx, sys, "brightness", JS_NewInt32(ctx, (bl_cur * 100) / bl_max)); 6030 } else { 6031 JS_SetPropertyStr(ctx, sys, "brightness", JS_NewInt32(ctx, -1)); 6032 } 6033 6034 // USB-C Type-C power role — scan /sys/class/typec/port*/power_role 6035 { 6036 JSValue typec = JS_NewArray(ctx); 6037 int ti = 0; 6038 DIR *tcdir = opendir("/sys/class/typec"); 6039 if (tcdir) { 6040 struct dirent *te; 6041 while ((te = readdir(tcdir))) { 6042 if (strncmp(te->d_name, "port", 4) != 0) continue; 6043 // Skip partner/plug entries like "port0-partner" 6044 if (strchr(te->d_name + 4, '-')) continue; 6045 char tmp[256], rbuf[64] = {0}; 6046 snprintf(tmp, sizeof(tmp), "/sys/class/typec/%s/power_role", te->d_name); 6047 if (read_sysfs(tmp, rbuf, sizeof(rbuf)) <= 0) continue; 6048 // rbuf is like "[source] sink" or "source [sink]" 6049 const char *current_role = "unknown"; 6050 int can_swap = 0; 6051 if (strstr(rbuf, "[source]")) current_role = "source"; 6052 else if (strstr(rbuf, "[sink]")) current_role = "sink"; 6053 // If both roles appear, swap is supported 6054 if (strstr(rbuf, "source") && strstr(rbuf, "sink")) can_swap = 1; 6055 // Data role 6056 char drbuf[64] = {0}; 6057 snprintf(tmp, sizeof(tmp), "/sys/class/typec/%s/data_role", te->d_name); 6058 read_sysfs(tmp, drbuf, sizeof(drbuf)); 6059 const char *data_role = "unknown"; 6060 if (strstr(drbuf, "[host]")) data_role = "host"; 6061 else if (strstr(drbuf, "[device]")) data_role = "device"; 6062 6063 JSValue port = JS_NewObject(ctx); 6064 JS_SetPropertyStr(ctx, port, "port", JS_NewString(ctx, te->d_name)); 6065 JS_SetPropertyStr(ctx, port, "powerRole", JS_NewString(ctx, current_role)); 6066 JS_SetPropertyStr(ctx, port, "dataRole", JS_NewString(ctx, data_role)); 6067 JS_SetPropertyStr(ctx, port, "canSwap", JS_NewBool(ctx, can_swap)); 6068 JS_SetPropertyUint32(ctx, typec, ti++, port); 6069 } 6070 closedir(tcdir); 6071 } 6072 JS_SetPropertyStr(ctx, sys, "typec", typec); 6073 } 6074 6075 // system.setPowerRole(port, role) — swap USB-C power role ("source" or "sink") 6076 JS_SetPropertyStr(ctx, sys, "setPowerRole", JS_NewCFunction(ctx, js_set_power_role, "setPowerRole", 2)); 6077 6078 // Tablet mode (lid folded back on convertible laptops) 6079 JS_SetPropertyStr(ctx, sys, "tabletMode", 6080 JS_NewBool(ctx, current_rt && current_rt->input && current_rt->input->tablet_mode)); 6081 6082 // HDMI secondary display 6083 int has_hdmi = (current_rt && current_rt->hdmi && current_rt->hdmi->active); 6084 JS_SetPropertyStr(ctx, sys, "hasHdmi", JS_NewBool(ctx, has_hdmi)); 6085 JS_SetPropertyStr(ctx, sys, "hdmi", JS_NewCFunction(ctx, js_hdmi_fill, "hdmi", 3)); 6086 6087 // WebSocket client — system.ws 6088 JS_SetPropertyStr(ctx, sys, "ws", build_ws_obj(ctx, current_phase)); 6089 6090 // Raw UDP fairy co-presence — system.udp 6091 JS_SetPropertyStr(ctx, sys, "udp", build_udp_obj(ctx, current_phase)); 6092 6093 // Machine identity — system.machineId 6094 { 6095 extern char g_machine_id[64]; 6096 JS_SetPropertyStr(ctx, sys, "machineId", 6097 JS_NewString(ctx, g_machine_id[0] ? g_machine_id : "unknown")); 6098 } 6099 6100 // Remote reboot / poweroff — system.reboot() / system.poweroff() 6101 JS_SetPropertyStr(ctx, sys, "reboot", JS_NewCFunction(ctx, js_reboot, "reboot", 0)); 6102 JS_SetPropertyStr(ctx, sys, "poweroff", JS_NewCFunction(ctx, js_poweroff, "poweroff", 0)); 6103 6104 // USB MIDI gadget status + control 6105 { 6106 JSValue usb_midi = build_usb_midi_state_obj(ctx); 6107 JS_SetPropertyStr(ctx, usb_midi, "status", JS_NewCFunction(ctx, js_usb_midi_status, "status", 0)); 6108 JS_SetPropertyStr(ctx, usb_midi, "enable", JS_NewCFunction(ctx, js_usb_midi_enable, "enable", 0)); 6109 JS_SetPropertyStr(ctx, usb_midi, "disable", JS_NewCFunction(ctx, js_usb_midi_disable, "disable", 0)); 6110 JS_SetPropertyStr(ctx, usb_midi, "refresh", JS_NewCFunction(ctx, js_usb_midi_refresh, "refresh", 0)); 6111 JS_SetPropertyStr(ctx, usb_midi, "noteOn", JS_NewCFunction(ctx, js_usb_midi_note_on, "noteOn", 3)); 6112 JS_SetPropertyStr(ctx, usb_midi, "noteOff", JS_NewCFunction(ctx, js_usb_midi_note_off, "noteOff", 3)); 6113 JS_SetPropertyStr(ctx, usb_midi, "allNotesOff", JS_NewCFunction(ctx, js_usb_midi_all_notes_off, "allNotesOff", 1)); 6114 JS_SetPropertyStr(ctx, sys, "usbMidi", usb_midi); 6115 } 6116 6117 // File I/O — system.readFile(path) / system.writeFile(path, data) 6118 JS_SetPropertyStr(ctx, sys, "readFile", JS_NewCFunction(ctx, js_read_file, "readFile", 1)); 6119 JS_SetPropertyStr(ctx, sys, "writeFile", JS_NewCFunction(ctx, js_write_file, "writeFile", 2)); 6120 JS_SetPropertyStr(ctx, sys, "deleteFile",JS_NewCFunction(ctx, js_delete_file,"deleteFile",1)); 6121 JS_SetPropertyStr(ctx, sys, "listDir", JS_NewCFunction(ctx, js_list_dir, "listDir", 1)); 6122 JS_SetPropertyStr(ctx, sys, "diskInfo", JS_NewCFunction(ctx, js_disk_info, "diskInfo", 1)); 6123 JS_SetPropertyStr(ctx, sys, "blockDevices", JS_NewCFunction(ctx, js_block_devices, "blockDevices", 0)); 6124 JS_SetPropertyStr(ctx, sys, "mountMusic", JS_NewCFunction(ctx, js_mount_music, "mountMusic", 0)); 6125 JS_SetPropertyStr(ctx, sys, "mountMusicMounted", JS_NewBool(ctx, music_mount_state)); 6126 JS_SetPropertyStr(ctx, sys, "mountMusicPending", JS_NewBool(ctx, music_mount_pending)); 6127 6128 // Async HTTP fetch — system.fetch(url) / system.fetchCancel() / system.fetchResult / system.fetchPending 6129 JS_SetPropertyStr(ctx, sys, "fetch", JS_NewCFunction(ctx, js_fetch, "fetch", 1)); 6130 JS_SetPropertyStr(ctx, sys, "fetchPost", JS_NewCFunction(ctx, js_fetch_post, "fetchPost", 3)); 6131 JS_SetPropertyStr(ctx, sys, "fetchCancel", JS_NewCFunction(ctx, js_fetch_cancel, "fetchCancel", 0)); 6132 JS_SetPropertyStr(ctx, sys, "fetchPending", 6133 JS_NewBool(ctx, current_rt ? current_rt->fetch_pending : 0)); 6134 if (current_rt && current_rt->fetch_pending) { 6135 FILE *rc = fopen("/tmp/ac_fetch_rc", "r"); 6136 if (rc) { 6137 int code = -1; 6138 fscanf(rc, "%d", &code); 6139 fclose(rc); 6140 unlink("/tmp/ac_fetch_rc"); 6141 ac_log("[fetch] done: curl exit=%d\n", code); 6142 current_rt->fetch_result[0] = 0; 6143 current_rt->fetch_error[0] = 0; 6144 if (code == 0) { 6145 FILE *fp = fopen("/tmp/ac_fetch.json", "r"); 6146 if (fp) { 6147 int n = (int)fread(current_rt->fetch_result, 6148 1, sizeof(current_rt->fetch_result) - 1, fp); 6149 fclose(fp); 6150 current_rt->fetch_result[n] = 0; 6151 unlink("/tmp/ac_fetch.json"); 6152 } else { 6153 snprintf(current_rt->fetch_error, sizeof(current_rt->fetch_error), 6154 "request failed: missing response body"); 6155 } 6156 unlink("/tmp/ac_fetch_err"); 6157 } else { 6158 char emsg[160] = {0}; 6159 FILE *ef = fopen("/tmp/ac_fetch_err", "r"); 6160 if (ef) { 6161 int en = (int)fread(emsg, 1, sizeof(emsg) - 1, ef); 6162 fclose(ef); 6163 emsg[en] = 0; 6164 for (int i = 0; emsg[i]; i++) { 6165 if (emsg[i] == '\n' || emsg[i] == '\r' || emsg[i] == '\t') emsg[i] = ' '; 6166 } 6167 } 6168 unlink("/tmp/ac_fetch_err"); 6169 ac_log("[fetch] error (%d): %s\n", code, emsg[0] ? emsg : "(no stderr)"); 6170 if (emsg[0]) { 6171 snprintf(current_rt->fetch_error, sizeof(current_rt->fetch_error), 6172 "request failed (%d): %s", code, emsg); 6173 } else { 6174 snprintf(current_rt->fetch_error, sizeof(current_rt->fetch_error), 6175 "request failed (%d)", code); 6176 } 6177 } 6178 current_rt->fetch_pending = 0; 6179 } 6180 } 6181 // Deliver fetch result only during paint phase (where JS consumes it) 6182 // and clear after delivery (one-shot) 6183 if (current_rt && current_rt->fetch_result[0] 6184 && strcmp(current_phase, "paint") == 0) { 6185 JS_SetPropertyStr(ctx, sys, "fetchResult", 6186 JS_NewString(ctx, current_rt->fetch_result)); 6187 current_rt->fetch_result[0] = 0; 6188 } else { 6189 JS_SetPropertyStr(ctx, sys, "fetchResult", JS_NULL); 6190 } 6191 // Fetch error is also one-shot, delivered during paint. 6192 if (current_rt && current_rt->fetch_error[0] 6193 && strcmp(current_phase, "paint") == 0) { 6194 JS_SetPropertyStr(ctx, sys, "fetchError", 6195 JS_NewString(ctx, current_rt->fetch_error)); 6196 current_rt->fetch_error[0] = 0; 6197 } else { 6198 JS_SetPropertyStr(ctx, sys, "fetchError", JS_NULL); 6199 } 6200 6201 // QR camera scanning — system.scanQR() / system.scanQRStop() 6202 JS_SetPropertyStr(ctx, sys, "scanQR", JS_NewCFunction(ctx, js_scan_qr, "scanQR", 0)); 6203 JS_SetPropertyStr(ctx, sys, "scanQRStop", JS_NewCFunction(ctx, js_scan_qr_stop, "scanQRStop", 0)); 6204 JS_SetPropertyStr(ctx, sys, "cameraBlit", JS_NewCFunction(ctx, js_camera_blit, "cameraBlit", 4)); 6205 JS_SetPropertyStr(ctx, sys, "qrPending", 6206 JS_NewBool(ctx, current_rt ? current_rt->qr_scan_active : 0)); 6207 // Deliver QR result one-shot during sim phase 6208 if (current_rt && current_rt->camera.scan_done) { 6209 if (current_rt->camera.scan_result[0]) { 6210 JS_SetPropertyStr(ctx, sys, "qrResult", 6211 JS_NewString(ctx, current_rt->camera.scan_result)); 6212 current_rt->camera.scan_result[0] = 0; 6213 } else if (current_rt->camera.scan_error[0]) { 6214 JS_SetPropertyStr(ctx, sys, "qrError", 6215 JS_NewString(ctx, current_rt->camera.scan_error)); 6216 current_rt->camera.scan_error[0] = 0; 6217 } else { 6218 JS_SetPropertyStr(ctx, sys, "qrResult", JS_NULL); 6219 } 6220 current_rt->camera.scan_done = 0; 6221 } else { 6222 JS_SetPropertyStr(ctx, sys, "qrResult", JS_NULL); 6223 JS_SetPropertyStr(ctx, sys, "qrError", JS_NULL); 6224 } 6225 6226 // OS update version string — matches OTA format: "buildname githash-buildts" 6227#ifdef AC_BUILD_NAME 6228# ifdef AC_GIT_HASH 6229# ifdef AC_BUILD_TS 6230 JS_SetPropertyStr(ctx, sys, "version", 6231 JS_NewString(ctx, AC_BUILD_NAME " " AC_GIT_HASH "-" AC_BUILD_TS)); 6232# else 6233 JS_SetPropertyStr(ctx, sys, "version", 6234 JS_NewString(ctx, AC_BUILD_NAME " " AC_GIT_HASH)); 6235# endif 6236# else 6237 JS_SetPropertyStr(ctx, sys, "version", JS_NewString(ctx, AC_BUILD_NAME)); 6238# endif 6239#elif defined(AC_GIT_HASH) 6240# ifdef AC_BUILD_TS 6241 JS_SetPropertyStr(ctx, sys, "version", 6242 JS_NewString(ctx, AC_GIT_HASH "-" AC_BUILD_TS)); 6243# else 6244 JS_SetPropertyStr(ctx, sys, "version", JS_NewString(ctx, AC_GIT_HASH)); 6245# endif 6246#else 6247 JS_SetPropertyStr(ctx, sys, "version", JS_NewString(ctx, "unknown")); 6248#endif 6249 6250 // Firmware capability probe — exposed as `system.firmware` so os.mjs can 6251 // gate the firmware-update panel on machines where flashing is actually 6252 // viable. Criteria, all of which must be true: 6253 // 1. /dev/mtd0 exists — kernel has a usable SPI-NOR driver bound to 6254 // the motherboard's flash chip (CONFIG_SPI_INTEL_PCI + CONFIG_MTD). 6255 // Absent this, flashrom's internal programmer can't open the chip. 6256 // 2. bios_vendor contains "coreboot" — the only firmware we support 6257 // reflashing is MrChromebox's coreboot+edk2 build. Stock OEM AMI / 6258 // Insyde firmware would either reject writes (SMM) or brick. 6259 // 3. Optional: a plausible Chromebook/supported-board identifier in 6260 // product_name, which lets us suggest a MrChromebox ROM URL. 6261 // This is an advisory flag; the actual flash is gated by a second probe 6262 // + `flashrom --probe` step inside the update thread. 6263 { 6264 JSValue firmware = JS_NewObject(ctx); 6265 int mtd_ok = (access("/dev/mtd0", F_OK) == 0); 6266 char bios_vendor[128] = ""; 6267 char product_name[128] = ""; 6268 char bios_version[128] = ""; 6269 read_sysfs("/sys/class/dmi/id/bios_vendor", bios_vendor, sizeof(bios_vendor)); 6270 read_sysfs("/sys/class/dmi/id/product_name", product_name, sizeof(product_name)); 6271 read_sysfs("/sys/class/dmi/id/bios_version", bios_version, sizeof(bios_version)); 6272 // Trim trailing newlines from sysfs reads 6273 for (char *p = bios_vendor; *p; p++) if (*p == '\n') { *p = 0; break; } 6274 for (char *p = product_name; *p; p++) if (*p == '\n') { *p = 0; break; } 6275 for (char *p = bios_version; *p; p++) if (*p == '\n') { *p = 0; break; } 6276 int coreboot = 6277 (strstr(bios_vendor, "coreboot") != NULL) || 6278 (strstr(bios_version, "MrChromebox") != NULL); 6279 int available = mtd_ok && coreboot; 6280 JS_SetPropertyStr(ctx, firmware, "available", JS_NewBool(ctx, available)); 6281 JS_SetPropertyStr(ctx, firmware, "mtdOk", JS_NewBool(ctx, mtd_ok)); 6282 JS_SetPropertyStr(ctx, firmware, "coreboot", JS_NewBool(ctx, coreboot)); 6283 JS_SetPropertyStr(ctx, firmware, "biosVendor", JS_NewString(ctx, bios_vendor)); 6284 JS_SetPropertyStr(ctx, firmware, "biosVersion", JS_NewString(ctx, bios_version)); 6285 JS_SetPropertyStr(ctx, firmware, "productName", JS_NewString(ctx, product_name)); 6286 // Lowercase board name (first word of product_name) is the URL slug 6287 // MrChromebox uses: e.g. "Google Drawman/Drawcia" -> "drawcia" 6288 char board[64] = ""; 6289 { 6290 const char *src = product_name; 6291 // Skip "Google " prefix if present 6292 if (strncmp(src, "Google ", 7) == 0) src += 7; 6293 // Copy up to '/' or ' ' or end, lowercasing 6294 int i = 0; 6295 while (*src && *src != '/' && *src != ' ' && i < (int)sizeof(board) - 1) { 6296 board[i++] = (*src >= 'A' && *src <= 'Z') ? (*src + 32) : *src; 6297 src++; 6298 } 6299 board[i] = 0; 6300 } 6301 JS_SetPropertyStr(ctx, firmware, "board", JS_NewString(ctx, board)); 6302 // Runtime install state — live values updated by fw_thread_fn. 6303 if (current_rt) { 6304 JS_SetPropertyStr(ctx, firmware, "pending", 6305 JS_NewBool(ctx, current_rt->fw_pending)); 6306 JS_SetPropertyStr(ctx, firmware, "done", 6307 JS_NewBool(ctx, current_rt->fw_done)); 6308 JS_SetPropertyStr(ctx, firmware, "ok", 6309 JS_NewBool(ctx, current_rt->fw_ok)); 6310 if (current_rt->fw_backup_path[0]) 6311 JS_SetPropertyStr(ctx, firmware, "backupPath", 6312 JS_NewString(ctx, current_rt->fw_backup_path)); 6313 JSValue log = JS_NewArray(ctx); 6314 int count = current_rt->fw_log_count; 6315 int show = count < 32 ? count : 32; 6316 int start = count - show; 6317 for (int i = 0; i < show; i++) { 6318 JS_SetPropertyUint32(ctx, log, i, 6319 JS_NewString(ctx, current_rt->fw_log[(start + i) % 32])); 6320 } 6321 JS_SetPropertyStr(ctx, firmware, "log", log); 6322 } 6323 // Action: system.firmware.install(mode?) — "install" | "dry-run" | "restore" 6324 JS_SetPropertyStr(ctx, firmware, "install", 6325 JS_NewCFunction(ctx, js_firmware_install, "install", 1)); 6326 JS_SetPropertyStr(ctx, sys, "firmware", firmware); 6327 } 6328 6329 // Audio diagnostic API — speaker.mjs piece uses these to probe which 6330 // ALSA PCM actually produces sound on this machine. listPcms scans 6331 // /proc/asound; testPcm plays a short sine tone on the named device 6332 // in a detached thread. Both are read-only — no state change to the 6333 // main audio thread. 6334 { 6335 JSValue audio = JS_NewObject(ctx); 6336 JS_SetPropertyStr(ctx, audio, "listPcms", 6337 JS_NewCFunction(ctx, js_audio_list_pcms, "listPcms", 0)); 6338 JS_SetPropertyStr(ctx, audio, "testPcm", 6339 JS_NewCFunction(ctx, js_audio_test_pcm, "testPcm", 4)); 6340 /* Report which device ac-native's main audio thread is currently 6341 * using — helpful context when the user is probing alternatives. */ 6342 if (current_rt && current_rt->audio && 6343 current_rt->audio->audio_device[0]) { 6344 JS_SetPropertyStr(ctx, audio, "activeDevice", 6345 JS_NewString(ctx, current_rt->audio->audio_device)); 6346 } 6347 JS_SetPropertyStr(ctx, sys, "audio", audio); 6348 } 6349 6350 // User config (handle, piece — read from /mnt/config.json at boot) 6351 { 6352 JSValue config = JS_NewObject(ctx); 6353 if (current_rt && current_rt->handle[0]) 6354 JS_SetPropertyStr(ctx, config, "handle", JS_NewString(ctx, current_rt->handle)); 6355 if (current_rt && current_rt->piece[0]) 6356 JS_SetPropertyStr(ctx, config, "piece", JS_NewString(ctx, current_rt->piece)); 6357 // Expose raw config JSON so pieces can read arbitrary fields 6358 { 6359 static char cfg_cache[8192] = {0}; 6360 if (config_cache_dirty) { 6361 cfg_cache[0] = '\0'; 6362 FILE *cf = fopen("/mnt/config.json", "r"); 6363 if (cf) { 6364 size_t n = fread(cfg_cache, 1, sizeof(cfg_cache) - 1, cf); 6365 cfg_cache[n] = '\0'; 6366 fclose(cf); 6367 } 6368 config_cache_dirty = 0; 6369 } 6370 if (cfg_cache[0]) 6371 JS_SetPropertyStr(ctx, config, "raw", JS_NewString(ctx, cfg_cache)); 6372 } 6373 JS_SetPropertyStr(ctx, sys, "config", config); 6374 } 6375 6376 // Boot device detection (cached — detect once, not every frame) 6377 { 6378 static char boot_dev_cache[64] = {0}; 6379 if (!boot_dev_cache[0]) detect_boot_device(boot_dev_cache, sizeof(boot_dev_cache)); 6380 JS_SetPropertyStr(ctx, sys, "bootDevice", JS_NewString(ctx, boot_dev_cache)); 6381 } 6382 6383 // Flash targets: enumerate all devices that could receive an EFI flash 6384 { 6385 JSValue targets = JS_NewArray(ctx); 6386 int idx = 0; 6387 // Check USB (/dev/sda1) 6388 if (access("/dev/sda1", F_OK) == 0) { 6389 char rem[8] = {0}; 6390 read_sysfs("/sys/block/sda/removable", rem, sizeof(rem)); 6391 int is_removable = (rem[0] == '1'); 6392 JSValue t = JS_NewObject(ctx); 6393 JS_SetPropertyStr(ctx, t, "device", JS_NewString(ctx, "/dev/sda1")); 6394 JS_SetPropertyStr(ctx, t, "label", 6395 JS_NewString(ctx, is_removable ? "USB" : "Disk (sda)")); 6396 JS_SetPropertyStr(ctx, t, "removable", JS_NewBool(ctx, is_removable)); 6397 JS_SetPropertyUint32(ctx, targets, idx++, t); 6398 } 6399 // Check NVMe (/dev/nvme0n1p1) 6400 if (access("/dev/nvme0n1p1", F_OK) == 0) { 6401 JSValue t = JS_NewObject(ctx); 6402 JS_SetPropertyStr(ctx, t, "device", JS_NewString(ctx, "/dev/nvme0n1p1")); 6403 JS_SetPropertyStr(ctx, t, "label", JS_NewString(ctx, "Internal (NVMe)")); 6404 JS_SetPropertyStr(ctx, t, "removable", JS_NewBool(ctx, 0)); 6405 JS_SetPropertyUint32(ctx, targets, idx++, t); 6406 } 6407 // Check eMMC (/dev/mmcblk0p1) — Chromebooks + some budget laptops use 6408 // eMMC instead of NVMe. Parent device is mmcblk0, partitions are 6409 // mmcblk0p1 etc. — same "p<N>" suffix scheme as NVMe. 6410 if (access("/dev/mmcblk0p1", F_OK) == 0) { 6411 JSValue t = JS_NewObject(ctx); 6412 JS_SetPropertyStr(ctx, t, "device", JS_NewString(ctx, "/dev/mmcblk0p1")); 6413 JS_SetPropertyStr(ctx, t, "label", JS_NewString(ctx, "Internal (eMMC)")); 6414 JS_SetPropertyStr(ctx, t, "removable", JS_NewBool(ctx, 0)); 6415 JS_SetPropertyUint32(ctx, targets, idx++, t); 6416 } 6417 // Check sdb1 (second USB) 6418 if (access("/dev/sdb1", F_OK) == 0) { 6419 char rem[8] = {0}; 6420 read_sysfs("/sys/block/sdb/removable", rem, sizeof(rem)); 6421 JSValue t = JS_NewObject(ctx); 6422 JS_SetPropertyStr(ctx, t, "device", JS_NewString(ctx, "/dev/sdb1")); 6423 JS_SetPropertyStr(ctx, t, "label", 6424 JS_NewString(ctx, rem[0] == '1' ? "USB (sdb)" : "Disk (sdb)")); 6425 JS_SetPropertyStr(ctx, t, "removable", JS_NewBool(ctx, rem[0] == '1')); 6426 JS_SetPropertyUint32(ctx, targets, idx++, t); 6427 } 6428 JS_SetPropertyStr(ctx, sys, "flashTargets", targets); 6429 } 6430 6431 // Binary fetch for OS update 6432 JS_SetPropertyStr(ctx, sys, "fetchBinary", 6433 JS_NewCFunction(ctx, js_fetch_binary, "fetchBinary", 3)); 6434 if (current_rt) { 6435 // Poll progress from file size 6436 if (current_rt->fetch_binary_pending && current_rt->fetch_binary_expected > 0 6437 && current_rt->fetch_binary_dest[0]) { 6438 struct stat fst; 6439 if (stat(current_rt->fetch_binary_dest, &fst) == 0) { 6440 float p = (float)fst.st_size / (float)current_rt->fetch_binary_expected; 6441 if (p > 1.0f) p = 1.0f; 6442 current_rt->fetch_binary_progress = p; 6443 } 6444 } 6445 // Check if curl finished 6446 if (current_rt->fetch_binary_pending) { 6447 FILE *fbrc = fopen("/tmp/ac_fb_rc", "r"); 6448 if (fbrc) { 6449 int rc = -1; 6450 fscanf(fbrc, "%d", &rc); 6451 fclose(fbrc); 6452 unlink("/tmp/ac_fb_rc"); 6453 if (rc != 0) { 6454 char emsg[160] = {0}; 6455 FILE *ef = fopen("/tmp/ac_fb_err", "r"); 6456 if (ef) { 6457 int en = (int)fread(emsg, 1, sizeof(emsg) - 1, ef); 6458 fclose(ef); 6459 emsg[en] = 0; 6460 for (int i = 0; emsg[i]; i++) { 6461 if (emsg[i] == '\n' || emsg[i] == '\r' || emsg[i] == '\t') emsg[i] = ' '; 6462 } 6463 } 6464 ac_log("[fetchBinary] error (%d): %s\n", rc, emsg[0] ? emsg : "(no stderr)"); 6465 } 6466 unlink("/tmp/ac_fb_err"); 6467 current_rt->fetch_binary_pending = 0; 6468 current_rt->fetch_binary_done = 1; 6469 current_rt->fetch_binary_ok = (rc == 0) ? 1 : 0; 6470 current_rt->fetch_binary_progress = (rc == 0) ? 1.0f : 0.0f; 6471 ac_log("[fetchBinary] complete: rc=%d ok=%d dest=%s\n", 6472 rc, current_rt->fetch_binary_ok, current_rt->fetch_binary_dest); 6473 } 6474 } 6475 JS_SetPropertyStr(ctx, sys, "fetchBinaryProgress", 6476 JS_NewFloat64(ctx, (double)current_rt->fetch_binary_progress)); 6477 JS_SetPropertyStr(ctx, sys, "fetchBinaryDone", 6478 JS_NewBool(ctx, current_rt->fetch_binary_done)); 6479 JS_SetPropertyStr(ctx, sys, "fetchBinaryOk", 6480 JS_NewBool(ctx, current_rt->fetch_binary_ok)); 6481 // Keep done latched until next fetchBinary() call resets it. 6482 // Act/sim/paint run in separate calls; one-shot pulses can be lost. 6483 } 6484 6485 // Flash update 6486 JS_SetPropertyStr(ctx, sys, "flashUpdate", 6487 JS_NewCFunction(ctx, js_flash_update, "flashUpdate", 1)); 6488 JS_SetPropertyStr(ctx, sys, "reboot", 6489 JS_NewCFunction(ctx, js_reboot, "reboot", 0)); 6490 if (current_rt) { 6491 JS_SetPropertyStr(ctx, sys, "flashDone", 6492 JS_NewBool(ctx, current_rt->flash_done)); 6493 JS_SetPropertyStr(ctx, sys, "flashOk", 6494 JS_NewBool(ctx, current_rt->flash_ok)); 6495 // Phase: 0=idle 1=writing 2=syncing 3=verifying 4=done 6496 JS_SetPropertyStr(ctx, sys, "flashPhase", 6497 JS_NewInt32(ctx, current_rt->flash_phase)); 6498 JS_SetPropertyStr(ctx, sys, "flashVerifiedBytes", 6499 JS_NewFloat64(ctx, (double)current_rt->flash_verified_bytes)); 6500 // Flash telemetry: destination path, same-device flag, log lines 6501 if (current_rt->flash_dst[0]) 6502 JS_SetPropertyStr(ctx, sys, "flashDst", 6503 JS_NewString(ctx, current_rt->flash_dst)); 6504 JS_SetPropertyStr(ctx, sys, "flashSameDevice", 6505 JS_NewBool(ctx, current_rt->flash_same_device)); 6506 // Flash log ring buffer 6507 { 6508 int count = current_rt->flash_log_count; 6509 int start = count > 16 ? count - 16 : 0; 6510 JSValue arr = JS_NewArray(ctx); 6511 for (int i = start; i < count; i++) { 6512 JS_SetPropertyUint32(ctx, arr, i - start, 6513 JS_NewString(ctx, current_rt->flash_log[i % 16])); 6514 } 6515 JS_SetPropertyStr(ctx, sys, "flashLog", arr); 6516 } 6517 } 6518 6519 // Config persistence 6520 JS_SetPropertyStr(ctx, sys, "saveConfig", 6521 JS_NewCFunction(ctx, js_save_config, "saveConfig", 2)); 6522 6523 // SSH daemon 6524 JS_SetPropertyStr(ctx, sys, "startSSH", 6525 JS_NewCFunction(ctx, js_start_ssh, "startSSH", 0)); 6526 JS_SetPropertyStr(ctx, sys, "sshStarted", JS_NewBool(ctx, ssh_started)); 6527 6528 // Node.js execution 6529 JS_SetPropertyStr(ctx, sys, "execNode", 6530 JS_NewCFunction(ctx, js_exec_node, "execNode", 1)); 6531 6532 // PTY terminal emulator 6533 { 6534 JSValue pty_obj = JS_NewObject(ctx); 6535 JS_SetPropertyStr(ctx, pty_obj, "spawn", 6536 JS_NewCFunction(ctx, js_pty_spawn, "spawn", 4)); 6537 JS_SetPropertyStr(ctx, pty_obj, "write", 6538 JS_NewCFunction(ctx, js_pty_write, "write", 1)); 6539 JS_SetPropertyStr(ctx, pty_obj, "resize", 6540 JS_NewCFunction(ctx, js_pty_resize, "resize", 2)); 6541 JS_SetPropertyStr(ctx, pty_obj, "kill", 6542 JS_NewCFunction(ctx, js_pty_kill, "kill", 0)); 6543 JS_SetPropertyStr(ctx, pty_obj, "active", 6544 JS_NewBool(ctx, current_rt->pty_active)); 6545 6546 // Pump PTY output and expose grid (only during paint phase) 6547 if (current_rt->pty_active) { 6548 pty_pump(&current_rt->pty); 6549 pty_check_alive(&current_rt->pty); 6550 6551 if (!current_rt->pty.alive) { 6552 // Drain remaining output (child error messages, etc.) before closing 6553 pty_pump(&current_rt->pty); 6554 JS_SetPropertyStr(ctx, pty_obj, "exitCode", 6555 JS_NewInt32(ctx, current_rt->pty.exit_code)); 6556 pty_destroy(&current_rt->pty); 6557 current_rt->pty_active = 0; 6558 } 6559 6560 JS_SetPropertyStr(ctx, pty_obj, "alive", 6561 JS_NewBool(ctx, current_rt->pty.alive)); 6562 JS_SetPropertyStr(ctx, pty_obj, "cursorX", 6563 JS_NewInt32(ctx, current_rt->pty.cursor_x)); 6564 JS_SetPropertyStr(ctx, pty_obj, "cursorY", 6565 JS_NewInt32(ctx, current_rt->pty.cursor_y)); 6566 JS_SetPropertyStr(ctx, pty_obj, "cols", 6567 JS_NewInt32(ctx, current_rt->pty.cols)); 6568 JS_SetPropertyStr(ctx, pty_obj, "rows", 6569 JS_NewInt32(ctx, current_rt->pty.rows)); 6570 JS_SetPropertyStr(ctx, pty_obj, "dirty", 6571 JS_NewBool(ctx, current_rt->pty.grid_dirty)); 6572 JS_SetPropertyStr(ctx, pty_obj, "cursorVisible", 6573 JS_NewBool(ctx, current_rt->pty.cursor_visible)); 6574 6575 // Expose grid as flat array: [ch, fg, bg, bold, ...] per cell, row by row 6576 // Only send if dirty (perf optimization) 6577 if (current_rt->pty.grid_dirty) { 6578 ACPty *p = &current_rt->pty; 6579 JSValue grid = JS_NewArray(ctx); 6580 int idx = 0; 6581 for (int y = 0; y < p->rows; y++) { 6582 for (int x = 0; x < p->cols; x++) { 6583 ACPtyCell *c = &p->grid[y][x]; 6584 // Pack cell: ch, fg, bg, bold, fg_r, fg_g, fg_b, bg_r, bg_g, bg_b 6585 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->ch)); 6586 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg)); 6587 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg)); 6588 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bold)); 6589 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_r)); 6590 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_g)); 6591 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_b)); 6592 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_r)); 6593 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_g)); 6594 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_b)); 6595 } 6596 } 6597 JS_SetPropertyStr(ctx, pty_obj, "grid", grid); 6598 p->grid_dirty = 0; 6599 } 6600 } else { 6601 // PTY not active — expose last exit code for diagnostics 6602 JS_SetPropertyStr(ctx, pty_obj, "exitCode", 6603 JS_NewInt32(ctx, current_rt->pty.exit_code)); 6604 } 6605 6606 JS_SetPropertyStr(ctx, sys, "pty", pty_obj); 6607 } 6608 6609 // PTY2 — second terminal emulator for split-screen 6610 { 6611 JSValue pty2_obj = JS_NewObject(ctx); 6612 JS_SetPropertyStr(ctx, pty2_obj, "spawn", 6613 JS_NewCFunction(ctx, js_pty2_spawn, "spawn", 4)); 6614 JS_SetPropertyStr(ctx, pty2_obj, "write", 6615 JS_NewCFunction(ctx, js_pty2_write, "write", 1)); 6616 JS_SetPropertyStr(ctx, pty2_obj, "resize", 6617 JS_NewCFunction(ctx, js_pty2_resize, "resize", 2)); 6618 JS_SetPropertyStr(ctx, pty2_obj, "kill", 6619 JS_NewCFunction(ctx, js_pty2_kill, "kill", 0)); 6620 JS_SetPropertyStr(ctx, pty2_obj, "active", 6621 JS_NewBool(ctx, current_rt->pty2_active)); 6622 6623 if (current_rt->pty2_active) { 6624 pty_pump(&current_rt->pty2); 6625 pty_check_alive(&current_rt->pty2); 6626 6627 if (!current_rt->pty2.alive) { 6628 pty_pump(&current_rt->pty2); 6629 JS_SetPropertyStr(ctx, pty2_obj, "exitCode", 6630 JS_NewInt32(ctx, current_rt->pty2.exit_code)); 6631 pty_destroy(&current_rt->pty2); 6632 current_rt->pty2_active = 0; 6633 } 6634 6635 JS_SetPropertyStr(ctx, pty2_obj, "alive", 6636 JS_NewBool(ctx, current_rt->pty2.alive)); 6637 JS_SetPropertyStr(ctx, pty2_obj, "cursorX", 6638 JS_NewInt32(ctx, current_rt->pty2.cursor_x)); 6639 JS_SetPropertyStr(ctx, pty2_obj, "cursorY", 6640 JS_NewInt32(ctx, current_rt->pty2.cursor_y)); 6641 JS_SetPropertyStr(ctx, pty2_obj, "cols", 6642 JS_NewInt32(ctx, current_rt->pty2.cols)); 6643 JS_SetPropertyStr(ctx, pty2_obj, "rows", 6644 JS_NewInt32(ctx, current_rt->pty2.rows)); 6645 JS_SetPropertyStr(ctx, pty2_obj, "dirty", 6646 JS_NewBool(ctx, current_rt->pty2.grid_dirty)); 6647 JS_SetPropertyStr(ctx, pty2_obj, "cursorVisible", 6648 JS_NewBool(ctx, current_rt->pty2.cursor_visible)); 6649 6650 if (current_rt->pty2.grid_dirty) { 6651 ACPty *p = &current_rt->pty2; 6652 JSValue grid = JS_NewArray(ctx); 6653 int idx = 0; 6654 for (int y = 0; y < p->rows; y++) { 6655 for (int x = 0; x < p->cols; x++) { 6656 ACPtyCell *c = &p->grid[y][x]; 6657 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->ch)); 6658 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg)); 6659 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg)); 6660 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bold)); 6661 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_r)); 6662 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_g)); 6663 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_b)); 6664 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_r)); 6665 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_g)); 6666 JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_b)); 6667 } 6668 } 6669 JS_SetPropertyStr(ctx, pty2_obj, "grid", grid); 6670 p->grid_dirty = 0; 6671 } 6672 } else { 6673 JS_SetPropertyStr(ctx, pty2_obj, "exitCode", 6674 JS_NewInt32(ctx, current_rt->pty2.exit_code)); 6675 } 6676 6677 JS_SetPropertyStr(ctx, sys, "pty2", pty2_obj); 6678 } 6679 6680 // Piece navigation 6681 JS_SetPropertyStr(ctx, sys, "jump", 6682 JS_NewCFunction(ctx, js_jump, "jump", 1)); 6683 6684 // system.listPieces() — scan /pieces/*.mjs and return name array 6685 JS_SetPropertyStr(ctx, sys, "listPieces", 6686 JS_NewCFunction(ctx, js_list_pieces, "listPieces", 0)); 6687 6688 // Printer detection and raw printing 6689 JS_SetPropertyStr(ctx, sys, "listPrinters", 6690 JS_NewCFunction(ctx, js_list_printers, "listPrinters", 0)); 6691 JS_SetPropertyStr(ctx, sys, "printRaw", 6692 JS_NewCFunction(ctx, js_print_raw, "printRaw", 2)); 6693 6694 // Volume and brightness control from JS 6695 JS_SetPropertyStr(ctx, sys, "volumeAdjust", 6696 JS_NewCFunction(ctx, js_volume_adjust, "volumeAdjust", 1)); 6697 JS_SetPropertyStr(ctx, sys, "brightnessAdjust", 6698 JS_NewCFunction(ctx, js_brightness_adjust, "brightnessAdjust", 1)); 6699 JS_SetPropertyStr(ctx, sys, "qrEncode", 6700 JS_NewCFunction(ctx, js_qr_encode, "qrEncode", 1)); 6701 JS_SetPropertyStr(ctx, sys, "openBrowser", 6702 JS_NewCFunction(ctx, js_open_browser, "openBrowser", 1)); 6703 6704 // Nopaint system — persistent painting canvas 6705 if (current_rt && strcmp(current_rt->system_mode, "nopaint") == 0) { 6706 // Create painting + buffer on first use 6707 if (!current_rt->nopaint_painting) { 6708 int w = current_rt->graph->screen->width; 6709 int h = current_rt->graph->screen->height; 6710 current_rt->nopaint_painting = fb_create(w, h); 6711 current_rt->nopaint_buffer = fb_create(w, h); 6712 // Fill painting with theme background 6713 fb_clear(current_rt->nopaint_painting, 0xFFF0EEE8); // light bg default 6714 fb_clear(current_rt->nopaint_buffer, 0x00000000); // transparent 6715 current_rt->nopaint_active = 1; 6716 ac_log("[nopaint] Created painting %dx%d\n", w, h); 6717 } 6718 6719 JSValue np = JS_NewObject(ctx); 6720 6721 // nopaint.is(state) — check nopaint state 6722 JS_SetPropertyStr(ctx, np, "is", JS_NewCFunction(ctx, js_nopaint_is, "is", 1)); 6723 JS_SetPropertyStr(ctx, np, "cancelStroke", JS_NewCFunction(ctx, js_nopaint_cancel, "cancelStroke", 0)); 6724 6725 // nopaint.brush — current brush position 6726 JSValue brush = JS_NewObject(ctx); 6727 JS_SetPropertyStr(ctx, brush, "x", JS_NewInt32(ctx, current_rt->nopaint_brush_x)); 6728 JS_SetPropertyStr(ctx, brush, "y", JS_NewInt32(ctx, current_rt->nopaint_brush_y)); 6729 JS_SetPropertyStr(ctx, np, "brush", brush); 6730 6731 // nopaint.buffer — temporary stroke overlay (as Painting object) 6732 JSValue buf_obj = JS_NewObjectClass(ctx, painting_class_id); 6733 JS_SetOpaque(buf_obj, current_rt->nopaint_buffer); 6734 JS_SetPropertyStr(ctx, buf_obj, "width", JS_NewInt32(ctx, current_rt->nopaint_buffer->width)); 6735 JS_SetPropertyStr(ctx, buf_obj, "height", JS_NewInt32(ctx, current_rt->nopaint_buffer->height)); 6736 JS_SetPropertyStr(ctx, np, "buffer", buf_obj); 6737 6738 // nopaint.needsBake 6739 JS_SetPropertyStr(ctx, np, "needsBake", JS_NewBool(ctx, current_rt->nopaint_needs_bake)); 6740 6741 JS_SetPropertyStr(ctx, sys, "nopaint", np); 6742 6743 // system.painting — the persistent canvas (as Painting object) 6744 JSValue ptg_obj = JS_NewObjectClass(ctx, painting_class_id); 6745 JS_SetOpaque(ptg_obj, current_rt->nopaint_painting); 6746 JS_SetPropertyStr(ctx, ptg_obj, "width", JS_NewInt32(ctx, current_rt->nopaint_painting->width)); 6747 JS_SetPropertyStr(ctx, ptg_obj, "height", JS_NewInt32(ctx, current_rt->nopaint_painting->height)); 6748 JS_SetPropertyStr(ctx, sys, "painting", ptg_obj); 6749 } else if (current_rt && current_rt->nopaint_painting) { 6750 // Even non-nopaint pieces (like prompt) can access system.painting to show it 6751 JSValue ptg_obj = JS_NewObjectClass(ctx, painting_class_id); 6752 JS_SetOpaque(ptg_obj, current_rt->nopaint_painting); 6753 JS_SetPropertyStr(ctx, ptg_obj, "width", JS_NewInt32(ctx, current_rt->nopaint_painting->width)); 6754 JS_SetPropertyStr(ctx, ptg_obj, "height", JS_NewInt32(ctx, current_rt->nopaint_painting->height)); 6755 JS_SetPropertyStr(ctx, sys, "painting", ptg_obj); 6756 } 6757 6758 return sys; 6759} 6760 6761// Build the API object passed to lifecycle functions 6762static JSValue build_api(JSContext *ctx, ACRuntime *rt, const char *phase) { 6763 current_phase = phase; 6764 JSValue api = JS_NewObject(ctx); 6765 JSValue global = JS_GetGlobalObject(ctx); 6766 6767 // Graphics 6768 JS_SetPropertyStr(ctx, api, "wipe", JS_GetPropertyStr(ctx, global, "wipe")); 6769 JS_SetPropertyStr(ctx, api, "ink", JS_GetPropertyStr(ctx, global, "ink")); 6770 JS_SetPropertyStr(ctx, api, "line", JS_GetPropertyStr(ctx, global, "line")); 6771 JS_SetPropertyStr(ctx, api, "box", JS_GetPropertyStr(ctx, global, "box")); 6772 JS_SetPropertyStr(ctx, api, "circle", JS_GetPropertyStr(ctx, global, "circle")); 6773 JS_SetPropertyStr(ctx, api, "plot", JS_GetPropertyStr(ctx, global, "plot")); 6774 JS_SetPropertyStr(ctx, api, "write", JS_GetPropertyStr(ctx, global, "write")); 6775 JS_SetPropertyStr(ctx, api, "scroll", JS_GetPropertyStr(ctx, global, "scroll")); 6776 JS_SetPropertyStr(ctx, api, "blur", JS_GetPropertyStr(ctx, global, "blur")); 6777 JS_SetPropertyStr(ctx, api, "zoom", JS_GetPropertyStr(ctx, global, "zoom")); 6778 JS_SetPropertyStr(ctx, api, "contrast", JS_GetPropertyStr(ctx, global, "contrast")); 6779 JS_SetPropertyStr(ctx, api, "spin", JS_GetPropertyStr(ctx, global, "spin")); 6780 JS_SetPropertyStr(ctx, api, "qr", JS_GetPropertyStr(ctx, global, "qr")); 6781 6782 // screen 6783 { 6784 JSValue s = JS_NewObject(ctx); 6785 JS_SetPropertyStr(ctx, s, "width", JS_NewInt32(ctx, rt->graph->fb->width)); 6786 JS_SetPropertyStr(ctx, s, "height", JS_NewInt32(ctx, rt->graph->fb->height)); 6787 JS_SetPropertyStr(ctx, api, "screen", s); 6788 } 6789 6790 // Counters 6791 if (strcmp(phase, "paint") == 0) { 6792 JS_SetPropertyStr(ctx, api, "paintCount", JS_NewInt32(ctx, rt->paint_count)); 6793 } 6794 if (strcmp(phase, "sim") == 0) { 6795 JS_SetPropertyStr(ctx, api, "simCount", JS_NewInt32(ctx, rt->sim_count)); 6796 } 6797 6798 // Params (colon-separated args from system.jump, e.g. "chat:clock" → ["clock"]) 6799 if (strcmp(phase, "boot") == 0 && rt->jump_param_count > 0) { 6800 JSValue params = JS_NewArray(ctx); 6801 for (int i = 0; i < rt->jump_param_count; i++) { 6802 JS_SetPropertyUint32(ctx, params, i, JS_NewString(ctx, rt->jump_params[i])); 6803 } 6804 JS_SetPropertyStr(ctx, api, "params", params); 6805 } 6806 6807 // needsPaint (noop — native always paints every frame) 6808 JS_SetPropertyStr(ctx, api, "needsPaint", JS_NewCFunction(ctx, js_noop, "needsPaint", 0)); 6809 6810 // Sound 6811 JS_SetPropertyStr(ctx, api, "sound", build_sound_obj(ctx, rt)); 6812 6813 // System (battery, etc) 6814 JS_SetPropertyStr(ctx, api, "system", build_system_obj(ctx)); 6815 6816 // WiFi 6817 JS_SetPropertyStr(ctx, api, "wifi", build_wifi_obj(ctx, rt->wifi)); 6818 6819 // Trackpad delta (per-frame relative movement) 6820 if (rt->input) { 6821 JSValue trackpad = JS_NewObject(ctx); 6822 JS_SetPropertyStr(ctx, trackpad, "dx", JS_NewInt32(ctx, rt->input->delta_x)); 6823 JS_SetPropertyStr(ctx, trackpad, "dy", JS_NewInt32(ctx, rt->input->delta_y)); 6824 JS_SetPropertyStr(ctx, api, "trackpad", trackpad); 6825 6826 // Analog key pressures — object mapping key names to 0.0-1.0 pressure 6827 if (rt->input->has_analog) { 6828 JSValue pressures = JS_NewObject(ctx); 6829 for (int i = 0; i < MAX_ANALOG_KEYS; i++) { 6830 if (rt->input->analog_keys[i].active) { 6831 const char *name = input_key_name(rt->input->analog_keys[i].key_code); 6832 if (name) { 6833 JS_SetPropertyStr(ctx, pressures, name, 6834 JS_NewFloat64(ctx, (double)rt->input->analog_keys[i].pressure)); 6835 } 6836 } 6837 } 6838 JS_SetPropertyStr(ctx, api, "pressures", pressures); 6839 } 6840 } 6841 6842 // params, colon, query 6843 JS_SetPropertyStr(ctx, api, "params", JS_NewArray(ctx)); 6844 JS_SetPropertyStr(ctx, api, "colon", JS_NewArray(ctx)); 6845 JS_SetPropertyStr(ctx, api, "query", JS_NewObject(ctx)); 6846 6847 // Form constructor 6848 JS_SetPropertyStr(ctx, api, "Form", JS_NewCFunction2(ctx, js_form_constructor, "Form", 2, 6849 JS_CFUNC_constructor, 0)); 6850 6851 // CUBEL and QUAD geometry constants 6852 JS_SetPropertyStr(ctx, api, "CUBEL", build_cubel(ctx)); 6853 JS_SetPropertyStr(ctx, api, "QUAD", build_quad(ctx)); 6854 6855 // penLock 6856 JS_SetPropertyStr(ctx, api, "penLock", JS_NewCFunction(ctx, js_pen_lock, "penLock", 0)); 6857 6858 // get (stub for texture loading — fps.mjs checks if get is available) 6859 JS_SetPropertyStr(ctx, api, "get", JS_NULL); 6860 6861 // setShowClippedWireframes, clearWireframeBuffer, drawBufferedWireframes, 6862 // getRenderStats — stubs/no-ops for fps.mjs debug panel 6863 JS_SetPropertyStr(ctx, api, "setShowClippedWireframes", JS_NewCFunction(ctx, js_noop, "setShowClippedWireframes", 1)); 6864 JS_SetPropertyStr(ctx, api, "clearWireframeBuffer", JS_NewCFunction(ctx, js_noop, "clearWireframeBuffer", 0)); 6865 JS_SetPropertyStr(ctx, api, "drawBufferedWireframes", JS_NewCFunction(ctx, js_noop, "drawBufferedWireframes", 0)); 6866 JS_SetPropertyStr(ctx, api, "getRenderStats", JS_NewCFunction(ctx, js_noop, "getRenderStats", 0)); 6867 6868 // system.fps — camera and render stats for debug panel 6869 { 6870 JSValue fps_obj = JS_NewObject(ctx); 6871 6872 // system.fps.renderStats 6873 JSValue rs = JS_NewObject(ctx); 6874 JS_SetPropertyStr(ctx, rs, "originalTriangles", JS_NewInt32(ctx, rt->render_stats.originalTriangles)); 6875 JS_SetPropertyStr(ctx, rs, "clippedTriangles", JS_NewInt32(ctx, rt->render_stats.clippedTriangles)); 6876 JS_SetPropertyStr(ctx, rs, "subdividedTriangles", JS_NewInt32(ctx, rt->render_stats.subdividedTriangles)); 6877 JS_SetPropertyStr(ctx, rs, "trianglesRejected", JS_NewInt32(ctx, rt->render_stats.trianglesRejected)); 6878 JS_SetPropertyStr(ctx, rs, "pixelsDrawn", JS_NewInt32(ctx, rt->render_stats.pixelsDrawn)); 6879 JS_SetPropertyStr(ctx, rs, "wireframeSegmentsTotal", JS_NewInt32(ctx, rt->render_stats.wireframeSegmentsTotal)); 6880 JS_SetPropertyStr(ctx, rs, "wireframeSegmentsTextured", JS_NewInt32(ctx, rt->render_stats.wireframeSegmentsTextured)); 6881 JS_SetPropertyStr(ctx, rs, "wireframeSegmentsGradient", JS_NewInt32(ctx, rt->render_stats.wireframeSegmentsGradient)); 6882 JS_SetPropertyStr(ctx, fps_obj, "renderStats", rs); 6883 6884 // system.fps.doll.cam — camera position/rotation for debug 6885 JSValue doll = JS_NewObject(ctx); 6886 JSValue cam = JS_NewObject(ctx); 6887 JS_SetPropertyStr(ctx, cam, "x", JS_NewFloat64(ctx, rt->camera3d.x)); 6888 JS_SetPropertyStr(ctx, cam, "y", JS_NewFloat64(ctx, rt->camera3d.y)); 6889 JS_SetPropertyStr(ctx, cam, "z", JS_NewFloat64(ctx, rt->camera3d.z)); 6890 JS_SetPropertyStr(ctx, cam, "rotX", JS_NewFloat64(ctx, rt->camera3d.rotX)); 6891 JS_SetPropertyStr(ctx, cam, "rotY", JS_NewFloat64(ctx, rt->camera3d.rotY)); 6892 JS_SetPropertyStr(ctx, cam, "rotZ", JS_NewFloat64(ctx, rt->camera3d.rotZ)); 6893 JS_SetPropertyStr(ctx, doll, "cam", cam); 6894 JS_SetPropertyStr(ctx, fps_obj, "doll", doll); 6895 6896 // Attach to system object on api 6897 JSValue sys = JS_GetPropertyStr(ctx, api, "system"); 6898 if (!JS_IsUndefined(sys) && !JS_IsNull(sys)) 6899 JS_SetPropertyStr(ctx, sys, "fps", fps_obj); 6900 else 6901 JS_FreeValue(ctx, fps_obj); 6902 JS_FreeValue(ctx, sys); 6903 } 6904 6905 // fps (no-op — kept for non-3D pieces) 6906 JS_SetPropertyStr(ctx, api, "fps", JS_NewCFunction(ctx, js_noop, "fps", 1)); 6907 6908 // hud 6909 { 6910 JSValue hud = JS_NewObject(ctx); 6911 JS_SetPropertyStr(ctx, hud, "label", JS_NewCFunction(ctx, js_noop, "label", 3)); 6912 JS_SetPropertyStr(ctx, hud, "superscript", JS_NewCFunction(ctx, js_noop, "superscript", 1)); 6913 JS_SetPropertyStr(ctx, api, "hud", hud); 6914 } 6915 6916 // net 6917 { 6918 JSValue net = JS_NewObject(ctx); 6919 JS_SetPropertyStr(ctx, net, "udp", JS_NewCFunction(ctx, js_net_udp, "udp", 0)); 6920 JS_SetPropertyStr(ctx, net, "preload", JS_NewCFunction(ctx, js_promise_null, "preload", 1)); 6921 JS_SetPropertyStr(ctx, net, "pieces", JS_NewCFunction(ctx, js_promise_null, "pieces", 1)); 6922 JS_SetPropertyStr(ctx, net, "rewrite", JS_NewCFunction(ctx, js_noop, "rewrite", 1)); 6923 JS_SetPropertyStr(ctx, net, "log", JS_NewCFunction(ctx, js_noop, "log", 2)); 6924 JS_SetPropertyStr(ctx, api, "net", net); 6925 } 6926 6927 // pen — current pointer position 6928 if (rt->input) { 6929 JSValue pen = JS_NewObject(ctx); 6930 JS_SetPropertyStr(ctx, pen, "x", JS_NewInt32(ctx, rt->input->pointer_x)); 6931 JS_SetPropertyStr(ctx, pen, "y", JS_NewInt32(ctx, rt->input->pointer_y)); 6932 JS_SetPropertyStr(ctx, pen, "down", JS_NewBool(ctx, rt->input->pointer_down)); 6933 JS_SetPropertyStr(ctx, api, "pen", pen); 6934 } 6935 6936 // store 6937 { 6938 JSValue store = JS_NewObject(ctx); 6939 JS_SetPropertyStr(ctx, store, "retrieve", JS_NewCFunction(ctx, js_promise_null, "retrieve", 2)); 6940 JS_SetPropertyStr(ctx, store, "persist", JS_NewCFunction(ctx, js_noop, "persist", 2)); 6941 JS_SetPropertyStr(ctx, store, "delete", JS_NewCFunction(ctx, js_promise_null, "delete", 2)); 6942 JS_SetPropertyStr(ctx, api, "store", store); 6943 } 6944 6945 // clock 6946 { 6947 JSValue clock = JS_NewObject(ctx); 6948 JS_SetPropertyStr(ctx, clock, "resync", JS_NewCFunction(ctx, js_noop, "resync", 0)); 6949 JS_SetPropertyStr(ctx, clock, "time", JS_NewCFunction(ctx, js_clock_time, "time", 0)); 6950 JS_SetPropertyStr(ctx, api, "clock", clock); 6951 } 6952 6953 // typeface (no-op function) 6954 JS_SetPropertyStr(ctx, api, "typeface", JS_NewCFunction(ctx, js_noop, "typeface", 1)); 6955 6956 // painting(w, h, callback) — creates a stub painting object with width/height 6957 JS_SetPropertyStr(ctx, api, "painting", JS_NewCFunction(ctx, js_painting, "painting", 3)); 6958 6959 // paste, page (real implementations), layer, sharpen (stubs) 6960 JS_SetPropertyStr(ctx, api, "paste", JS_NewCFunction(ctx, js_paste, "paste", 3)); 6961 JS_SetPropertyStr(ctx, api, "page", JS_NewCFunction(ctx, js_page, "page", 1)); 6962 JS_SetPropertyStr(ctx, api, "layer", JS_NewCFunction(ctx, js_noop, "layer", 1)); 6963 JS_SetPropertyStr(ctx, api, "sharpen", JS_NewCFunction(ctx, js_noop, "sharpen", 1)); 6964 6965 // num 6966 { 6967 JSValue num = JS_NewObject(ctx); 6968 JS_SetPropertyStr(ctx, num, "clamp", JS_NewCFunction(ctx, js_num_clamp, "clamp", 3)); 6969 JS_SetPropertyStr(ctx, num, "rand", JS_NewCFunction(ctx, js_num_rand, "rand", 0)); 6970 JS_SetPropertyStr(ctx, num, "randIntRange", JS_NewCFunction(ctx, js_num_randint, "randIntRange", 2)); 6971 6972 // max/min — just reference Math.max/min 6973 JSValue math = JS_GetPropertyStr(ctx, global, "Math"); 6974 JS_SetPropertyStr(ctx, num, "max", JS_GetPropertyStr(ctx, math, "max")); 6975 JS_SetPropertyStr(ctx, num, "min", JS_GetPropertyStr(ctx, math, "min")); 6976 JS_SetPropertyStr(ctx, num, "abs", JS_GetPropertyStr(ctx, math, "abs")); 6977 JS_SetPropertyStr(ctx, num, "floor", JS_GetPropertyStr(ctx, math, "floor")); 6978 JS_SetPropertyStr(ctx, num, "ceil", JS_GetPropertyStr(ctx, math, "ceil")); 6979 JS_SetPropertyStr(ctx, num, "round", JS_GetPropertyStr(ctx, math, "round")); 6980 JS_SetPropertyStr(ctx, num, "sign", JS_GetPropertyStr(ctx, math, "sign")); 6981 JS_FreeValue(ctx, math); 6982 JS_SetPropertyStr(ctx, num, "shiftRGB", JS_GetPropertyStr(ctx, global, "__shiftRGB")); 6983 JS_SetPropertyStr(ctx, num, "dist", JS_GetPropertyStr(ctx, global, "__dist")); 6984 JS_SetPropertyStr(ctx, num, "map", JS_GetPropertyStr(ctx, global, "__map")); 6985 JS_SetPropertyStr(ctx, num, "lerp", JS_GetPropertyStr(ctx, global, "__lerp")); 6986 JS_SetPropertyStr(ctx, num, "parseColor", JS_GetPropertyStr(ctx, global, "__parseColor")); 6987 JS_SetPropertyStr(ctx, num, "randIntArr", JS_GetPropertyStr(ctx, global, "__randIntArr")); 6988 JS_SetPropertyStr(ctx, num, "timestamp", JS_GetPropertyStr(ctx, global, "__timestamp")); 6989 JS_SetPropertyStr(ctx, api, "num", num); 6990 } 6991 6992 // help 6993 { 6994 JSValue help = JS_NewObject(ctx); 6995 JS_SetPropertyStr(ctx, help, "resampleArray", JS_NewCFunction(ctx, js_noop, "resampleArray", 2)); 6996 JS_SetPropertyStr(ctx, api, "help", help); 6997 } 6998 6999 // pens 7000 JS_SetPropertyStr(ctx, api, "pens", JS_NewCFunction(ctx, js_noop, "pens", 0)); 7001 7002 // ui 7003 { 7004 JSValue ui = JS_NewObject(ctx); 7005 JSValue btn_ctor = JS_GetPropertyStr(ctx, global, "__Button"); 7006 JS_SetPropertyStr(ctx, ui, "Button", btn_ctor); 7007 JS_SetPropertyStr(ctx, api, "ui", ui); 7008 } 7009 7010 // api sub-object (meta-API) 7011 // notepat passes this nested `api` to helpers like buildWaveButton(api) 7012 // which destructure { screen, ui, typeface, geo, sound, num, ... } from it 7013 { 7014 JSValue api_meta = JS_NewObject(ctx); 7015 JS_SetPropertyStr(ctx, api_meta, "Typeface", JS_GetPropertyStr(ctx, global, "__Typeface")); 7016 JS_SetPropertyStr(ctx, api_meta, "beep", JS_NewCFunction(ctx, js_noop, "beep", 1)); 7017 JS_SetPropertyStr(ctx, api_meta, "send", JS_NewCFunction(ctx, js_noop, "send", 1)); 7018 7019 // api.text.box 7020 JSValue api_text = JS_NewObject(ctx); 7021 JS_SetPropertyStr(ctx, api_text, "box", JS_NewCFunction(ctx, js_noop, "box", 5)); 7022 JS_SetPropertyStr(ctx, api_meta, "text", api_text); 7023 7024 // api.geo.Box 7025 JSValue api_geo = JS_NewObject(ctx); 7026 JS_SetPropertyStr(ctx, api_geo, "Box", JS_GetPropertyStr(ctx, global, "__Box")); 7027 JS_SetPropertyStr(ctx, api_meta, "geo", api_geo); 7028 7029 // Copy top-level API properties onto api_meta so helper functions 7030 // like buildWaveButton(api) can destructure { screen, ui, typeface, ... } 7031 JS_SetPropertyStr(ctx, api_meta, "screen", JS_GetPropertyStr(ctx, api, "screen")); 7032 JS_SetPropertyStr(ctx, api_meta, "ui", JS_GetPropertyStr(ctx, api, "ui")); 7033 JS_SetPropertyStr(ctx, api_meta, "typeface", JS_GetPropertyStr(ctx, api, "typeface")); 7034 JS_SetPropertyStr(ctx, api_meta, "sound", JS_GetPropertyStr(ctx, api, "sound")); 7035 JS_SetPropertyStr(ctx, api_meta, "num", JS_GetPropertyStr(ctx, api, "num")); 7036 JS_SetPropertyStr(ctx, api_meta, "hud", JS_GetPropertyStr(ctx, api, "hud")); 7037 JS_SetPropertyStr(ctx, api_meta, "net", JS_GetPropertyStr(ctx, api, "net")); 7038 JS_SetPropertyStr(ctx, api_meta, "store", JS_GetPropertyStr(ctx, api, "store")); 7039 JS_SetPropertyStr(ctx, api_meta, "help", JS_GetPropertyStr(ctx, api, "help")); 7040 JS_SetPropertyStr(ctx, api_meta, "painting", JS_GetPropertyStr(ctx, api, "painting")); 7041 JS_SetPropertyStr(ctx, api_meta, "paste", JS_GetPropertyStr(ctx, api, "paste")); 7042 JS_SetPropertyStr(ctx, api_meta, "page", JS_GetPropertyStr(ctx, api, "page")); 7043 JS_SetPropertyStr(ctx, api_meta, "layer", JS_GetPropertyStr(ctx, api, "layer")); 7044 JS_SetPropertyStr(ctx, api_meta, "pens", JS_GetPropertyStr(ctx, api, "pens")); 7045 JS_SetPropertyStr(ctx, api_meta, "params", JS_GetPropertyStr(ctx, api, "params")); 7046 JS_SetPropertyStr(ctx, api_meta, "colon", JS_GetPropertyStr(ctx, api, "colon")); 7047 JS_SetPropertyStr(ctx, api_meta, "wipe", JS_GetPropertyStr(ctx, api, "wipe")); 7048 JS_SetPropertyStr(ctx, api_meta, "ink", JS_GetPropertyStr(ctx, api, "ink")); 7049 JS_SetPropertyStr(ctx, api_meta, "box", JS_GetPropertyStr(ctx, api, "box")); 7050 JS_SetPropertyStr(ctx, api_meta, "line", JS_GetPropertyStr(ctx, api, "line")); 7051 JS_SetPropertyStr(ctx, api_meta, "circle", JS_GetPropertyStr(ctx, api, "circle")); 7052 JS_SetPropertyStr(ctx, api_meta, "plot", JS_GetPropertyStr(ctx, api, "plot")); 7053 JS_SetPropertyStr(ctx, api_meta, "write", JS_GetPropertyStr(ctx, api, "write")); 7054 JS_SetPropertyStr(ctx, api_meta, "scroll", JS_GetPropertyStr(ctx, api, "scroll")); 7055 JS_SetPropertyStr(ctx, api_meta, "blur", JS_GetPropertyStr(ctx, api, "blur")); 7056 JS_SetPropertyStr(ctx, api_meta, "zoom", JS_GetPropertyStr(ctx, api, "zoom")); 7057 JS_SetPropertyStr(ctx, api_meta, "contrast", JS_GetPropertyStr(ctx, api, "contrast")); 7058 JS_SetPropertyStr(ctx, api_meta, "spin", JS_GetPropertyStr(ctx, api, "spin")); 7059 7060 JS_SetPropertyStr(ctx, api, "api", api_meta); 7061 } 7062 7063 JS_FreeValue(ctx, global); 7064 return api; 7065} 7066 7067static int piece_load_counter = 0; 7068 7069int js_load_piece(ACRuntime *rt, const char *path) { 7070 JSContext *ctx = rt->ctx; 7071 current_rt = rt; 7072 7073 FILE *f = fopen(path, "r"); 7074 if (!f) { 7075 fprintf(stderr, "[js] Cannot open %s\n", path); 7076 return -1; 7077 } 7078 fseek(f, 0, SEEK_END); 7079 long len = ftell(f); 7080 fseek(f, 0, SEEK_SET); 7081 char *src = malloc(len + 1); 7082 fread(src, 1, len, f); 7083 src[len] = '\0'; 7084 fclose(f); 7085 7086 // Append globalThis export assignments so we can find lifecycle functions 7087 // after module evaluation (QuickJS modules don't expose exports publicly). 7088 // For pieces with `export const system = "fps"` (arena.mjs etc.) this 7089 // ALSO instantiates CamDoll from the preloaded FPS bundle and wraps 7090 // boot/sim/act/paint so `api.system.fps.doll` is live on every call 7091 // without the piece needing to know anything about the native runtime. 7092 const char *export_shim = 7093 "\n;if(typeof boot==='function')globalThis.boot=boot;" 7094 "if(typeof paint==='function')globalThis.paint=paint;" 7095 "if(typeof act==='function')globalThis.act=act;" 7096 "if(typeof sim==='function')globalThis.sim=sim;" 7097 "if(typeof leave==='function')globalThis.leave=leave;" 7098 "if(typeof beat==='function')globalThis.beat=beat;" 7099 "if(typeof configureAutopat==='function')globalThis.configureAutopat=configureAutopat;" 7100 "if(typeof system!=='undefined')globalThis.__pieceSystem=system;" 7101 "if(typeof fpsOpts!=='undefined')globalThis.__pieceFpsOpts=fpsOpts;" 7102 // FPS system wiring — runs only when piece opts in. 7103 "if(globalThis.__pieceSystem==='fps'&&globalThis.__FpsSystem){" 7104 "try{" 7105 "const FS=globalThis.__FpsSystem;" 7106 "const opts=globalThis.__pieceFpsOpts||{fov:80};" 7107 "const doll=new FS.CamDoll(FS.Camera,FS.Dolly,opts);" 7108 "globalThis.__fpsDoll=doll;" 7109 "const _b=globalThis.boot,_s=globalThis.sim,_a=globalThis.act,_p=globalThis.paint;" 7110 "const inject=(api)=>{if(api){if(!api.system)api.system={};api.system.fps={doll};}};" 7111 "globalThis.boot=(api)=>{inject(api);return _b?.(api);};" 7112 "globalThis.sim=(api)=>{inject(api);try{doll.sim?.();}catch(e){}return _s?.(api);};" 7113 "globalThis.act=(api)=>{inject(api);try{if(api?.event)doll.act?.(api.event);}catch(e){}return _a?.(api);};" 7114 "globalThis.paint=(api)=>{inject(api);return _p?.(api);};" 7115 "}catch(e){console.error('[fps] wire-up failed:',e.message);}" 7116 "}\n"; 7117 size_t shim_len = strlen(export_shim); 7118 char *patched = malloc(len + shim_len + 1); 7119 memcpy(patched, src, len); 7120 memcpy(patched + len, export_shim, shim_len); 7121 patched[len + shim_len] = '\0'; 7122 free(src); 7123 src = patched; 7124 len += shim_len; 7125 7126 // Use unique module name each load (QuickJS caches modules by name) 7127 char mod_name[300]; 7128 snprintf(mod_name, sizeof(mod_name), "%s#%d", path, piece_load_counter++); 7129 7130 // Evaluate as module 7131 JSValue val = JS_Eval(ctx, src, len, mod_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); 7132 free(src); 7133 7134 if (JS_IsException(val)) { 7135 JSValue exc = JS_GetException(ctx); 7136 const char *str = JS_ToCString(ctx, exc); 7137 fprintf(stderr, "[js] Parse error: %s\n", str); 7138 JS_FreeCString(ctx, str); 7139 JS_FreeValue(ctx, exc); 7140 return -1; 7141 } 7142 7143 JSValue result = JS_EvalFunction(ctx, val); 7144 if (JS_IsException(result)) { 7145 JSValue exc = JS_GetException(ctx); 7146 const char *str = JS_ToCString(ctx, exc); 7147 fprintf(stderr, "[js] Module error: %s\n", str); 7148 JS_FreeCString(ctx, str); 7149 JS_FreeValue(ctx, exc); 7150 return -1; 7151 } 7152 JS_FreeValue(ctx, result); 7153 7154 // Get lifecycle functions from globalThis 7155 JSValue global = JS_GetGlobalObject(ctx); 7156 rt->boot_fn = JS_GetPropertyStr(ctx, global, "boot"); 7157 rt->paint_fn = JS_GetPropertyStr(ctx, global, "paint"); 7158 rt->act_fn = JS_GetPropertyStr(ctx, global, "act"); 7159 rt->sim_fn = JS_GetPropertyStr(ctx, global, "sim"); 7160 rt->leave_fn = JS_GetPropertyStr(ctx, global, "leave"); 7161 rt->beat_fn = JS_GetPropertyStr(ctx, global, "beat"); 7162 7163 // Capture piece's `export const system` value (e.g. "prompt", "fps", "nopaint") 7164 rt->system_mode[0] = '\0'; 7165 JSValue sys_val = JS_GetPropertyStr(ctx, global, "__pieceSystem"); 7166 if (JS_IsString(sys_val)) { 7167 const char *s = JS_ToCString(ctx, sys_val); 7168 if (s) { 7169 strncpy(rt->system_mode, s, sizeof(rt->system_mode) - 1); 7170 rt->system_mode[sizeof(rt->system_mode) - 1] = '\0'; 7171 JS_FreeCString(ctx, s); 7172 } 7173 } 7174 JS_FreeValue(ctx, sys_val); 7175 7176 // Auto-detect FPS mode 7177 if (strcmp(rt->system_mode, "fps") == 0) 7178 rt->fps_system_active = 1; 7179 else 7180 rt->fps_system_active = 0; 7181 7182 JS_FreeValue(ctx, global); 7183 7184 fprintf(stderr, "[js] Loaded piece: %s (system=%s)\n", path, 7185 rt->system_mode[0] ? rt->system_mode : "(none)"); 7186 fprintf(stderr, "[js] boot=%s paint=%s act=%s sim=%s beat=%s\n", 7187 JS_IsFunction(ctx, rt->boot_fn) ? "yes" : "no", 7188 JS_IsFunction(ctx, rt->paint_fn) ? "yes" : "no", 7189 JS_IsFunction(ctx, rt->act_fn) ? "yes" : "no", 7190 JS_IsFunction(ctx, rt->sim_fn) ? "yes" : "no", 7191 JS_IsFunction(ctx, rt->beat_fn) ? "yes" : "no"); 7192 7193 return 0; 7194} 7195 7196void js_call_boot(ACRuntime *rt) { 7197 if (!JS_IsFunction(rt->ctx, rt->boot_fn)) return; 7198 current_rt = rt; 7199 JSValue api = build_api(rt->ctx, rt, "boot"); 7200 JSValue result = JS_Call(rt->ctx, rt->boot_fn, JS_UNDEFINED, 1, &api); 7201 if (JS_IsException(result)) { 7202 JSValue exc = JS_GetException(rt->ctx); 7203 const char *str = JS_ToCString(rt->ctx, exc); 7204 JSValue stack = JS_GetPropertyStr(rt->ctx, exc, "stack"); 7205 const char *stack_str = JS_ToCString(rt->ctx, stack); 7206 fprintf(stderr, "[js] boot() error: %s\n%s\n", str, stack_str ? stack_str : ""); 7207 if (stack_str) JS_FreeCString(rt->ctx, stack_str); 7208 JS_FreeValue(rt->ctx, stack); 7209 JS_FreeCString(rt->ctx, str); 7210 JS_FreeValue(rt->ctx, exc); 7211 } 7212 7213 // If boot() returned a Promise (async function), we need to catch rejections 7214 if (JS_IsObject(result)) { 7215 // Check if it has a .catch method (Promise) 7216 JSValue catch_fn = JS_GetPropertyStr(rt->ctx, result, "catch"); 7217 if (JS_IsFunction(rt->ctx, catch_fn)) { 7218 // Register an error handler: promise.catch(e => console.error("boot async error:", e)) 7219 const char *catch_code = 7220 "(function(p) { p.catch(function(e) { console.error('[js] boot() async error:', e, e.stack || ''); }); })"; 7221 JSValue catch_handler = JS_Eval(rt->ctx, catch_code, strlen(catch_code), "<catch>", JS_EVAL_TYPE_GLOBAL); 7222 if (JS_IsFunction(rt->ctx, catch_handler)) { 7223 JSValue catch_result = JS_Call(rt->ctx, catch_handler, JS_UNDEFINED, 1, &result); 7224 JS_FreeValue(rt->ctx, catch_result); 7225 } 7226 JS_FreeValue(rt->ctx, catch_handler); 7227 } 7228 JS_FreeValue(rt->ctx, catch_fn); 7229 } 7230 7231 JS_FreeValue(rt->ctx, result); 7232 JS_FreeValue(rt->ctx, api); 7233 7234 // Execute pending jobs (promises) — boot() is async, drain fully 7235 // Each await creates a new microtask; need multiple drain rounds 7236 JSContext *pctx; 7237 for (int round = 0; round < 100; round++) { 7238 int jobs = JS_ExecutePendingJob(rt->rt, &pctx); 7239 if (jobs <= 0) break; 7240 } 7241} 7242 7243// Helper: record a JS crash for the overlay 7244static void js_record_crash(ACRuntime *rt, const char *fn_name, const char *msg) { 7245 rt->crash_active = 1; 7246 rt->crash_count++; 7247 rt->crash_frame = 0; 7248 snprintf(rt->crash_msg, sizeof(rt->crash_msg), "%s(): %s", fn_name, msg ? msg : "unknown"); 7249} 7250 7251void js_call_paint(ACRuntime *rt) { 7252 if (!JS_IsFunction(rt->ctx, rt->paint_fn)) return; 7253 current_rt = rt; 7254 rt->paint_count++; 7255 7256 // Nopaint: paste the persistent painting as background before piece paints 7257 if (rt->nopaint_active && rt->nopaint_painting) { 7258 graph_paste(rt->graph, rt->nopaint_painting, 0, 0); 7259 } 7260 7261 JSValue api = build_api(rt->ctx, rt, "paint"); 7262 JSValue result = JS_Call(rt->ctx, rt->paint_fn, JS_UNDEFINED, 1, &api); 7263 if (JS_IsException(result)) { 7264 JSValue exc = JS_GetException(rt->ctx); 7265 const char *str = JS_ToCString(rt->ctx, exc); 7266 JSValue stack = JS_GetPropertyStr(rt->ctx, exc, "stack"); 7267 const char *stack_str = JS_ToCString(rt->ctx, stack); 7268 fprintf(stderr, "[js] paint() error: %s\n%s\n", str, stack_str ? stack_str : ""); 7269 js_record_crash(rt, "paint", str); 7270 if (stack_str) JS_FreeCString(rt->ctx, stack_str); 7271 JS_FreeValue(rt->ctx, stack); 7272 JS_FreeCString(rt->ctx, str); 7273 JS_FreeValue(rt->ctx, exc); 7274 } 7275 JS_FreeValue(rt->ctx, result); 7276 JS_FreeValue(rt->ctx, api); 7277 7278 // Nopaint: if bake was requested, composite buffer → painting and clear buffer 7279 if (rt->nopaint_active && rt->nopaint_needs_bake) { 7280 if (rt->nopaint_buffer && rt->nopaint_painting) { 7281 graph_paste(rt->graph, rt->nopaint_buffer, 0, 0); // show final stroke on screen 7282 // Bake: composite buffer onto persistent painting 7283 ACFramebuffer *saved = rt->graph->fb; 7284 graph_page(rt->graph, rt->nopaint_painting); 7285 graph_paste(rt->graph, rt->nopaint_buffer, 0, 0); 7286 graph_page(rt->graph, saved); 7287 // Clear buffer 7288 fb_clear(rt->nopaint_buffer, 0x00000000); 7289 } 7290 rt->nopaint_needs_bake = 0; 7291 } 7292} 7293 7294void js_call_act(ACRuntime *rt) { 7295 current_rt = rt; 7296 7297 // Track key held state for FPS camera (process even without act_fn) 7298 ACInput *input = rt->input; 7299 for (int i = 0; i < input->event_count; i++) { 7300 ACEvent *ev = &input->events[i]; 7301 if (ev->type == AC_EVENT_KEYBOARD_DOWN && ev->key_code > 0 && ev->key_code < KEY_MAX) 7302 rt->keys_held[ev->key_code] = 1; 7303 else if (ev->type == AC_EVENT_KEYBOARD_UP && ev->key_code > 0 && ev->key_code < KEY_MAX) 7304 rt->keys_held[ev->key_code] = 0; 7305 } 7306 7307 // System-level Escape → jump to prompt (mirrors web bios behavior). 7308 // Skip if current piece IS prompt, or if piece uses system="world" or "prompt". 7309 if (strcmp(rt->system_mode, "prompt") != 0 && 7310 strcmp(rt->system_mode, "world") != 0) { 7311 for (int i = 0; i < input->event_count; i++) { 7312 ACEvent *ev = &input->events[i]; 7313 if (ev->type == AC_EVENT_KEYBOARD_DOWN && ev->key_code == KEY_ESC) { 7314 // Reset FPS camera state if active 7315 if (rt->pen_locked) { 7316 rt->pen_locked = 0; 7317 rt->fps_system_active = 0; 7318 } 7319 strncpy(rt->jump_target, "prompt", sizeof(rt->jump_target) - 1); 7320 rt->jump_target[sizeof(rt->jump_target) - 1] = '\0'; 7321 rt->jump_requested = 1; 7322 rt->jump_param_count = 0; 7323 ac_log("[system] Escape → prompt\n"); 7324 return; // Skip passing events to piece 7325 } 7326 } 7327 } 7328 7329 // Nopaint: update brush position and state from touch/mouse events 7330 if (rt->nopaint_active && strcmp(rt->system_mode, "nopaint") == 0) { 7331 for (int i = 0; i < input->event_count; i++) { 7332 ACEvent *ev = &input->events[i]; 7333 if (ev->type == AC_EVENT_TOUCH) { 7334 rt->nopaint_state = 1; // painting 7335 rt->nopaint_brush_x = ev->x; 7336 rt->nopaint_brush_y = ev->y; 7337 rt->nopaint_needs_bake = 0; 7338 } else if (ev->type == AC_EVENT_DRAW) { 7339 if (rt->nopaint_state == 1) { 7340 rt->nopaint_brush_x = ev->x; 7341 rt->nopaint_brush_y = ev->y; 7342 } 7343 } else if (ev->type == AC_EVENT_LIFT) { 7344 if (rt->nopaint_state == 1) { 7345 rt->nopaint_state = 0; // idle 7346 rt->nopaint_needs_bake = 1; 7347 } 7348 } 7349 } 7350 } 7351 7352 if (!JS_IsFunction(rt->ctx, rt->act_fn)) return; 7353 7354 for (int i = 0; i < input->event_count; i++) { 7355 JSValue api = build_api(rt->ctx, rt, "act"); 7356 JSValue event = make_event_object(rt->ctx, &input->events[i]); 7357 JS_SetPropertyStr(rt->ctx, api, "event", event); 7358 7359 JSValue result = JS_Call(rt->ctx, rt->act_fn, JS_UNDEFINED, 1, &api); 7360 if (JS_IsException(result)) { 7361 JSValue exc = JS_GetException(rt->ctx); 7362 const char *str = JS_ToCString(rt->ctx, exc); 7363 JSValue stack = JS_GetPropertyStr(rt->ctx, exc, "stack"); 7364 const char *stack_str = JS_ToCString(rt->ctx, stack); 7365 fprintf(stderr, "[js] act() error: %s\n%s\n", str, stack_str ? stack_str : ""); 7366 ac_log("[js] act() error: %s\n%s\n", str, stack_str ? stack_str : ""); 7367 js_record_crash(rt, "act", str); 7368 if (stack_str) JS_FreeCString(rt->ctx, stack_str); 7369 JS_FreeValue(rt->ctx, stack); 7370 JS_FreeCString(rt->ctx, str); 7371 JS_FreeValue(rt->ctx, exc); 7372 } 7373 JS_FreeValue(rt->ctx, result); 7374 JS_FreeValue(rt->ctx, api); 7375 } 7376} 7377 7378void js_call_sim(ACRuntime *rt) { 7379 current_rt = rt; 7380 7381 // If the piece's FPS bundle installed a CamDoll (via the export 7382 // shim in js_load_piece), the doll's own sim() — invoked from the 7383 // wrapped piece sim() — is the camera authority. We just sync its 7384 // cam state back to rt->camera3d AFTER the piece sim runs (below). 7385 // Otherwise, fall back to the native WASD + trackpad driver. 7386 int have_doll = 0; 7387 { 7388 JSValue global = JS_GetGlobalObject(rt->ctx); 7389 JSValue doll = JS_GetPropertyStr(rt->ctx, global, "__fpsDoll"); 7390 have_doll = !JS_IsUndefined(doll) && !JS_IsNull(doll); 7391 JS_FreeValue(rt->ctx, doll); 7392 JS_FreeValue(rt->ctx, global); 7393 } 7394 if (!have_doll && rt->pen_locked && rt->fps_system_active) { 7395 camera3d_update(&rt->camera3d, 7396 rt->keys_held[KEY_W], 7397 rt->keys_held[KEY_S], 7398 rt->keys_held[KEY_A], 7399 rt->keys_held[KEY_D], 7400 rt->keys_held[KEY_SPACE], 7401 rt->keys_held[KEY_LEFTSHIFT], 7402 (float)(rt->input ? rt->input->delta_x : 0), 7403 (float)(rt->input ? rt->input->delta_y : 0)); 7404 // Consume trackpad delta 7405 if (rt->input) { rt->input->delta_x = 0; rt->input->delta_y = 0; } 7406 } 7407 7408 if (!JS_IsFunction(rt->ctx, rt->sim_fn)) return; 7409 rt->sim_count++; 7410 JSValue api = build_api(rt->ctx, rt, "sim"); 7411 JSValue result = JS_Call(rt->ctx, rt->sim_fn, JS_UNDEFINED, 1, &api); 7412 if (JS_IsException(result)) { 7413 JSValue exc = JS_GetException(rt->ctx); 7414 const char *str = JS_ToCString(rt->ctx, exc); 7415 JSValue stack = JS_GetPropertyStr(rt->ctx, exc, "stack"); 7416 const char *stack_str = JS_ToCString(rt->ctx, stack); 7417 fprintf(stderr, "[js] sim() error: %s\n%s\n", str, stack_str ? stack_str : ""); 7418 js_record_crash(rt, "sim", str); 7419 if (stack_str) JS_FreeCString(rt->ctx, stack_str); 7420 JS_FreeValue(rt->ctx, stack); 7421 JS_FreeCString(rt->ctx, str); 7422 JS_FreeValue(rt->ctx, exc); 7423 } 7424 JS_FreeValue(rt->ctx, result); 7425 JS_FreeValue(rt->ctx, api); 7426 7427 // FPS camera sync — after the wrapped piece sim() runs (which 7428 // invoked doll.sim() via the shim), copy doll.cam.{x,y,z,rotX/Y/Z} 7429 // back into rt->camera3d so the next frame's graph3d rendering 7430 // uses the doll's camera. Only syncs when the doll is present. 7431 if (have_doll) { 7432 JSValue global = JS_GetGlobalObject(rt->ctx); 7433 JSValue doll = JS_GetPropertyStr(rt->ctx, global, "__fpsDoll"); 7434 if (!JS_IsUndefined(doll) && !JS_IsNull(doll)) { 7435 JSValue cam = JS_GetPropertyStr(rt->ctx, doll, "cam"); 7436 if (!JS_IsUndefined(cam) && !JS_IsNull(cam)) { 7437 double x, y, z, rx, ry, rz; 7438 JSValue v; 7439 v = JS_GetPropertyStr(rt->ctx, cam, "x"); 7440 if (JS_ToFloat64(rt->ctx, &x, v) == 0) rt->camera3d.x = (float)x; 7441 JS_FreeValue(rt->ctx, v); 7442 v = JS_GetPropertyStr(rt->ctx, cam, "y"); 7443 if (JS_ToFloat64(rt->ctx, &y, v) == 0) rt->camera3d.y = (float)y; 7444 JS_FreeValue(rt->ctx, v); 7445 v = JS_GetPropertyStr(rt->ctx, cam, "z"); 7446 if (JS_ToFloat64(rt->ctx, &z, v) == 0) rt->camera3d.z = (float)z; 7447 JS_FreeValue(rt->ctx, v); 7448 v = JS_GetPropertyStr(rt->ctx, cam, "rotX"); 7449 if (JS_ToFloat64(rt->ctx, &rx, v) == 0) rt->camera3d.rotX = (float)rx; 7450 JS_FreeValue(rt->ctx, v); 7451 v = JS_GetPropertyStr(rt->ctx, cam, "rotY"); 7452 if (JS_ToFloat64(rt->ctx, &ry, v) == 0) rt->camera3d.rotY = (float)ry; 7453 JS_FreeValue(rt->ctx, v); 7454 v = JS_GetPropertyStr(rt->ctx, cam, "rotZ"); 7455 if (JS_ToFloat64(rt->ctx, &rz, v) == 0) rt->camera3d.rotZ = (float)rz; 7456 JS_FreeValue(rt->ctx, v); 7457 } 7458 JS_FreeValue(rt->ctx, cam); 7459 } 7460 JS_FreeValue(rt->ctx, doll); 7461 JS_FreeValue(rt->ctx, global); 7462 } 7463 7464 // Execute pending jobs (promises) 7465 JSContext *pctx; 7466 while (JS_ExecutePendingJob(rt->rt, &pctx) > 0) {} 7467} 7468 7469void js_call_beat(ACRuntime *rt) { 7470 if (!JS_IsFunction(rt->ctx, rt->beat_fn)) return; 7471 current_rt = rt; 7472 JSValue api = build_api(rt->ctx, rt, "beat"); 7473 JSValue result = JS_Call(rt->ctx, rt->beat_fn, JS_UNDEFINED, 1, &api); 7474 if (JS_IsException(result)) { 7475 JSValue exc = JS_GetException(rt->ctx); 7476 const char *str = JS_ToCString(rt->ctx, exc); 7477 fprintf(stderr, "[js] beat() error: %s\n", str); 7478 JS_FreeCString(rt->ctx, str); 7479 JS_FreeValue(rt->ctx, exc); 7480 } 7481 JS_FreeValue(rt->ctx, result); 7482 JS_FreeValue(rt->ctx, api); 7483} 7484 7485void js_call_leave(ACRuntime *rt) { 7486 if (!JS_IsFunction(rt->ctx, rt->leave_fn)) return; 7487 current_rt = rt; 7488 JSValue result = JS_Call(rt->ctx, rt->leave_fn, JS_UNDEFINED, 0, NULL); 7489 JS_FreeValue(rt->ctx, result); 7490} 7491 7492void js_destroy(ACRuntime *rt) { 7493 if (!rt) return; 7494 JS_FreeValue(rt->ctx, rt->boot_fn); 7495 JS_FreeValue(rt->ctx, rt->paint_fn); 7496 JS_FreeValue(rt->ctx, rt->act_fn); 7497 JS_FreeValue(rt->ctx, rt->sim_fn); 7498 JS_FreeValue(rt->ctx, rt->leave_fn); 7499 JS_FreeValue(rt->ctx, rt->beat_fn); 7500 ws_destroy(rt->ws); 7501 udp_destroy(rt->udp); 7502 depth_destroy(rt->depth_buf); 7503 JS_FreeContext(rt->ctx); 7504 JS_FreeRuntime(rt->rt); 7505 free(rt); 7506}