@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/src/strategy.ts CHANGED
@@ -187,6 +187,57 @@ function collectPwmOverrideSpecs(program: ProgramIR | undefined): { pin: number;
187
187
  * -Wunused-variable (Zephyr builds with warnings-as-errors), the same
188
188
  * hygiene collectUsedPins enforces for per-channel ADC/PWM state.
189
189
  */
190
+ /**
191
+ * Strips the program drives, keyed by SPI controller index → the
192
+ * construction chain length (the buffer size). Collected from strip.* ops.
193
+ */
194
+ /** Which HID protocol the program drives — 'mixed' when verbs from BOTH
195
+ * classes appear (one interface per program is the v1 ceiling; the caller
196
+ * turns mixed into a #error so the build names it clearly). */
197
+ function collectHidProtocol(program: ProgramIR | undefined): 'keyboard' | 'mouse' | 'mixed' | undefined {
198
+ if (!program) return undefined;
199
+ let kb = false;
200
+ let mouse = false;
201
+ const visit = (node: unknown): void => {
202
+ if (!node || typeof node !== 'object') return;
203
+ const n = node as Record<string, unknown>;
204
+ const op = n.operation;
205
+ if (op && typeof op === 'object') {
206
+ const name = (op as Record<string, unknown>).operation;
207
+ if (typeof name === 'string') {
208
+ if (name.startsWith('hid.kb_')) kb = true;
209
+ if (name.startsWith('hid.mouse_')) mouse = true;
210
+ }
211
+ }
212
+ for (const v of Object.values(n)) if (v && typeof v === 'object') visit(v);
213
+ };
214
+ visit(program);
215
+ if (kb && mouse) return 'mixed';
216
+ return kb ? 'keyboard' : mouse ? 'mouse' : undefined;
217
+ }
218
+
219
+ function collectStrips(program: ProgramIR | undefined): Map<number, number> | undefined {
220
+ if (!program) return undefined;
221
+ const strips = new Map<number, number>();
222
+ const visit = (node: unknown): void => {
223
+ if (!node || typeof node !== 'object') return;
224
+ const n = node as Record<string, unknown>;
225
+ const op = n.operation;
226
+ if (op && typeof op === 'object') {
227
+ const o = op as Record<string, unknown>;
228
+ if (o.operation === 'strip.set_pixel' || o.operation === 'strip.fill' || o.operation === 'strip.show') {
229
+ const m = String(o.bus ?? '').match(/(\d+)$/);
230
+ const idx = m ? parseInt(m[1]!, 10) : 0;
231
+ const count = Number(o.count ?? 1) || 1;
232
+ strips.set(idx, Math.max(strips.get(idx) ?? 0, count));
233
+ }
234
+ }
235
+ for (const v of Object.values(n)) if (v && typeof v === 'object') visit(v);
236
+ };
237
+ visit(program);
238
+ return strips;
239
+ }
240
+
190
241
  function collectUsedBusIndices(program: ProgramIR | undefined): { i2c: Set<number>; spi: Set<number>; uart: Set<number> } | undefined {
191
242
  if (!program) return undefined;
192
243
  const indices = { i2c: new Set<number>(), spi: new Set<number>(), uart: new Set<number>() };
@@ -228,9 +279,9 @@ function collectUsedBusIndices(program: ProgramIR | undefined): { i2c: Set<numbe
228
279
  * constructed sensors emit state, and only sensors whose ops were emitted
229
280
  * get a DT node.
230
281
  */
231
- export function collectSensors(program: ProgramIR | undefined): Map<string, { part: string; bus: string; port: number | string; busKind: string; spiHz: number | string; spiMode: number | string; alertPin: number | string }> | undefined {
282
+ export function collectSensors(program: ProgramIR | undefined): Map<string, { part: string; bus: string; port: number | string; busKind: string; spiHz: number | string; spiMode: number | string; alertPin: number | string; resolution: number | string }> | undefined {
232
283
  if (!program) return undefined;
233
- const sensors = new Map<string, { part: string; bus: string; port: number | string; busKind: string; spiHz: number | string; spiMode: number | string; alertPin: number | string }>();
284
+ const sensors = new Map<string, { part: string; bus: string; port: number | string; busKind: string; spiHz: number | string; spiMode: number | string; alertPin: number | string; resolution: number | string }>();
234
285
  const visit = (node: unknown): void => {
235
286
  if (!node || typeof node !== 'object') return;
236
287
  const n = node as Record<string, unknown>;
@@ -245,8 +296,11 @@ export function collectSensors(program: ProgramIR | undefined): Map<string, { pa
245
296
  const spiHz = (o.spiHz as number | string) ?? 0;
246
297
  const spiMode = (o.spiMode as number | string) ?? 0;
247
298
  const alertPin = (o.alertPin as number | string) ?? -1;
299
+ const resolution = (o.resolution as number | string) ?? 12;
248
300
  const key = `${part}|${bus}|${port}|${busKind}`;
249
- if (!sensors.has(key)) sensors.set(key, { part, bus, port, busKind, spiHz, spiMode, alertPin });
301
+ const existing = sensors.get(key);
302
+ if (!existing) sensors.set(key, { part, bus, port, busKind, spiHz, spiMode, alertPin, resolution });
303
+ else if (o.resolution !== undefined) existing.resolution = resolution;
250
304
  }
251
305
  }
252
306
  for (const v of Object.values(n)) {
@@ -259,6 +313,140 @@ export function collectSensors(program: ProgramIR | undefined): Map<string, { pa
259
313
  return sensors;
260
314
  }
261
315
 
316
+ /**
317
+ * The I2S construction facts the program's ops carry (the shim's slab and
318
+ * init helpers size from these) — first-seen wins, keyed by instance.
319
+ */
320
+ export function collectI2s(program: ProgramIR | undefined): Map<number, { hz: number; channels: number; bits: number; blockFrames: number }> | undefined {
321
+ if (!program) return undefined;
322
+ const out = new Map<number, { hz: number; channels: number; bits: number; blockFrames: number }>();
323
+ const visit = (node: unknown): void => {
324
+ if (!node || typeof node !== 'object') return;
325
+ const n = node as Record<string, unknown>;
326
+ const op = n.operation;
327
+ if (op && typeof op === 'object') {
328
+ const o = op as Record<string, unknown>;
329
+ const name = o.operation;
330
+ if (name === 'i2s.write' || name === 'i2s.read') {
331
+ // Raw instance (no clamp): the strategy loop folds usage through
332
+ // lowerI2s's clamp, so the facts must stay keyed by what the ops
333
+ // actually carry.
334
+ const inst = Number(o.instance ?? 0);
335
+ if (!out.has(inst)) {
336
+ out.set(inst, {
337
+ hz: Number(o.hz ?? 16000),
338
+ channels: Number(o.channels ?? 2),
339
+ bits: Number(o.bits ?? 16),
340
+ blockFrames: Number(o.blockFrames ?? 64),
341
+ });
342
+ }
343
+ }
344
+ }
345
+ for (const v of Object.values(n)) {
346
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
347
+ else if (v && typeof v === 'object') visit(v);
348
+ }
349
+ };
350
+ visit(program);
351
+ visitResolvedHalOps(program, visit);
352
+ return out;
353
+ }
354
+
355
+ /**
356
+ * Which can / i2s / clock verbs the program's ops actually use — the init
357
+ * blocks emit only the state those verbs reference (Zephyr's -Werror turns
358
+ * an unreferenced static function/variable into a build failure: a
359
+ * send-only CAN program must not carry the rx trampoline, a now()-only
360
+ * Clock program must not carry the set() conversion helper). Returns
361
+ * undefined when there is no program to scan (probe flows) — callers then
362
+ * fall back to emitting every controller with the full state set.
363
+ */
364
+ export function collectVerbUsage(program: ProgramIR | undefined): {
365
+ can: Map<number, { begin: boolean; send: boolean; onReceive: boolean }>;
366
+ i2s: Map<number, { write: boolean; read: boolean; readAt: boolean }>;
367
+ clock: { set: boolean; now: boolean };
368
+ } | undefined {
369
+ if (!program) return undefined;
370
+ const out = {
371
+ can: new Map<number, { begin: boolean; send: boolean; onReceive: boolean }>(),
372
+ i2s: new Map<number, { write: boolean; read: boolean; readAt: boolean }>(),
373
+ clock: { set: false, now: false },
374
+ };
375
+ const canFlags = (inst: number) => {
376
+ let f = out.can.get(inst);
377
+ if (!f) { f = { begin: false, send: false, onReceive: false }; out.can.set(inst, f); }
378
+ return f;
379
+ };
380
+ const i2sFlags = (inst: number) => {
381
+ let f = out.i2s.get(inst);
382
+ if (!f) { f = { write: false, read: false, readAt: false }; out.i2s.set(inst, f); }
383
+ return f;
384
+ };
385
+ const visit = (node: unknown): void => {
386
+ if (!node || typeof node !== 'object') return;
387
+ const n = node as Record<string, unknown>;
388
+ const op = n.operation;
389
+ if (op && typeof op === 'object') {
390
+ const o = op as Record<string, unknown>;
391
+ const name = o.operation;
392
+ const inst = Number(o.instance ?? 0);
393
+ switch (name) {
394
+ case 'can.begin': canFlags(inst).begin = true; break;
395
+ case 'can.send': canFlags(inst).send = true; break;
396
+ case 'can.on_receive': canFlags(inst).onReceive = true; break;
397
+ case 'i2s.write': i2sFlags(inst).write = true; break;
398
+ case 'i2s.read': i2sFlags(inst).read = true; break;
399
+ case 'i2s.read_at': i2sFlags(inst).readAt = true; break;
400
+ case 'clock.set': out.clock.set = true; break;
401
+ case 'clock.now': out.clock.now = true; break;
402
+ default: break;
403
+ }
404
+ }
405
+ for (const v of Object.values(n)) {
406
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
407
+ else if (v && typeof v === 'object') visit(v);
408
+ }
409
+ };
410
+ visit(program);
411
+ visitResolvedHalOps(program, visit);
412
+ return out;
413
+ }
414
+
415
+ /**
416
+ * Collect the distinct I2C responders the program's ops reference
417
+ * (i2c.resp_*), keyed `${bus}|${address}` — one target-mode address per
418
+ * constructed responder. Mirrors collectSpiTargets: the construction facts
419
+ * (rx/tx sizes) ride every op, so the first seen wins and the state block
420
+ * cannot drift from the call sites.
421
+ */
422
+ export function collectI2cResponders(program: ProgramIR | undefined): Map<string, { bus: string; address: number; rx: number; tx: number }> | undefined {
423
+ if (!program) return undefined;
424
+ const responders = new Map<string, { bus: string; address: number; rx: number; tx: number }>();
425
+ const visit = (node: unknown): void => {
426
+ if (!node || typeof node !== 'object') return;
427
+ const n = node as Record<string, unknown>;
428
+ const op = n.operation;
429
+ if (op && typeof op === 'object') {
430
+ const o = op as Record<string, unknown>;
431
+ if (String(o.operation).startsWith('i2c.resp_')) {
432
+ const bus = String(o.bus ?? '');
433
+ const address = Number(o.address ?? 0);
434
+ const key = `${bus}|${address}`;
435
+ if (!responders.has(key)) {
436
+ responders.set(key, { bus, address, rx: Number(o.rx ?? 64), tx: Number(o.tx ?? 32) });
437
+ }
438
+ }
439
+ }
440
+ for (const v of Object.values(n)) {
441
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
442
+ else if (v && typeof v === 'object') visit(v);
443
+ }
444
+ };
445
+ visit(program);
446
+ visitResolvedHalOps(program, visit);
447
+ return responders;
448
+ }
449
+
262
450
  /**
263
451
  * Collect the distinct thin SPI targets the program's ops reference
264
452
  * (spi.transceive / spi.dev_write), keyed `${bus}|${cs}`. Mirrors
@@ -363,10 +551,18 @@ import { pwmInitLines } from './lowering/pwm.js';
363
551
  import { dacInitLines } from './lowering/dac.js';
364
552
  import { fsInitLines } from './lowering/fs.js';
365
553
  import { hwtimerInitLines } from './lowering/hwtimer.js';
366
- import { i2cInitLines } from './lowering/i2c.js';
554
+ import { i2cInitLines, i2cResponderStateLines } from './lowering/i2c.js';
555
+ import { parseControllerIndex } from './lowering/util.js';
556
+ import { stripInitLines } from './lowering/strip.js';
557
+ import { hidInitLines } from './lowering/hid.js';
558
+ import { matrixInitLines } from './lowering/matrix.js';
559
+ import { clockInitLines } from './lowering/clock.js';
560
+ import { canInitLines } from './lowering/can.js';
561
+ import { i2sInitLines } from './lowering/i2s.js';
367
562
  import { sensorStateLines } from './lowering/sensor.js';
368
563
  import { spiTargetStateLines } from './lowering/spi.js';
369
564
  import { threadStateLines } from './lowering/thread.js';
565
+ import { traceHeartbeatLines, uiFrameTraceLines, clampTraceIntervalMs } from './lowering/trace.js';
370
566
  import { uartRingStateLines } from './lowering/uart.js';
371
567
  import { spiInitLines } from './lowering/spi.js';
372
568
  import { uartInitLines } from './lowering/uart.js';
@@ -405,7 +601,8 @@ import { randomInitLines } from './lowering/random.js';
405
601
  import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
406
602
  import { resolveZephyrDisplayOp, newDisplayState, type DisplayState } from './display/index.js';
407
603
  import { buildDisplayRuntime } from './display/gfx.js';
408
- import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES } from './display/profiles.js';
604
+ import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES, isDtCompatible, isMonoDisplay, isGrayDisplay, isEinkDisplay } from './display/profiles.js';
605
+ import { listBoundDisplayCompatibles } from './display/bindings.js';
409
606
  import { zephyrDisplayAdapterGenerator } from './display/ui-adapter.js';
410
607
  import { zephyrTouchAdapter } from './display/touch-adapter.js';
411
608
 
@@ -496,6 +693,21 @@ export class ZephyrStrategy implements PlatformStrategy {
496
693
  if (uses('usesI2C')) inc.push('<zephyr/drivers/i2c.h>');
497
694
  if (uses('usesSensor')) inc.push('<zephyr/drivers/sensor.h>');
498
695
  if (uses('usesSPI')) inc.push('<zephyr/drivers/spi.h>');
696
+ if (uses('usesStrip')) inc.push('<zephyr/drivers/led_strip.h>');
697
+ if (uses('usesHid')) {
698
+ inc.push('<zephyr/usb/class/hid.h>');
699
+ inc.push('<zephyr/usb/class/usbd_hid.h>');
700
+ inc.push('<zephyr/usb/usbd.h>');
701
+ inc.push('<zephyr/drivers/usb/usb_buf.h>');
702
+ }
703
+ if (uses('usesMatrix')) inc.push('<zephyr/input/input.h>');
704
+ if (uses('usesPower')) inc.push('<zephyr/sys/poweroff.h>');
705
+ // esp_sleep.h exists only on the espressif HAL — gate on the harvested
706
+ // RTC wake-timer fact (the includes' uses() reads op flags, not src).
707
+ if (uses('usesPower') && chip.powerWakeTimer) inc.push('<esp_sleep.h>');
708
+ if (uses('usesClock')) inc.push('<zephyr/drivers/rtc.h>');
709
+ if (uses('usesCan')) inc.push('<zephyr/drivers/can.h>');
710
+ if (uses('usesI2s')) inc.push('<zephyr/drivers/i2s.h>');
499
711
  if (uses('usesUart')) inc.push('<zephyr/drivers/uart.h>');
500
712
  // USB CDC serial: the class instance is a UART device (uart.h); the
501
713
  // device context macros + usbd_* API live in the next-stack header.
@@ -805,16 +1017,16 @@ export class ZephyrStrategy implements PlatformStrategy {
805
1017
  'static inline char* __tc_fmt_num_buf(double v, char* out, size_t cap) {',
806
1018
  ' if (v != v) { snprintf(out, cap, "nan"); return out; }',
807
1019
  ' double a = v < 0 ? -v : v;',
808
- ' long long ip = (long long)a;',
809
- ' long long fr = (long long)((a - (double)ip) * 1000000.0 + 0.5);',
1020
+ ' long long ip = static_cast<long long>(a);',
1021
+ ' long long fr = static_cast<long long>((a - static_cast<double>(ip)) * 1000000.0 + 0.5);',
810
1022
  ' if (fr >= 1000000LL) { ip += 1LL; fr = 0LL; }',
811
1023
  ' int used = 0;',
812
1024
  ' if (v < 0 && (ip != 0LL || fr != 0LL)) { out[used++] = \'-\'; out[used] = \'\\0\'; }',
813
- ' if (fr == 0LL) { snprintf(out + used, cap - (size_t)used, "%lld", ip); return out; }',
1025
+ ' if (fr == 0LL) { snprintf(out + used, cap - static_cast<size_t>(used), "%lld", ip); return out; }',
814
1026
  ' char fbuf[8];',
815
1027
  ' int len = snprintf(fbuf, sizeof(fbuf), "%06lld", fr);',
816
1028
  ' while (len > 0 && fbuf[len - 1] == \'0\') { fbuf[--len] = \'\\0\'; }',
817
- ' snprintf(out + used, cap - (size_t)used, "%lld.%s", ip, fbuf);',
1029
+ ' snprintf(out + used, cap - static_cast<size_t>(used), "%lld.%s", ip, fbuf);',
818
1030
  ' return out;',
819
1031
  '}',
820
1032
  'static void __tc_fmt_num(double v) { char __b[32]; printf("%s", __tc_fmt_num_buf(v, __b, sizeof(__b))); }',
@@ -824,6 +1036,22 @@ export class ZephyrStrategy implements PlatformStrategy {
824
1036
  'inline void __tc_println(double v) { __tc_fmt_num(v); printf("\\n"); }',
825
1037
  );
826
1038
  }
1039
+ // Trace user-event helpers (Trace.mark / Trace.event → trace.mark /
1040
+ // trace.event ops): one [TR:EV:<uptime_ms>:<name>[:<value>] console line
1041
+ // per call, captured host-side as timeline events. Emitted
1042
+ // UNCONDITIONALLY like __tc_print — a Trace.mark() call site must never
1043
+ // fail to link because zephyr.trace was disabled; with no capture
1044
+ // listening the lines are ordinary console output. Value formatting
1045
+ // reuses __tc_fmt_num_buf (integer-only, the picolibc %g trap).
1046
+ guardBody.push(
1047
+ 'inline void __tc_trace_mark(const char* name) {',
1048
+ ' printf("[TR:EV:%u:%s\\n", static_cast<unsigned int>(k_uptime_get_32()), name);',
1049
+ '}',
1050
+ 'inline void __tc_trace_event(const char* name, double v) {',
1051
+ ' char __b[32];',
1052
+ ' printf("[TR:EV:%u:%s:%s\\n", static_cast<unsigned int>(k_uptime_get_32()), name, __tc_fmt_num_buf(v, __b, sizeof(__b)));',
1053
+ '}',
1054
+ );
827
1055
  // Serial-port write helper: writes a scalar to a UART/CDC device a byte at
828
1056
  // a time. Overloaded on const char* (strings, snprintf buffers) and double
829
1057
  // (numbers/booleans) so a single __tc_dev_put(dev, value) call site formats
@@ -848,11 +1076,27 @@ export class ZephyrStrategy implements PlatformStrategy {
848
1076
  // declared controller's __tc_<bus>N_dev trips -Wunused-variable under
849
1077
  // Zephyr's -Werror (collectUsedBusIndices; probes with no program emit all).
850
1078
  const usedBuses = collectUsedBusIndices(program);
1079
+ // Verb-level usage for the can.*/i2s.*/clock.* init blocks (see
1080
+ // collectVerbUsage) — undefined when there is no program (probe flows
1081
+ // emit the full state set).
1082
+ const verbUsage = collectVerbUsage(program);
851
1083
  if (uses('usesI2C') && chip.i2c) {
852
1084
  for (let i = 0; i < chip.i2c.controllers.length; i++) {
853
1085
  if (usedBuses && !usedBuses.i2c.has(i)) continue;
854
1086
  guardBody.push(...i2cInitLines(chip, i));
855
1087
  }
1088
+ // I2C responders — one target-mode state block per constructed
1089
+ // responder (bus|address), against the controller handles above
1090
+ // (collectI2cResponders; the ops mark the bus used, so the handle
1091
+ // the registration references is always present).
1092
+ const responders = collectI2cResponders(program);
1093
+ if (responders) {
1094
+ for (const r of responders.values()) {
1095
+ const idx = parseControllerIndex(r.bus);
1096
+ if (idx >= chip.i2c.controllers.length) continue;
1097
+ guardBody.push(...i2cResponderStateLines(chip, idx, r.address, r.rx, r.tx));
1098
+ }
1099
+ }
856
1100
  }
857
1101
  if (uses('usesSPI') && chip.spi) {
858
1102
  for (let i = 0; i < chip.spi.controllers.length; i++) {
@@ -860,6 +1104,77 @@ export class ZephyrStrategy implements PlatformStrategy {
860
1104
  guardBody.push(...spiInitLines(chip, i));
861
1105
  }
862
1106
  }
1107
+ // LED strips — one buffer + device handle per driven SPI controller
1108
+ // (chain length from the construction facts).
1109
+ {
1110
+ const strips = collectStrips(program);
1111
+ if (uses('usesStrip') && strips) {
1112
+ for (const [idx, count] of strips) {
1113
+ guardBody.push(...stripInitLines(chip, idx, count));
1114
+ }
1115
+ }
1116
+ }
1117
+ // Key matrix — the input-subsystem trampoline (the DT node comes from
1118
+ // the overlay generator, driven by the toolchain's matrix scan).
1119
+ if (uses('usesMatrix')) {
1120
+ guardBody.push(...matrixInitLines());
1121
+ }
1122
+ // Wall-clock shim — the epoch<->civil helpers + the rtc alias handle
1123
+ // (the DT node comes from the overlay generator's shim synthesis).
1124
+ if (uses('usesClock')) {
1125
+ const clockUsage = verbUsage?.clock ?? { set: true, now: true };
1126
+ guardBody.push(...clockInitLines(clockUsage));
1127
+ }
1128
+ // CAN — per-controller device handle + the rx trampoline (the DT node
1129
+ // is the board's own can@ node, enabled by the overlay). Only the
1130
+ // controllers the program addresses, and only the state its verbs
1131
+ // reference (collectVerbUsage; probes with no program emit all). Usage
1132
+ // folds through the same clamp lowerCan applies to out-of-range
1133
+ // instances, so the state block for the clamped controller emits.
1134
+ if (uses('usesCan') && chip.can) {
1135
+ const nCtrl = chip.can.controllers.length;
1136
+ const canUsage = verbUsage?.can;
1137
+ const folded = Array.from({ length: nCtrl }, () => ({ begin: false, send: false, onReceive: false }));
1138
+ if (canUsage) {
1139
+ for (const [inst, flags] of canUsage) {
1140
+ const f = folded[Math.max(0, Math.min(inst, nCtrl - 1))]!;
1141
+ f.begin = f.begin || flags.begin;
1142
+ f.send = f.send || flags.send;
1143
+ f.onReceive = f.onReceive || flags.onReceive;
1144
+ }
1145
+ }
1146
+ for (let i = 0; i < nCtrl; i++) {
1147
+ const f = folded[i]!;
1148
+ if (canUsage && !f.begin && !f.send && !f.onReceive) continue;
1149
+ guardBody.push(...canInitLines(chip, i, canUsage ? f : { begin: true, send: true, onReceive: true }));
1150
+ }
1151
+ }
1152
+ // I2S — per-controller slab + block buffers + lazy direction inits,
1153
+ // sized from the construction facts the ops carry. Same used-instance
1154
+ // and clamp-fold discipline as CAN.
1155
+ if (uses('usesI2s') && chip.i2s) {
1156
+ const i2sFacts = collectI2s(program);
1157
+ const nCtrl = chip.i2s.controllers.length;
1158
+ const i2sUsage = verbUsage?.i2s;
1159
+ const folded = Array.from({ length: nCtrl }, () => ({ write: false, read: false, readAt: false }));
1160
+ if (i2sUsage) {
1161
+ for (const [inst, flags] of i2sUsage) {
1162
+ const f = folded[Math.max(0, Math.min(inst, nCtrl - 1))]!;
1163
+ f.write = f.write || flags.write;
1164
+ f.read = f.read || flags.read;
1165
+ f.readAt = f.readAt || flags.readAt;
1166
+ }
1167
+ }
1168
+ for (let i = 0; i < nCtrl; i++) {
1169
+ const f = folded[i]!;
1170
+ if (i2sUsage && !f.write && !f.read && !f.readAt) continue;
1171
+ const facts = i2sFacts?.get(i);
1172
+ const channels = facts?.channels ?? 2;
1173
+ const bits = facts?.bits ?? 16;
1174
+ const blockFrames = facts?.blockFrames ?? 64;
1175
+ guardBody.push(...i2sInitLines(chip, i, blockFrames * channels * (bits / 8), i2sUsage ? f : { write: true, read: true, readAt: true }));
1176
+ }
1177
+ }
863
1178
  if (uses('usesUart') && chip.uart) {
864
1179
  for (let i = 0; i < chip.uart.controllers.length; i++) {
865
1180
  if (usedBuses && !usedBuses.uart.has(i)) continue;
@@ -881,7 +1196,7 @@ export class ZephyrStrategy implements PlatformStrategy {
881
1196
  const sensors = collectSensors(program);
882
1197
  if (sensors) {
883
1198
  for (const s of sensors.values()) {
884
- guardBody.push(...sensorStateLines(s.part, s.bus, s.port, s.busKind, s.spiHz, s.spiMode, s.alertPin));
1199
+ guardBody.push(...sensorStateLines(s.part, s.bus, s.port, s.busKind, s.spiHz, s.spiMode, s.alertPin, s.resolution));
885
1200
  }
886
1201
  }
887
1202
  }
@@ -907,10 +1222,56 @@ export class ZephyrStrategy implements PlatformStrategy {
907
1222
  }
908
1223
  }
909
1224
  }
910
- if (uses('usesUsb') && chip.usb) {
911
- guardBody.push(...usbdDeviceLines(chip));
912
- for (let i = 0; i < chip.usb.cdcInstances; i++) guardBody.push(...usbInitLines(chip, i));
1225
+ // Trace heartbeat — the runtime-stats sampler behind `typecad-hal trace
1226
+ // capture`. Opt-in via zephyr.trace.enabled in typecad-hal.config.ts (the
1227
+ // config loader threads the zephyr record through platformContext). The
1228
+ // block compiles under CUTTLEFISH_ENTRY_TU, so split-file TUs carry no
1229
+ // second copy (a duplicate SYS_INIT would double every heartbeat).
1230
+ // UI-mounted programs additionally get the frame-stats block (always
1231
+ // defined, active only under CUTTLEFISH_TRACE_UI) so the ui_tick call
1232
+ // site's instrumentation never depends on the trace config.
1233
+ {
1234
+ const traceCfg = (ctx as { zephyr?: { trace?: { enabled?: boolean; intervalMs?: number; alarms?: { stackMinBytes?: number; frameMaxMs?: number } } } } | undefined)
1235
+ ?.zephyr?.trace;
1236
+ const traceEnabled = traceCfg?.enabled === true;
1237
+ if (traceEnabled && entryHasUI()) {
1238
+ guardBody.push(
1239
+ '#if defined(CUTTLEFISH_ENTRY_TU)',
1240
+ '#define CUTTLEFISH_TRACE_UI 1',
1241
+ '#endif',
1242
+ );
1243
+ }
1244
+ if (entryHasUI()) {
1245
+ guardBody.push(...uiFrameTraceLines(traceCfg?.alarms?.frameMaxMs));
1246
+ }
1247
+ if (traceEnabled) {
1248
+ guardBody.push(...traceHeartbeatLines(clampTraceIntervalMs(traceCfg?.intervalMs), entryHasUI(), traceCfg?.alarms));
1249
+ }
1250
+ }
1251
+ // The usbd context is shared by CDC and HID; the CDC per-instance
1252
+ // state only emits when the program actually uses usb.* (an HID-only
1253
+ // program would otherwise carry an unused __tc_usb0_dev).
1254
+ if ((uses('usesUsb') || uses('usesHid')) && chip.usb) {
1255
+ guardBody.push(...usbdDeviceLines(chip, { hid: uses('usesHid') }));
1256
+ if (uses('usesUsb')) {
1257
+ for (let i = 0; i < chip.usb.cdcInstances; i++) guardBody.push(...usbInitLines(chip, i));
1258
+ }
1259
+ }
1260
+ // USB HID — one interface per program; the protocol comes from which
1261
+ // class's verbs the program used (keyboard vs mouse). Requires the USB
1262
+ // device context (the same one CDC rides — the register helper calls
1263
+ // __tc_usbd_start).
1264
+ if (uses('usesHid') && chip.usb) {
1265
+ const protocol = collectHidProtocol(program) ?? 'keyboard';
1266
+ if (protocol === 'mixed') {
1267
+ // One zephyr,hid-device interface per program (the v1 ceiling) —
1268
+ // name it at compile time instead of an undefined-symbol error.
1269
+ guardBody.push('#error "typecad-hal: one HID interface per program (v1) — Keyboard and Mouse cannot be mixed in one app"');
1270
+ } else {
1271
+ guardBody.push(...hidInitLines(protocol));
1272
+ }
913
1273
  }
1274
+
914
1275
  // PWM init also fires (with alias vars for the override pins) when a
915
1276
  // program drives ONLY inline-override channels on a chip with no pwm
916
1277
  // facts — the lowered calls reference those aliases.
@@ -1042,6 +1403,10 @@ export class ZephyrStrategy implements PlatformStrategy {
1042
1403
  if (this.needsGpioReadShim(program, ctx)) {
1043
1404
  lines.push(
1044
1405
  '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))); }',
1406
+ // Runtime input configuration for ui.watchPin / ui.press setup
1407
+ // (the strategy's setPinMode lowers here). Idempotent — setup calls
1408
+ // it once per watched pin before the poll loop reads it.
1409
+ '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); }',
1045
1410
  );
1046
1411
  }
1047
1412
  // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
@@ -1879,6 +2244,17 @@ export class ZephyrStrategy implements PlatformStrategy {
1879
2244
  };
1880
2245
  }
1881
2246
 
2247
+ /**
2248
+ * Per-frame wall-time feed for the trace heartbeat's UI stats. The helper
2249
+ * is emitted unconditionally for UI programs (compiles to a no-op unless
2250
+ * zephyr.trace defined CUTTLEFISH_TRACE_UI), so this call is safe with
2251
+ * tracing on or off.
2252
+ */
2253
+ uiFrameTimingLines(deltaMsExpr: string): string[] | null {
2254
+ if (!entryHasUI()) return null;
2255
+ return [`__tc_trace_ui_frame(static_cast<uint32_t>(${deltaMsExpr}));`];
2256
+ }
2257
+
1882
2258
  // ── Type aliases ────────────────────────────────────────────────────────
1883
2259
 
1884
2260
  shouldSkipTypeAlias(): boolean {
@@ -2131,9 +2507,14 @@ struct __tc_StaticArray {
2131
2507
  writeDigitalPin(pin: string, val: string): string {
2132
2508
  return `__tc_gpio_write(${pin}, ${val})`;
2133
2509
  }
2134
- setPinMode(_pin: string, _mode: string): string {
2135
- // Zephyr configures pin direction via devicetree, not a runtime pinMode.
2136
- return `/* pin mode configured via devicetree */`;
2510
+ setPinMode(pin: string, _mode: string): string {
2511
+ // Runtime input configuration: Zephyr has no pinMode — the GPIO API
2512
+ // configures direction/pull explicitly. The mode argument is ignored:
2513
+ // every watchPin/press consumer wants an input with pull-up (buttons are
2514
+ // active-low), and __tc_gpio_configure_input hardcodes exactly that.
2515
+ // The helper's definition rides the GPIO read shim (same gate — UI builds
2516
+ // with pin watchers always carry the read surface too).
2517
+ return `__tc_gpio_configure_input(static_cast<uint32_t>(${pin}));`;
2137
2518
  }
2138
2519
  delayMs(ms: string): string {
2139
2520
  return `k_msleep(${ms})`;
@@ -2178,17 +2559,30 @@ struct __tc_StaticArray {
2178
2559
  }
2179
2560
 
2180
2561
  supportedDisplayDrivers(): ReadonlySet<string> {
2181
- return new Set<string>(Object.keys(ZEPHYR_DISPLAY_PROFILES));
2562
+ // Registry profiles plus every panel compatible with a display binding in
2563
+ // the user's Zephyr tree — the drop-in path (display.driver = compatible)
2564
+ // validates at mount like any curated profile. No tree discoverable →
2565
+ // registry only.
2566
+ const drivers = new Set<string>(Object.keys(ZEPHYR_DISPLAY_PROFILES));
2567
+ try {
2568
+ for (const compatible of listBoundDisplayCompatibles()) drivers.add(compatible);
2569
+ } catch {
2570
+ // Tree unreadable — the registry set stands.
2571
+ }
2572
+ return drivers;
2182
2573
  }
2183
2574
 
2184
2575
  // ── Strategy-owned display/touch adapter seam ────────────────────────────
2185
2576
  // Zephyr owns its display + touch adapters: the UI display adapter bridges
2186
- // the in-tree CuttlefishGFX class to the panel (per-controller init + wire
2187
- // format, see src/display/ui-adapter.ts), and the touch adapters drive the
2188
- // FT6336U (I2C capacitive) and XPT2046 (SPI resistive) controllers via
2189
- // Zephyr's bus APIs (src/display/touch-adapter.ts). Both live in this
2190
- // package so cuttlefish carries no Zephyr/Wiring-specific display or touch
2191
- // knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
2577
+ // the in-tree CuttlefishGFX class to the panel via one of two transports
2578
+ // (per profile — 'direct-spi': per-controller init + wire format over
2579
+ // spi_write, see src/display/ui-adapter.ts; 'zephyr-display': Zephyr's
2580
+ // display API with in-tree panel drivers, see src/display/ui-adapter-native.ts),
2581
+ // and the touch adapters drive the FT6336U (I2C capacitive) and XPT2046
2582
+ // (SPI resistive) controllers via Zephyr's bus APIs (src/display/touch-adapter.ts).
2583
+ // Both live in this package so cuttlefish carries no Zephyr/Wiring-specific
2584
+ // display or touch knowledge. Mirrors ArduinoStrategy's provides*/resolve*
2585
+ // pattern.
2192
2586
 
2193
2587
  providesDisplayAdapter(): boolean { return true; }
2194
2588
 
@@ -2212,8 +2606,29 @@ struct __tc_StaticArray {
2212
2606
  return new Map(Object.entries(BUILT_IN_PROFILES));
2213
2607
  }
2214
2608
 
2215
- colorFormat(): 'rgb565' | 'rgb666' | 'rgb888' | 'mono' {
2216
- return 'rgb565';
2609
+ colorFormat(): 'rgb565' | 'rgb666' | 'rgb888' | 'mono' | 'gray8' {
2610
+ // The seeded display state is the truth after display.init (drop-in mono
2611
+ // panels synthesize 'mono'); the default profile keeps the pre-init
2612
+ // rgb565 answer byte-identical with history.
2613
+ return this._displayState.profile.colorFormat;
2614
+ }
2615
+
2616
+ colorFormatForDriver(driver: string): 'rgb565' | 'rgb666' | 'rgb888' | 'mono' | 'gray8' | undefined {
2617
+ // Drop-in panel class from the compatible table (DATA): the 1bpp OLED
2618
+ // family lowers mono engine-side and the 16-gray family lowers gray8,
2619
+ // so colors flatten at build time instead of rgb565-lowering into an
2620
+ // adapter that cannot show them. Registry profiles carry their own
2621
+ // colorFormat; non-compatible driver ids keep the default.
2622
+ if (isDtCompatible(driver) && isMonoDisplay({ driver })) return 'mono';
2623
+ if (isDtCompatible(driver) && isEinkDisplay({ driver })) return 'mono';
2624
+ if (isDtCompatible(driver) && isGrayDisplay({ driver })) return 'gray8';
2625
+ return undefined;
2626
+ }
2627
+
2628
+ displayClassForDriver(driver: string): 'eink' | 'oled' | undefined {
2629
+ // The compatible table decides (DATA) — ssd16xx/uc81xx are e-ink.
2630
+ if (isDtCompatible(driver) && isEinkDisplay({ driver })) return 'eink';
2631
+ return undefined;
2217
2632
  }
2218
2633
 
2219
2634
  graphicsCapacity(): GraphicsCapacity {