@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
@@ -122,6 +122,29 @@ export function wifiInitLines() {
122
122
  ` }`,
123
123
  ` __tc_wifi.inited = true;`,
124
124
  `}`,
125
+ ``,
126
+ `// ── blocking-wait pump ───────────────────────────────────────────────────`,
127
+ `// Blocking wifi waits hold the main thread, which suspends the loop()-driven`,
128
+ `// ui_tick — a 15s association would freeze the display for its whole`,
129
+ `// duration. Each wait slice sleeps 20ms and, in the entry TU of a UI build`,
130
+ `// (the only TU carrying the ui_tick definition), pumps one frame so`,
131
+ `// animations and touch stay live during the wait. Constraint: do NOT call`,
132
+ `// blocking wifi waits from inside UI event handlers — that re-enters`,
133
+ `// ui_tick mid-tick; use wifi.connect_start + wifi.is_connected there.`,
134
+ `#ifdef CUTTLEFISH_ENTRY_UI_TU`,
135
+ `static void ui_tick(uint16_t deltaMs); // defined by the UI runtime header`,
136
+ `#endif`,
137
+ `static void __tc_wifi_wait_slice(uint32_t slice_ms) {`,
138
+ ` k_msleep(slice_ms);`,
139
+ `#ifdef CUTTLEFISH_ENTRY_UI_TU`,
140
+ ` static uint32_t last_ms = 0U;`,
141
+ ` uint32_t now_ms = k_uptime_get_32();`,
142
+ ` uint32_t delta = (last_ms != 0U) ? (now_ms - last_ms) : 0U;`,
143
+ ` if (delta > 250U) delta = 250U; // same clamp as the loop() tick injection`,
144
+ ` last_ms = now_ms;`,
145
+ ` ui_tick(static_cast<uint16_t>(delta));`,
146
+ `#endif`,
147
+ `}`,
125
148
  `// CUTTLEFISH_WIFI_CORE_END`,
126
149
  ``,
127
150
  `// ── connect / disconnect (net_mgmt — conn_mgr monitor supplies L4 events) ─`,
@@ -176,8 +199,9 @@ export function wifiInitLines() {
176
199
  `static void __tc_wifi_connect(const char* ssid, const char* password, int32_t timeout_ms) {`,
177
200
  ` __tc_wifi_connect_start(ssid, password);`,
178
201
  ` // Block until the L4 handler signals connectivity or the deadline passes.`,
202
+ ` // 20ms slices let __tc_wifi_wait_slice pump the UI between polls.`,
179
203
  ` int32_t waited = 0;`,
180
- ` while (!__tc_wifi.connected && waited < timeout_ms) { k_msleep(100); waited += 100; }`,
204
+ ` while (!__tc_wifi.connected && waited < timeout_ms) { __tc_wifi_wait_slice(20); waited += 20; }`,
181
205
  ` printk("tc-wifi: connect %s after %dms\\n", __tc_wifi.connected ? "ok" : "timeout", waited);`,
182
206
  `}`,
183
207
  `// CUTTLEFISH_WIFI_CONNECT_BLOCKING_END`,
@@ -241,7 +265,7 @@ export function wifiInitLines() {
241
265
  ``,
242
266
  `static void __tc_wifi_scan_blocking(void) {`,
243
267
  ` __tc_wifi_scan_start();`,
244
- ` while (__tc_wifi.scanning) { k_msleep(100); }`,
268
+ ` while (__tc_wifi.scanning) { __tc_wifi_wait_slice(20); }`,
245
269
  `}`,
246
270
  ``,
247
271
  `static const char* __tc_wifi_scan_ssid(int32_t i) {`,
@@ -309,13 +333,12 @@ export function wifiInitLines() {
309
333
  `static void __tc_wifi_wait_connected(int32_t timeout_ms) {`,
310
334
  ` __tc_wifi_ensure_init();`,
311
335
  ` int32_t waited = 0;`,
312
- ` while (!__tc_wifi.connected && waited < timeout_ms) { k_msleep(100); waited += 100; }`,
336
+ ` while (!__tc_wifi.connected && waited < timeout_ms) { __tc_wifi_wait_slice(20); waited += 20; }`,
313
337
  `}`,
314
- ``,
315
338
  `static void __tc_wifi_wait_disconnected(void) {`,
316
339
  ` __tc_wifi_ensure_init();`,
317
340
  ` // No timeout in the HAL surface — block until the L4 handler clears the flag.`,
318
- ` while (__tc_wifi.connected) { k_msleep(100); }`,
341
+ ` while (__tc_wifi.connected) { __tc_wifi_wait_slice(20); }`,
319
342
  `}`,
320
343
  ``,
321
344
  `// ── AP mode (NET_REQUEST_WIFI_AP_ENABLE / DISABLE) ──────────────────────`,
@@ -32,6 +32,26 @@ export declare class ZephyrStrategy implements PlatformStrategy {
32
32
  * nodes whose resolved `raw` code references it.
33
33
  */
34
34
  private programUsesAsyncRuntime;
35
+ /** Pins referenced by gpio.* hal-ops in the program IR. lowerGpio routes a
36
+ * pin to its devicetree spec by pin NUMBER, so the structured hal-op pins
37
+ * are the authoritative signal for which __tc_dt_* specs are needed —
38
+ * regardless of when the final call text is rendered. */
39
+ private collectGpioPinUsage;
40
+ /** Run `re` (global) against every raw string in the IR — raw expression
41
+ * values plus raw hal-op codes — returning capture group 1 of each match
42
+ * (the full match when the regex has no group). This is how references the
43
+ * text scanners must see but that never appear as IR call nodes (e.g. a
44
+ * rawCpp() escape hatch naming `__tc_dt_sw0` directly) are discovered. */
45
+ private collectRawMatches;
46
+ /** Whether the wiring-compat GPIO read surface (__tc_gpio_read definition,
47
+ * __tc_gpio_dev dispatcher, and the wiring_compat polyfill's digitalRead /
48
+ * HIGH / LOW macros) must be emitted. Consumers: user digitalRead() calls
49
+ * (usesDigitalRead), the @typecad/safety voter (calls __tc_gpio_read
50
+ * directly via lowered raw text), and the UI runtime header's
51
+ * unconditional digitalRead() poll (entryHasUI — build-global, so every TU
52
+ * in a UI build carries the macros). With no analysis present (capability
53
+ * query), default to emitting — same convention as the uses() helper. */
54
+ private needsGpioReadShim;
35
55
  shimLines(program?: ProgramIR, ctx?: PlatformContext): string[];
36
56
  profileDiagnostics(program?: ProgramIR, ctx?: PlatformContext): Diagnostic[];
37
57
  /**
package/dist/strategy.js CHANGED
@@ -21,6 +21,58 @@ import { entryHasUI } from '@typecad/cuttlefish/ui-hook';
21
21
  import { chipForTarget, setActiveChip } from './chips/index.js';
22
22
  import { resolveChipFromBoard } from './chips/resolve.js';
23
23
  import { emitGpioDevDispatcher } from './chips/controllers.js';
24
+ /**
25
+ * Deep-walk the program IR and collect the HAL pin numbers the program
26
+ * actually touches for a peripheral family ('adc' | 'pwm') — the same walk
27
+ * profileDiagnostics does. Emit paths gate per-channel state on these sets
28
+ * so nothing unused reaches the single generated TU (-Wunused-function
29
+ * hygiene: every emitted function/variable is referenced). `undefined`
30
+ * (no program — probe paths) means "no information": callers emit every
31
+ * descriptor channel, preserving probe behavior.
32
+ *
33
+ * `pwm` also covers tone.* — the tone lowering drives the descriptor's
34
+ * first PWM spec regardless of pin, so any tone op marks it used.
35
+ */
36
+ function collectUsedPins(program, kind, chip) {
37
+ if (!program)
38
+ return undefined;
39
+ const pins = new Set();
40
+ let usesTone = false;
41
+ const visit = (node) => {
42
+ if (!node || typeof node !== 'object')
43
+ return;
44
+ const n = node;
45
+ const op = n.operation;
46
+ if (op && typeof op === 'object') {
47
+ const o = op;
48
+ const name = o.operation;
49
+ const pin = o.pin;
50
+ if (typeof name === 'string' && typeof pin === 'number') {
51
+ if (kind === 'adc' && (name === 'adc.read' || name === 'adc.read_voltage'))
52
+ pins.add(pin);
53
+ if (kind === 'pwm' && name.startsWith('pwm.'))
54
+ pins.add(pin);
55
+ }
56
+ if (kind === 'pwm' && typeof name === 'string' && name.startsWith('tone.'))
57
+ usesTone = true;
58
+ }
59
+ for (const v of Object.values(n)) {
60
+ if (Array.isArray(v)) {
61
+ for (const item of v)
62
+ visit(item);
63
+ }
64
+ else if (v && typeof v === 'object')
65
+ visit(v);
66
+ }
67
+ };
68
+ visit(program);
69
+ if (kind === 'pwm' && usesTone) {
70
+ const first = chip?.pwm?.specs[0];
71
+ if (first)
72
+ pins.add(first.pin);
73
+ }
74
+ return pins;
75
+ }
24
76
  import { lowerHalOp } from './lowering/index.js';
25
77
  import { buildZephyrWorkerBacking } from './lowering/worker-backing.js';
26
78
  import { adcInitLines } from './lowering/adc.js';
@@ -44,7 +96,7 @@ import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
44
96
  import { buildTimerPolyfill } from './async/timer-polyfill.js';
45
97
  import { resolveZephyrDisplayOp, newDisplayState } from './display/index.js';
46
98
  import { buildDisplayRuntime } from './display/gfx.js';
47
- import { ZEPHYR_DISPLAY_PROFILES } from './display/profiles.js';
99
+ import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES } from './display/profiles.js';
48
100
  import { zephyrDisplayAdapterGenerator } from './display/ui-adapter.js';
49
101
  import { zephyrTouchAdapter } from './display/touch-adapter.js';
50
102
  export class ZephyrStrategy {
@@ -113,7 +165,23 @@ export class ZephyrStrategy {
113
165
  // true so nothing is stripped — mirrors framework-esp32's forcedIncludes.
114
166
  const a = ctx?.analysis;
115
167
  const uses = (f) => (a ? !!a[f] : true);
116
- const inc = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdio>', '<cstdint>'];
168
+ // <zephyr/drivers/gpio.h> and <cstdint> stay unconditional: gpio.h is
169
+ // cross-cutting (gpio/power/interrupt/spi/pulse lowerings + the DT-spec
170
+ // machinery all reference its API, and no single usesX flag owns it), and
171
+ // the fixed-width types come via <zephyr/kernel.h> regardless — DIRECT_CPP_TYPE_MAP
172
+ // passes int32_t/uint8_t through verbatim.
173
+ const inc = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdint>'];
174
+ // <cstdio> backs the printf family only: __tc_print/__tc_println (emitted
175
+ // solely when @typecad/expect's preprocessor injected them — tracked via
176
+ // usedPolyfillHelpers), raw printf/snprintf in user code (usesCstdio), and
177
+ // the fs/preferences/uart shims (their lowerings snprintf into buffers).
178
+ // A program touching none of those needs no <cstdio>.
179
+ const helpers = a?.usedPolyfillHelpers;
180
+ const needsCstdio = uses('usesCstdio') || uses('usesFS') || uses('usesPreferences')
181
+ || uses('usesUart')
182
+ || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println');
183
+ if (needsCstdio)
184
+ inc.push('<cstdio>');
117
185
  if (uses('usesI2C'))
118
186
  inc.push('<zephyr/drivers/i2c.h>');
119
187
  if (uses('usesSPI'))
@@ -239,83 +307,171 @@ export class ZephyrStrategy {
239
307
  visit(program);
240
308
  return found;
241
309
  }
310
+ /** Pins referenced by gpio.* hal-ops in the program IR. lowerGpio routes a
311
+ * pin to its devicetree spec by pin NUMBER, so the structured hal-op pins
312
+ * are the authoritative signal for which __tc_dt_* specs are needed —
313
+ * regardless of when the final call text is rendered. */
314
+ collectGpioPinUsage(program) {
315
+ const pins = new Set();
316
+ if (!program)
317
+ return pins;
318
+ const visit = (node) => {
319
+ if (!node || typeof node !== 'object')
320
+ return;
321
+ if (node.operation && typeof node.operation === 'object'
322
+ && typeof node.operation.operation === 'string'
323
+ && node.operation.operation.startsWith('gpio.')
324
+ && typeof node.operation.pin === 'number') {
325
+ pins.add(node.operation.pin);
326
+ }
327
+ for (const v of Object.values(node)) {
328
+ if (Array.isArray(v)) {
329
+ for (const item of v)
330
+ visit(item);
331
+ }
332
+ else if (v && typeof v === 'object')
333
+ visit(v);
334
+ }
335
+ };
336
+ visit(program);
337
+ return pins;
338
+ }
339
+ /** Run `re` (global) against every raw string in the IR — raw expression
340
+ * values plus raw hal-op codes — returning capture group 1 of each match
341
+ * (the full match when the regex has no group). This is how references the
342
+ * text scanners must see but that never appear as IR call nodes (e.g. a
343
+ * rawCpp() escape hatch naming `__tc_dt_sw0` directly) are discovered. */
344
+ collectRawMatches(program, re) {
345
+ const found = new Set();
346
+ if (!program)
347
+ return found;
348
+ const scan = (text) => {
349
+ for (const m of text.matchAll(re))
350
+ found.add(m[1] ?? m[0]);
351
+ };
352
+ const visit = (node) => {
353
+ if (!node || typeof node !== 'object')
354
+ return;
355
+ if (node.kind === 'raw' && typeof node.value === 'string')
356
+ scan(node.value);
357
+ if (node.operation && typeof node.operation === 'object'
358
+ && node.operation.operation === 'raw' && typeof node.operation.code === 'string') {
359
+ scan(node.operation.code);
360
+ }
361
+ for (const v of Object.values(node)) {
362
+ if (Array.isArray(v)) {
363
+ for (const item of v)
364
+ visit(item);
365
+ }
366
+ else if (v && typeof v === 'object')
367
+ visit(v);
368
+ }
369
+ };
370
+ visit(program);
371
+ return found;
372
+ }
373
+ /** Whether the wiring-compat GPIO read surface (__tc_gpio_read definition,
374
+ * __tc_gpio_dev dispatcher, and the wiring_compat polyfill's digitalRead /
375
+ * HIGH / LOW macros) must be emitted. Consumers: user digitalRead() calls
376
+ * (usesDigitalRead), the @typecad/safety voter (calls __tc_gpio_read
377
+ * directly via lowered raw text), and the UI runtime header's
378
+ * unconditional digitalRead() poll (entryHasUI — build-global, so every TU
379
+ * in a UI build carries the macros). With no analysis present (capability
380
+ * query), default to emitting — same convention as the uses() helper. */
381
+ needsGpioReadShim(program, ctx) {
382
+ if (program && programUsesSafety(program))
383
+ return true;
384
+ if (entryHasUI())
385
+ return true;
386
+ const a = ctx?.analysis;
387
+ return a ? !!a.usesDigitalRead : true;
388
+ }
242
389
  shimLines(program, ctx) {
243
390
  const chip = this.resolveChip(ctx, program);
244
391
  const isPrintf = this.resolveDebugMode(ctx) === 'printf';
245
- const lines = [
246
- '// cuttlefish runtime shim. Wrapped in a single include guard so the',
247
- '// block is safe to emit into multiple headers and .cpp files within',
248
- '// one translation unit (a .cpp may #include several headers that each',
249
- '// carry the shim). The guard ensures the definitions are seen exactly',
250
- '// once per TU.',
251
- '#ifndef CUTTLEFISH_SHIM_DEFINED',
252
- '#define CUTTLEFISH_SHIM_DEFINED',
253
- '#ifndef CUTTLEFISH_UNDEFINED',
254
- '#define CUTTLEFISH_UNDEFINED 0',
255
- '#endif',
256
- 'template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }',
257
- 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }',
258
- 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }',
259
- 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }',
260
- 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }',
261
- 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }',
262
- 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }',
263
- 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }',
264
- // millis() backed by the Zephyr uptime counter. uint32_t return matches
265
- // the Arduino API the shared runtime expects (wraps every ~49.7 days).
266
- 'inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }',
267
- // Arduino-compat defines referenced by the shared runtime polyfills.
268
- '#ifndef HIGH', '#define HIGH 1', '#endif',
269
- '#ifndef LOW', '#define LOW 0', '#endif',
270
- '#ifndef PROGMEM', '#define PROGMEM', '#endif',
271
- '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; }',
272
- 'inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }',
273
- // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
274
- // for protocol output. Overloaded for string (const char*) and numeric
275
- // (double) so the same call site works for markers and test values.
276
- 'inline void __tc_print(const char* s) { printf("%s", s); }',
277
- 'inline void __tc_print(double v) { printf("%g", v); }',
278
- 'inline void __tc_println(const char* s) { printf("%s\\n", s); }',
279
- 'inline void __tc_println(double v) { printf("%g\\n", v); }',
280
- ];
281
- // Devicetree specs for every board-defined GPIO pin. Emitted unconditionally
282
- // (guarded by the include guard) so any of them is available whether or not
283
- // a given program uses it. Safe because every spec references a node that
284
- // exists in the active board's devicetree.
285
- for (const spec of chip.gpio.dtSpecs) {
286
- lines.push(`static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`);
392
+ const a = ctx?.analysis;
393
+ const uses = (f) => (a ? !!a[f] : true);
394
+ const helpers = a?.usedPolyfillHelpers;
395
+ // --- Core shim, gated item by item on actual use ------------------------
396
+ // A minimal program (blink) uses none of these, and its output carries no
397
+ // shim block at all. Everything up to the #endif composes into one guard
398
+ // body; the guard itself is only stamped when the body is non-empty.
399
+ const guardBody = [];
400
+ // CUTTLEFISH_UNDEFINED: needed when the file references null/undefined
401
+ // literals (usesNullish), emits nullish helper CALLS (usesNullishHelper),
402
+ // or has async functions (the async state machine uses the macro for
403
+ // default waitFor* timeouts not visible to the nullish scanners).
404
+ if (uses('usesNullish') || uses('usesNullishHelper') || uses('hasAsync')) {
405
+ guardBody.push('#ifndef CUTTLEFISH_UNDEFINED', '#define CUTTLEFISH_UNDEFINED 0', '#endif');
406
+ }
407
+ // Nullish helpers: only when the file actually emits cuttlefish_nullish /
408
+ // cuttlefish_exists CALLS (?? / ?. lowering). A file that only references
409
+ // null/undefined literals needs just the macro above the same
410
+ // distinction the setup emitter's strip filter documents.
411
+ if (uses('usesNullishHelper')) {
412
+ guardBody.push('template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }', 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }', 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }', 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }', 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }', 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }', 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }', 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }');
413
+ }
414
+ // millis() backed by the Zephyr uptime counter. uint32_t return matches
415
+ // the Arduino API the shared runtime expects (wraps every ~49.7 days).
416
+ // Kept when the program reads the clock itself — usesWallClock,
417
+ // deliberately WITHOUT the delay() conflation usesMillis carries, because
418
+ // Zephyr's delay lowers straight to k_msleep or has a hidden poller:
419
+ // async functions / the async runtime, the setInterval/setTimeout
420
+ // scheduler, or a mounted UI's per-frame tick.
421
+ if (uses('usesWallClock') || uses('hasAsync') || (!a || a.timerCallCount > 0)
422
+ || this.programUsesAsyncRuntime(program) || entryHasUI()) {
423
+ guardBody.push('inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }');
424
+ }
425
+ // PROGMEM: only the (Arduino-oriented) UI runtime header can reference it.
426
+ if (entryHasUI()) {
427
+ guardBody.push('#ifndef PROGMEM', '#define PROGMEM', '#endif');
428
+ }
429
+ // map()/constrain() Arduino-API helpers dead code unless called. The
430
+ // setup emitter ORs entryHasUI() into usesConstrain before we see it (the
431
+ // UI runtime's progress/range draw calls constrain).
432
+ if (uses('usesMap')) {
433
+ guardBody.push('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; }');
434
+ }
435
+ if (uses('usesConstrain')) {
436
+ guardBody.push('inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }');
437
+ }
438
+ // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
439
+ // for protocol output. Overloaded for string (const char*) and numeric
440
+ // (double) so the same call site works for markers and test values.
441
+ // Emitted only when the expect preprocessor actually injected the calls
442
+ // (tracked as usedPolyfillHelpers).
443
+ if (!a || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println')) {
444
+ guardBody.push('inline void __tc_print(const char* s) { printf("%s", s); }', 'inline void __tc_print(double v) { printf("%g", v); }', 'inline void __tc_println(const char* s) { printf("%s\\n", s); }', 'inline void __tc_println(double v) { printf("%g\\n", v); }');
287
445
  }
288
446
  // Per-peripheral bus state — gated on the same ctx.analysis.usesX flags as
289
447
  // forcedIncludes, so an unused peripheral emits no state (and its header is
290
448
  // not included). Mirrors framework-esp32's shimLines espInit block.
291
- const a = ctx?.analysis;
292
- const uses = (f) => (a ? !!a[f] : true);
293
449
  if (uses('usesI2C') && chip.i2c) {
294
450
  for (let i = 0; i < chip.i2c.controllers.length; i++)
295
- lines.push(...i2cInitLines(chip, i));
451
+ guardBody.push(...i2cInitLines(chip, i));
296
452
  }
297
453
  if (uses('usesSPI') && chip.spi) {
298
454
  for (let i = 0; i < chip.spi.controllers.length; i++)
299
- lines.push(...spiInitLines(chip, i));
455
+ guardBody.push(...spiInitLines(chip, i));
300
456
  }
301
457
  if (uses('usesUart') && chip.uart) {
302
458
  for (let i = 0; i < chip.uart.controllers.length; i++)
303
- lines.push(...uartInitLines(chip, i));
459
+ guardBody.push(...uartInitLines(chip, i));
304
460
  }
305
461
  if (uses('usesADC') && chip.adc)
306
- lines.push(...adcInitLines(chip));
462
+ guardBody.push(...adcInitLines(chip, collectUsedPins(program, 'adc')));
307
463
  if (uses('usesPWM') && chip.pwm)
308
- lines.push(...pwmInitLines(chip));
464
+ guardBody.push(...pwmInitLines(chip, collectUsedPins(program, 'pwm', chip)));
309
465
  if (uses('usesDAC') && chip.dac)
310
- lines.push(...dacInitLines(chip));
466
+ guardBody.push(...dacInitLines(chip));
311
467
  if (uses('usesHwtimer') && chip.hwtimer)
312
- lines.push(...hwtimerInitLines(chip));
468
+ guardBody.push(...hwtimerInitLines(chip));
313
469
  if (uses('usesInterrupts'))
314
- lines.push(...interruptInitLines(chip));
470
+ guardBody.push(...interruptInitLines(chip));
315
471
  if (uses('usesWDT') && chip.wdt)
316
- lines.push(...wdtInitLines(chip));
472
+ guardBody.push(...wdtInitLines(chip));
317
473
  if (uses('usesBle'))
318
- lines.push(...bleInitLines());
474
+ guardBody.push(...bleInitLines());
319
475
  // Display runtime (rect/text renderer): the DIRECT-call display path (user
320
476
  // code calling screen.display.fillRect etc., no @typecad/ui). Emitted only
321
477
  // when the program uses display.* but is NOT a UI program — the UI display
@@ -329,23 +485,42 @@ export class ZephyrStrategy {
329
485
  // previously dead code).
330
486
  if (uses('usesDisplay') && !entryHasUI()) {
331
487
  const rt = buildDisplayRuntime(this._displayState.profile);
332
- lines.push(...rt.stateLines);
333
- lines.push(rt.fontTable);
334
- lines.push(rt.helpers);
488
+ guardBody.push(...rt.stateLines);
489
+ guardBody.push(rt.fontTable);
490
+ guardBody.push(rt.helpers);
335
491
  }
336
492
  if (uses('usesWifi'))
337
- lines.push(...wifiInitLines());
493
+ guardBody.push(...wifiInitLines());
338
494
  if (uses('usesHttp'))
339
- lines.push(...httpInitLines());
495
+ guardBody.push(...httpInitLines());
340
496
  if (uses('usesMqtt'))
341
- lines.push(...mqttInitLines());
497
+ guardBody.push(...mqttInitLines());
342
498
  if (uses('usesPreferences'))
343
- lines.push(...preferencesInitLines());
499
+ guardBody.push(...preferencesInitLines());
344
500
  if (uses('usesFS'))
345
- lines.push(...fsInitLines());
501
+ guardBody.push(...fsInitLines());
346
502
  if (uses('usesRandom'))
347
- lines.push(...randomInitLines());
348
- lines.push('#endif // CUTTLEFISH_SHIM_DEFINED');
503
+ guardBody.push(...randomInitLines());
504
+ const lines = [];
505
+ if (guardBody.length > 0) {
506
+ 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');
507
+ }
508
+ // Devicetree specs — one per board-defined GPIO pin, but ONLY for pins the
509
+ // program actually addresses (lowerGpio routes by pin number, and the
510
+ // structured gpio.* hal-op pins are visible here) plus aliases named
511
+ // verbatim in raw code (rawCpp escape hatches). Emitted OUTSIDE the single
512
+ // CUTTLEFISH_SHIM_DEFINED guard with a per-symbol guard: per-file pin sets
513
+ // differ, and in a multi-header TU the first header's TU-wide guard would
514
+ // otherwise hide the second header's specs. Without a program (capability
515
+ // query), emit them all.
516
+ const usedPins = this.collectGpioPinUsage(program);
517
+ const dtTextRefs = this.collectRawMatches(program, /__tc_dt_([A-Za-z0-9_]+)/g);
518
+ for (const spec of chip.gpio.dtSpecs) {
519
+ if (program && !usedPins.has(spec.pin) && !dtTextRefs.has(spec.dtSpec))
520
+ continue;
521
+ const guard = `__TC_DT_${spec.dtSpec.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}_SPEC`;
522
+ 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}`);
523
+ }
349
524
  // --- Debug-mode halt + per-breakpoint disable registry ---
350
525
  //
351
526
  // Printf mode only. In gdb mode the cuttlefish debug preprocessor is
@@ -378,9 +553,12 @@ export class ZephyrStrategy {
378
553
  // separate translation unit when generateHeaderFile() splits them into the
379
554
  // header.
380
555
  lines.push('extern void setup(void);', 'extern void loop(void);', '', 'int main(void) {', ' setup();', ' for (;;) {', ' loop();', ' k_msleep(1);', ' }', ' return 0;', '}');
381
- // GPIO read shim: the wiring_compat polyfill routes the UI runtime
382
- // header's unconditional digitalRead() poll (init-press-input.ts) to
383
- // __tc_gpio_read, so the definition must NOT be gated on @typecad/safety.
556
+ // GPIO read shim: emitted only when something actually reads a pin at
557
+ // runtime user digitalRead() calls, the @typecad/safety voter (calls
558
+ // __tc_gpio_read directly), or the UI runtime header's digitalRead() poll
559
+ // (init-press-input.ts). A program that only writes/toggles GPIO needs
560
+ // neither the dispatcher nor the reader.
561
+ //
384
562
  // The signature is `int` to match wiring_compat's forward declaration —
385
563
  // a uint32_t definition alongside it would leave the declared int
386
564
  // overload undefined (int wins overload resolution for small integer
@@ -393,12 +571,14 @@ export class ZephyrStrategy {
393
571
  // dispatcher that resolves the owning controller's device per pin;
394
572
  // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
395
573
  // still compile-time-resolved per branch, so it is always statically valid.
396
- lines.push(...emitGpioDevDispatcher(chip));
397
- lines.push('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)); }');
574
+ if (this.needsGpioReadShim(program, ctx)) {
575
+ lines.push(...emitGpioDevDispatcher(chip));
576
+ 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))); }');
577
+ }
398
578
  // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
399
579
  // lowering, so they stay gated on it.
400
580
  if (program && programUsesSafety(program)) {
401
- lines.push('inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }', '#ifndef __TC_DELAY_US_DEFINED', '#define __TC_DELAY_US_DEFINED', 'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }', '#endif');
581
+ lines.push('inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), __tc_gpio_pin(pin), value); }', '#ifndef __TC_DELAY_US_DEFINED', '#define __TC_DELAY_US_DEFINED', 'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }', '#endif');
402
582
  }
403
583
  return lines;
404
584
  }
@@ -1017,6 +1197,45 @@ export class ZephyrStrategy {
1017
1197
  ]);
1018
1198
  }
1019
1199
  generateNativePolyfills(program, ctx) {
1200
+ // wiring_compat (digitalRead/HIGH/LOW macros + the __tc_gpio_read forward
1201
+ // declaration) is emitted only when something reads a pin: user
1202
+ // digitalRead() calls, the @typecad/safety voter, or the UI runtime
1203
+ // header's unconditional digitalRead() poll (init-press-input.ts — the
1204
+ // loop body is dead when no pin watchers are configured but must
1205
+ // compile). needsGpioReadShim defaults to true without analysis so
1206
+ // capability queries keep seeing it.
1207
+ const wiringCompat = {
1208
+ // Wiring-compatibility shims for symbols the UI runtime header
1209
+ // references unconditionally (e.g. init-press-input.ts polls pin
1210
+ // watchers via digitalRead/HIGH/LOW even when none are configured —
1211
+ // the loop body is dead but must compile). Zephyr lowers GPIO through
1212
+ // its __tc_gpio_* helpers (defined in shimLines); these macros route
1213
+ // the Wiring tokens to them.
1214
+ kind: 'polyfill',
1215
+ id: 'wiring_compat',
1216
+ domain: 'standard',
1217
+ requiredIncludes: [],
1218
+ forwardDeclarations: [
1219
+ // Forward-declared so the digitalRead macro (below) can reference it
1220
+ // before the shim block defines the body. The shim emits the full
1221
+ // definition via gpio_pin_get_raw.
1222
+ 'int __tc_gpio_read(int pin);',
1223
+ ],
1224
+ helperStructs: [],
1225
+ helperFunctions: [],
1226
+ shimMacros: [
1227
+ '#ifndef HIGH',
1228
+ '#define HIGH 1',
1229
+ '#endif',
1230
+ '#ifndef LOW',
1231
+ '#define LOW 0',
1232
+ '#endif',
1233
+ '#ifndef digitalRead',
1234
+ '#define digitalRead(pin) __tc_gpio_read(pin)',
1235
+ '#endif',
1236
+ ],
1237
+ dependencies: [],
1238
+ };
1020
1239
  const polyfills = [
1021
1240
  {
1022
1241
  kind: 'polyfill',
@@ -1031,38 +1250,7 @@ export class ZephyrStrategy {
1031
1250
  shimMacros: [],
1032
1251
  dependencies: [],
1033
1252
  },
1034
- {
1035
- // Wiring-compatibility shims for symbols the UI runtime header
1036
- // references unconditionally (e.g. init-press-input.ts polls pin
1037
- // watchers via digitalRead/HIGH/LOW even when none are configured —
1038
- // the loop body is dead but must compile). Zephyr lowers GPIO through
1039
- // its __tc_gpio_* helpers (defined in shimLines); these macros route
1040
- // the Wiring tokens to them.
1041
- kind: 'polyfill',
1042
- id: 'wiring_compat',
1043
- domain: 'standard',
1044
- requiredIncludes: [],
1045
- forwardDeclarations: [
1046
- // Forward-declared so the digitalRead macro (below) can reference it
1047
- // before the shim block defines the body. The shim emits the full
1048
- // definition via gpio_pin_get_raw.
1049
- 'int __tc_gpio_read(int pin);',
1050
- ],
1051
- helperStructs: [],
1052
- helperFunctions: [],
1053
- shimMacros: [
1054
- '#ifndef HIGH',
1055
- '#define HIGH 1',
1056
- '#endif',
1057
- '#ifndef LOW',
1058
- '#define LOW 0',
1059
- '#endif',
1060
- '#ifndef digitalRead',
1061
- '#define digitalRead(pin) __tc_gpio_read(pin)',
1062
- '#endif',
1063
- ],
1064
- dependencies: [],
1065
- },
1253
+ ...(this.needsGpioReadShim(program, ctx) ? [wiringCompat] : []),
1066
1254
  {
1067
1255
  // STL-free string-method polyfills. String methods (.toUpperCase(),
1068
1256
  // .includes(), .substring(), …) lower at IR level to __tc_* helpers for
@@ -1269,22 +1457,10 @@ struct __tc_StaticArray {
1269
1457
  // Named display-profile registry: maps config `profile` values (e.g.
1270
1458
  // "st7796-zephyr") to the shared DisplayProfile shape so transpile.ts can
1271
1459
  // resolve them per-framework. The Zephyr profiles are DT-binding descriptors;
1272
- // they're mapped to the shared shape (driver/width/height/colorFormat/
1273
- // rotation) the profile resolver expects.
1460
+ // BUILT_IN_PROFILES (display/profiles.ts) is the single DT-binding
1461
+ // shared-shape mapping, shared with the preview's registry loader.
1274
1462
  getProfileRegistry() {
1275
- const m = new Map();
1276
- for (const [name, p] of Object.entries(ZEPHYR_DISPLAY_PROFILES)) {
1277
- m.set(name, {
1278
- driver: p.driver,
1279
- width: p.width,
1280
- height: p.height,
1281
- nativeWidth: p.nativeWidth,
1282
- nativeHeight: p.nativeHeight,
1283
- colorFormat: p.colorFormat,
1284
- rotation: p.rotation ?? 1,
1285
- });
1286
- }
1287
- return m;
1463
+ return new Map(Object.entries(BUILT_IN_PROFILES));
1288
1464
  }
1289
1465
  colorFormat() {
1290
1466
  return 'rgb565';