@typecad/framework-zephyr 1.0.0-alpha.12 → 1.0.0-alpha.14

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 (67) hide show
  1. package/README.md +13 -1
  2. package/dist/chips/controllers.d.ts +28 -8
  3. package/dist/chips/controllers.js +49 -12
  4. package/dist/chips/resolve.js +32 -6
  5. package/dist/chips/types.d.ts +63 -12
  6. package/dist/chips/xiao-ble.js +12 -0
  7. package/dist/display/index.d.ts +1 -1
  8. package/dist/display/index.js +1 -1
  9. package/dist/display/profiles.d.ts +8 -0
  10. package/dist/display/profiles.js +19 -0
  11. package/dist/display/ui-adapter.d.ts +4 -0
  12. package/dist/display/ui-adapter.js +46 -0
  13. package/dist/dt-config/kconfig.d.ts +11 -0
  14. package/dist/dt-config/kconfig.js +1 -1
  15. package/dist/dt-config/overlay.d.ts +8 -1
  16. package/dist/dt-config/overlay.js +104 -1
  17. package/dist/framework.manifest.d.ts +20 -30
  18. package/dist/framework.manifest.js +12 -3
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +5 -0
  21. package/dist/lowering/adc.d.ts +7 -4
  22. package/dist/lowering/adc.js +25 -11
  23. package/dist/lowering/gpio.js +15 -8
  24. package/dist/lowering/mqtt.js +9 -1
  25. package/dist/lowering/pulse.js +7 -7
  26. package/dist/lowering/pwm.d.ts +21 -3
  27. package/dist/lowering/pwm.js +28 -4
  28. package/dist/lowering/spi.js +2 -2
  29. package/dist/lowering/tone.js +3 -2
  30. package/dist/lowering/wifi.js +28 -5
  31. package/dist/strategy.d.ts +20 -0
  32. package/dist/strategy.js +295 -119
  33. package/dist/toolchain/debug-config.d.ts +43 -2
  34. package/dist/toolchain/debug-config.js +129 -17
  35. package/dist/toolchain/index.d.ts +14 -1
  36. package/dist/toolchain/index.js +146 -20
  37. package/dist/toolchain/scaffold.d.ts +9 -0
  38. package/dist/toolchain/scaffold.js +84 -19
  39. package/dist/toolchain/west-discover.d.ts +4 -1
  40. package/dist/toolchain/west-discover.js +2 -0
  41. package/dist/toolchain/west-spawn.js +17 -5
  42. package/package.json +5 -5
  43. package/src/chips/controllers.ts +61 -12
  44. package/src/chips/resolve.ts +32 -5
  45. package/src/chips/types.ts +63 -12
  46. package/src/chips/xiao-ble.ts +82 -70
  47. package/src/display/index.ts +1 -1
  48. package/src/display/profiles.ts +23 -0
  49. package/src/display/ui-adapter.ts +51 -0
  50. package/src/dt-config/kconfig.ts +12 -1
  51. package/src/dt-config/overlay.ts +123 -0
  52. package/src/framework.manifest.ts +12 -3
  53. package/src/index.ts +6 -0
  54. package/src/lowering/adc.ts +28 -12
  55. package/src/lowering/gpio.ts +15 -8
  56. package/src/lowering/mqtt.ts +9 -1
  57. package/src/lowering/pulse.ts +7 -7
  58. package/src/lowering/pwm.ts +29 -4
  59. package/src/lowering/spi.ts +2 -2
  60. package/src/lowering/tone.ts +3 -3
  61. package/src/lowering/wifi.ts +29 -5
  62. package/src/strategy.ts +320 -123
  63. package/src/toolchain/debug-config.ts +137 -14
  64. package/src/toolchain/index.ts +645 -513
  65. package/src/toolchain/scaffold.ts +81 -17
  66. package/src/toolchain/west-discover.ts +321 -316
  67. package/src/toolchain/west-spawn.ts +17 -5
package/src/strategy.ts CHANGED
@@ -42,6 +42,54 @@ import { entryHasUI } from '@typecad/cuttlefish/ui-hook';
42
42
  import { chipForTarget, setActiveChip, getActiveChip } from './chips/index.js';
43
43
  import { resolveChipFromBoard } from './chips/resolve.js';
44
44
  import { emitGpioDevDispatcher } from './chips/controllers.js';
45
+ import type { ZephyrChipDescriptor } from './chips/types.js';
46
+
47
+ /**
48
+ * Deep-walk the program IR and collect the HAL pin numbers the program
49
+ * actually touches for a peripheral family ('adc' | 'pwm') — the same walk
50
+ * profileDiagnostics does. Emit paths gate per-channel state on these sets
51
+ * so nothing unused reaches the single generated TU (-Wunused-function
52
+ * hygiene: every emitted function/variable is referenced). `undefined`
53
+ * (no program — probe paths) means "no information": callers emit every
54
+ * descriptor channel, preserving probe behavior.
55
+ *
56
+ * `pwm` also covers tone.* — the tone lowering drives the descriptor's
57
+ * first PWM spec regardless of pin, so any tone op marks it used.
58
+ */
59
+ function collectUsedPins(
60
+ program: ProgramIR | undefined,
61
+ kind: 'adc' | 'pwm',
62
+ chip?: ZephyrChipDescriptor,
63
+ ): Set<number> | undefined {
64
+ if (!program) return undefined;
65
+ const pins = new Set<number>();
66
+ let usesTone = false;
67
+ const visit = (node: unknown): void => {
68
+ if (!node || typeof node !== 'object') return;
69
+ const n = node as Record<string, unknown>;
70
+ const op = n.operation;
71
+ if (op && typeof op === 'object') {
72
+ const o = op as Record<string, unknown>;
73
+ const name = o.operation;
74
+ const pin = o.pin;
75
+ if (typeof name === 'string' && typeof pin === 'number') {
76
+ if (kind === 'adc' && (name === 'adc.read' || name === 'adc.read_voltage')) pins.add(pin);
77
+ if (kind === 'pwm' && name.startsWith('pwm.')) pins.add(pin);
78
+ }
79
+ if (kind === 'pwm' && typeof name === 'string' && name.startsWith('tone.')) usesTone = true;
80
+ }
81
+ for (const v of Object.values(n)) {
82
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
83
+ else if (v && typeof v === 'object') visit(v);
84
+ }
85
+ };
86
+ visit(program);
87
+ if (kind === 'pwm' && usesTone) {
88
+ const first = chip?.pwm?.specs[0];
89
+ if (first) pins.add(first.pin);
90
+ }
91
+ return pins;
92
+ }
45
93
  import { lowerHalOp } from './lowering/index.js';
46
94
  import { buildZephyrWorkerBacking } from './lowering/worker-backing.js';
47
95
  import { adcInitLines } from './lowering/adc.js';
@@ -65,7 +113,7 @@ import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
65
113
  import { buildTimerPolyfill } from './async/timer-polyfill.js';
66
114
  import { resolveZephyrDisplayOp, newDisplayState, type DisplayState } from './display/index.js';
67
115
  import { buildDisplayRuntime } from './display/gfx.js';
68
- import { ZEPHYR_DISPLAY_PROFILES } from './display/profiles.js';
116
+ import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES } from './display/profiles.js';
69
117
  import { zephyrDisplayAdapterGenerator } from './display/ui-adapter.js';
70
118
  import { zephyrTouchAdapter } from './display/touch-adapter.js';
71
119
 
@@ -131,7 +179,22 @@ export class ZephyrStrategy implements PlatformStrategy {
131
179
  // true so nothing is stripped — mirrors framework-esp32's forcedIncludes.
132
180
  const a = (ctx as any)?.analysis;
133
181
  const uses = (f: string): boolean => (a ? !!a[f] : true);
134
- const inc: string[] = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdio>', '<cstdint>'];
182
+ // <zephyr/drivers/gpio.h> and <cstdint> stay unconditional: gpio.h is
183
+ // cross-cutting (gpio/power/interrupt/spi/pulse lowerings + the DT-spec
184
+ // machinery all reference its API, and no single usesX flag owns it), and
185
+ // the fixed-width types come via <zephyr/kernel.h> regardless — DIRECT_CPP_TYPE_MAP
186
+ // passes int32_t/uint8_t through verbatim.
187
+ const inc: string[] = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdint>'];
188
+ // <cstdio> backs the printf family only: __tc_print/__tc_println (emitted
189
+ // solely when @typecad/expect's preprocessor injected them — tracked via
190
+ // usedPolyfillHelpers), raw printf/snprintf in user code (usesCstdio), and
191
+ // the fs/preferences/uart shims (their lowerings snprintf into buffers).
192
+ // A program touching none of those needs no <cstdio>.
193
+ const helpers = (a as { usedPolyfillHelpers?: Set<string> } | undefined)?.usedPolyfillHelpers;
194
+ const needsCstdio = uses('usesCstdio') || uses('usesFS') || uses('usesPreferences')
195
+ || uses('usesUart')
196
+ || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println');
197
+ if (needsCstdio) inc.push('<cstdio>');
135
198
  if (uses('usesI2C')) inc.push('<zephyr/drivers/i2c.h>');
136
199
  if (uses('usesSPI')) inc.push('<zephyr/drivers/spi.h>');
137
200
  if (uses('usesUart')) inc.push('<zephyr/drivers/uart.h>');
@@ -247,77 +310,176 @@ export class ZephyrStrategy implements PlatformStrategy {
247
310
  return found;
248
311
  }
249
312
 
313
+ /** Pins referenced by gpio.* hal-ops in the program IR. lowerGpio routes a
314
+ * pin to its devicetree spec by pin NUMBER, so the structured hal-op pins
315
+ * are the authoritative signal for which __tc_dt_* specs are needed —
316
+ * regardless of when the final call text is rendered. */
317
+ private collectGpioPinUsage(program?: ProgramIR): Set<number> {
318
+ const pins = new Set<number>();
319
+ if (!program) return pins;
320
+ const visit = (node: any): void => {
321
+ if (!node || typeof node !== 'object') return;
322
+ if (node.operation && typeof node.operation === 'object'
323
+ && typeof node.operation.operation === 'string'
324
+ && node.operation.operation.startsWith('gpio.')
325
+ && typeof node.operation.pin === 'number') {
326
+ pins.add(node.operation.pin);
327
+ }
328
+ for (const v of Object.values(node)) {
329
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
330
+ else if (v && typeof v === 'object') visit(v);
331
+ }
332
+ };
333
+ visit(program);
334
+ return pins;
335
+ }
336
+
337
+ /** Run `re` (global) against every raw string in the IR — raw expression
338
+ * values plus raw hal-op codes — returning capture group 1 of each match
339
+ * (the full match when the regex has no group). This is how references the
340
+ * text scanners must see but that never appear as IR call nodes (e.g. a
341
+ * rawCpp() escape hatch naming `__tc_dt_sw0` directly) are discovered. */
342
+ private collectRawMatches(program: ProgramIR | undefined, re: RegExp): Set<string> {
343
+ const found = new Set<string>();
344
+ if (!program) return found;
345
+ const scan = (text: string): void => {
346
+ for (const m of text.matchAll(re)) found.add(m[1] ?? m[0]);
347
+ };
348
+ const visit = (node: any): void => {
349
+ if (!node || typeof node !== 'object') return;
350
+ if (node.kind === 'raw' && typeof node.value === 'string') scan(node.value);
351
+ if (node.operation && typeof node.operation === 'object'
352
+ && node.operation.operation === 'raw' && typeof node.operation.code === 'string') {
353
+ scan(node.operation.code);
354
+ }
355
+ for (const v of Object.values(node)) {
356
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
357
+ else if (v && typeof v === 'object') visit(v);
358
+ }
359
+ };
360
+ visit(program);
361
+ return found;
362
+ }
363
+
364
+ /** Whether the wiring-compat GPIO read surface (__tc_gpio_read definition,
365
+ * __tc_gpio_dev dispatcher, and the wiring_compat polyfill's digitalRead /
366
+ * HIGH / LOW macros) must be emitted. Consumers: user digitalRead() calls
367
+ * (usesDigitalRead), the @typecad/safety voter (calls __tc_gpio_read
368
+ * directly via lowered raw text), and the UI runtime header's
369
+ * unconditional digitalRead() poll (entryHasUI — build-global, so every TU
370
+ * in a UI build carries the macros). With no analysis present (capability
371
+ * query), default to emitting — same convention as the uses() helper. */
372
+ private needsGpioReadShim(program?: ProgramIR, ctx?: PlatformContext): boolean {
373
+ if (program && programUsesSafety(program)) return true;
374
+ if (entryHasUI()) return true;
375
+ const a = (ctx as any)?.analysis;
376
+ return a ? !!a.usesDigitalRead : true;
377
+ }
378
+
250
379
  shimLines(program?: ProgramIR, ctx?: PlatformContext): string[] {
251
380
  const chip = this.resolveChip(ctx, program);
252
381
  const isPrintf = this.resolveDebugMode(ctx) === 'printf';
253
- const lines: string[] = [
254
- '// cuttlefish runtime shim. Wrapped in a single include guard so the',
255
- '// block is safe to emit into multiple headers and .cpp files within',
256
- '// one translation unit (a .cpp may #include several headers that each',
257
- '// carry the shim). The guard ensures the definitions are seen exactly',
258
- '// once per TU.',
259
- '#ifndef CUTTLEFISH_SHIM_DEFINED',
260
- '#define CUTTLEFISH_SHIM_DEFINED',
261
- '#ifndef CUTTLEFISH_UNDEFINED',
262
- '#define CUTTLEFISH_UNDEFINED 0',
263
- '#endif',
264
- 'template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }',
265
- 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }',
266
- 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }',
267
- 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }',
268
- 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }',
269
- 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }',
270
- 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }',
271
- 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }',
272
- // millis() backed by the Zephyr uptime counter. uint32_t return matches
273
- // the Arduino API the shared runtime expects (wraps every ~49.7 days).
274
- 'inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }',
275
- // Arduino-compat defines referenced by the shared runtime polyfills.
276
- '#ifndef HIGH', '#define HIGH 1', '#endif',
277
- '#ifndef LOW', '#define LOW 0', '#endif',
278
- '#ifndef PROGMEM', '#define PROGMEM', '#endif',
279
- 'inline long map(long x, long in_min, long in_max, long out_min, long out_max) { return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; }',
280
- 'inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }',
281
- // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
282
- // for protocol output. Overloaded for string (const char*) and numeric
283
- // (double) so the same call site works for markers and test values.
284
- 'inline void __tc_print(const char* s) { printf("%s", s); }',
285
- 'inline void __tc_print(double v) { printf("%g", v); }',
286
- 'inline void __tc_println(const char* s) { printf("%s\\n", s); }',
287
- 'inline void __tc_println(double v) { printf("%g\\n", v); }',
288
- ];
289
-
290
- // Devicetree specs for every board-defined GPIO pin. Emitted unconditionally
291
- // (guarded by the include guard) so any of them is available whether or not
292
- // a given program uses it. Safe because every spec references a node that
293
- // exists in the active board's devicetree.
294
- for (const spec of chip.gpio.dtSpecs) {
295
- lines.push(
296
- `static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`,
382
+ const a = (ctx as any)?.analysis;
383
+ const uses = (f: string): boolean => (a ? !!a[f] : true);
384
+ const helpers = (a as { usedPolyfillHelpers?: Set<string> } | undefined)?.usedPolyfillHelpers;
385
+
386
+ // --- Core shim, gated item by item on actual use ------------------------
387
+ // A minimal program (blink) uses none of these, and its output carries no
388
+ // shim block at all. Everything up to the #endif composes into one guard
389
+ // body; the guard itself is only stamped when the body is non-empty.
390
+ const guardBody: string[] = [];
391
+ // CUTTLEFISH_UNDEFINED: needed when the file references null/undefined
392
+ // literals (usesNullish), emits nullish helper CALLS (usesNullishHelper),
393
+ // or has async functions (the async state machine uses the macro for
394
+ // default waitFor* timeouts not visible to the nullish scanners).
395
+ if (uses('usesNullish') || uses('usesNullishHelper') || uses('hasAsync')) {
396
+ guardBody.push(
397
+ '#ifndef CUTTLEFISH_UNDEFINED',
398
+ '#define CUTTLEFISH_UNDEFINED 0',
399
+ '#endif',
400
+ );
401
+ }
402
+ // Nullish helpers: only when the file actually emits cuttlefish_nullish /
403
+ // cuttlefish_exists CALLS (?? / ?. lowering). A file that only references
404
+ // null/undefined literals needs just the macro above — the same
405
+ // distinction the setup emitter's strip filter documents.
406
+ if (uses('usesNullishHelper')) {
407
+ guardBody.push(
408
+ 'template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }',
409
+ 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }',
410
+ 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }',
411
+ 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }',
412
+ 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }',
413
+ 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }',
414
+ 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }',
415
+ 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }',
416
+ );
417
+ }
418
+ // millis() backed by the Zephyr uptime counter. uint32_t return matches
419
+ // the Arduino API the shared runtime expects (wraps every ~49.7 days).
420
+ // Kept when the program reads the clock itself usesWallClock,
421
+ // deliberately WITHOUT the delay() conflation usesMillis carries, because
422
+ // Zephyr's delay lowers straight to k_msleep — or has a hidden poller:
423
+ // async functions / the async runtime, the setInterval/setTimeout
424
+ // scheduler, or a mounted UI's per-frame tick.
425
+ if (uses('usesWallClock') || uses('hasAsync') || (!a || a.timerCallCount > 0)
426
+ || this.programUsesAsyncRuntime(program) || entryHasUI()) {
427
+ guardBody.push(
428
+ 'inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }',
429
+ );
430
+ }
431
+ // PROGMEM: only the (Arduino-oriented) UI runtime header can reference it.
432
+ if (entryHasUI()) {
433
+ guardBody.push(
434
+ '#ifndef PROGMEM', '#define PROGMEM', '#endif',
435
+ );
436
+ }
437
+ // map()/constrain() Arduino-API helpers — dead code unless called. The
438
+ // setup emitter ORs entryHasUI() into usesConstrain before we see it (the
439
+ // UI runtime's progress/range draw calls constrain).
440
+ if (uses('usesMap')) {
441
+ guardBody.push(
442
+ 'inline long map(long x, long in_min, long in_max, long out_min, long out_max) { return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; }',
443
+ );
444
+ }
445
+ if (uses('usesConstrain')) {
446
+ guardBody.push(
447
+ 'inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }',
448
+ );
449
+ }
450
+ // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
451
+ // for protocol output. Overloaded for string (const char*) and numeric
452
+ // (double) so the same call site works for markers and test values.
453
+ // Emitted only when the expect preprocessor actually injected the calls
454
+ // (tracked as usedPolyfillHelpers).
455
+ if (!a || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println')) {
456
+ guardBody.push(
457
+ 'inline void __tc_print(const char* s) { printf("%s", s); }',
458
+ 'inline void __tc_print(double v) { printf("%g", v); }',
459
+ 'inline void __tc_println(const char* s) { printf("%s\\n", s); }',
460
+ 'inline void __tc_println(double v) { printf("%g\\n", v); }',
297
461
  );
298
462
  }
299
463
 
300
464
  // Per-peripheral bus state — gated on the same ctx.analysis.usesX flags as
301
465
  // forcedIncludes, so an unused peripheral emits no state (and its header is
302
466
  // not included). Mirrors framework-esp32's shimLines espInit block.
303
- const a = (ctx as any)?.analysis;
304
- const uses = (f: string): boolean => (a ? !!a[f] : true);
305
467
  if (uses('usesI2C') && chip.i2c) {
306
- for (let i = 0; i < chip.i2c.controllers.length; i++) lines.push(...i2cInitLines(chip, i));
468
+ for (let i = 0; i < chip.i2c.controllers.length; i++) guardBody.push(...i2cInitLines(chip, i));
307
469
  }
308
470
  if (uses('usesSPI') && chip.spi) {
309
- for (let i = 0; i < chip.spi.controllers.length; i++) lines.push(...spiInitLines(chip, i));
471
+ for (let i = 0; i < chip.spi.controllers.length; i++) guardBody.push(...spiInitLines(chip, i));
310
472
  }
311
473
  if (uses('usesUart') && chip.uart) {
312
- for (let i = 0; i < chip.uart.controllers.length; i++) lines.push(...uartInitLines(chip, i));
474
+ for (let i = 0; i < chip.uart.controllers.length; i++) guardBody.push(...uartInitLines(chip, i));
313
475
  }
314
- if (uses('usesADC') && chip.adc) lines.push(...adcInitLines(chip));
315
- if (uses('usesPWM') && chip.pwm) lines.push(...pwmInitLines(chip));
316
- if (uses('usesDAC') && chip.dac) lines.push(...dacInitLines(chip));
317
- if (uses('usesHwtimer') && chip.hwtimer) lines.push(...hwtimerInitLines(chip));
318
- if (uses('usesInterrupts')) lines.push(...interruptInitLines(chip));
319
- if (uses('usesWDT') && chip.wdt) lines.push(...wdtInitLines(chip));
320
- if (uses('usesBle')) lines.push(...bleInitLines());
476
+ if (uses('usesADC') && chip.adc) guardBody.push(...adcInitLines(chip, collectUsedPins(program, 'adc')));
477
+ if (uses('usesPWM') && chip.pwm) guardBody.push(...pwmInitLines(chip, collectUsedPins(program, 'pwm', chip)));
478
+ if (uses('usesDAC') && chip.dac) guardBody.push(...dacInitLines(chip));
479
+ if (uses('usesHwtimer') && chip.hwtimer) guardBody.push(...hwtimerInitLines(chip));
480
+ if (uses('usesInterrupts')) guardBody.push(...interruptInitLines(chip));
481
+ if (uses('usesWDT') && chip.wdt) guardBody.push(...wdtInitLines(chip));
482
+ if (uses('usesBle')) guardBody.push(...bleInitLines());
321
483
  // Display runtime (rect/text renderer): the DIRECT-call display path (user
322
484
  // code calling screen.display.fillRect etc., no @typecad/ui). Emitted only
323
485
  // when the program uses display.* but is NOT a UI program — the UI display
@@ -331,18 +493,52 @@ export class ZephyrStrategy implements PlatformStrategy {
331
493
  // previously dead code).
332
494
  if (uses('usesDisplay') && !entryHasUI()) {
333
495
  const rt = buildDisplayRuntime(this._displayState.profile);
334
- lines.push(...rt.stateLines);
335
- lines.push(rt.fontTable);
336
- lines.push(rt.helpers);
496
+ guardBody.push(...rt.stateLines);
497
+ guardBody.push(rt.fontTable);
498
+ guardBody.push(rt.helpers);
337
499
  }
338
- if (uses('usesWifi')) lines.push(...wifiInitLines());
339
- if (uses('usesHttp')) lines.push(...httpInitLines());
340
- if (uses('usesMqtt')) lines.push(...mqttInitLines());
341
- if (uses('usesPreferences')) lines.push(...preferencesInitLines());
342
- if (uses('usesFS')) lines.push(...fsInitLines());
343
- if (uses('usesRandom')) lines.push(...randomInitLines());
500
+ if (uses('usesWifi')) guardBody.push(...wifiInitLines());
501
+ if (uses('usesHttp')) guardBody.push(...httpInitLines());
502
+ if (uses('usesMqtt')) guardBody.push(...mqttInitLines());
503
+ if (uses('usesPreferences')) guardBody.push(...preferencesInitLines());
504
+ if (uses('usesFS')) guardBody.push(...fsInitLines());
505
+ if (uses('usesRandom')) guardBody.push(...randomInitLines());
344
506
 
345
- lines.push('#endif // CUTTLEFISH_SHIM_DEFINED');
507
+ const lines: string[] = [];
508
+ if (guardBody.length > 0) {
509
+ lines.push(
510
+ '// cuttlefish runtime shim. Wrapped in a single include guard so the',
511
+ '// block is safe to emit into multiple headers and .cpp files within',
512
+ '// one translation unit (a .cpp may #include several headers that each',
513
+ '// carry the shim). The guard ensures the definitions are seen exactly',
514
+ '// once per TU.',
515
+ '#ifndef CUTTLEFISH_SHIM_DEFINED',
516
+ '#define CUTTLEFISH_SHIM_DEFINED',
517
+ ...guardBody,
518
+ '#endif // CUTTLEFISH_SHIM_DEFINED',
519
+ );
520
+ }
521
+
522
+ // Devicetree specs — one per board-defined GPIO pin, but ONLY for pins the
523
+ // program actually addresses (lowerGpio routes by pin number, and the
524
+ // structured gpio.* hal-op pins are visible here) plus aliases named
525
+ // verbatim in raw code (rawCpp escape hatches). Emitted OUTSIDE the single
526
+ // CUTTLEFISH_SHIM_DEFINED guard with a per-symbol guard: per-file pin sets
527
+ // differ, and in a multi-header TU the first header's TU-wide guard would
528
+ // otherwise hide the second header's specs. Without a program (capability
529
+ // query), emit them all.
530
+ const usedPins = this.collectGpioPinUsage(program);
531
+ const dtTextRefs = this.collectRawMatches(program, /__tc_dt_([A-Za-z0-9_]+)/g);
532
+ for (const spec of chip.gpio.dtSpecs) {
533
+ if (program && !usedPins.has(spec.pin) && !dtTextRefs.has(spec.dtSpec)) continue;
534
+ const guard = `__TC_DT_${spec.dtSpec.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}_SPEC`;
535
+ lines.push(
536
+ `#ifndef ${guard}`,
537
+ `#define ${guard}`,
538
+ `static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`,
539
+ `#endif // ${guard}`,
540
+ );
541
+ }
346
542
 
347
543
  // --- Debug-mode halt + per-breakpoint disable registry ---
348
544
  //
@@ -408,9 +604,12 @@ export class ZephyrStrategy implements PlatformStrategy {
408
604
  '}',
409
605
  );
410
606
 
411
- // GPIO read shim: the wiring_compat polyfill routes the UI runtime
412
- // header's unconditional digitalRead() poll (init-press-input.ts) to
413
- // __tc_gpio_read, so the definition must NOT be gated on @typecad/safety.
607
+ // GPIO read shim: emitted only when something actually reads a pin at
608
+ // runtime user digitalRead() calls, the @typecad/safety voter (calls
609
+ // __tc_gpio_read directly), or the UI runtime header's digitalRead() poll
610
+ // (init-press-input.ts). A program that only writes/toggles GPIO needs
611
+ // neither the dispatcher nor the reader.
612
+ //
414
613
  // The signature is `int` to match wiring_compat's forward declaration —
415
614
  // a uint32_t definition alongside it would leave the declared int
416
615
  // overload undefined (int wins overload resolution for small integer
@@ -423,15 +622,17 @@ export class ZephyrStrategy implements PlatformStrategy {
423
622
  // dispatcher that resolves the owning controller's device per pin;
424
623
  // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
425
624
  // still compile-time-resolved per branch, so it is always statically valid.
426
- lines.push(...emitGpioDevDispatcher(chip));
427
- lines.push(
428
- 'inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), static_cast<gpio_pin_t>(pin)); }',
429
- );
625
+ if (this.needsGpioReadShim(program, ctx)) {
626
+ lines.push(...emitGpioDevDispatcher(chip));
627
+ lines.push(
628
+ '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))); }',
629
+ );
630
+ }
430
631
  // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
431
632
  // lowering, so they stay gated on it.
432
633
  if (program && programUsesSafety(program)) {
433
634
  lines.push(
434
- 'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }',
635
+ 'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), __tc_gpio_pin(pin), value); }',
435
636
  '#ifndef __TC_DELAY_US_DEFINED',
436
637
  '#define __TC_DELAY_US_DEFINED',
437
638
  'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }',
@@ -1137,6 +1338,45 @@ export class ZephyrStrategy implements PlatformStrategy {
1137
1338
  }
1138
1339
 
1139
1340
  generateNativePolyfills(program?: ProgramIR, ctx?: PlatformContext): RuntimePolyfillIR[] {
1341
+ // wiring_compat (digitalRead/HIGH/LOW macros + the __tc_gpio_read forward
1342
+ // declaration) is emitted only when something reads a pin: user
1343
+ // digitalRead() calls, the @typecad/safety voter, or the UI runtime
1344
+ // header's unconditional digitalRead() poll (init-press-input.ts — the
1345
+ // loop body is dead when no pin watchers are configured but must
1346
+ // compile). needsGpioReadShim defaults to true without analysis so
1347
+ // capability queries keep seeing it.
1348
+ const wiringCompat: RuntimePolyfillIR = {
1349
+ // Wiring-compatibility shims for symbols the UI runtime header
1350
+ // references unconditionally (e.g. init-press-input.ts polls pin
1351
+ // watchers via digitalRead/HIGH/LOW even when none are configured —
1352
+ // the loop body is dead but must compile). Zephyr lowers GPIO through
1353
+ // its __tc_gpio_* helpers (defined in shimLines); these macros route
1354
+ // the Wiring tokens to them.
1355
+ kind: 'polyfill',
1356
+ id: 'wiring_compat',
1357
+ domain: 'standard' as const,
1358
+ requiredIncludes: [],
1359
+ forwardDeclarations: [
1360
+ // Forward-declared so the digitalRead macro (below) can reference it
1361
+ // before the shim block defines the body. The shim emits the full
1362
+ // definition via gpio_pin_get_raw.
1363
+ 'int __tc_gpio_read(int pin);',
1364
+ ],
1365
+ helperStructs: [],
1366
+ helperFunctions: [],
1367
+ shimMacros: [
1368
+ '#ifndef HIGH',
1369
+ '#define HIGH 1',
1370
+ '#endif',
1371
+ '#ifndef LOW',
1372
+ '#define LOW 0',
1373
+ '#endif',
1374
+ '#ifndef digitalRead',
1375
+ '#define digitalRead(pin) __tc_gpio_read(pin)',
1376
+ '#endif',
1377
+ ],
1378
+ dependencies: [],
1379
+ };
1140
1380
  const polyfills: RuntimePolyfillIR[] = [
1141
1381
  {
1142
1382
  kind: 'polyfill',
@@ -1151,38 +1391,7 @@ export class ZephyrStrategy implements PlatformStrategy {
1151
1391
  shimMacros: [],
1152
1392
  dependencies: [],
1153
1393
  },
1154
- {
1155
- // Wiring-compatibility shims for symbols the UI runtime header
1156
- // references unconditionally (e.g. init-press-input.ts polls pin
1157
- // watchers via digitalRead/HIGH/LOW even when none are configured —
1158
- // the loop body is dead but must compile). Zephyr lowers GPIO through
1159
- // its __tc_gpio_* helpers (defined in shimLines); these macros route
1160
- // the Wiring tokens to them.
1161
- kind: 'polyfill',
1162
- id: 'wiring_compat',
1163
- domain: 'standard' as const,
1164
- requiredIncludes: [],
1165
- forwardDeclarations: [
1166
- // Forward-declared so the digitalRead macro (below) can reference it
1167
- // before the shim block defines the body. The shim emits the full
1168
- // definition via gpio_pin_get_raw.
1169
- 'int __tc_gpio_read(int pin);',
1170
- ],
1171
- helperStructs: [],
1172
- helperFunctions: [],
1173
- shimMacros: [
1174
- '#ifndef HIGH',
1175
- '#define HIGH 1',
1176
- '#endif',
1177
- '#ifndef LOW',
1178
- '#define LOW 0',
1179
- '#endif',
1180
- '#ifndef digitalRead',
1181
- '#define digitalRead(pin) __tc_gpio_read(pin)',
1182
- '#endif',
1183
- ],
1184
- dependencies: [],
1185
- },
1394
+ ...(this.needsGpioReadShim(program, ctx) ? [wiringCompat] : []),
1186
1395
  {
1187
1396
  // STL-free string-method polyfills. String methods (.toUpperCase(),
1188
1397
  // .includes(), .substring(), …) lower at IR level to __tc_* helpers for
@@ -1417,22 +1626,10 @@ struct __tc_StaticArray {
1417
1626
  // Named display-profile registry: maps config `profile` values (e.g.
1418
1627
  // "st7796-zephyr") to the shared DisplayProfile shape so transpile.ts can
1419
1628
  // resolve them per-framework. The Zephyr profiles are DT-binding descriptors;
1420
- // they're mapped to the shared shape (driver/width/height/colorFormat/
1421
- // rotation) the profile resolver expects.
1629
+ // BUILT_IN_PROFILES (display/profiles.ts) is the single DT-binding
1630
+ // shared-shape mapping, shared with the preview's registry loader.
1422
1631
  getProfileRegistry(): Map<string, DisplayProfile> {
1423
- const m = new Map<string, DisplayProfile>();
1424
- for (const [name, p] of Object.entries(ZEPHYR_DISPLAY_PROFILES)) {
1425
- m.set(name, {
1426
- driver: p.driver,
1427
- width: p.width,
1428
- height: p.height,
1429
- nativeWidth: p.nativeWidth,
1430
- nativeHeight: p.nativeHeight,
1431
- colorFormat: p.colorFormat,
1432
- rotation: p.rotation ?? 1,
1433
- });
1434
- }
1435
- return m;
1632
+ return new Map(Object.entries(BUILT_IN_PROFILES));
1436
1633
  }
1437
1634
 
1438
1635
  colorFormat(): 'rgb565' | 'rgb666' | 'rgb888' | 'mono' {