@typecad/framework-zephyr 1.0.0-alpha.11 → 1.0.0-alpha.13

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 (42) hide show
  1. package/dist/display/index.d.ts +1 -1
  2. package/dist/display/index.js +1 -1
  3. package/dist/display/profiles.d.ts +33 -0
  4. package/dist/display/profiles.js +36 -0
  5. package/dist/display/touch-adapter.d.ts +3 -4
  6. package/dist/display/touch-adapter.js +119 -16
  7. package/dist/display/ui-adapter.d.ts +4 -0
  8. package/dist/display/ui-adapter.js +348 -139
  9. package/dist/dt-config/kconfig.d.ts +5 -1
  10. package/dist/dt-config/kconfig.js +22 -5
  11. package/dist/dt-config/overlay.d.ts +26 -2
  12. package/dist/dt-config/overlay.js +138 -27
  13. package/dist/framework.manifest.d.ts +29 -28
  14. package/dist/framework.manifest.js +9 -3
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +5 -0
  17. package/dist/lowering/ble.js +3 -1
  18. package/dist/lowering/gpio.js +7 -3
  19. package/dist/strategy.d.ts +24 -0
  20. package/dist/strategy.js +326 -134
  21. package/dist/toolchain/debug-config.d.ts +43 -2
  22. package/dist/toolchain/debug-config.js +129 -17
  23. package/dist/toolchain/index.d.ts +13 -0
  24. package/dist/toolchain/index.js +104 -23
  25. package/dist/toolchain/scaffold.js +40 -18
  26. package/dist/toolchain/west-discover.js +4 -1
  27. package/package.json +4 -4
  28. package/src/display/index.ts +1 -1
  29. package/src/display/profiles.ts +63 -0
  30. package/src/display/touch-adapter.ts +119 -15
  31. package/src/display/ui-adapter.ts +357 -139
  32. package/src/dt-config/kconfig.ts +26 -6
  33. package/src/dt-config/overlay.ts +450 -298
  34. package/src/framework.manifest.ts +9 -3
  35. package/src/index.ts +6 -0
  36. package/src/lowering/ble.ts +3 -1
  37. package/src/lowering/gpio.ts +7 -3
  38. package/src/strategy.ts +355 -136
  39. package/src/toolchain/debug-config.ts +137 -14
  40. package/src/toolchain/index.ts +107 -24
  41. package/src/toolchain/scaffold.ts +39 -16
  42. package/src/toolchain/west-discover.ts +4 -1
package/src/strategy.ts CHANGED
@@ -65,7 +65,7 @@ import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
65
65
  import { buildTimerPolyfill } from './async/timer-polyfill.js';
66
66
  import { resolveZephyrDisplayOp, newDisplayState, type DisplayState } from './display/index.js';
67
67
  import { buildDisplayRuntime } from './display/gfx.js';
68
- import { ZEPHYR_DISPLAY_PROFILES } from './display/profiles.js';
68
+ import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES } from './display/profiles.js';
69
69
  import { zephyrDisplayAdapterGenerator } from './display/ui-adapter.js';
70
70
  import { zephyrTouchAdapter } from './display/touch-adapter.js';
71
71
 
@@ -131,7 +131,22 @@ export class ZephyrStrategy implements PlatformStrategy {
131
131
  // true so nothing is stripped — mirrors framework-esp32's forcedIncludes.
132
132
  const a = (ctx as any)?.analysis;
133
133
  const uses = (f: string): boolean => (a ? !!a[f] : true);
134
- const inc: string[] = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdio>', '<cstdint>'];
134
+ // <zephyr/drivers/gpio.h> and <cstdint> stay unconditional: gpio.h is
135
+ // cross-cutting (gpio/power/interrupt/spi/pulse lowerings + the DT-spec
136
+ // machinery all reference its API, and no single usesX flag owns it), and
137
+ // the fixed-width types come via <zephyr/kernel.h> regardless — DIRECT_CPP_TYPE_MAP
138
+ // passes int32_t/uint8_t through verbatim.
139
+ const inc: string[] = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdint>'];
140
+ // <cstdio> backs the printf family only: __tc_print/__tc_println (emitted
141
+ // solely when @typecad/expect's preprocessor injected them — tracked via
142
+ // usedPolyfillHelpers), raw printf/snprintf in user code (usesCstdio), and
143
+ // the fs/preferences/uart shims (their lowerings snprintf into buffers).
144
+ // A program touching none of those needs no <cstdio>.
145
+ const helpers = (a as { usedPolyfillHelpers?: Set<string> } | undefined)?.usedPolyfillHelpers;
146
+ const needsCstdio = uses('usesCstdio') || uses('usesFS') || uses('usesPreferences')
147
+ || uses('usesUart')
148
+ || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println');
149
+ if (needsCstdio) inc.push('<cstdio>');
135
150
  if (uses('usesI2C')) inc.push('<zephyr/drivers/i2c.h>');
136
151
  if (uses('usesSPI')) inc.push('<zephyr/drivers/spi.h>');
137
152
  if (uses('usesUart')) inc.push('<zephyr/drivers/uart.h>');
@@ -157,9 +172,10 @@ export class ZephyrStrategy implements PlatformStrategy {
157
172
  // program-analysis usesStdString detector doesn't see types generated by
158
173
  // the BLE lowering layer — so without forcing <string> here, any BLE server
159
174
  // with a Utf8 characteristic fails to compile ('std::string does not name a
160
- // type'). Uses <string>, not <string.h>: the latter is the C flat-string
161
- // header (already included for the shim's strncpy/strcmp).
162
- if (uses('usesBle')) inc.push('<stdlib.h>', '<string.h>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
175
+ // type'). <cstdlib>/<cstring> (not <stdlib.h>/<string.h>) back the shim's
176
+ // strtol/strcmp/strncpy the same AUTOSAR-compliant spelling the HTTP,
177
+ // MQTT, and Preferences paths below already use.
178
+ if (uses('usesBle')) inc.push('<cstdlib>', '<cstring>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
163
179
  // Display: the analyzer's usesDisplay flag (set by display.* hal-ops) drives
164
180
  // this include. When ctx.analysis is absent (capability query), uses()
165
181
  // defaults to true so a real build never strips it.
@@ -246,77 +262,176 @@ export class ZephyrStrategy implements PlatformStrategy {
246
262
  return found;
247
263
  }
248
264
 
265
+ /** Pins referenced by gpio.* hal-ops in the program IR. lowerGpio routes a
266
+ * pin to its devicetree spec by pin NUMBER, so the structured hal-op pins
267
+ * are the authoritative signal for which __tc_dt_* specs are needed —
268
+ * regardless of when the final call text is rendered. */
269
+ private collectGpioPinUsage(program?: ProgramIR): Set<number> {
270
+ const pins = new Set<number>();
271
+ if (!program) return pins;
272
+ const visit = (node: any): void => {
273
+ if (!node || typeof node !== 'object') return;
274
+ if (node.operation && typeof node.operation === 'object'
275
+ && typeof node.operation.operation === 'string'
276
+ && node.operation.operation.startsWith('gpio.')
277
+ && typeof node.operation.pin === 'number') {
278
+ pins.add(node.operation.pin);
279
+ }
280
+ for (const v of Object.values(node)) {
281
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
282
+ else if (v && typeof v === 'object') visit(v);
283
+ }
284
+ };
285
+ visit(program);
286
+ return pins;
287
+ }
288
+
289
+ /** Run `re` (global) against every raw string in the IR — raw expression
290
+ * values plus raw hal-op codes — returning capture group 1 of each match
291
+ * (the full match when the regex has no group). This is how references the
292
+ * text scanners must see but that never appear as IR call nodes (e.g. a
293
+ * rawCpp() escape hatch naming `__tc_dt_sw0` directly) are discovered. */
294
+ private collectRawMatches(program: ProgramIR | undefined, re: RegExp): Set<string> {
295
+ const found = new Set<string>();
296
+ if (!program) return found;
297
+ const scan = (text: string): void => {
298
+ for (const m of text.matchAll(re)) found.add(m[1] ?? m[0]);
299
+ };
300
+ const visit = (node: any): void => {
301
+ if (!node || typeof node !== 'object') return;
302
+ if (node.kind === 'raw' && typeof node.value === 'string') scan(node.value);
303
+ if (node.operation && typeof node.operation === 'object'
304
+ && node.operation.operation === 'raw' && typeof node.operation.code === 'string') {
305
+ scan(node.operation.code);
306
+ }
307
+ for (const v of Object.values(node)) {
308
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
309
+ else if (v && typeof v === 'object') visit(v);
310
+ }
311
+ };
312
+ visit(program);
313
+ return found;
314
+ }
315
+
316
+ /** Whether the wiring-compat GPIO read surface (__tc_gpio_read definition,
317
+ * __tc_gpio_dev dispatcher, and the wiring_compat polyfill's digitalRead /
318
+ * HIGH / LOW macros) must be emitted. Consumers: user digitalRead() calls
319
+ * (usesDigitalRead), the @typecad/safety voter (calls __tc_gpio_read
320
+ * directly via lowered raw text), and the UI runtime header's
321
+ * unconditional digitalRead() poll (entryHasUI — build-global, so every TU
322
+ * in a UI build carries the macros). With no analysis present (capability
323
+ * query), default to emitting — same convention as the uses() helper. */
324
+ private needsGpioReadShim(program?: ProgramIR, ctx?: PlatformContext): boolean {
325
+ if (program && programUsesSafety(program)) return true;
326
+ if (entryHasUI()) return true;
327
+ const a = (ctx as any)?.analysis;
328
+ return a ? !!a.usesDigitalRead : true;
329
+ }
330
+
249
331
  shimLines(program?: ProgramIR, ctx?: PlatformContext): string[] {
250
332
  const chip = this.resolveChip(ctx, program);
251
333
  const isPrintf = this.resolveDebugMode(ctx) === 'printf';
252
- const lines: string[] = [
253
- '// cuttlefish runtime shim. Wrapped in a single include guard so the',
254
- '// block is safe to emit into multiple headers and .cpp files within',
255
- '// one translation unit (a .cpp may #include several headers that each',
256
- '// carry the shim). The guard ensures the definitions are seen exactly',
257
- '// once per TU.',
258
- '#ifndef CUTTLEFISH_SHIM_DEFINED',
259
- '#define CUTTLEFISH_SHIM_DEFINED',
260
- '#ifndef CUTTLEFISH_UNDEFINED',
261
- '#define CUTTLEFISH_UNDEFINED 0',
262
- '#endif',
263
- 'template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }',
264
- 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }',
265
- 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }',
266
- 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }',
267
- 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }',
268
- 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }',
269
- 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }',
270
- 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }',
271
- // millis() backed by the Zephyr uptime counter. uint32_t return matches
272
- // the Arduino API the shared runtime expects (wraps every ~49.7 days).
273
- 'inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }',
274
- // Arduino-compat defines referenced by the shared runtime polyfills.
275
- '#ifndef HIGH', '#define HIGH 1', '#endif',
276
- '#ifndef LOW', '#define LOW 0', '#endif',
277
- '#ifndef PROGMEM', '#define PROGMEM', '#endif',
278
- '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; }',
279
- 'inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }',
280
- // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
281
- // for protocol output. Overloaded for string (const char*) and numeric
282
- // (double) so the same call site works for markers and test values.
283
- 'inline void __tc_print(const char* s) { printf("%s", s); }',
284
- 'inline void __tc_print(double v) { printf("%g", v); }',
285
- 'inline void __tc_println(const char* s) { printf("%s\\n", s); }',
286
- 'inline void __tc_println(double v) { printf("%g\\n", v); }',
287
- ];
288
-
289
- // Devicetree specs for every board-defined GPIO pin. Emitted unconditionally
290
- // (guarded by the include guard) so any of them is available whether or not
291
- // a given program uses it. Safe because every spec references a node that
292
- // exists in the active board's devicetree.
293
- for (const spec of chip.gpio.dtSpecs) {
294
- lines.push(
295
- `static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`,
334
+ const a = (ctx as any)?.analysis;
335
+ const uses = (f: string): boolean => (a ? !!a[f] : true);
336
+ const helpers = (a as { usedPolyfillHelpers?: Set<string> } | undefined)?.usedPolyfillHelpers;
337
+
338
+ // --- Core shim, gated item by item on actual use ------------------------
339
+ // A minimal program (blink) uses none of these, and its output carries no
340
+ // shim block at all. Everything up to the #endif composes into one guard
341
+ // body; the guard itself is only stamped when the body is non-empty.
342
+ const guardBody: string[] = [];
343
+ // CUTTLEFISH_UNDEFINED: needed when the file references null/undefined
344
+ // literals (usesNullish), emits nullish helper CALLS (usesNullishHelper),
345
+ // or has async functions (the async state machine uses the macro for
346
+ // default waitFor* timeouts not visible to the nullish scanners).
347
+ if (uses('usesNullish') || uses('usesNullishHelper') || uses('hasAsync')) {
348
+ guardBody.push(
349
+ '#ifndef CUTTLEFISH_UNDEFINED',
350
+ '#define CUTTLEFISH_UNDEFINED 0',
351
+ '#endif',
352
+ );
353
+ }
354
+ // Nullish helpers: only when the file actually emits cuttlefish_nullish /
355
+ // cuttlefish_exists CALLS (?? / ?. lowering). A file that only references
356
+ // null/undefined literals needs just the macro above — the same
357
+ // distinction the setup emitter's strip filter documents.
358
+ if (uses('usesNullishHelper')) {
359
+ guardBody.push(
360
+ 'template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }',
361
+ 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }',
362
+ 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }',
363
+ 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }',
364
+ 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }',
365
+ 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }',
366
+ 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }',
367
+ 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }',
368
+ );
369
+ }
370
+ // millis() backed by the Zephyr uptime counter. uint32_t return matches
371
+ // the Arduino API the shared runtime expects (wraps every ~49.7 days).
372
+ // Kept when the program reads the clock itself usesWallClock,
373
+ // deliberately WITHOUT the delay() conflation usesMillis carries, because
374
+ // Zephyr's delay lowers straight to k_msleep — or has a hidden poller:
375
+ // async functions / the async runtime, the setInterval/setTimeout
376
+ // scheduler, or a mounted UI's per-frame tick.
377
+ if (uses('usesWallClock') || uses('hasAsync') || (!a || a.timerCallCount > 0)
378
+ || this.programUsesAsyncRuntime(program) || entryHasUI()) {
379
+ guardBody.push(
380
+ 'inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }',
381
+ );
382
+ }
383
+ // PROGMEM: only the (Arduino-oriented) UI runtime header can reference it.
384
+ if (entryHasUI()) {
385
+ guardBody.push(
386
+ '#ifndef PROGMEM', '#define PROGMEM', '#endif',
387
+ );
388
+ }
389
+ // map()/constrain() Arduino-API helpers — dead code unless called. The
390
+ // setup emitter ORs entryHasUI() into usesConstrain before we see it (the
391
+ // UI runtime's progress/range draw calls constrain).
392
+ if (uses('usesMap')) {
393
+ guardBody.push(
394
+ '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; }',
395
+ );
396
+ }
397
+ if (uses('usesConstrain')) {
398
+ guardBody.push(
399
+ 'inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }',
400
+ );
401
+ }
402
+ // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
403
+ // for protocol output. Overloaded for string (const char*) and numeric
404
+ // (double) so the same call site works for markers and test values.
405
+ // Emitted only when the expect preprocessor actually injected the calls
406
+ // (tracked as usedPolyfillHelpers).
407
+ if (!a || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println')) {
408
+ guardBody.push(
409
+ 'inline void __tc_print(const char* s) { printf("%s", s); }',
410
+ 'inline void __tc_print(double v) { printf("%g", v); }',
411
+ 'inline void __tc_println(const char* s) { printf("%s\\n", s); }',
412
+ 'inline void __tc_println(double v) { printf("%g\\n", v); }',
296
413
  );
297
414
  }
298
415
 
299
416
  // Per-peripheral bus state — gated on the same ctx.analysis.usesX flags as
300
417
  // forcedIncludes, so an unused peripheral emits no state (and its header is
301
418
  // not included). Mirrors framework-esp32's shimLines espInit block.
302
- const a = (ctx as any)?.analysis;
303
- const uses = (f: string): boolean => (a ? !!a[f] : true);
304
419
  if (uses('usesI2C') && chip.i2c) {
305
- for (let i = 0; i < chip.i2c.controllers.length; i++) lines.push(...i2cInitLines(chip, i));
420
+ for (let i = 0; i < chip.i2c.controllers.length; i++) guardBody.push(...i2cInitLines(chip, i));
306
421
  }
307
422
  if (uses('usesSPI') && chip.spi) {
308
- for (let i = 0; i < chip.spi.controllers.length; i++) lines.push(...spiInitLines(chip, i));
423
+ for (let i = 0; i < chip.spi.controllers.length; i++) guardBody.push(...spiInitLines(chip, i));
309
424
  }
310
425
  if (uses('usesUart') && chip.uart) {
311
- for (let i = 0; i < chip.uart.controllers.length; i++) lines.push(...uartInitLines(chip, i));
426
+ for (let i = 0; i < chip.uart.controllers.length; i++) guardBody.push(...uartInitLines(chip, i));
312
427
  }
313
- if (uses('usesADC') && chip.adc) lines.push(...adcInitLines(chip));
314
- if (uses('usesPWM') && chip.pwm) lines.push(...pwmInitLines(chip));
315
- if (uses('usesDAC') && chip.dac) lines.push(...dacInitLines(chip));
316
- if (uses('usesHwtimer') && chip.hwtimer) lines.push(...hwtimerInitLines(chip));
317
- if (uses('usesInterrupts')) lines.push(...interruptInitLines(chip));
318
- if (uses('usesWDT') && chip.wdt) lines.push(...wdtInitLines(chip));
319
- if (uses('usesBle')) lines.push(...bleInitLines());
428
+ if (uses('usesADC') && chip.adc) guardBody.push(...adcInitLines(chip));
429
+ if (uses('usesPWM') && chip.pwm) guardBody.push(...pwmInitLines(chip));
430
+ if (uses('usesDAC') && chip.dac) guardBody.push(...dacInitLines(chip));
431
+ if (uses('usesHwtimer') && chip.hwtimer) guardBody.push(...hwtimerInitLines(chip));
432
+ if (uses('usesInterrupts')) guardBody.push(...interruptInitLines(chip));
433
+ if (uses('usesWDT') && chip.wdt) guardBody.push(...wdtInitLines(chip));
434
+ if (uses('usesBle')) guardBody.push(...bleInitLines());
320
435
  // Display runtime (rect/text renderer): the DIRECT-call display path (user
321
436
  // code calling screen.display.fillRect etc., no @typecad/ui). Emitted only
322
437
  // when the program uses display.* but is NOT a UI program — the UI display
@@ -330,18 +445,52 @@ export class ZephyrStrategy implements PlatformStrategy {
330
445
  // previously dead code).
331
446
  if (uses('usesDisplay') && !entryHasUI()) {
332
447
  const rt = buildDisplayRuntime(this._displayState.profile);
333
- lines.push(...rt.stateLines);
334
- lines.push(rt.fontTable);
335
- lines.push(rt.helpers);
448
+ guardBody.push(...rt.stateLines);
449
+ guardBody.push(rt.fontTable);
450
+ guardBody.push(rt.helpers);
336
451
  }
337
- if (uses('usesWifi')) lines.push(...wifiInitLines());
338
- if (uses('usesHttp')) lines.push(...httpInitLines());
339
- if (uses('usesMqtt')) lines.push(...mqttInitLines());
340
- if (uses('usesPreferences')) lines.push(...preferencesInitLines());
341
- if (uses('usesFS')) lines.push(...fsInitLines());
342
- if (uses('usesRandom')) lines.push(...randomInitLines());
452
+ if (uses('usesWifi')) guardBody.push(...wifiInitLines());
453
+ if (uses('usesHttp')) guardBody.push(...httpInitLines());
454
+ if (uses('usesMqtt')) guardBody.push(...mqttInitLines());
455
+ if (uses('usesPreferences')) guardBody.push(...preferencesInitLines());
456
+ if (uses('usesFS')) guardBody.push(...fsInitLines());
457
+ if (uses('usesRandom')) guardBody.push(...randomInitLines());
343
458
 
344
- lines.push('#endif // CUTTLEFISH_SHIM_DEFINED');
459
+ const lines: string[] = [];
460
+ if (guardBody.length > 0) {
461
+ lines.push(
462
+ '// cuttlefish runtime shim. Wrapped in a single include guard so the',
463
+ '// block is safe to emit into multiple headers and .cpp files within',
464
+ '// one translation unit (a .cpp may #include several headers that each',
465
+ '// carry the shim). The guard ensures the definitions are seen exactly',
466
+ '// once per TU.',
467
+ '#ifndef CUTTLEFISH_SHIM_DEFINED',
468
+ '#define CUTTLEFISH_SHIM_DEFINED',
469
+ ...guardBody,
470
+ '#endif // CUTTLEFISH_SHIM_DEFINED',
471
+ );
472
+ }
473
+
474
+ // Devicetree specs — one per board-defined GPIO pin, but ONLY for pins the
475
+ // program actually addresses (lowerGpio routes by pin number, and the
476
+ // structured gpio.* hal-op pins are visible here) plus aliases named
477
+ // verbatim in raw code (rawCpp escape hatches). Emitted OUTSIDE the single
478
+ // CUTTLEFISH_SHIM_DEFINED guard with a per-symbol guard: per-file pin sets
479
+ // differ, and in a multi-header TU the first header's TU-wide guard would
480
+ // otherwise hide the second header's specs. Without a program (capability
481
+ // query), emit them all.
482
+ const usedPins = this.collectGpioPinUsage(program);
483
+ const dtTextRefs = this.collectRawMatches(program, /__tc_dt_([A-Za-z0-9_]+)/g);
484
+ for (const spec of chip.gpio.dtSpecs) {
485
+ if (program && !usedPins.has(spec.pin) && !dtTextRefs.has(spec.dtSpec)) continue;
486
+ const guard = `__TC_DT_${spec.dtSpec.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}_SPEC`;
487
+ lines.push(
488
+ `#ifndef ${guard}`,
489
+ `#define ${guard}`,
490
+ `static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`,
491
+ `#endif // ${guard}`,
492
+ );
493
+ }
345
494
 
346
495
  // --- Debug-mode halt + per-breakpoint disable registry ---
347
496
  //
@@ -407,21 +556,34 @@ export class ZephyrStrategy implements PlatformStrategy {
407
556
  '}',
408
557
  );
409
558
 
410
- // Safety shims: when the program uses @typecad/safety, provide __tc_gpio_read
411
- // / __tc_gpio_write backed by the raw controller (a best-effort read that
412
- // does not depend on a pin having a DT spec). __tc_delay_us uses k_busy_wait.
559
+ // GPIO read shim: emitted only when something actually reads a pin at
560
+ // runtime user digitalRead() calls, the @typecad/safety voter (calls
561
+ // __tc_gpio_read directly), or the UI runtime header's digitalRead() poll
562
+ // (init-press-input.ts). A program that only writes/toggles GPIO needs
563
+ // neither the dispatcher nor the reader.
413
564
  //
414
- // The pin is a RUNTIME value here (safety's voter passes whatever pin it
415
- // was handed), so the controller cannot be baked in as a single DT_NODELABEL
416
- // on a multi-controller SoC (ESP32-S3: pins 0–31 gpio0, 32–48 → gpio1).
417
- // Emit a tiny __tc_gpio_dev(pin) dispatcher that resolves the owning
418
- // controller's device per pin; single-controller SoCs collapse it to a
419
- // one-liner. Each DT_NODELABEL is still compile-time-resolved per branch, so
420
- // it is always statically valid.
421
- if (program && programUsesSafety(program)) {
565
+ // The signature is `int` to match wiring_compat's forward declaration
566
+ // a uint32_t definition alongside it would leave the declared int
567
+ // overload undefined (int wins overload resolution for small integer
568
+ // arguments).
569
+ //
570
+ // The pin is a RUNTIME value here (the UI pin-watch table and safety's
571
+ // voter pass whatever pin they were handed), so the controller cannot be
572
+ // baked in as a single DT_NODELABEL on a multi-controller SoC (ESP32-S3:
573
+ // pins 0–31 → gpio0, 32–48 → gpio1). Emit a tiny __tc_gpio_dev(pin)
574
+ // dispatcher that resolves the owning controller's device per pin;
575
+ // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
576
+ // still compile-time-resolved per branch, so it is always statically valid.
577
+ if (this.needsGpioReadShim(program, ctx)) {
422
578
  lines.push(...emitGpioDevDispatcher(chip));
423
579
  lines.push(
424
- 'inline int __tc_gpio_read(uint32_t pin) { return gpio_pin_get_raw(__tc_gpio_dev(pin), pin); }',
580
+ '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)); }',
581
+ );
582
+ }
583
+ // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
584
+ // lowering, so they stay gated on it.
585
+ if (program && programUsesSafety(program)) {
586
+ lines.push(
425
587
  'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }',
426
588
  '#ifndef __TC_DELAY_US_DEFINED',
427
589
  '#define __TC_DELAY_US_DEFINED',
@@ -860,6 +1022,62 @@ export class ZephyrStrategy implements PlatformStrategy {
860
1022
  return '';
861
1023
  }
862
1024
 
1025
+ // ── Interrupt safety ─────────────────────────────────────────────────────
1026
+ // Zephyr ISRs run above thread context: anything that sleeps (k_msleep),
1027
+ // pends, or takes a driver lock is illegal there (asserted by the kernel in
1028
+ // debug builds; corrupts scheduler state otherwise). The names below are the
1029
+ // IR-level callees cuttlefish's interrupt-analysis pass matches (the same
1030
+ // keys ArduinoStrategy uses; timing.delay/delay_microseconds hal-ops are
1031
+ // mapped back to the bare names by the analyzer itself).
1032
+ isrUnsafeOperations(): Map<string, { reason: string; severity: 'warning' | 'info' }> {
1033
+ return new Map<string, { reason: string; severity: 'warning' | 'info' }>([
1034
+ ['delay', {
1035
+ reason: 'delay() lowers to k_msleep(), which sleeps the calling thread — illegal in Zephyr interrupt context (submit a k_work item or arm a k_timer instead)',
1036
+ severity: 'warning',
1037
+ }],
1038
+ ['delayMicroseconds', {
1039
+ reason: 'delayMicroseconds() busy-waits the CPU for the full delay, stalling every lower-priority interrupt and the scheduler for its duration',
1040
+ severity: 'warning',
1041
+ }],
1042
+ ['console.log', {
1043
+ reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
1044
+ severity: 'info',
1045
+ }],
1046
+ ['console.error', {
1047
+ reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
1048
+ severity: 'info',
1049
+ }],
1050
+ ['console.warn', {
1051
+ reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
1052
+ severity: 'info',
1053
+ }],
1054
+ ['I2C0', {
1055
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
1056
+ severity: 'warning',
1057
+ }],
1058
+ ['I2C1', {
1059
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
1060
+ severity: 'warning',
1061
+ }],
1062
+ ['SPI0', {
1063
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
1064
+ severity: 'warning',
1065
+ }],
1066
+ ['SPI1', {
1067
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
1068
+ severity: 'warning',
1069
+ }],
1070
+ ['UART0', {
1071
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
1072
+ severity: 'info',
1073
+ }],
1074
+ ['UART1', {
1075
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
1076
+ severity: 'info',
1077
+ }],
1078
+ ]);
1079
+ }
1080
+
863
1081
  ambientTypeDeclarations(): string[] {
864
1082
  // Preferences is the only HAL surface the framework lowers that is used as
865
1083
  // a bare global (the HAL Preferences class is exported, but the canonical
@@ -1072,6 +1290,45 @@ export class ZephyrStrategy implements PlatformStrategy {
1072
1290
  }
1073
1291
 
1074
1292
  generateNativePolyfills(program?: ProgramIR, ctx?: PlatformContext): RuntimePolyfillIR[] {
1293
+ // wiring_compat (digitalRead/HIGH/LOW macros + the __tc_gpio_read forward
1294
+ // declaration) is emitted only when something reads a pin: user
1295
+ // digitalRead() calls, the @typecad/safety voter, or the UI runtime
1296
+ // header's unconditional digitalRead() poll (init-press-input.ts — the
1297
+ // loop body is dead when no pin watchers are configured but must
1298
+ // compile). needsGpioReadShim defaults to true without analysis so
1299
+ // capability queries keep seeing it.
1300
+ const wiringCompat: RuntimePolyfillIR = {
1301
+ // Wiring-compatibility shims for symbols the UI runtime header
1302
+ // references unconditionally (e.g. init-press-input.ts polls pin
1303
+ // watchers via digitalRead/HIGH/LOW even when none are configured —
1304
+ // the loop body is dead but must compile). Zephyr lowers GPIO through
1305
+ // its __tc_gpio_* helpers (defined in shimLines); these macros route
1306
+ // the Wiring tokens to them.
1307
+ kind: 'polyfill',
1308
+ id: 'wiring_compat',
1309
+ domain: 'standard' as const,
1310
+ requiredIncludes: [],
1311
+ forwardDeclarations: [
1312
+ // Forward-declared so the digitalRead macro (below) can reference it
1313
+ // before the shim block defines the body. The shim emits the full
1314
+ // definition via gpio_pin_get_raw.
1315
+ 'int __tc_gpio_read(int pin);',
1316
+ ],
1317
+ helperStructs: [],
1318
+ helperFunctions: [],
1319
+ shimMacros: [
1320
+ '#ifndef HIGH',
1321
+ '#define HIGH 1',
1322
+ '#endif',
1323
+ '#ifndef LOW',
1324
+ '#define LOW 0',
1325
+ '#endif',
1326
+ '#ifndef digitalRead',
1327
+ '#define digitalRead(pin) __tc_gpio_read(pin)',
1328
+ '#endif',
1329
+ ],
1330
+ dependencies: [],
1331
+ };
1075
1332
  const polyfills: RuntimePolyfillIR[] = [
1076
1333
  {
1077
1334
  kind: 'polyfill',
@@ -1086,38 +1343,7 @@ export class ZephyrStrategy implements PlatformStrategy {
1086
1343
  shimMacros: [],
1087
1344
  dependencies: [],
1088
1345
  },
1089
- {
1090
- // Wiring-compatibility shims for symbols the UI runtime header
1091
- // references unconditionally (e.g. init-press-input.ts polls pin
1092
- // watchers via digitalRead/HIGH/LOW even when none are configured —
1093
- // the loop body is dead but must compile). Zephyr lowers GPIO through
1094
- // its __tc_gpio_* helpers (defined in shimLines); these macros route
1095
- // the Wiring tokens to them.
1096
- kind: 'polyfill',
1097
- id: 'wiring_compat',
1098
- domain: 'standard' as const,
1099
- requiredIncludes: [],
1100
- forwardDeclarations: [
1101
- // Forward-declared so the digitalRead macro (below) can reference it
1102
- // before the shim block defines the body. The shim emits the full
1103
- // definition via gpio_pin_get_raw.
1104
- 'int __tc_gpio_read(int pin);',
1105
- ],
1106
- helperStructs: [],
1107
- helperFunctions: [],
1108
- shimMacros: [
1109
- '#ifndef HIGH',
1110
- '#define HIGH 1',
1111
- '#endif',
1112
- '#ifndef LOW',
1113
- '#define LOW 0',
1114
- '#endif',
1115
- '#ifndef digitalRead',
1116
- '#define digitalRead(pin) __tc_gpio_read(pin)',
1117
- '#endif',
1118
- ],
1119
- dependencies: [],
1120
- },
1346
+ ...(this.needsGpioReadShim(program, ctx) ? [wiringCompat] : []),
1121
1347
  {
1122
1348
  // STL-free string-method polyfills. String methods (.toUpperCase(),
1123
1349
  // .includes(), .substring(), …) lower at IR level to __tc_* helpers for
@@ -1218,7 +1444,11 @@ struct __tc_StaticArray {
1218
1444
  id: 'async_runtime',
1219
1445
  domain: 'embedded',
1220
1446
  requiredIncludes: [],
1221
- forwardDeclarations: [],
1447
+ // Polyfill definitions emit before shimLines, but the runtime's
1448
+ // timer bodies call millis() (defined in shimLines) — declare it
1449
+ // first so the polyfill compiles even for programs whose source
1450
+ // has no explicit timing call.
1451
+ forwardDeclarations: ['unsigned long millis();'],
1222
1452
  helperStructs: [generateStaticAsyncRuntime(8, this.getAsyncRuntimeConfig().waitForPinEdge)],
1223
1453
  helperFunctions: [],
1224
1454
  shimMacros: [],
@@ -1325,11 +1555,12 @@ struct __tc_StaticArray {
1325
1555
 
1326
1556
  // ── Strategy-owned display/touch adapter seam ────────────────────────────
1327
1557
  // Zephyr owns its display + touch adapters: the UI display adapter bridges
1328
- // the in-tree CuttlefishGFX class to Zephyr's display_write() API (see
1329
- // src/display/ui-adapter.ts), and the FT6336U touch adapter drives the I2C
1330
- // controller via Zephyr's i2c API (src/display/touch-adapter.ts). Both live
1331
- // in this package so cuttlefish carries no Zephyr/Wiring-specific display or
1332
- // touch knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1558
+ // the in-tree CuttlefishGFX class to the panel (per-controller init + wire
1559
+ // format, see src/display/ui-adapter.ts), and the touch adapters drive the
1560
+ // FT6336U (I2C capacitive) and XPT2046 (SPI resistive) controllers via
1561
+ // Zephyr's bus APIs (src/display/touch-adapter.ts). Both live in this
1562
+ // package so cuttlefish carries no Zephyr/Wiring-specific display or touch
1563
+ // knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1333
1564
 
1334
1565
  providesDisplayAdapter(): boolean { return true; }
1335
1566
 
@@ -1347,22 +1578,10 @@ struct __tc_StaticArray {
1347
1578
  // Named display-profile registry: maps config `profile` values (e.g.
1348
1579
  // "st7796-zephyr") to the shared DisplayProfile shape so transpile.ts can
1349
1580
  // resolve them per-framework. The Zephyr profiles are DT-binding descriptors;
1350
- // they're mapped to the shared shape (driver/width/height/colorFormat/
1351
- // rotation) the profile resolver expects.
1581
+ // BUILT_IN_PROFILES (display/profiles.ts) is the single DT-binding
1582
+ // shared-shape mapping, shared with the preview's registry loader.
1352
1583
  getProfileRegistry(): Map<string, DisplayProfile> {
1353
- const m = new Map<string, DisplayProfile>();
1354
- for (const [name, p] of Object.entries(ZEPHYR_DISPLAY_PROFILES)) {
1355
- m.set(name, {
1356
- driver: p.driver,
1357
- width: p.width,
1358
- height: p.height,
1359
- nativeWidth: p.nativeWidth,
1360
- nativeHeight: p.nativeHeight,
1361
- colorFormat: p.colorFormat,
1362
- rotation: p.rotation ?? 1,
1363
- });
1364
- }
1365
- return m;
1584
+ return new Map(Object.entries(BUILT_IN_PROFILES));
1366
1585
  }
1367
1586
 
1368
1587
  colorFormat(): 'rgb565' | 'rgb666' | 'rgb888' | 'mono' {