@typecad/framework-zephyr 1.0.0-alpha.18 → 1.0.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/audit.d.ts +111 -0
  2. package/dist/audit.js +416 -0
  3. package/dist/boardgen.js +63 -5
  4. package/dist/chips/resolve.js +20 -0
  5. package/dist/chips/types.d.ts +21 -0
  6. package/dist/display/bindings.d.ts +55 -0
  7. package/dist/display/bindings.js +316 -0
  8. package/dist/display/gfx.d.ts +2 -3
  9. package/dist/display/gfx.js +166 -154
  10. package/dist/display/index.js +20 -1
  11. package/dist/display/mipi-dbi-host.d.ts +9 -0
  12. package/dist/display/mipi-dbi-host.js +174 -0
  13. package/dist/display/profiles.d.ts +109 -4
  14. package/dist/display/profiles.js +270 -7
  15. package/dist/display/touch-adapter.js +118 -48
  16. package/dist/display/ui-adapter-eink.d.ts +2 -0
  17. package/dist/display/ui-adapter-eink.js +4 -0
  18. package/dist/display/ui-adapter-gray.d.ts +8 -0
  19. package/dist/display/ui-adapter-gray.js +170 -0
  20. package/dist/display/ui-adapter-mono.d.ts +13 -0
  21. package/dist/display/ui-adapter-mono.js +230 -0
  22. package/dist/display/ui-adapter-native.d.ts +10 -0
  23. package/dist/display/ui-adapter-native.js +295 -0
  24. package/dist/display/ui-adapter-shared.d.ts +11 -0
  25. package/dist/display/ui-adapter-shared.js +122 -0
  26. package/dist/display/ui-adapter.js +53 -101
  27. package/dist/dt-config/kconfig.d.ts +62 -1
  28. package/dist/dt-config/kconfig.js +123 -26
  29. package/dist/dt-config/overlay.d.ts +13 -0
  30. package/dist/dt-config/overlay.js +431 -16
  31. package/dist/framework.manifest.d.ts +9 -3
  32. package/dist/framework.manifest.js +123 -10
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +9 -1
  35. package/dist/licenses.js +5 -84
  36. package/dist/lowering/can.d.ts +25 -0
  37. package/dist/lowering/can.js +97 -0
  38. package/dist/lowering/clock.d.ts +17 -0
  39. package/dist/lowering/clock.js +58 -0
  40. package/dist/lowering/hid.d.ts +27 -0
  41. package/dist/lowering/hid.js +244 -0
  42. package/dist/lowering/i2c.d.ts +8 -0
  43. package/dist/lowering/i2c.js +140 -0
  44. package/dist/lowering/i2s.d.ts +27 -0
  45. package/dist/lowering/i2s.js +98 -0
  46. package/dist/lowering/index.d.ts +9 -1
  47. package/dist/lowering/index.js +25 -1
  48. package/dist/lowering/matrix.d.ts +15 -0
  49. package/dist/lowering/matrix.js +63 -0
  50. package/dist/lowering/power.d.ts +10 -0
  51. package/dist/lowering/power.js +35 -0
  52. package/dist/lowering/pwm.js +25 -0
  53. package/dist/lowering/sensor.d.ts +2 -2
  54. package/dist/lowering/sensor.js +8 -4
  55. package/dist/lowering/strip.d.ts +16 -0
  56. package/dist/lowering/strip.js +70 -0
  57. package/dist/lowering/thread.js +5 -1
  58. package/dist/lowering/trace.d.ts +44 -0
  59. package/dist/lowering/trace.js +239 -0
  60. package/dist/lowering/uart.js +6 -1
  61. package/dist/lowering/usb.d.ts +3 -1
  62. package/dist/lowering/usb.js +6 -3
  63. package/dist/sbom.d.ts +181 -0
  64. package/dist/sbom.js +901 -0
  65. package/dist/strategy.d.ts +60 -2
  66. package/dist/strategy.js +485 -25
  67. package/dist/toolchain/index.d.ts +12 -1
  68. package/dist/toolchain/index.js +115 -27
  69. package/dist/toolchain/scaffold.d.ts +4 -1
  70. package/dist/toolchain/scaffold.js +67 -3
  71. package/dist/west-inventory.d.ts +25 -0
  72. package/dist/west-inventory.js +97 -0
  73. package/package.json +6 -6
  74. package/src/audit.ts +529 -0
  75. package/src/boardgen.ts +54 -5
  76. package/src/chips/resolve.ts +21 -0
  77. package/src/chips/types.ts +576 -567
  78. package/src/display/bindings.ts +347 -0
  79. package/src/display/gfx.ts +318 -306
  80. package/src/display/index.ts +87 -70
  81. package/src/display/mipi-dbi-host.ts +183 -0
  82. package/src/display/profiles.ts +458 -139
  83. package/src/display/touch-adapter.ts +274 -204
  84. package/src/display/ui-adapter-eink.ts +13 -0
  85. package/src/display/ui-adapter-gray.ts +178 -0
  86. package/src/display/ui-adapter-mono.ts +238 -0
  87. package/src/display/ui-adapter-native.ts +304 -0
  88. package/src/display/ui-adapter-shared.ts +125 -0
  89. package/src/display/ui-adapter.ts +51 -100
  90. package/src/dt-config/kconfig.ts +647 -511
  91. package/src/dt-config/overlay.ts +433 -16
  92. package/src/framework.manifest.ts +131 -10
  93. package/src/index.ts +11 -1
  94. package/src/licenses.ts +5 -84
  95. package/src/lowering/can.ts +140 -0
  96. package/src/lowering/clock.ts +91 -0
  97. package/src/lowering/hid.ts +261 -0
  98. package/src/lowering/i2c.ts +146 -0
  99. package/src/lowering/i2s.ts +143 -0
  100. package/src/lowering/index.ts +18 -1
  101. package/src/lowering/matrix.ts +70 -0
  102. package/src/lowering/power.ts +41 -0
  103. package/src/lowering/pwm.ts +192 -167
  104. package/src/lowering/sensor.ts +159 -155
  105. package/src/lowering/strip.ts +81 -0
  106. package/src/lowering/thread.ts +5 -1
  107. package/src/lowering/trace.ts +270 -0
  108. package/src/lowering/uart.ts +6 -1
  109. package/src/lowering/usb.ts +224 -221
  110. package/src/sbom.ts +1117 -0
  111. package/src/strategy.ts +440 -25
  112. package/src/toolchain/index.ts +117 -28
  113. package/src/toolchain/scaffold.ts +62 -3
  114. package/src/west-inventory.ts +102 -0
package/dist/strategy.js CHANGED
@@ -168,6 +168,66 @@ function collectPwmOverrideSpecs(program) {
168
168
  * -Wunused-variable (Zephyr builds with warnings-as-errors), the same
169
169
  * hygiene collectUsedPins enforces for per-channel ADC/PWM state.
170
170
  */
171
+ /**
172
+ * Strips the program drives, keyed by SPI controller index → the
173
+ * construction chain length (the buffer size). Collected from strip.* ops.
174
+ */
175
+ /** Which HID protocol the program drives — 'mixed' when verbs from BOTH
176
+ * classes appear (one interface per program is the v1 ceiling; the caller
177
+ * turns mixed into a #error so the build names it clearly). */
178
+ function collectHidProtocol(program) {
179
+ if (!program)
180
+ return undefined;
181
+ let kb = false;
182
+ let mouse = false;
183
+ const visit = (node) => {
184
+ if (!node || typeof node !== 'object')
185
+ return;
186
+ const n = node;
187
+ const op = n.operation;
188
+ if (op && typeof op === 'object') {
189
+ const name = op.operation;
190
+ if (typeof name === 'string') {
191
+ if (name.startsWith('hid.kb_'))
192
+ kb = true;
193
+ if (name.startsWith('hid.mouse_'))
194
+ mouse = true;
195
+ }
196
+ }
197
+ for (const v of Object.values(n))
198
+ if (v && typeof v === 'object')
199
+ visit(v);
200
+ };
201
+ visit(program);
202
+ if (kb && mouse)
203
+ return 'mixed';
204
+ return kb ? 'keyboard' : mouse ? 'mouse' : undefined;
205
+ }
206
+ function collectStrips(program) {
207
+ if (!program)
208
+ return undefined;
209
+ const strips = new Map();
210
+ const visit = (node) => {
211
+ if (!node || typeof node !== 'object')
212
+ return;
213
+ const n = node;
214
+ const op = n.operation;
215
+ if (op && typeof op === 'object') {
216
+ const o = op;
217
+ if (o.operation === 'strip.set_pixel' || o.operation === 'strip.fill' || o.operation === 'strip.show') {
218
+ const m = String(o.bus ?? '').match(/(\d+)$/);
219
+ const idx = m ? parseInt(m[1], 10) : 0;
220
+ const count = Number(o.count ?? 1) || 1;
221
+ strips.set(idx, Math.max(strips.get(idx) ?? 0, count));
222
+ }
223
+ }
224
+ for (const v of Object.values(n))
225
+ if (v && typeof v === 'object')
226
+ visit(v);
227
+ };
228
+ visit(program);
229
+ return strips;
230
+ }
171
231
  function collectUsedBusIndices(program) {
172
232
  if (!program)
173
233
  return undefined;
@@ -233,9 +293,13 @@ export function collectSensors(program) {
233
293
  const spiHz = o.spiHz ?? 0;
234
294
  const spiMode = o.spiMode ?? 0;
235
295
  const alertPin = o.alertPin ?? -1;
296
+ const resolution = o.resolution ?? 12;
236
297
  const key = `${part}|${bus}|${port}|${busKind}`;
237
- if (!sensors.has(key))
238
- sensors.set(key, { part, bus, port, busKind, spiHz, spiMode, alertPin });
298
+ const existing = sensors.get(key);
299
+ if (!existing)
300
+ sensors.set(key, { part, bus, port, busKind, spiHz, spiMode, alertPin, resolution });
301
+ else if (o.resolution !== undefined)
302
+ existing.resolution = resolution;
239
303
  }
240
304
  }
241
305
  for (const v of Object.values(n)) {
@@ -251,6 +315,173 @@ export function collectSensors(program) {
251
315
  visitResolvedHalOps(program, visit);
252
316
  return sensors;
253
317
  }
318
+ /**
319
+ * The I2S construction facts the program's ops carry (the shim's slab and
320
+ * init helpers size from these) — first-seen wins, keyed by instance.
321
+ */
322
+ export function collectI2s(program) {
323
+ if (!program)
324
+ return undefined;
325
+ const out = new Map();
326
+ const visit = (node) => {
327
+ if (!node || typeof node !== 'object')
328
+ return;
329
+ const n = node;
330
+ const op = n.operation;
331
+ if (op && typeof op === 'object') {
332
+ const o = op;
333
+ const name = o.operation;
334
+ if (name === 'i2s.write' || name === 'i2s.read') {
335
+ // Raw instance (no clamp): the strategy loop folds usage through
336
+ // lowerI2s's clamp, so the facts must stay keyed by what the ops
337
+ // actually carry.
338
+ const inst = Number(o.instance ?? 0);
339
+ if (!out.has(inst)) {
340
+ out.set(inst, {
341
+ hz: Number(o.hz ?? 16000),
342
+ channels: Number(o.channels ?? 2),
343
+ bits: Number(o.bits ?? 16),
344
+ blockFrames: Number(o.blockFrames ?? 64),
345
+ });
346
+ }
347
+ }
348
+ }
349
+ for (const v of Object.values(n)) {
350
+ if (Array.isArray(v)) {
351
+ for (const item of v)
352
+ visit(item);
353
+ }
354
+ else if (v && typeof v === 'object')
355
+ visit(v);
356
+ }
357
+ };
358
+ visit(program);
359
+ visitResolvedHalOps(program, visit);
360
+ return out;
361
+ }
362
+ /**
363
+ * Which can / i2s / clock verbs the program's ops actually use — the init
364
+ * blocks emit only the state those verbs reference (Zephyr's -Werror turns
365
+ * an unreferenced static function/variable into a build failure: a
366
+ * send-only CAN program must not carry the rx trampoline, a now()-only
367
+ * Clock program must not carry the set() conversion helper). Returns
368
+ * undefined when there is no program to scan (probe flows) — callers then
369
+ * fall back to emitting every controller with the full state set.
370
+ */
371
+ export function collectVerbUsage(program) {
372
+ if (!program)
373
+ return undefined;
374
+ const out = {
375
+ can: new Map(),
376
+ i2s: new Map(),
377
+ clock: { set: false, now: false },
378
+ };
379
+ const canFlags = (inst) => {
380
+ let f = out.can.get(inst);
381
+ if (!f) {
382
+ f = { begin: false, send: false, onReceive: false };
383
+ out.can.set(inst, f);
384
+ }
385
+ return f;
386
+ };
387
+ const i2sFlags = (inst) => {
388
+ let f = out.i2s.get(inst);
389
+ if (!f) {
390
+ f = { write: false, read: false, readAt: false };
391
+ out.i2s.set(inst, f);
392
+ }
393
+ return f;
394
+ };
395
+ const visit = (node) => {
396
+ if (!node || typeof node !== 'object')
397
+ return;
398
+ const n = node;
399
+ const op = n.operation;
400
+ if (op && typeof op === 'object') {
401
+ const o = op;
402
+ const name = o.operation;
403
+ const inst = Number(o.instance ?? 0);
404
+ switch (name) {
405
+ case 'can.begin':
406
+ canFlags(inst).begin = true;
407
+ break;
408
+ case 'can.send':
409
+ canFlags(inst).send = true;
410
+ break;
411
+ case 'can.on_receive':
412
+ canFlags(inst).onReceive = true;
413
+ break;
414
+ case 'i2s.write':
415
+ i2sFlags(inst).write = true;
416
+ break;
417
+ case 'i2s.read':
418
+ i2sFlags(inst).read = true;
419
+ break;
420
+ case 'i2s.read_at':
421
+ i2sFlags(inst).readAt = true;
422
+ break;
423
+ case 'clock.set':
424
+ out.clock.set = true;
425
+ break;
426
+ case 'clock.now':
427
+ out.clock.now = true;
428
+ break;
429
+ default: break;
430
+ }
431
+ }
432
+ for (const v of Object.values(n)) {
433
+ if (Array.isArray(v)) {
434
+ for (const item of v)
435
+ visit(item);
436
+ }
437
+ else if (v && typeof v === 'object')
438
+ visit(v);
439
+ }
440
+ };
441
+ visit(program);
442
+ visitResolvedHalOps(program, visit);
443
+ return out;
444
+ }
445
+ /**
446
+ * Collect the distinct I2C responders the program's ops reference
447
+ * (i2c.resp_*), keyed `${bus}|${address}` — one target-mode address per
448
+ * constructed responder. Mirrors collectSpiTargets: the construction facts
449
+ * (rx/tx sizes) ride every op, so the first seen wins and the state block
450
+ * cannot drift from the call sites.
451
+ */
452
+ export function collectI2cResponders(program) {
453
+ if (!program)
454
+ return undefined;
455
+ const responders = new Map();
456
+ const visit = (node) => {
457
+ if (!node || typeof node !== 'object')
458
+ return;
459
+ const n = node;
460
+ const op = n.operation;
461
+ if (op && typeof op === 'object') {
462
+ const o = op;
463
+ if (String(o.operation).startsWith('i2c.resp_')) {
464
+ const bus = String(o.bus ?? '');
465
+ const address = Number(o.address ?? 0);
466
+ const key = `${bus}|${address}`;
467
+ if (!responders.has(key)) {
468
+ responders.set(key, { bus, address, rx: Number(o.rx ?? 64), tx: Number(o.tx ?? 32) });
469
+ }
470
+ }
471
+ }
472
+ for (const v of Object.values(n)) {
473
+ if (Array.isArray(v)) {
474
+ for (const item of v)
475
+ visit(item);
476
+ }
477
+ else if (v && typeof v === 'object')
478
+ visit(v);
479
+ }
480
+ };
481
+ visit(program);
482
+ visitResolvedHalOps(program, visit);
483
+ return responders;
484
+ }
254
485
  /**
255
486
  * Collect the distinct thin SPI targets the program's ops reference
256
487
  * (spi.transceive / spi.dev_write), keyed `${bus}|${cs}`. Mirrors
@@ -371,10 +602,18 @@ import { pwmInitLines } from './lowering/pwm.js';
371
602
  import { dacInitLines } from './lowering/dac.js';
372
603
  import { fsInitLines } from './lowering/fs.js';
373
604
  import { hwtimerInitLines } from './lowering/hwtimer.js';
374
- import { i2cInitLines } from './lowering/i2c.js';
605
+ import { i2cInitLines, i2cResponderStateLines } from './lowering/i2c.js';
606
+ import { parseControllerIndex } from './lowering/util.js';
607
+ import { stripInitLines } from './lowering/strip.js';
608
+ import { hidInitLines } from './lowering/hid.js';
609
+ import { matrixInitLines } from './lowering/matrix.js';
610
+ import { clockInitLines } from './lowering/clock.js';
611
+ import { canInitLines } from './lowering/can.js';
612
+ import { i2sInitLines } from './lowering/i2s.js';
375
613
  import { sensorStateLines } from './lowering/sensor.js';
376
614
  import { spiTargetStateLines } from './lowering/spi.js';
377
615
  import { threadStateLines } from './lowering/thread.js';
616
+ import { traceHeartbeatLines, uiFrameTraceLines, clampTraceIntervalMs } from './lowering/trace.js';
378
617
  import { uartRingStateLines } from './lowering/uart.js';
379
618
  import { spiInitLines } from './lowering/spi.js';
380
619
  import { uartInitLines } from './lowering/uart.js';
@@ -412,7 +651,8 @@ import { randomInitLines } from './lowering/random.js';
412
651
  import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
413
652
  import { resolveZephyrDisplayOp, newDisplayState } from './display/index.js';
414
653
  import { buildDisplayRuntime } from './display/gfx.js';
415
- import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES } from './display/profiles.js';
654
+ import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES, isDtCompatible, isMonoDisplay, isGrayDisplay, isEinkDisplay } from './display/profiles.js';
655
+ import { listBoundDisplayCompatibles } from './display/bindings.js';
416
656
  import { zephyrDisplayAdapterGenerator } from './display/ui-adapter.js';
417
657
  import { zephyrTouchAdapter } from './display/touch-adapter.js';
418
658
  /** debugMode() memo — the boardgen facts lookup is not free and the method is
@@ -500,6 +740,28 @@ export class ZephyrStrategy {
500
740
  inc.push('<zephyr/drivers/sensor.h>');
501
741
  if (uses('usesSPI'))
502
742
  inc.push('<zephyr/drivers/spi.h>');
743
+ if (uses('usesStrip'))
744
+ inc.push('<zephyr/drivers/led_strip.h>');
745
+ if (uses('usesHid')) {
746
+ inc.push('<zephyr/usb/class/hid.h>');
747
+ inc.push('<zephyr/usb/class/usbd_hid.h>');
748
+ inc.push('<zephyr/usb/usbd.h>');
749
+ inc.push('<zephyr/drivers/usb/usb_buf.h>');
750
+ }
751
+ if (uses('usesMatrix'))
752
+ inc.push('<zephyr/input/input.h>');
753
+ if (uses('usesPower'))
754
+ inc.push('<zephyr/sys/poweroff.h>');
755
+ // esp_sleep.h exists only on the espressif HAL — gate on the harvested
756
+ // RTC wake-timer fact (the includes' uses() reads op flags, not src).
757
+ if (uses('usesPower') && chip.powerWakeTimer)
758
+ inc.push('<esp_sleep.h>');
759
+ if (uses('usesClock'))
760
+ inc.push('<zephyr/drivers/rtc.h>');
761
+ if (uses('usesCan'))
762
+ inc.push('<zephyr/drivers/can.h>');
763
+ if (uses('usesI2s'))
764
+ inc.push('<zephyr/drivers/i2s.h>');
503
765
  if (uses('usesUart'))
504
766
  inc.push('<zephyr/drivers/uart.h>');
505
767
  // USB CDC serial: the class instance is a UART device (uart.h); the
@@ -800,21 +1062,29 @@ export class ZephyrStrategy {
800
1062
  'static inline char* __tc_fmt_num_buf(double v, char* out, size_t cap) {',
801
1063
  ' if (v != v) { snprintf(out, cap, "nan"); return out; }',
802
1064
  ' double a = v < 0 ? -v : v;',
803
- ' long long ip = (long long)a;',
804
- ' long long fr = (long long)((a - (double)ip) * 1000000.0 + 0.5);',
1065
+ ' long long ip = static_cast<long long>(a);',
1066
+ ' long long fr = static_cast<long long>((a - static_cast<double>(ip)) * 1000000.0 + 0.5);',
805
1067
  ' if (fr >= 1000000LL) { ip += 1LL; fr = 0LL; }',
806
1068
  ' int used = 0;',
807
1069
  ' if (v < 0 && (ip != 0LL || fr != 0LL)) { out[used++] = \'-\'; out[used] = \'\\0\'; }',
808
- ' if (fr == 0LL) { snprintf(out + used, cap - (size_t)used, "%lld", ip); return out; }',
1070
+ ' if (fr == 0LL) { snprintf(out + used, cap - static_cast<size_t>(used), "%lld", ip); return out; }',
809
1071
  ' char fbuf[8];',
810
1072
  ' int len = snprintf(fbuf, sizeof(fbuf), "%06lld", fr);',
811
1073
  ' while (len > 0 && fbuf[len - 1] == \'0\') { fbuf[--len] = \'\\0\'; }',
812
- ' snprintf(out + used, cap - (size_t)used, "%lld.%s", ip, fbuf);',
1074
+ ' snprintf(out + used, cap - static_cast<size_t>(used), "%lld.%s", ip, fbuf);',
813
1075
  ' return out;',
814
1076
  '}',
815
1077
  'static void __tc_fmt_num(double v) { char __b[32]; printf("%s", __tc_fmt_num_buf(v, __b, sizeof(__b))); }',
816
1078
  ].join('\n'), 'inline void __tc_print(double v) { __tc_fmt_num(v); }', 'inline void __tc_println(const char* s) { printf("%s\\n", s); }', 'inline void __tc_println(double v) { __tc_fmt_num(v); printf("\\n"); }');
817
1079
  }
1080
+ // Trace user-event helpers (Trace.mark / Trace.event → trace.mark /
1081
+ // trace.event ops): one [TR:EV:<uptime_ms>:<name>[:<value>] console line
1082
+ // per call, captured host-side as timeline events. Emitted
1083
+ // UNCONDITIONALLY like __tc_print — a Trace.mark() call site must never
1084
+ // fail to link because zephyr.trace was disabled; with no capture
1085
+ // listening the lines are ordinary console output. Value formatting
1086
+ // reuses __tc_fmt_num_buf (integer-only, the picolibc %g trap).
1087
+ guardBody.push('inline void __tc_trace_mark(const char* name) {', ' printf("[TR:EV:%u:%s\\n", static_cast<unsigned int>(k_uptime_get_32()), name);', '}', 'inline void __tc_trace_event(const char* name, double v) {', ' char __b[32];', ' printf("[TR:EV:%u:%s:%s\\n", static_cast<unsigned int>(k_uptime_get_32()), name, __tc_fmt_num_buf(v, __b, sizeof(__b)));', '}');
818
1088
  // Serial-port write helper: writes a scalar to a UART/CDC device a byte at
819
1089
  // a time. Overloaded on const char* (strings, snprintf buffers) and double
820
1090
  // (numbers/booleans) so a single __tc_dev_put(dev, value) call site formats
@@ -830,12 +1100,29 @@ export class ZephyrStrategy {
830
1100
  // declared controller's __tc_<bus>N_dev trips -Wunused-variable under
831
1101
  // Zephyr's -Werror (collectUsedBusIndices; probes with no program emit all).
832
1102
  const usedBuses = collectUsedBusIndices(program);
1103
+ // Verb-level usage for the can.*/i2s.*/clock.* init blocks (see
1104
+ // collectVerbUsage) — undefined when there is no program (probe flows
1105
+ // emit the full state set).
1106
+ const verbUsage = collectVerbUsage(program);
833
1107
  if (uses('usesI2C') && chip.i2c) {
834
1108
  for (let i = 0; i < chip.i2c.controllers.length; i++) {
835
1109
  if (usedBuses && !usedBuses.i2c.has(i))
836
1110
  continue;
837
1111
  guardBody.push(...i2cInitLines(chip, i));
838
1112
  }
1113
+ // I2C responders — one target-mode state block per constructed
1114
+ // responder (bus|address), against the controller handles above
1115
+ // (collectI2cResponders; the ops mark the bus used, so the handle
1116
+ // the registration references is always present).
1117
+ const responders = collectI2cResponders(program);
1118
+ if (responders) {
1119
+ for (const r of responders.values()) {
1120
+ const idx = parseControllerIndex(r.bus);
1121
+ if (idx >= chip.i2c.controllers.length)
1122
+ continue;
1123
+ guardBody.push(...i2cResponderStateLines(chip, idx, r.address, r.rx, r.tx));
1124
+ }
1125
+ }
839
1126
  }
840
1127
  if (uses('usesSPI') && chip.spi) {
841
1128
  for (let i = 0; i < chip.spi.controllers.length; i++) {
@@ -844,6 +1131,79 @@ export class ZephyrStrategy {
844
1131
  guardBody.push(...spiInitLines(chip, i));
845
1132
  }
846
1133
  }
1134
+ // LED strips — one buffer + device handle per driven SPI controller
1135
+ // (chain length from the construction facts).
1136
+ {
1137
+ const strips = collectStrips(program);
1138
+ if (uses('usesStrip') && strips) {
1139
+ for (const [idx, count] of strips) {
1140
+ guardBody.push(...stripInitLines(chip, idx, count));
1141
+ }
1142
+ }
1143
+ }
1144
+ // Key matrix — the input-subsystem trampoline (the DT node comes from
1145
+ // the overlay generator, driven by the toolchain's matrix scan).
1146
+ if (uses('usesMatrix')) {
1147
+ guardBody.push(...matrixInitLines());
1148
+ }
1149
+ // Wall-clock shim — the epoch<->civil helpers + the rtc alias handle
1150
+ // (the DT node comes from the overlay generator's shim synthesis).
1151
+ if (uses('usesClock')) {
1152
+ const clockUsage = verbUsage?.clock ?? { set: true, now: true };
1153
+ guardBody.push(...clockInitLines(clockUsage));
1154
+ }
1155
+ // CAN — per-controller device handle + the rx trampoline (the DT node
1156
+ // is the board's own can@ node, enabled by the overlay). Only the
1157
+ // controllers the program addresses, and only the state its verbs
1158
+ // reference (collectVerbUsage; probes with no program emit all). Usage
1159
+ // folds through the same clamp lowerCan applies to out-of-range
1160
+ // instances, so the state block for the clamped controller emits.
1161
+ if (uses('usesCan') && chip.can) {
1162
+ const nCtrl = chip.can.controllers.length;
1163
+ const canUsage = verbUsage?.can;
1164
+ const folded = Array.from({ length: nCtrl }, () => ({ begin: false, send: false, onReceive: false }));
1165
+ if (canUsage) {
1166
+ for (const [inst, flags] of canUsage) {
1167
+ const f = folded[Math.max(0, Math.min(inst, nCtrl - 1))];
1168
+ f.begin = f.begin || flags.begin;
1169
+ f.send = f.send || flags.send;
1170
+ f.onReceive = f.onReceive || flags.onReceive;
1171
+ }
1172
+ }
1173
+ for (let i = 0; i < nCtrl; i++) {
1174
+ const f = folded[i];
1175
+ if (canUsage && !f.begin && !f.send && !f.onReceive)
1176
+ continue;
1177
+ guardBody.push(...canInitLines(chip, i, canUsage ? f : { begin: true, send: true, onReceive: true }));
1178
+ }
1179
+ }
1180
+ // I2S — per-controller slab + block buffers + lazy direction inits,
1181
+ // sized from the construction facts the ops carry. Same used-instance
1182
+ // and clamp-fold discipline as CAN.
1183
+ if (uses('usesI2s') && chip.i2s) {
1184
+ const i2sFacts = collectI2s(program);
1185
+ const nCtrl = chip.i2s.controllers.length;
1186
+ const i2sUsage = verbUsage?.i2s;
1187
+ const folded = Array.from({ length: nCtrl }, () => ({ write: false, read: false, readAt: false }));
1188
+ if (i2sUsage) {
1189
+ for (const [inst, flags] of i2sUsage) {
1190
+ const f = folded[Math.max(0, Math.min(inst, nCtrl - 1))];
1191
+ f.write = f.write || flags.write;
1192
+ f.read = f.read || flags.read;
1193
+ f.readAt = f.readAt || flags.readAt;
1194
+ }
1195
+ }
1196
+ for (let i = 0; i < nCtrl; i++) {
1197
+ const f = folded[i];
1198
+ if (i2sUsage && !f.write && !f.read && !f.readAt)
1199
+ continue;
1200
+ const facts = i2sFacts?.get(i);
1201
+ const channels = facts?.channels ?? 2;
1202
+ const bits = facts?.bits ?? 16;
1203
+ const blockFrames = facts?.blockFrames ?? 64;
1204
+ guardBody.push(...i2sInitLines(chip, i, blockFrames * channels * (bits / 8), i2sUsage ? f : { write: true, read: true, readAt: true }));
1205
+ }
1206
+ }
847
1207
  if (uses('usesUart') && chip.uart) {
848
1208
  for (let i = 0; i < chip.uart.controllers.length; i++) {
849
1209
  if (usedBuses && !usedBuses.uart.has(i))
@@ -866,7 +1226,7 @@ export class ZephyrStrategy {
866
1226
  const sensors = collectSensors(program);
867
1227
  if (sensors) {
868
1228
  for (const s of sensors.values()) {
869
- guardBody.push(...sensorStateLines(s.part, s.bus, s.port, s.busKind, s.spiHz, s.spiMode, s.alertPin));
1229
+ guardBody.push(...sensorStateLines(s.part, s.bus, s.port, s.busKind, s.spiHz, s.spiMode, s.alertPin, s.resolution));
870
1230
  }
871
1231
  }
872
1232
  }
@@ -892,10 +1252,52 @@ export class ZephyrStrategy {
892
1252
  }
893
1253
  }
894
1254
  }
895
- if (uses('usesUsb') && chip.usb) {
896
- guardBody.push(...usbdDeviceLines(chip));
897
- for (let i = 0; i < chip.usb.cdcInstances; i++)
898
- guardBody.push(...usbInitLines(chip, i));
1255
+ // Trace heartbeat — the runtime-stats sampler behind `typecad-hal trace
1256
+ // capture`. Opt-in via zephyr.trace.enabled in typecad-hal.config.ts (the
1257
+ // config loader threads the zephyr record through platformContext). The
1258
+ // block compiles under CUTTLEFISH_ENTRY_TU, so split-file TUs carry no
1259
+ // second copy (a duplicate SYS_INIT would double every heartbeat).
1260
+ // UI-mounted programs additionally get the frame-stats block (always
1261
+ // defined, active only under CUTTLEFISH_TRACE_UI) so the ui_tick call
1262
+ // site's instrumentation never depends on the trace config.
1263
+ {
1264
+ const traceCfg = ctx
1265
+ ?.zephyr?.trace;
1266
+ const traceEnabled = traceCfg?.enabled === true;
1267
+ if (traceEnabled && entryHasUI()) {
1268
+ guardBody.push('#if defined(CUTTLEFISH_ENTRY_TU)', '#define CUTTLEFISH_TRACE_UI 1', '#endif');
1269
+ }
1270
+ if (entryHasUI()) {
1271
+ guardBody.push(...uiFrameTraceLines(traceCfg?.alarms?.frameMaxMs));
1272
+ }
1273
+ if (traceEnabled) {
1274
+ guardBody.push(...traceHeartbeatLines(clampTraceIntervalMs(traceCfg?.intervalMs), entryHasUI(), traceCfg?.alarms));
1275
+ }
1276
+ }
1277
+ // The usbd context is shared by CDC and HID; the CDC per-instance
1278
+ // state only emits when the program actually uses usb.* (an HID-only
1279
+ // program would otherwise carry an unused __tc_usb0_dev).
1280
+ if ((uses('usesUsb') || uses('usesHid')) && chip.usb) {
1281
+ guardBody.push(...usbdDeviceLines(chip, { hid: uses('usesHid') }));
1282
+ if (uses('usesUsb')) {
1283
+ for (let i = 0; i < chip.usb.cdcInstances; i++)
1284
+ guardBody.push(...usbInitLines(chip, i));
1285
+ }
1286
+ }
1287
+ // USB HID — one interface per program; the protocol comes from which
1288
+ // class's verbs the program used (keyboard vs mouse). Requires the USB
1289
+ // device context (the same one CDC rides — the register helper calls
1290
+ // __tc_usbd_start).
1291
+ if (uses('usesHid') && chip.usb) {
1292
+ const protocol = collectHidProtocol(program) ?? 'keyboard';
1293
+ if (protocol === 'mixed') {
1294
+ // One zephyr,hid-device interface per program (the v1 ceiling) —
1295
+ // name it at compile time instead of an undefined-symbol error.
1296
+ guardBody.push('#error "typecad-hal: one HID interface per program (v1) — Keyboard and Mouse cannot be mixed in one app"');
1297
+ }
1298
+ else {
1299
+ guardBody.push(...hidInitLines(protocol));
1300
+ }
899
1301
  }
900
1302
  // PWM init also fires (with alias vars for the override pins) when a
901
1303
  // program drives ONLY inline-override channels on a chip with no pwm
@@ -1017,7 +1419,11 @@ export class ZephyrStrategy {
1017
1419
  // precede the wiring-ambient shims and the PinGroup factory there; the
1018
1420
  // __tc_gpio_read body below is what still needs emitting here.)
1019
1421
  if (this.needsGpioReadShim(program, ctx)) {
1020
- lines.push('inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), __tc_gpio_pin(static_cast<uint32_t>(pin))); }');
1422
+ lines.push('inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), __tc_gpio_pin(static_cast<uint32_t>(pin))); }',
1423
+ // Runtime input configuration for ui.watchPin / ui.press setup
1424
+ // (the strategy's setPinMode lowers here). Idempotent — setup calls
1425
+ // it once per watched pin before the poll loop reads it.
1426
+ 'inline void __tc_gpio_configure_input(uint32_t pin) { gpio_pin_configure(__tc_gpio_dev(pin), __tc_gpio_pin(pin), GPIO_INPUT | GPIO_PULL_UP); }');
1021
1427
  }
1022
1428
  // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
1023
1429
  // lowering, so they stay gated on it.
@@ -1771,6 +2177,17 @@ export class ZephyrStrategy {
1771
2177
  postIteration: 'k_msleep(1);',
1772
2178
  };
1773
2179
  }
2180
+ /**
2181
+ * Per-frame wall-time feed for the trace heartbeat's UI stats. The helper
2182
+ * is emitted unconditionally for UI programs (compiles to a no-op unless
2183
+ * zephyr.trace defined CUTTLEFISH_TRACE_UI), so this call is safe with
2184
+ * tracing on or off.
2185
+ */
2186
+ uiFrameTimingLines(deltaMsExpr) {
2187
+ if (!entryHasUI())
2188
+ return null;
2189
+ return [`__tc_trace_ui_frame(static_cast<uint32_t>(${deltaMsExpr}));`];
2190
+ }
1774
2191
  // ── Type aliases ────────────────────────────────────────────────────────
1775
2192
  shouldSkipTypeAlias() {
1776
2193
  return false;
@@ -2005,9 +2422,14 @@ struct __tc_StaticArray {
2005
2422
  writeDigitalPin(pin, val) {
2006
2423
  return `__tc_gpio_write(${pin}, ${val})`;
2007
2424
  }
2008
- setPinMode(_pin, _mode) {
2009
- // Zephyr configures pin direction via devicetree, not a runtime pinMode.
2010
- return `/* pin mode configured via devicetree */`;
2425
+ setPinMode(pin, _mode) {
2426
+ // Runtime input configuration: Zephyr has no pinMode — the GPIO API
2427
+ // configures direction/pull explicitly. The mode argument is ignored:
2428
+ // every watchPin/press consumer wants an input with pull-up (buttons are
2429
+ // active-low), and __tc_gpio_configure_input hardcodes exactly that.
2430
+ // The helper's definition rides the GPIO read shim (same gate — UI builds
2431
+ // with pin watchers always carry the read surface too).
2432
+ return `__tc_gpio_configure_input(static_cast<uint32_t>(${pin}));`;
2011
2433
  }
2012
2434
  delayMs(ms) {
2013
2435
  return `k_msleep(${ms})`;
@@ -2043,16 +2465,31 @@ struct __tc_StaticArray {
2043
2465
  return resolveZephyrDisplayOp(op, this._displayState);
2044
2466
  }
2045
2467
  supportedDisplayDrivers() {
2046
- return new Set(Object.keys(ZEPHYR_DISPLAY_PROFILES));
2468
+ // Registry profiles plus every panel compatible with a display binding in
2469
+ // the user's Zephyr tree — the drop-in path (display.driver = compatible)
2470
+ // validates at mount like any curated profile. No tree discoverable →
2471
+ // registry only.
2472
+ const drivers = new Set(Object.keys(ZEPHYR_DISPLAY_PROFILES));
2473
+ try {
2474
+ for (const compatible of listBoundDisplayCompatibles())
2475
+ drivers.add(compatible);
2476
+ }
2477
+ catch {
2478
+ // Tree unreadable — the registry set stands.
2479
+ }
2480
+ return drivers;
2047
2481
  }
2048
2482
  // ── Strategy-owned display/touch adapter seam ────────────────────────────
2049
2483
  // Zephyr owns its display + touch adapters: the UI display adapter bridges
2050
- // the in-tree CuttlefishGFX class to the panel (per-controller init + wire
2051
- // format, see src/display/ui-adapter.ts), and the touch adapters drive the
2052
- // FT6336U (I2C capacitive) and XPT2046 (SPI resistive) controllers via
2053
- // Zephyr's bus APIs (src/display/touch-adapter.ts). Both live in this
2054
- // package so cuttlefish carries no Zephyr/Wiring-specific display or touch
2055
- // knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
2484
+ // the in-tree CuttlefishGFX class to the panel via one of two transports
2485
+ // (per profile — 'direct-spi': per-controller init + wire format over
2486
+ // spi_write, see src/display/ui-adapter.ts; 'zephyr-display': Zephyr's
2487
+ // display API with in-tree panel drivers, see src/display/ui-adapter-native.ts),
2488
+ // and the touch adapters drive the FT6336U (I2C capacitive) and XPT2046
2489
+ // (SPI resistive) controllers via Zephyr's bus APIs (src/display/touch-adapter.ts).
2490
+ // Both live in this package so cuttlefish carries no Zephyr/Wiring-specific
2491
+ // display or touch knowledge. Mirrors ArduinoStrategy's provides*/resolve*
2492
+ // pattern.
2056
2493
  providesDisplayAdapter() { return true; }
2057
2494
  resolveDisplayAdapter(display) {
2058
2495
  const code = zephyrDisplayAdapterGenerator(display);
@@ -2071,7 +2508,30 @@ struct __tc_StaticArray {
2071
2508
  return new Map(Object.entries(BUILT_IN_PROFILES));
2072
2509
  }
2073
2510
  colorFormat() {
2074
- return 'rgb565';
2511
+ // The seeded display state is the truth after display.init (drop-in mono
2512
+ // panels synthesize 'mono'); the default profile keeps the pre-init
2513
+ // rgb565 answer byte-identical with history.
2514
+ return this._displayState.profile.colorFormat;
2515
+ }
2516
+ colorFormatForDriver(driver) {
2517
+ // Drop-in panel class from the compatible table (DATA): the 1bpp OLED
2518
+ // family lowers mono engine-side and the 16-gray family lowers gray8,
2519
+ // so colors flatten at build time instead of rgb565-lowering into an
2520
+ // adapter that cannot show them. Registry profiles carry their own
2521
+ // colorFormat; non-compatible driver ids keep the default.
2522
+ if (isDtCompatible(driver) && isMonoDisplay({ driver }))
2523
+ return 'mono';
2524
+ if (isDtCompatible(driver) && isEinkDisplay({ driver }))
2525
+ return 'mono';
2526
+ if (isDtCompatible(driver) && isGrayDisplay({ driver }))
2527
+ return 'gray8';
2528
+ return undefined;
2529
+ }
2530
+ displayClassForDriver(driver) {
2531
+ // The compatible table decides (DATA) — ssd16xx/uc81xx are e-ink.
2532
+ if (isDtCompatible(driver) && isEinkDisplay({ driver }))
2533
+ return 'eink';
2534
+ return undefined;
2075
2535
  }
2076
2536
  graphicsCapacity() {
2077
2537
  return {
@@ -1,5 +1,14 @@
1
1
  import type { ToolchainOptions, CompileResult, UploadResult } from '@typecad/cuttlefish/api/shared';
2
2
  import type { ZephyrChipDescriptor } from '../chips/types.js';
3
+ export declare function scanMatrix(src: string): {
4
+ rows: number[];
5
+ cols: number[];
6
+ } | undefined;
7
+ export declare function scanHid(src: string): 'keyboard' | 'mouse' | undefined;
8
+ export declare function scanStrips(src: string): Array<{
9
+ index: number;
10
+ count: number;
11
+ }>;
3
12
  /**
4
13
  * Distinct DT-bound sensors the emitted sources reference — the lowering's
5
14
  * state blocks name each one `__tc_sensor_<part>_i2c<N>_0x<addr>_dev`
@@ -11,10 +20,12 @@ export interface ScannedSensorPart {
11
20
  part: string;
12
21
  busIndex: number;
13
22
  port: number;
14
- busKind: 'i2c' | 'spi';
23
+ busKind: 'i2c' | 'spi' | 'w1';
15
24
  spiHz: number;
16
25
  spiMode: number;
17
26
  alertPin: number;
27
+ /** 1-Wire parts: the converter resolution in bits. */
28
+ resolution: number;
18
29
  }
19
30
  export interface ScannedSpiTarget {
20
31
  busIndex: number;