@typecad/framework-zephyr 1.0.0-alpha.16 → 1.0.0-alpha.19

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 (210) hide show
  1. package/LICENSE +202 -21
  2. package/README.md +49 -87
  3. package/dist/as-built.d.ts +2 -2
  4. package/dist/as-built.js +2 -2
  5. package/dist/audit.d.ts +111 -0
  6. package/dist/audit.js +416 -0
  7. package/dist/boardgen.d.ts +1 -9
  8. package/dist/boardgen.js +288 -47
  9. package/dist/chips/resolve.js +20 -0
  10. package/dist/chips/types.d.ts +22 -1
  11. package/dist/debug-codegen.js +1 -1
  12. package/dist/display/bindings.d.ts +55 -0
  13. package/dist/display/bindings.js +316 -0
  14. package/dist/display/gfx.d.ts +2 -3
  15. package/dist/display/gfx.js +166 -154
  16. package/dist/display/index.js +20 -1
  17. package/dist/display/mipi-dbi-host.d.ts +9 -0
  18. package/dist/display/mipi-dbi-host.js +174 -0
  19. package/dist/display/profiles.d.ts +109 -4
  20. package/dist/display/profiles.js +270 -7
  21. package/dist/display/touch-adapter.js +119 -49
  22. package/dist/display/ui-adapter-eink.d.ts +2 -0
  23. package/dist/display/ui-adapter-eink.js +4 -0
  24. package/dist/display/ui-adapter-gray.d.ts +8 -0
  25. package/dist/display/ui-adapter-gray.js +170 -0
  26. package/dist/display/ui-adapter-mono.d.ts +13 -0
  27. package/dist/display/ui-adapter-mono.js +230 -0
  28. package/dist/display/ui-adapter-native.d.ts +10 -0
  29. package/dist/display/ui-adapter-native.js +295 -0
  30. package/dist/display/ui-adapter-shared.d.ts +11 -0
  31. package/dist/display/ui-adapter-shared.js +122 -0
  32. package/dist/display/ui-adapter.js +510 -558
  33. package/dist/doctor.js +4 -4
  34. package/dist/dt-config/custom-board.js +2 -2
  35. package/dist/dt-config/kconfig.d.ts +62 -1
  36. package/dist/dt-config/kconfig.js +141 -38
  37. package/dist/dt-config/overlay.d.ts +15 -2
  38. package/dist/dt-config/overlay.js +433 -18
  39. package/dist/framework.manifest.d.ts +10 -4
  40. package/dist/framework.manifest.js +133 -17
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +14 -6
  43. package/dist/licenses.d.ts +2 -2
  44. package/dist/licenses.js +13 -92
  45. package/dist/lowering/can.d.ts +25 -0
  46. package/dist/lowering/can.js +97 -0
  47. package/dist/lowering/clock.d.ts +17 -0
  48. package/dist/lowering/clock.js +58 -0
  49. package/dist/lowering/fs.js +1 -1
  50. package/dist/lowering/gpio.js +0 -32
  51. package/dist/lowering/hid.d.ts +27 -0
  52. package/dist/lowering/hid.js +244 -0
  53. package/dist/lowering/http.js +264 -32
  54. package/dist/lowering/i2c.d.ts +8 -0
  55. package/dist/lowering/i2c.js +137 -5
  56. package/dist/lowering/i2s.d.ts +27 -0
  57. package/dist/lowering/i2s.js +98 -0
  58. package/dist/lowering/index.d.ts +9 -1
  59. package/dist/lowering/index.js +25 -1
  60. package/dist/lowering/interrupts.js +6 -0
  61. package/dist/lowering/matrix.d.ts +15 -0
  62. package/dist/lowering/matrix.js +63 -0
  63. package/dist/lowering/mqtt.js +110 -8
  64. package/dist/lowering/power.d.ts +3 -2
  65. package/dist/lowering/power.js +20 -45
  66. package/dist/lowering/pwm.js +25 -0
  67. package/dist/lowering/sensor.d.ts +2 -2
  68. package/dist/lowering/sensor.js +8 -4
  69. package/dist/lowering/strip.d.ts +16 -0
  70. package/dist/lowering/strip.js +70 -0
  71. package/dist/lowering/thread.js +5 -1
  72. package/dist/lowering/trace.d.ts +44 -0
  73. package/dist/lowering/trace.js +239 -0
  74. package/dist/lowering/uart.js +6 -1
  75. package/dist/lowering/usb.d.ts +3 -1
  76. package/dist/lowering/usb.js +16 -13
  77. package/dist/lowering/wdt.js +2 -29
  78. package/dist/sbom.d.ts +181 -0
  79. package/dist/sbom.js +901 -0
  80. package/dist/sdk/board-catalog-sync.d.ts +1 -3
  81. package/dist/sdk/board-catalog-sync.js +4 -10
  82. package/dist/strategy.d.ts +82 -46
  83. package/dist/strategy.js +639 -182
  84. package/dist/tmp-probe.d.ts +2 -0
  85. package/dist/tmp-probe.js +9 -0
  86. package/dist/toolchain/debug-config.d.ts +50 -90
  87. package/dist/toolchain/debug-config.js +239 -510
  88. package/dist/toolchain/env-check.d.ts +1 -3
  89. package/dist/toolchain/env-check.js +2 -7
  90. package/dist/toolchain/index.d.ts +28 -3
  91. package/dist/toolchain/index.js +498 -62
  92. package/dist/toolchain/runners.d.ts +16 -0
  93. package/dist/toolchain/runners.js +75 -0
  94. package/dist/toolchain/scaffold.d.ts +5 -2
  95. package/dist/toolchain/scaffold.js +79 -15
  96. package/dist/toolchain/west-discover.d.ts +6 -0
  97. package/dist/toolchain/west-discover.js +36 -13
  98. package/dist/toolchain/west-spawn.js +8 -2
  99. package/dist/west-inventory.d.ts +25 -0
  100. package/dist/west-inventory.js +97 -0
  101. package/installer/README.md +328 -328
  102. package/installer/install.sh +2 -2
  103. package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
  104. package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
  105. package/installer/templates/project/.vscode/settings.json +1 -1
  106. package/installer/templates/project/README.md +2 -2
  107. package/package.json +5 -5
  108. package/src/as-built.ts +206 -206
  109. package/src/audit.ts +529 -0
  110. package/src/boardgen.ts +265 -50
  111. package/src/chips/resolve.ts +21 -0
  112. package/src/chips/types.ts +10 -1
  113. package/src/display/bindings.ts +347 -0
  114. package/src/display/gfx.ts +318 -306
  115. package/src/display/index.ts +87 -70
  116. package/src/display/mipi-dbi-host.ts +183 -0
  117. package/src/display/profiles.ts +458 -139
  118. package/src/display/touch-adapter.ts +119 -49
  119. package/src/display/ui-adapter-eink.ts +13 -0
  120. package/src/display/ui-adapter-gray.ts +178 -0
  121. package/src/display/ui-adapter-mono.ts +238 -0
  122. package/src/display/ui-adapter-native.ts +304 -0
  123. package/src/display/ui-adapter-shared.ts +125 -0
  124. package/src/display/ui-adapter.ts +732 -781
  125. package/src/doctor.ts +4 -4
  126. package/src/dt-config/custom-board.ts +2 -2
  127. package/src/dt-config/kconfig.ts +647 -505
  128. package/src/dt-config/overlay.ts +1475 -1058
  129. package/src/framework.manifest.ts +659 -535
  130. package/src/index.ts +16 -6
  131. package/src/licenses.ts +346 -425
  132. package/src/lowering/can.ts +140 -0
  133. package/src/lowering/clock.ts +91 -0
  134. package/src/lowering/fs.ts +135 -135
  135. package/src/lowering/gpio.ts +0 -33
  136. package/src/lowering/hid.ts +261 -0
  137. package/src/lowering/http.ts +264 -32
  138. package/src/lowering/i2c.ts +142 -5
  139. package/src/lowering/i2s.ts +143 -0
  140. package/src/lowering/index.ts +18 -1
  141. package/src/lowering/interrupts.ts +6 -0
  142. package/src/lowering/matrix.ts +70 -0
  143. package/src/lowering/mqtt.ts +109 -8
  144. package/src/lowering/power.ts +41 -0
  145. package/src/lowering/pwm.ts +192 -167
  146. package/src/lowering/sensor.ts +159 -155
  147. package/src/lowering/strip.ts +81 -0
  148. package/src/lowering/thread.ts +5 -1
  149. package/src/lowering/trace.ts +270 -0
  150. package/src/lowering/uart.ts +6 -1
  151. package/src/lowering/usb.ts +16 -13
  152. package/src/lowering/wdt.ts +2 -25
  153. package/src/sbom.ts +1117 -0
  154. package/src/sdk/board-catalog-sync.ts +4 -25
  155. package/src/strategy.ts +2680 -2309
  156. package/src/toolchain/debug-config.ts +262 -522
  157. package/src/toolchain/env-check.ts +279 -285
  158. package/src/toolchain/index.ts +1792 -1359
  159. package/src/toolchain/runners.ts +80 -0
  160. package/src/toolchain/scaffold.ts +355 -296
  161. package/src/toolchain/west-discover.ts +35 -13
  162. package/src/toolchain/west-spawn.ts +174 -168
  163. package/src/west-inventory.ts +102 -0
  164. package/dist/async/timer-polyfill.d.ts +0 -10
  165. package/dist/async/timer-polyfill.js +0 -95
  166. package/dist/chips/board-overrides.d.ts +0 -7
  167. package/dist/chips/board-overrides.js +0 -11
  168. package/dist/chips/esp32.d.ts +0 -2
  169. package/dist/chips/esp32.js +0 -71
  170. package/dist/chips/esp32s3.d.ts +0 -2
  171. package/dist/chips/esp32s3.js +0 -103
  172. package/dist/chips/soc/.d.ts +0 -2
  173. package/dist/chips/soc/.js +0 -129
  174. package/dist/chips/soc/esp32.d.ts +0 -2
  175. package/dist/chips/soc/esp32.js +0 -120
  176. package/dist/chips/soc/esp32c3.d.ts +0 -2
  177. package/dist/chips/soc/esp32c3.js +0 -90
  178. package/dist/chips/soc/esp32c6.d.ts +0 -2
  179. package/dist/chips/soc/esp32c6.js +0 -109
  180. package/dist/chips/soc/esp32s3.d.ts +0 -2
  181. package/dist/chips/soc/esp32s3.js +0 -189
  182. package/dist/chips/soc/index.d.ts +0 -2
  183. package/dist/chips/soc/index.js +0 -23
  184. package/dist/chips/soc/nrf52840.d.ts +0 -2
  185. package/dist/chips/soc/nrf52840.js +0 -130
  186. package/dist/chips/soc/rp2040.d.ts +0 -2
  187. package/dist/chips/soc/rp2040.js +0 -141
  188. package/dist/chips/soc/rp2350a.d.ts +0 -2
  189. package/dist/chips/soc/rp2350a.js +0 -145
  190. package/dist/chips/soc/samd21g18a.d.ts +0 -2
  191. package/dist/chips/soc/samd21g18a.js +0 -143
  192. package/dist/chips/soc/stm32f411xe.d.ts +0 -2
  193. package/dist/chips/soc/stm32f411xe.js +0 -251
  194. package/dist/chips/xiao-ble.d.ts +0 -2
  195. package/dist/chips/xiao-ble.js +0 -100
  196. package/dist/lowering/pulse.d.ts +0 -7
  197. package/dist/lowering/pulse.js +0 -51
  198. package/dist/lowering/tone.d.ts +0 -10
  199. package/dist/lowering/tone.js +0 -63
  200. package/dist/lowering/worker-backing.d.ts +0 -14
  201. package/dist/lowering/worker-backing.js +0 -79
  202. package/dist/lowering/worker.d.ts +0 -6
  203. package/dist/lowering/worker.js +0 -14
  204. package/dist/sdk/board-data.generated.d.ts +0 -2
  205. package/dist/sdk/board-data.generated.js +0 -4
  206. package/dist/sdk/catalog-walker.d.ts +0 -90
  207. package/dist/sdk/catalog-walker.js +0 -682
  208. package/dist/sdk/dts-reader.d.ts +0 -83
  209. package/dist/sdk/dts-reader.js +0 -596
  210. package/src/debug-codegen.ts +0 -207
package/dist/strategy.js CHANGED
@@ -25,6 +25,22 @@ import { syncBoardCatalog, ensureFreshBoardCatalog, } from './sdk/board-catalog-
25
25
  import { setActiveChip, NO_BOARD_CHIP } from './chips/index.js';
26
26
  import { resolveChipFromBoard } from './chips/resolve.js';
27
27
  import { emitGpioDevDispatcher } from './chips/controllers.js';
28
+ /**
29
+ * Visit the program's transpile-resolved HAL ops with the same `visit` the IR
30
+ * walk uses. A HAL call inlined into another HAL call's template literal
31
+ * (`USB0.writeLine(\`n: ${gps.available()}\`)`) is lowered to C++ text while
32
+ * the IR is built — it never becomes a hal-op/hal-expr IR node, so the walk
33
+ * alone cannot see it. Such ops ride on ProgramIR.resolvedHalOps; wrapped as
34
+ * `{ operation: op }` they match the hal-op node shape every collector below
35
+ * tests for. Without this fold, shims keyed on these scans (UART RX rings,
36
+ * sensor device handles, per-controller bus state, ADC channels/overrides,
37
+ * dt specs) go undeclared while the baked text still references them — the
38
+ * `'__tc_dt_led0' was not declared` failure class.
39
+ */
40
+ function visitResolvedHalOps(program, visit) {
41
+ for (const op of program?.resolvedHalOps ?? [])
42
+ visit({ operation: op });
43
+ }
28
44
  /**
29
45
  * Deep-walk the program IR and collect the HAL pin numbers the program
30
46
  * actually touches for a peripheral family ('adc' | 'pwm') — the same walk
@@ -64,6 +80,7 @@ function collectUsedPins(program, kind, chip) {
64
80
  }
65
81
  };
66
82
  visit(program);
83
+ visitResolvedHalOps(program, visit);
67
84
  return pins;
68
85
  }
69
86
  /**
@@ -99,6 +116,7 @@ function collectAdcOverrideDevices(program) {
99
116
  }
100
117
  };
101
118
  visit(program);
119
+ visitResolvedHalOps(program, visit);
102
120
  return devices;
103
121
  }
104
122
  /**
@@ -136,6 +154,7 @@ function collectPwmOverrideSpecs(program) {
136
154
  }
137
155
  };
138
156
  visit(program);
157
+ visitResolvedHalOps(program, visit);
139
158
  return specs;
140
159
  }
141
160
  /**
@@ -149,6 +168,66 @@ function collectPwmOverrideSpecs(program) {
149
168
  * -Wunused-variable (Zephyr builds with warnings-as-errors), the same
150
169
  * hygiene collectUsedPins enforces for per-channel ADC/PWM state.
151
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
+ }
152
231
  function collectUsedBusIndices(program) {
153
232
  if (!program)
154
233
  return undefined;
@@ -185,6 +264,7 @@ function collectUsedBusIndices(program) {
185
264
  }
186
265
  };
187
266
  visit(program);
267
+ visitResolvedHalOps(program, visit);
188
268
  return indices;
189
269
  }
190
270
  /**
@@ -213,9 +293,13 @@ export function collectSensors(program) {
213
293
  const spiHz = o.spiHz ?? 0;
214
294
  const spiMode = o.spiMode ?? 0;
215
295
  const alertPin = o.alertPin ?? -1;
296
+ const resolution = o.resolution ?? 12;
216
297
  const key = `${part}|${bus}|${port}|${busKind}`;
217
- if (!sensors.has(key))
218
- 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;
219
303
  }
220
304
  }
221
305
  for (const v of Object.values(n)) {
@@ -228,8 +312,176 @@ export function collectSensors(program) {
228
312
  }
229
313
  };
230
314
  visit(program);
315
+ visitResolvedHalOps(program, visit);
231
316
  return sensors;
232
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
+ }
233
485
  /**
234
486
  * Collect the distinct thin SPI targets the program's ops reference
235
487
  * (spi.transceive / spi.dev_write), keyed `${bus}|${cs}`. Mirrors
@@ -266,6 +518,7 @@ export function collectSpiTargets(program) {
266
518
  }
267
519
  };
268
520
  visit(program);
521
+ visitResolvedHalOps(program, visit);
269
522
  return targets;
270
523
  }
271
524
  /**
@@ -305,6 +558,7 @@ export function collectUartRings(program) {
305
558
  }
306
559
  };
307
560
  visit(program);
561
+ visitResolvedHalOps(program, visit);
308
562
  return rings;
309
563
  }
310
564
  /**
@@ -339,6 +593,7 @@ export function collectThreads(program) {
339
593
  }
340
594
  };
341
595
  visit(program);
596
+ visitResolvedHalOps(program, visit);
342
597
  return threads;
343
598
  }
344
599
  import { lowerHalOp } from './lowering/index.js';
@@ -347,10 +602,18 @@ import { pwmInitLines } from './lowering/pwm.js';
347
602
  import { dacInitLines } from './lowering/dac.js';
348
603
  import { fsInitLines } from './lowering/fs.js';
349
604
  import { hwtimerInitLines } from './lowering/hwtimer.js';
350
- 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';
351
613
  import { sensorStateLines } from './lowering/sensor.js';
352
614
  import { spiTargetStateLines } from './lowering/spi.js';
353
615
  import { threadStateLines } from './lowering/thread.js';
616
+ import { traceHeartbeatLines, uiFrameTraceLines, clampTraceIntervalMs } from './lowering/trace.js';
354
617
  import { uartRingStateLines } from './lowering/uart.js';
355
618
  import { spiInitLines } from './lowering/spi.js';
356
619
  import { uartInitLines } from './lowering/uart.js';
@@ -385,13 +648,16 @@ import { httpInitLines } from './lowering/http.js';
385
648
  import { mqttInitLines } from './lowering/mqtt.js';
386
649
  import { preferencesInitLines } from './lowering/preferences.js';
387
650
  import { randomInitLines } from './lowering/random.js';
388
- import { generateZephyrInitCode, generateZephyrBreakpointCode, generateZephyrLogpointCode } from './debug-codegen.js';
389
651
  import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
390
652
  import { resolveZephyrDisplayOp, newDisplayState } from './display/index.js';
391
653
  import { buildDisplayRuntime } from './display/gfx.js';
392
- 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';
393
656
  import { zephyrDisplayAdapterGenerator } from './display/ui-adapter.js';
394
657
  import { zephyrTouchAdapter } from './display/touch-adapter.js';
658
+ /** debugMode() memo — the boardgen facts lookup is not free and the method is
659
+ * called several times per build (transpile gate + artifact writers). */
660
+ const debugModeMemo = new Map();
395
661
  export class ZephyrStrategy {
396
662
  constructor() {
397
663
  this.id = 'zephyr';
@@ -437,22 +703,7 @@ export class ZephyrStrategy {
437
703
  setActiveChip(fromBoard ?? NO_BOARD_CHIP);
438
704
  return fromBoard ?? NO_BOARD_CHIP;
439
705
  }
440
- /**
441
- * Resolve the debug mode for the active target from the platform context.
442
- * Mirrors resolveChip's target extraction so shimLines/forcedIncludes can
443
- * gate the printf halt shim + console UART include to printf builds only
444
- * (gdb builds use VS Code native breakpoints + #line markers, so the
445
- * __tc_debug_wait_for_continue shim and its <zephyr/drivers/uart.h> include
446
- * are dead code there).
447
- */
448
- resolveDebugMode(ctx) {
449
- const fd = ctx?.frameworkData;
450
- const target = fd?.target ??
451
- fd?.buildTarget;
452
- return this.debugMode(target);
453
- }
454
706
  forcedIncludes(_program, ctx) {
455
- const isPrintf = this.resolveDebugMode(ctx) === 'printf';
456
707
  // <zephyr/kernel.h> for k_msleep / k_uptime_get_32 / k_busy_wait / printk.
457
708
  // <zephyr/drivers/gpio.h> for the gpio_pin_*_dt / gpio_dt_spec API.
458
709
  // <cstdint> because DIRECT_CPP_TYPE_MAP passes int32_t/uint8_t through
@@ -473,7 +724,7 @@ export class ZephyrStrategy {
473
724
  // passes int32_t/uint8_t through verbatim.
474
725
  const inc = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdint>'];
475
726
  // <cstdio> backs the printf family only: __tc_print/__tc_println (emitted
476
- // solely when @typecad/expect's preprocessor injected them — tracked via
727
+ // solely when the test-runner preprocessor injected them — tracked via
477
728
  // usedPolyfillHelpers), raw printf/snprintf in user code (usesCstdio), and
478
729
  // the fs/preferences/uart shims (their lowerings snprintf into buffers).
479
730
  // A program touching none of those needs no <cstdio>.
@@ -489,14 +740,30 @@ export class ZephyrStrategy {
489
740
  inc.push('<zephyr/drivers/sensor.h>');
490
741
  if (uses('usesSPI'))
491
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>');
492
765
  if (uses('usesUart'))
493
766
  inc.push('<zephyr/drivers/uart.h>');
494
- // uart.h is also needed by the printf-mode debug halt shim
495
- // (__tc_debug_wait_for_continue polls the console UART) even when the
496
- // program itself does not use the UART HAL. In gdb mode the shim is not
497
- // emitted, so skip the include there to avoid pulling in an unused header.
498
- if (isPrintf && !inc.includes('<zephyr/drivers/uart.h>'))
499
- inc.push('<zephyr/drivers/uart.h>');
500
767
  // USB CDC serial: the class instance is a UART device (uart.h); the
501
768
  // device context macros + usbd_* API live in the next-stack header.
502
769
  if (uses('usesUsb')) {
@@ -550,10 +817,11 @@ export class ZephyrStrategy {
550
817
  // HTTP/S client: Zephyr's http_client_req runs over a pre-connected socket,
551
818
  // so the shim pulls in the BSD socket + POSIX DNS surfaces alongside the
552
819
  // http client/parser headers. TLS sec tags need tls_credentials; <cstring>
553
- // /<cstdlib> back the shim's memcpy/strlen/new-nothrow usage (the core
554
- // shim only includes <cstdio>/<cstdint>).
820
+ // /<cstdlib>/<new> back the shim's memcpy/strlen/new-nothrow usage (the
821
+ // core shim only includes <cstdio>/<cstdint> — std::nothrow is not
822
+ // guaranteed to arrive transitively).
555
823
  if (uses('usesHttp'))
556
- inc.push('<zephyr/net/socket.h>', '<zephyr/net/http/client.h>', '<zephyr/net/http/parser.h>', '<zephyr/net/tls_credentials.h>', '<zephyr/posix/sys/socket.h>', '<cstring>', '<cstdlib>');
824
+ inc.push('<zephyr/net/socket.h>', '<zephyr/net/http/client.h>', '<zephyr/net/http/parser.h>', '<zephyr/net/tls_credentials.h>', '<zephyr/posix/sys/socket.h>', '<cstring>', '<cstdlib>', '<new>');
557
825
  // MQTT client: <zephyr/net/mqtt.h> for mqtt_connect/publish/subscribe, plus
558
826
  // <zephyr/net/socket.h> for the zsock_* poll/getaddrinfo API the shim's poll
559
827
  // thread uses, and <zephyr/net/tls_credentials.h> for the mqtts:// path
@@ -608,6 +876,10 @@ export class ZephyrStrategy {
608
876
  return;
609
877
  }
610
878
  for (const v of Object.values(node)) {
879
+ if (typeof v === 'string' && v.includes(TOKEN)) {
880
+ found = true;
881
+ return;
882
+ }
611
883
  if (Array.isArray(v)) {
612
884
  for (const item of v)
613
885
  visit(item);
@@ -620,9 +892,17 @@ export class ZephyrStrategy {
620
892
  return found;
621
893
  }
622
894
  /** Pins referenced by gpio.* hal-ops in the program IR. lowerGpio routes a
623
- * pin to its devicetree spec by pin NUMBER, so the structured hal-op pins
624
- * are the authoritative signal for which __tc_dt_* specs are needed —
625
- * regardless of when the final call text is rendered. */
895
+ * pin to its devicetree spec by pin NUMBER, so the structured hal-op pins
896
+ * are the authoritative signal for which __tc_dt_* specs are needed —
897
+ * regardless of when the final call text is rendered.
898
+ *
899
+ * A gpio op interpolated into another HAL call's template literal
900
+ * (`USB0.writeLine(\`led: ${led.get()}\`)`) never becomes an IR node —
901
+ * resolveHALExprToText lowers it to C++ text while building the IR and bakes
902
+ * it into an __EMIT__ snprintf prelude, so the walk cannot see its pin. The
903
+ * spec NAME it references is still in that text; collectRawMatches finds it
904
+ * there, or the spec goes undeclared and the build fails on `__tc_dt_led0
905
+ * was not declared in this scope`. */
626
906
  collectGpioPinUsage(program) {
627
907
  const pins = new Set();
628
908
  if (!program)
@@ -646,13 +926,18 @@ export class ZephyrStrategy {
646
926
  }
647
927
  };
648
928
  visit(program);
929
+ visitResolvedHalOps(program, visit);
649
930
  return pins;
650
931
  }
651
932
  /** Run `re` (global) against every raw string in the IR — raw expression
652
- * values plus raw hal-op codes — returning capture group 1 of each match
653
- * (the full match when the regex has no group). This is how references the
654
- * text scanners must see but that never appear as IR call nodes (e.g. a
655
- * rawCpp() escape hatch naming `__tc_dt_sw0` directly) are discovered. */
933
+ * values plus raw hal-op codes — returning capture group 1 of each match
934
+ * (the full match when the regex has no group). This is how references the
935
+ * text scanners must see but that never appear as IR call nodes are
936
+ * discovered: a rawCpp() escape hatch naming `__tc_dt_sw0` directly, and —
937
+ * the reason every string LEAF is scanned, not just raw nodes — the
938
+ * pre-lowered C++ text of a HAL call inlined into another HAL call's
939
+ * template literal, which the IR builder bakes into plain `string` payloads
940
+ * (__EMIT__ snprintf preludes, op data fields) rather than raw nodes. */
656
941
  collectRawMatches(program, re) {
657
942
  const found = new Set();
658
943
  if (!program)
@@ -664,14 +949,10 @@ export class ZephyrStrategy {
664
949
  const visit = (node) => {
665
950
  if (!node || typeof node !== 'object')
666
951
  return;
667
- if (node.kind === 'raw' && typeof node.value === 'string')
668
- scan(node.value);
669
- if (node.operation && typeof node.operation === 'object'
670
- && node.operation.operation === 'raw' && typeof node.operation.code === 'string') {
671
- scan(node.operation.code);
672
- }
673
952
  for (const v of Object.values(node)) {
674
- if (Array.isArray(v)) {
953
+ if (typeof v === 'string')
954
+ scan(v);
955
+ else if (Array.isArray(v)) {
675
956
  for (const item of v)
676
957
  visit(item);
677
958
  }
@@ -700,7 +981,6 @@ export class ZephyrStrategy {
700
981
  }
701
982
  shimLines(program, ctx) {
702
983
  const chip = this.resolveChip(ctx, program);
703
- const isPrintf = this.resolveDebugMode(ctx) === 'printf';
704
984
  const a = ctx?.analysis;
705
985
  const uses = (f) => (a ? !!a[f] : true);
706
986
  const helpers = a?.usedPolyfillHelpers;
@@ -764,7 +1044,7 @@ export class ZephyrStrategy {
764
1044
  if (entryHasUI()) {
765
1045
  guardBody.push('#ifndef PROGMEM', '#define PROGMEM', '#endif');
766
1046
  }
767
- // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
1047
+ // Test-runner console helpers: the test-runner's Zephyr shim calls these
768
1048
  // for protocol output. Overloaded for string (const char*) and numeric
769
1049
  // (double) so the same call site works for markers and test values.
770
1050
  // The fs lowering bakes __tc_println into its error paths too, so the
@@ -782,21 +1062,29 @@ export class ZephyrStrategy {
782
1062
  'static inline char* __tc_fmt_num_buf(double v, char* out, size_t cap) {',
783
1063
  ' if (v != v) { snprintf(out, cap, "nan"); return out; }',
784
1064
  ' double a = v < 0 ? -v : v;',
785
- ' long long ip = (long long)a;',
786
- ' 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);',
787
1067
  ' if (fr >= 1000000LL) { ip += 1LL; fr = 0LL; }',
788
1068
  ' int used = 0;',
789
1069
  ' if (v < 0 && (ip != 0LL || fr != 0LL)) { out[used++] = \'-\'; out[used] = \'\\0\'; }',
790
- ' 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; }',
791
1071
  ' char fbuf[8];',
792
1072
  ' int len = snprintf(fbuf, sizeof(fbuf), "%06lld", fr);',
793
1073
  ' while (len > 0 && fbuf[len - 1] == \'0\') { fbuf[--len] = \'\\0\'; }',
794
- ' 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);',
795
1075
  ' return out;',
796
1076
  '}',
797
1077
  'static void __tc_fmt_num(double v) { char __b[32]; printf("%s", __tc_fmt_num_buf(v, __b, sizeof(__b))); }',
798
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"); }');
799
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)));', '}');
800
1088
  // Serial-port write helper: writes a scalar to a UART/CDC device a byte at
801
1089
  // a time. Overloaded on const char* (strings, snprintf buffers) and double
802
1090
  // (numbers/booleans) so a single __tc_dev_put(dev, value) call site formats
@@ -812,12 +1100,29 @@ export class ZephyrStrategy {
812
1100
  // declared controller's __tc_<bus>N_dev trips -Wunused-variable under
813
1101
  // Zephyr's -Werror (collectUsedBusIndices; probes with no program emit all).
814
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);
815
1107
  if (uses('usesI2C') && chip.i2c) {
816
1108
  for (let i = 0; i < chip.i2c.controllers.length; i++) {
817
1109
  if (usedBuses && !usedBuses.i2c.has(i))
818
1110
  continue;
819
1111
  guardBody.push(...i2cInitLines(chip, i));
820
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
+ }
821
1126
  }
822
1127
  if (uses('usesSPI') && chip.spi) {
823
1128
  for (let i = 0; i < chip.spi.controllers.length; i++) {
@@ -826,6 +1131,79 @@ export class ZephyrStrategy {
826
1131
  guardBody.push(...spiInitLines(chip, i));
827
1132
  }
828
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
+ }
829
1207
  if (uses('usesUart') && chip.uart) {
830
1208
  for (let i = 0; i < chip.uart.controllers.length; i++) {
831
1209
  if (usedBuses && !usedBuses.uart.has(i))
@@ -848,7 +1226,7 @@ export class ZephyrStrategy {
848
1226
  const sensors = collectSensors(program);
849
1227
  if (sensors) {
850
1228
  for (const s of sensors.values()) {
851
- 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));
852
1230
  }
853
1231
  }
854
1232
  }
@@ -874,10 +1252,52 @@ export class ZephyrStrategy {
874
1252
  }
875
1253
  }
876
1254
  }
877
- if (uses('usesUsb') && chip.usb) {
878
- guardBody.push(...usbdDeviceLines(chip));
879
- for (let i = 0; i < chip.usb.cdcInstances; i++)
880
- 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
+ }
881
1301
  }
882
1302
  // PWM init also fires (with alias vars for the override pins) when a
883
1303
  // program drives ONLY inline-override channels on a chip with no pwm
@@ -953,13 +1373,15 @@ export class ZephyrStrategy {
953
1373
  lines.push('// cuttlefish runtime shim. Wrapped in a single include guard so the', '// block is safe to emit into multiple headers and .cpp files within', '// one translation unit (a .cpp may #include several headers that each', '// carry the shim). The guard ensures the definitions are seen exactly', '// once per TU.', '#ifndef CUTTLEFISH_SHIM_DEFINED', '#define CUTTLEFISH_SHIM_DEFINED', ...guardBody, '#endif // CUTTLEFISH_SHIM_DEFINED');
954
1374
  }
955
1375
  // Devicetree specs — one per board-defined GPIO pin, but ONLY for pins the
956
- // program actually addresses (lowerGpio routes by pin number, and the
957
- // structured gpio.* hal-op pins are visible here) plus aliases named
958
- // verbatim in raw code (rawCpp escape hatches). Emitted OUTSIDE the single
959
- // CUTTLEFISH_SHIM_DEFINED guard with a per-symbol guard: per-file pin sets
960
- // differ, and in a multi-header TU the first header's TU-wide guard would
961
- // otherwise hide the second header's specs. Without a program (capability
962
- // query), emit them all.
1376
+ // program actually addresses (lowerGpio routes by pin number; the
1377
+ // structured gpio.* hal-op pins plus the spec names appearing in any IR
1378
+ // string — including the pre-lowered text of a HAL call inlined into
1379
+ // another HAL call's template literal — are visible here) and aliases
1380
+ // named verbatim in raw code (rawCpp escape hatches). Emitted OUTSIDE the
1381
+ // single CUTTLEFISH_SHIM_DEFINED guard with a per-symbol guard: per-file
1382
+ // pin sets differ, and in a multi-header TU the first header's TU-wide
1383
+ // guard would otherwise hide the second header's specs. Without a program
1384
+ // (capability query), emit them all.
963
1385
  const usedPins = this.collectGpioPinUsage(program);
964
1386
  const dtTextRefs = this.collectRawMatches(program, /__tc_dt_([A-Za-z0-9_]+)/g);
965
1387
  for (const spec of chip.gpio.dtSpecs) {
@@ -968,32 +1390,6 @@ export class ZephyrStrategy {
968
1390
  const guard = `__TC_DT_${spec.dtSpec.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}_SPEC`;
969
1391
  lines.push(`#ifndef ${guard}`, `#define ${guard}`, `static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`, `#endif // ${guard}`);
970
1392
  }
971
- // --- Debug-mode halt + per-breakpoint disable registry ---
972
- //
973
- // Printf mode only. In gdb mode the cuttlefish debug preprocessor is
974
- // skipped (core emits #line markers + VS Code native breakpoints instead),
975
- // so __tc_debug_wait_for_continue is never called — skip the shim and its
976
- // <zephyr/drivers/uart.h> dependency entirely (forcedIncludes mirrors this).
977
- //
978
- // The cuttlefish debug preprocessor injects __tc_debug_wait_for_continue(id)
979
- // calls at each breakpoint; without these definitions the emitted code
980
- // would not link.
981
- //
982
- // Zephyr's minimal libc has no getchar()/EOF, so the halt polls the console
983
- // UART directly via uart_poll_in on the system console device, yielding to
984
- // the scheduler with k_msleep between polls so an unattended breakpoint
985
- // does not starve the system. ENTER (or any non-'s' byte) = continue;
986
- // 's'/'S' = skip this breakpoint for the rest of the run (records the id).
987
- if (isPrintf) {
988
- lines.push('#ifndef __TC_BP_DISABLED_DEFINED', '#define __TC_BP_DISABLED_DEFINED', 'static bool __tc_bp_disabled[256] = {0};', 'static inline bool __tc_bp_is_disabled(int id) { return id >= 0 && id < 256 && __tc_bp_disabled[id]; }',
989
- // Console input: poll the UART console device. DEVICE_DT_GET(DT_CHOSEN(zephyr_console))
990
- // resolves to the board's console (UART0 USB-CDC on the XIAO nRF52840).
991
- // Not every board DTS declares a zephyr,console chosen (STM32MP1 M-side,
992
- // display/carrier boards): there is nothing to print a prompt on and
993
- // nothing to read a key from, so breakpoints auto-continue instead of
994
- // hanging an unattended run.
995
- '#if DT_HAS_CHOSEN(zephyr_console)', 'static inline char __tc_debug_wait_for_continue(int id) {', ' const struct device* __con = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));', ' unsigned char __c = 0;', " while (uart_poll_in(__con, &__c) != 0) {", ' k_msleep(10);', ' }', " // Drain the rest of the typed line so the next breakpoint waits fresh.", " unsigned char __peek = 0;", " while (uart_poll_in(__con, &__peek) == 0 && __peek != '\\n') { (void)0; }", " if ((__c == 's') || (__c == 'S')) { if (id >= 0 && id < 256) __tc_bp_disabled[id] = true; }", ' return static_cast<char>(__c);', '}', '#else', 'static inline char __tc_debug_wait_for_continue(int id) { (void)id; return static_cast<char>(0); }', '#endif // DT_HAS_CHOSEN(zephyr_console)', '#endif // __TC_BP_DISABLED_DEFINED', '');
996
- }
997
1393
  // --- Zephyr entrypoint ---------------------------------------------------
998
1394
  // No bridge here: the synthesizer emits main() itself (it keys off
999
1395
  // entrypointFunctionName()="main" and requiresLoopFunction()=false), with
@@ -1023,7 +1419,11 @@ export class ZephyrStrategy {
1023
1419
  // precede the wiring-ambient shims and the PinGroup factory there; the
1024
1420
  // __tc_gpio_read body below is what still needs emitting here.)
1025
1421
  if (this.needsGpioReadShim(program, ctx)) {
1026
- 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); }');
1027
1427
  }
1028
1428
  // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
1029
1429
  // lowering, so they stay gated on it.
@@ -1556,10 +1956,13 @@ export class ZephyrStrategy {
1556
1956
  // ── Interrupt safety ─────────────────────────────────────────────────────
1557
1957
  // Zephyr ISRs run above thread context: anything that sleeps (k_msleep),
1558
1958
  // pends, or takes a driver lock is illegal there (asserted by the kernel in
1559
- // debug builds; corrupts scheduler state otherwise). The names below are the
1560
- // IR-level callees cuttlefish's interrupt-analysis pass matches (the same
1561
- // keys ArduinoStrategy uses; timing.delay/delay_microseconds hal-ops are
1562
- // mapped back to the bare names by the analyzer itself).
1959
+ // debug builds; corrupts scheduler state otherwise). The keys below are
1960
+ // matched by cuttlefish's interrupt-analysis pass in two shapes: bare/qualified
1961
+ // callee names (call statements) and resolved hal-op payloads — the analyzer
1962
+ // maps timing.delay/delay_microseconds/sleep/busy_wait_us ops back to the
1963
+ // bare timing keys, and matches the op payload's port/bus field (UART0,
1964
+ // I2C0, SPI0, …) against these keys directly. The same keys ArduinoStrategy
1965
+ // uses.
1563
1966
  isrUnsafeOperations() {
1564
1967
  return new Map([
1565
1968
  ['delay', {
@@ -1698,14 +2101,14 @@ export class ZephyrStrategy {
1698
2101
  catch (err) {
1699
2102
  // A malformed facts/as-built file is the USER's error — surface it
1700
2103
  // verbatim instead of the generic "cannot generate" below.
1701
- if (err instanceof Error && (err.message.includes('cuttlefish.facts.json') || err.message.includes('as-built.json')))
2104
+ if (err instanceof Error && (err.message.includes('typecad-hal.facts.json') || err.message.includes('as-built.json')))
1702
2105
  throw err;
1703
2106
  return undefined;
1704
2107
  }
1705
2108
  }
1706
2109
  /**
1707
2110
  * Regenerate the board catalog overlay from the user's own Zephyr tree —
1708
- * `cuttlefish board sync`. After a `west update`, this is how new/changed/
2111
+ * `typecad-hal board sync`. After a `west update`, this is how new/changed/
1709
2112
  * removed boards reach projects without a cuttlefish release. See
1710
2113
  * src/sdk/board-catalog-sync.ts.
1711
2114
  */
@@ -1714,7 +2117,7 @@ export class ZephyrStrategy {
1714
2117
  }
1715
2118
  /**
1716
2119
  * Refresh the overlay only when it is stale (provenance no longer matches
1717
- * the tree) — the pre-step `cuttlefish board regen` runs so a regen after
2120
+ * the tree) — the pre-step `typecad-hal board regen` runs so a regen after
1718
2121
  * `west update` picks up the tree's boards automatically. Tree walk only
1719
2122
  * happens when there is actual refreshing to do.
1720
2123
  */
@@ -1774,6 +2177,17 @@ export class ZephyrStrategy {
1774
2177
  postIteration: 'k_msleep(1);',
1775
2178
  };
1776
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
+ }
1777
2191
  // ── Type aliases ────────────────────────────────────────────────────────
1778
2192
  shouldSkipTypeAlias() {
1779
2193
  return false;
@@ -1899,23 +2313,23 @@ export class ZephyrStrategy {
1899
2313
  requiredIncludes: ['<cstring>'],
1900
2314
  forwardDeclarations: [],
1901
2315
  helperStructs: [],
1902
- helperFunctions: [`
1903
- // TypeCAD string method polyfills (Zephyr, minimal-libc).
1904
- #ifndef CUTTLEFISH_STR_BUF_SIZE
1905
- #define CUTTLEFISH_STR_BUF_SIZE 64
1906
- #endif
1907
- bool __tc_endsWith(const char* s, const char* suffix) { int sl = strlen(s), tl = strlen(suffix); return sl >= tl && strcmp(s + sl - tl, suffix) == 0; }
1908
- const char* __tc_toUpperCase(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; for (char* p = b; *p; p++) { if (*p >= 'a' && *p <= 'z') { *p = static_cast<char>(*p - 32); } } return b; }
1909
- const char* __tc_toLowerCase(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; for (char* p = b; *p; p++) { if (*p >= 'A' && *p <= 'Z') { *p = static_cast<char>(*p + 32); } } return b; }
1910
- const char* __tc_trim(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; while (*s == ' ' || *s == '\\t' || *s == '\\n' || *s == '\\r') s++; int len = strlen(s); while (len > 0 && (s[len-1] == ' ' || s[len-1] == '\\t' || s[len-1] == '\\n' || s[len-1] == '\\r')) len--; int cplen = len < CUTTLEFISH_STR_BUF_SIZE - 1 ? len : CUTTLEFISH_STR_BUF_SIZE - 1; strncpy(b, s, cplen); b[cplen] = '\\0'; return b; }
1911
- const char* __tc_substring2(const char* s, int start, int end) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; int slen = strlen(s); if (start < 0) start = 0; if (end > slen) end = slen; if (end < start) end = start; int len = end - start; if (len >= CUTTLEFISH_STR_BUF_SIZE) len = CUTTLEFISH_STR_BUF_SIZE - 1; strncpy(b, s + start, len); b[len] = '\\0'; return b; }
1912
- const char* __tc_substring1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
1913
- const char* __tc_slice2(const char* s, int start, int end) { return __tc_substring2(s, start, end); }
1914
- const char* __tc_slice1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
1915
- const char* __tc_replace(const char* s, const char* old, const char* repl) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; const char* pos = strstr(s, old); if (!pos) { strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; return b; } int beforeLen = static_cast<int>(pos - s); int oldLen = static_cast<int>(strlen(old)); int replLen = static_cast<int>(strlen(repl)); if (beforeLen + replLen + static_cast<int>(strlen(pos + oldLen)) >= CUTTLEFISH_STR_BUF_SIZE) { strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; return b; } memcpy(b, s, beforeLen); memcpy(b + beforeLen, repl, replLen); strcpy(b + beforeLen + replLen, pos + oldLen); return b; }
1916
- const char* __tc_charAt(const char* s, int idx) { static char buf[2][2]; static uint8_t slot = 0; slot ^= 1; buf[slot][0] = s[idx]; buf[slot][1] = '\\0'; return buf[slot]; }
1917
- int __tc_charCodeAt(const char* s, int idx) { return static_cast<int>(static_cast<unsigned char>(s[idx])); }
1918
- int __tc_indexOf(const char* s, const char* needle) { const char* p = strstr(s, needle); return p ? static_cast<int>(p - s) : -1; }
2316
+ helperFunctions: [`
2317
+ // TypeCAD string method polyfills (Zephyr, minimal-libc).
2318
+ #ifndef CUTTLEFISH_STR_BUF_SIZE
2319
+ #define CUTTLEFISH_STR_BUF_SIZE 64
2320
+ #endif
2321
+ bool __tc_endsWith(const char* s, const char* suffix) { int sl = strlen(s), tl = strlen(suffix); return sl >= tl && strcmp(s + sl - tl, suffix) == 0; }
2322
+ const char* __tc_toUpperCase(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; for (char* p = b; *p; p++) { if (*p >= 'a' && *p <= 'z') { *p = static_cast<char>(*p - 32); } } return b; }
2323
+ const char* __tc_toLowerCase(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; for (char* p = b; *p; p++) { if (*p >= 'A' && *p <= 'Z') { *p = static_cast<char>(*p + 32); } } return b; }
2324
+ const char* __tc_trim(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; while (*s == ' ' || *s == '\\t' || *s == '\\n' || *s == '\\r') s++; int len = strlen(s); while (len > 0 && (s[len-1] == ' ' || s[len-1] == '\\t' || s[len-1] == '\\n' || s[len-1] == '\\r')) len--; int cplen = len < CUTTLEFISH_STR_BUF_SIZE - 1 ? len : CUTTLEFISH_STR_BUF_SIZE - 1; strncpy(b, s, cplen); b[cplen] = '\\0'; return b; }
2325
+ const char* __tc_substring2(const char* s, int start, int end) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; int slen = strlen(s); if (start < 0) start = 0; if (end > slen) end = slen; if (end < start) end = start; int len = end - start; if (len >= CUTTLEFISH_STR_BUF_SIZE) len = CUTTLEFISH_STR_BUF_SIZE - 1; strncpy(b, s + start, len); b[len] = '\\0'; return b; }
2326
+ const char* __tc_substring1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
2327
+ const char* __tc_slice2(const char* s, int start, int end) { return __tc_substring2(s, start, end); }
2328
+ const char* __tc_slice1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
2329
+ const char* __tc_replace(const char* s, const char* old, const char* repl) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; const char* pos = strstr(s, old); if (!pos) { strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; return b; } int beforeLen = static_cast<int>(pos - s); int oldLen = static_cast<int>(strlen(old)); int replLen = static_cast<int>(strlen(repl)); if (beforeLen + replLen + static_cast<int>(strlen(pos + oldLen)) >= CUTTLEFISH_STR_BUF_SIZE) { strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; return b; } memcpy(b, s, beforeLen); memcpy(b + beforeLen, repl, replLen); strcpy(b + beforeLen + replLen, pos + oldLen); return b; }
2330
+ const char* __tc_charAt(const char* s, int idx) { static char buf[2][2]; static uint8_t slot = 0; slot ^= 1; buf[slot][0] = s[idx]; buf[slot][1] = '\\0'; return buf[slot]; }
2331
+ int __tc_charCodeAt(const char* s, int idx) { return static_cast<int>(static_cast<unsigned char>(s[idx])); }
2332
+ int __tc_indexOf(const char* s, const char* needle) { const char* p = strstr(s, needle); return p ? static_cast<int>(p - s) : -1; }
1919
2333
  `],
1920
2334
  shimMacros: [],
1921
2335
  dependencies: [],
@@ -1931,27 +2345,27 @@ int __tc_indexOf(const char* s, const char* needle) { const char* p = strstr(s,
1931
2345
  requiredIncludes: [],
1932
2346
  forwardDeclarations: [],
1933
2347
  helperStructs: [],
1934
- helperFunctions: [`
1935
- #ifndef __TC_STATIC_ARRAY_DEFINED
1936
- #define __TC_STATIC_ARRAY_DEFINED
1937
- template<typename T, int N>
1938
- struct __tc_StaticArray {
1939
- T data[N];
1940
- int _size;
1941
- __tc_StaticArray() : _size(0) {}
1942
- int length() const { return _size; }
1943
- int size() const { return _size; }
1944
- void push(T val) { if (_size < N) data[_size++] = val; }
1945
- T pop() { return (_size > 0) ? data[--_size] : T(); }
1946
- int indexOf(T val) const { for (int i = 0; i < _size; i++) if (data[i] == val) return i; return -1; }
1947
- T& operator[](int i) { return data[i]; }
1948
- const T& operator[](int i) const { return data[i]; }
1949
- T* begin() { return &data[0]; }
1950
- T* end() { return &data[_size]; }
1951
- const T* begin() const { return &data[0]; }
1952
- const T* end() const { return &data[_size]; }
1953
- };
1954
- #endif
2348
+ helperFunctions: [`
2349
+ #ifndef __TC_STATIC_ARRAY_DEFINED
2350
+ #define __TC_STATIC_ARRAY_DEFINED
2351
+ template<typename T, int N>
2352
+ struct __tc_StaticArray {
2353
+ T data[N];
2354
+ int _size;
2355
+ __tc_StaticArray() : _size(0) {}
2356
+ int length() const { return _size; }
2357
+ int size() const { return _size; }
2358
+ void push(T val) { if (_size < N) data[_size++] = val; }
2359
+ T pop() { return (_size > 0) ? data[--_size] : T(); }
2360
+ int indexOf(T val) const { for (int i = 0; i < _size; i++) if (data[i] == val) return i; return -1; }
2361
+ T& operator[](int i) { return data[i]; }
2362
+ const T& operator[](int i) const { return data[i]; }
2363
+ T* begin() { return &data[0]; }
2364
+ T* end() { return &data[_size]; }
2365
+ const T* begin() const { return &data[0]; }
2366
+ const T* end() const { return &data[_size]; }
2367
+ };
2368
+ #endif
1955
2369
  `],
1956
2370
  shimMacros: [],
1957
2371
  dependencies: [],
@@ -2008,9 +2422,14 @@ struct __tc_StaticArray {
2008
2422
  writeDigitalPin(pin, val) {
2009
2423
  return `__tc_gpio_write(${pin}, ${val})`;
2010
2424
  }
2011
- setPinMode(_pin, _mode) {
2012
- // Zephyr configures pin direction via devicetree, not a runtime pinMode.
2013
- 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}));`;
2014
2433
  }
2015
2434
  delayMs(ms) {
2016
2435
  return `k_msleep(${ms})`;
@@ -2046,16 +2465,31 @@ struct __tc_StaticArray {
2046
2465
  return resolveZephyrDisplayOp(op, this._displayState);
2047
2466
  }
2048
2467
  supportedDisplayDrivers() {
2049
- 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;
2050
2481
  }
2051
2482
  // ── Strategy-owned display/touch adapter seam ────────────────────────────
2052
2483
  // Zephyr owns its display + touch adapters: the UI display adapter bridges
2053
- // the in-tree CuttlefishGFX class to the panel (per-controller init + wire
2054
- // format, see src/display/ui-adapter.ts), and the touch adapters drive the
2055
- // FT6336U (I2C capacitive) and XPT2046 (SPI resistive) controllers via
2056
- // Zephyr's bus APIs (src/display/touch-adapter.ts). Both live in this
2057
- // package so cuttlefish carries no Zephyr/Wiring-specific display or touch
2058
- // 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.
2059
2493
  providesDisplayAdapter() { return true; }
2060
2494
  resolveDisplayAdapter(display) {
2061
2495
  const code = zephyrDisplayAdapterGenerator(display);
@@ -2074,7 +2508,30 @@ struct __tc_StaticArray {
2074
2508
  return new Map(Object.entries(BUILT_IN_PROFILES));
2075
2509
  }
2076
2510
  colorFormat() {
2077
- 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;
2078
2535
  }
2079
2536
  graphicsCapacity() {
2080
2537
  return {
@@ -2085,43 +2542,43 @@ struct __tc_StaticArray {
2085
2542
  };
2086
2543
  }
2087
2544
  // ── Debug ─────────────────────────────────────────────────────────────────
2088
- // Zephyr's minimal C++ config has no <iostream>, so the GenericStrategy
2089
- // std::cout fallback the debug preprocessor uses by default would NOT
2090
- // compile. Override the debug surface to route through printk (always
2091
- // available, no CONFIG_CONSOLE dependency) and the __tc_debug_wait_for_continue
2092
- // halt emitted in shimLines. See src/debug-codegen.ts.
2093
- //
2094
- // Target-selective: targets with a debug probe get native GDB source-level
2095
- // debugging (core emits #line markers + skips printf instrumentation); the
2096
- // rest fall back to the printk instrumentation path. The ESP32-S3 has a
2097
- // built-in USB-JTAG (single-cable GDB via OpenOCD) so it selects 'gdb'.
2098
- // The XIAO nRF52840 needs its J-Link wired up; its GDB path is a follow-on,
2099
- // so it stays on printf for now.
2545
+ // Source-level debugging is the GDB flow only (the west-owned gdb server
2546
+ // serves F5 via cortex-debug's external-server mode — see
2547
+ // toolchain/debug-config.ts). The printf/Serial breakpoint instrumentation
2548
+ // pipeline was removed from the product.
2100
2549
  debugMode(target) {
2101
- // `target` is the Zephyr board id (optionally with a /qualifier suffix,
2102
- // e.g. 'esp32s3_devkitc/esp32s3/procpu'). Match on the bare board id.
2103
- const boardId = (target ?? '').split('/')[0];
2104
- if (boardId === 'esp32s3_devkitc' || boardId.startsWith('esp32s3')) {
2105
- return 'gdb';
2106
- }
2107
- // The STM32 Black Pill ships a verified ST-Link probe method in its board
2108
- // package (openocd runner over SWD, with the reset_config quirk for the
2109
- // unwired SRST line), so F5 attaches natively out of the box.
2110
- if (boardId.startsWith('blackpill_')) {
2111
- return 'gdb';
2112
- }
2113
- // The plain ESP32 (esp32_devkitc) intentionally stays on 'printf': unlike
2114
- // the S3 it has NO built-in USB-JTAG, so gdb needs an external ESP-PROG
2115
- // probe + a different OpenOCD cfg/toolchain dir (deferred). Falls through.
2116
- return 'printf';
2117
- }
2118
- generateDebugInitCode() {
2119
- return generateZephyrInitCode();
2120
- }
2121
- generateDebugBreakpointCode(params) {
2122
- return generateZephyrBreakpointCode(params.fileName, params.lineNum, params.originalLine, params.variables, params.normalizedCondition, params.breakpointId);
2123
- }
2124
- generateDebugLogpointCode(params) {
2125
- return generateZephyrLogpointCode(params.fileName, params.lineNum, params.parts, params.variables);
2550
+ // `target` is the Zephyr board id (optionally with a /qualifier suffix).
2551
+ // gdb capability is a FACT of the board: its probe-method table carries a
2552
+ // debug-capable entry (bootloaders mark debug:false; SWD/JTAG probes and
2553
+ // built-in USB-JTAG carry debug:true) — the same table west's runner
2554
+ // selection uses. Boards without a table (or unresolvable targets, e.g.
2555
+ // generated custom boards) report 'none', and --debug fails with an
2556
+ // explicit diagnostic (transpile.ts) rather than building silently.
2557
+ const t = (target ?? '').trim();
2558
+ if (!t)
2559
+ return 'none';
2560
+ const memo = debugModeMemo.get(t);
2561
+ if (memo)
2562
+ return memo;
2563
+ let mode = 'none';
2564
+ try {
2565
+ const g = generateBoard(t);
2566
+ const constants = JSON.parse(g.boardJson).constants;
2567
+ for (let i = 0;; i++) {
2568
+ const id = constants[`zephyr.probeMethods.${i}.id`];
2569
+ if (typeof id !== 'string')
2570
+ break;
2571
+ if (constants[`zephyr.probeMethods.${i}.debug`] !== false) {
2572
+ mode = 'gdb';
2573
+ break;
2574
+ }
2575
+ }
2576
+ }
2577
+ catch {
2578
+ // Board not resolvable from facts — 'none' (never crash codegen over
2579
+ // the debug-capability gate).
2580
+ }
2581
+ debugModeMemo.set(t, mode);
2582
+ return mode;
2126
2583
  }
2127
2584
  }