@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/dist/strategy.js CHANGED
@@ -44,7 +44,7 @@ import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
44
44
  import { buildTimerPolyfill } from './async/timer-polyfill.js';
45
45
  import { resolveZephyrDisplayOp, newDisplayState } from './display/index.js';
46
46
  import { buildDisplayRuntime } from './display/gfx.js';
47
- import { ZEPHYR_DISPLAY_PROFILES } from './display/profiles.js';
47
+ import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES } from './display/profiles.js';
48
48
  import { zephyrDisplayAdapterGenerator } from './display/ui-adapter.js';
49
49
  import { zephyrTouchAdapter } from './display/touch-adapter.js';
50
50
  export class ZephyrStrategy {
@@ -113,7 +113,23 @@ export class ZephyrStrategy {
113
113
  // true so nothing is stripped — mirrors framework-esp32's forcedIncludes.
114
114
  const a = ctx?.analysis;
115
115
  const uses = (f) => (a ? !!a[f] : true);
116
- const inc = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdio>', '<cstdint>'];
116
+ // <zephyr/drivers/gpio.h> and <cstdint> stay unconditional: gpio.h is
117
+ // cross-cutting (gpio/power/interrupt/spi/pulse lowerings + the DT-spec
118
+ // machinery all reference its API, and no single usesX flag owns it), and
119
+ // the fixed-width types come via <zephyr/kernel.h> regardless — DIRECT_CPP_TYPE_MAP
120
+ // passes int32_t/uint8_t through verbatim.
121
+ const inc = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdint>'];
122
+ // <cstdio> backs the printf family only: __tc_print/__tc_println (emitted
123
+ // solely when @typecad/expect's preprocessor injected them — tracked via
124
+ // usedPolyfillHelpers), raw printf/snprintf in user code (usesCstdio), and
125
+ // the fs/preferences/uart shims (their lowerings snprintf into buffers).
126
+ // A program touching none of those needs no <cstdio>.
127
+ const helpers = a?.usedPolyfillHelpers;
128
+ const needsCstdio = uses('usesCstdio') || uses('usesFS') || uses('usesPreferences')
129
+ || uses('usesUart')
130
+ || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println');
131
+ if (needsCstdio)
132
+ inc.push('<cstdio>');
117
133
  if (uses('usesI2C'))
118
134
  inc.push('<zephyr/drivers/i2c.h>');
119
135
  if (uses('usesSPI'))
@@ -150,10 +166,11 @@ export class ZephyrStrategy {
150
166
  // program-analysis usesStdString detector doesn't see types generated by
151
167
  // the BLE lowering layer — so without forcing <string> here, any BLE server
152
168
  // with a Utf8 characteristic fails to compile ('std::string does not name a
153
- // type'). Uses <string>, not <string.h>: the latter is the C flat-string
154
- // header (already included for the shim's strncpy/strcmp).
169
+ // type'). <cstdlib>/<cstring> (not <stdlib.h>/<string.h>) back the shim's
170
+ // strtol/strcmp/strncpy the same AUTOSAR-compliant spelling the HTTP,
171
+ // MQTT, and Preferences paths below already use.
155
172
  if (uses('usesBle'))
156
- inc.push('<stdlib.h>', '<string.h>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
173
+ inc.push('<cstdlib>', '<cstring>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
157
174
  // Display: the analyzer's usesDisplay flag (set by display.* hal-ops) drives
158
175
  // this include. When ctx.analysis is absent (capability query), uses()
159
176
  // defaults to true so a real build never strips it.
@@ -238,83 +255,171 @@ export class ZephyrStrategy {
238
255
  visit(program);
239
256
  return found;
240
257
  }
258
+ /** Pins referenced by gpio.* hal-ops in the program IR. lowerGpio routes a
259
+ * pin to its devicetree spec by pin NUMBER, so the structured hal-op pins
260
+ * are the authoritative signal for which __tc_dt_* specs are needed —
261
+ * regardless of when the final call text is rendered. */
262
+ collectGpioPinUsage(program) {
263
+ const pins = new Set();
264
+ if (!program)
265
+ return pins;
266
+ const visit = (node) => {
267
+ if (!node || typeof node !== 'object')
268
+ return;
269
+ if (node.operation && typeof node.operation === 'object'
270
+ && typeof node.operation.operation === 'string'
271
+ && node.operation.operation.startsWith('gpio.')
272
+ && typeof node.operation.pin === 'number') {
273
+ pins.add(node.operation.pin);
274
+ }
275
+ for (const v of Object.values(node)) {
276
+ if (Array.isArray(v)) {
277
+ for (const item of v)
278
+ visit(item);
279
+ }
280
+ else if (v && typeof v === 'object')
281
+ visit(v);
282
+ }
283
+ };
284
+ visit(program);
285
+ return pins;
286
+ }
287
+ /** Run `re` (global) against every raw string in the IR — raw expression
288
+ * values plus raw hal-op codes — returning capture group 1 of each match
289
+ * (the full match when the regex has no group). This is how references the
290
+ * text scanners must see but that never appear as IR call nodes (e.g. a
291
+ * rawCpp() escape hatch naming `__tc_dt_sw0` directly) are discovered. */
292
+ collectRawMatches(program, re) {
293
+ const found = new Set();
294
+ if (!program)
295
+ return found;
296
+ const scan = (text) => {
297
+ for (const m of text.matchAll(re))
298
+ found.add(m[1] ?? m[0]);
299
+ };
300
+ const visit = (node) => {
301
+ if (!node || typeof node !== 'object')
302
+ return;
303
+ if (node.kind === 'raw' && typeof node.value === 'string')
304
+ scan(node.value);
305
+ if (node.operation && typeof node.operation === 'object'
306
+ && node.operation.operation === 'raw' && typeof node.operation.code === 'string') {
307
+ scan(node.operation.code);
308
+ }
309
+ for (const v of Object.values(node)) {
310
+ if (Array.isArray(v)) {
311
+ for (const item of v)
312
+ visit(item);
313
+ }
314
+ else if (v && typeof v === 'object')
315
+ visit(v);
316
+ }
317
+ };
318
+ visit(program);
319
+ return found;
320
+ }
321
+ /** Whether the wiring-compat GPIO read surface (__tc_gpio_read definition,
322
+ * __tc_gpio_dev dispatcher, and the wiring_compat polyfill's digitalRead /
323
+ * HIGH / LOW macros) must be emitted. Consumers: user digitalRead() calls
324
+ * (usesDigitalRead), the @typecad/safety voter (calls __tc_gpio_read
325
+ * directly via lowered raw text), and the UI runtime header's
326
+ * unconditional digitalRead() poll (entryHasUI — build-global, so every TU
327
+ * in a UI build carries the macros). With no analysis present (capability
328
+ * query), default to emitting — same convention as the uses() helper. */
329
+ needsGpioReadShim(program, ctx) {
330
+ if (program && programUsesSafety(program))
331
+ return true;
332
+ if (entryHasUI())
333
+ return true;
334
+ const a = ctx?.analysis;
335
+ return a ? !!a.usesDigitalRead : true;
336
+ }
241
337
  shimLines(program, ctx) {
242
338
  const chip = this.resolveChip(ctx, program);
243
339
  const isPrintf = this.resolveDebugMode(ctx) === 'printf';
244
- const lines = [
245
- '// cuttlefish runtime shim. Wrapped in a single include guard so the',
246
- '// block is safe to emit into multiple headers and .cpp files within',
247
- '// one translation unit (a .cpp may #include several headers that each',
248
- '// carry the shim). The guard ensures the definitions are seen exactly',
249
- '// once per TU.',
250
- '#ifndef CUTTLEFISH_SHIM_DEFINED',
251
- '#define CUTTLEFISH_SHIM_DEFINED',
252
- '#ifndef CUTTLEFISH_UNDEFINED',
253
- '#define CUTTLEFISH_UNDEFINED 0',
254
- '#endif',
255
- 'template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }',
256
- 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }',
257
- 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }',
258
- 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }',
259
- 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }',
260
- 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }',
261
- 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }',
262
- 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }',
263
- // millis() backed by the Zephyr uptime counter. uint32_t return matches
264
- // the Arduino API the shared runtime expects (wraps every ~49.7 days).
265
- 'inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }',
266
- // Arduino-compat defines referenced by the shared runtime polyfills.
267
- '#ifndef HIGH', '#define HIGH 1', '#endif',
268
- '#ifndef LOW', '#define LOW 0', '#endif',
269
- '#ifndef PROGMEM', '#define PROGMEM', '#endif',
270
- '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; }',
271
- 'inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }',
272
- // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
273
- // for protocol output. Overloaded for string (const char*) and numeric
274
- // (double) so the same call site works for markers and test values.
275
- 'inline void __tc_print(const char* s) { printf("%s", s); }',
276
- 'inline void __tc_print(double v) { printf("%g", v); }',
277
- 'inline void __tc_println(const char* s) { printf("%s\\n", s); }',
278
- 'inline void __tc_println(double v) { printf("%g\\n", v); }',
279
- ];
280
- // Devicetree specs for every board-defined GPIO pin. Emitted unconditionally
281
- // (guarded by the include guard) so any of them is available whether or not
282
- // a given program uses it. Safe because every spec references a node that
283
- // exists in the active board's devicetree.
284
- for (const spec of chip.gpio.dtSpecs) {
285
- lines.push(`static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`);
340
+ const a = ctx?.analysis;
341
+ const uses = (f) => (a ? !!a[f] : true);
342
+ const helpers = a?.usedPolyfillHelpers;
343
+ // --- Core shim, gated item by item on actual use ------------------------
344
+ // A minimal program (blink) uses none of these, and its output carries no
345
+ // shim block at all. Everything up to the #endif composes into one guard
346
+ // body; the guard itself is only stamped when the body is non-empty.
347
+ const guardBody = [];
348
+ // CUTTLEFISH_UNDEFINED: needed when the file references null/undefined
349
+ // literals (usesNullish), emits nullish helper CALLS (usesNullishHelper),
350
+ // or has async functions (the async state machine uses the macro for
351
+ // default waitFor* timeouts not visible to the nullish scanners).
352
+ if (uses('usesNullish') || uses('usesNullishHelper') || uses('hasAsync')) {
353
+ guardBody.push('#ifndef CUTTLEFISH_UNDEFINED', '#define CUTTLEFISH_UNDEFINED 0', '#endif');
354
+ }
355
+ // Nullish helpers: only when the file actually emits cuttlefish_nullish /
356
+ // cuttlefish_exists CALLS (?? / ?. lowering). A file that only references
357
+ // null/undefined literals needs just the macro above the same
358
+ // distinction the setup emitter's strip filter documents.
359
+ if (uses('usesNullishHelper')) {
360
+ 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; }');
361
+ }
362
+ // millis() backed by the Zephyr uptime counter. uint32_t return matches
363
+ // the Arduino API the shared runtime expects (wraps every ~49.7 days).
364
+ // Kept when the program reads the clock itself — usesWallClock,
365
+ // deliberately WITHOUT the delay() conflation usesMillis carries, because
366
+ // Zephyr's delay lowers straight to k_msleep or has a hidden poller:
367
+ // async functions / the async runtime, the setInterval/setTimeout
368
+ // scheduler, or a mounted UI's per-frame tick.
369
+ if (uses('usesWallClock') || uses('hasAsync') || (!a || a.timerCallCount > 0)
370
+ || this.programUsesAsyncRuntime(program) || entryHasUI()) {
371
+ guardBody.push('inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }');
372
+ }
373
+ // PROGMEM: only the (Arduino-oriented) UI runtime header can reference it.
374
+ if (entryHasUI()) {
375
+ guardBody.push('#ifndef PROGMEM', '#define PROGMEM', '#endif');
376
+ }
377
+ // map()/constrain() Arduino-API helpers dead code unless called. The
378
+ // setup emitter ORs entryHasUI() into usesConstrain before we see it (the
379
+ // UI runtime's progress/range draw calls constrain).
380
+ if (uses('usesMap')) {
381
+ 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; }');
382
+ }
383
+ if (uses('usesConstrain')) {
384
+ guardBody.push('inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }');
385
+ }
386
+ // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
387
+ // for protocol output. Overloaded for string (const char*) and numeric
388
+ // (double) so the same call site works for markers and test values.
389
+ // Emitted only when the expect preprocessor actually injected the calls
390
+ // (tracked as usedPolyfillHelpers).
391
+ if (!a || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println')) {
392
+ 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); }');
286
393
  }
287
394
  // Per-peripheral bus state — gated on the same ctx.analysis.usesX flags as
288
395
  // forcedIncludes, so an unused peripheral emits no state (and its header is
289
396
  // not included). Mirrors framework-esp32's shimLines espInit block.
290
- const a = ctx?.analysis;
291
- const uses = (f) => (a ? !!a[f] : true);
292
397
  if (uses('usesI2C') && chip.i2c) {
293
398
  for (let i = 0; i < chip.i2c.controllers.length; i++)
294
- lines.push(...i2cInitLines(chip, i));
399
+ guardBody.push(...i2cInitLines(chip, i));
295
400
  }
296
401
  if (uses('usesSPI') && chip.spi) {
297
402
  for (let i = 0; i < chip.spi.controllers.length; i++)
298
- lines.push(...spiInitLines(chip, i));
403
+ guardBody.push(...spiInitLines(chip, i));
299
404
  }
300
405
  if (uses('usesUart') && chip.uart) {
301
406
  for (let i = 0; i < chip.uart.controllers.length; i++)
302
- lines.push(...uartInitLines(chip, i));
407
+ guardBody.push(...uartInitLines(chip, i));
303
408
  }
304
409
  if (uses('usesADC') && chip.adc)
305
- lines.push(...adcInitLines(chip));
410
+ guardBody.push(...adcInitLines(chip));
306
411
  if (uses('usesPWM') && chip.pwm)
307
- lines.push(...pwmInitLines(chip));
412
+ guardBody.push(...pwmInitLines(chip));
308
413
  if (uses('usesDAC') && chip.dac)
309
- lines.push(...dacInitLines(chip));
414
+ guardBody.push(...dacInitLines(chip));
310
415
  if (uses('usesHwtimer') && chip.hwtimer)
311
- lines.push(...hwtimerInitLines(chip));
416
+ guardBody.push(...hwtimerInitLines(chip));
312
417
  if (uses('usesInterrupts'))
313
- lines.push(...interruptInitLines(chip));
418
+ guardBody.push(...interruptInitLines(chip));
314
419
  if (uses('usesWDT') && chip.wdt)
315
- lines.push(...wdtInitLines(chip));
420
+ guardBody.push(...wdtInitLines(chip));
316
421
  if (uses('usesBle'))
317
- lines.push(...bleInitLines());
422
+ guardBody.push(...bleInitLines());
318
423
  // Display runtime (rect/text renderer): the DIRECT-call display path (user
319
424
  // code calling screen.display.fillRect etc., no @typecad/ui). Emitted only
320
425
  // when the program uses display.* but is NOT a UI program — the UI display
@@ -328,23 +433,42 @@ export class ZephyrStrategy {
328
433
  // previously dead code).
329
434
  if (uses('usesDisplay') && !entryHasUI()) {
330
435
  const rt = buildDisplayRuntime(this._displayState.profile);
331
- lines.push(...rt.stateLines);
332
- lines.push(rt.fontTable);
333
- lines.push(rt.helpers);
436
+ guardBody.push(...rt.stateLines);
437
+ guardBody.push(rt.fontTable);
438
+ guardBody.push(rt.helpers);
334
439
  }
335
440
  if (uses('usesWifi'))
336
- lines.push(...wifiInitLines());
441
+ guardBody.push(...wifiInitLines());
337
442
  if (uses('usesHttp'))
338
- lines.push(...httpInitLines());
443
+ guardBody.push(...httpInitLines());
339
444
  if (uses('usesMqtt'))
340
- lines.push(...mqttInitLines());
445
+ guardBody.push(...mqttInitLines());
341
446
  if (uses('usesPreferences'))
342
- lines.push(...preferencesInitLines());
447
+ guardBody.push(...preferencesInitLines());
343
448
  if (uses('usesFS'))
344
- lines.push(...fsInitLines());
449
+ guardBody.push(...fsInitLines());
345
450
  if (uses('usesRandom'))
346
- lines.push(...randomInitLines());
347
- lines.push('#endif // CUTTLEFISH_SHIM_DEFINED');
451
+ guardBody.push(...randomInitLines());
452
+ const lines = [];
453
+ if (guardBody.length > 0) {
454
+ 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');
455
+ }
456
+ // Devicetree specs — one per board-defined GPIO pin, but ONLY for pins the
457
+ // program actually addresses (lowerGpio routes by pin number, and the
458
+ // structured gpio.* hal-op pins are visible here) plus aliases named
459
+ // verbatim in raw code (rawCpp escape hatches). Emitted OUTSIDE the single
460
+ // CUTTLEFISH_SHIM_DEFINED guard with a per-symbol guard: per-file pin sets
461
+ // differ, and in a multi-header TU the first header's TU-wide guard would
462
+ // otherwise hide the second header's specs. Without a program (capability
463
+ // query), emit them all.
464
+ const usedPins = this.collectGpioPinUsage(program);
465
+ const dtTextRefs = this.collectRawMatches(program, /__tc_dt_([A-Za-z0-9_]+)/g);
466
+ for (const spec of chip.gpio.dtSpecs) {
467
+ if (program && !usedPins.has(spec.pin) && !dtTextRefs.has(spec.dtSpec))
468
+ continue;
469
+ const guard = `__TC_DT_${spec.dtSpec.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}_SPEC`;
470
+ 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}`);
471
+ }
348
472
  // --- Debug-mode halt + per-breakpoint disable registry ---
349
473
  //
350
474
  // Printf mode only. In gdb mode the cuttlefish debug preprocessor is
@@ -377,20 +501,32 @@ export class ZephyrStrategy {
377
501
  // separate translation unit when generateHeaderFile() splits them into the
378
502
  // header.
379
503
  lines.push('extern void setup(void);', 'extern void loop(void);', '', 'int main(void) {', ' setup();', ' for (;;) {', ' loop();', ' k_msleep(1);', ' }', ' return 0;', '}');
380
- // Safety shims: when the program uses @typecad/safety, provide __tc_gpio_read
381
- // / __tc_gpio_write backed by the raw controller (a best-effort read that
382
- // does not depend on a pin having a DT spec). __tc_delay_us uses k_busy_wait.
504
+ // GPIO read shim: emitted only when something actually reads a pin at
505
+ // runtime user digitalRead() calls, the @typecad/safety voter (calls
506
+ // __tc_gpio_read directly), or the UI runtime header's digitalRead() poll
507
+ // (init-press-input.ts). A program that only writes/toggles GPIO needs
508
+ // neither the dispatcher nor the reader.
383
509
  //
384
- // The pin is a RUNTIME value here (safety's voter passes whatever pin it
385
- // was handed), so the controller cannot be baked in as a single DT_NODELABEL
386
- // on a multi-controller SoC (ESP32-S3: pins 0–31 gpio0, 32–48 → gpio1).
387
- // Emit a tiny __tc_gpio_dev(pin) dispatcher that resolves the owning
388
- // controller's device per pin; single-controller SoCs collapse it to a
389
- // one-liner. Each DT_NODELABEL is still compile-time-resolved per branch, so
390
- // it is always statically valid.
391
- if (program && programUsesSafety(program)) {
510
+ // The signature is `int` to match wiring_compat's forward declaration
511
+ // a uint32_t definition alongside it would leave the declared int
512
+ // overload undefined (int wins overload resolution for small integer
513
+ // arguments).
514
+ //
515
+ // The pin is a RUNTIME value here (the UI pin-watch table and safety's
516
+ // voter pass whatever pin they were handed), so the controller cannot be
517
+ // baked in as a single DT_NODELABEL on a multi-controller SoC (ESP32-S3:
518
+ // pins 0–31 → gpio0, 32–48 → gpio1). Emit a tiny __tc_gpio_dev(pin)
519
+ // dispatcher that resolves the owning controller's device per pin;
520
+ // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
521
+ // still compile-time-resolved per branch, so it is always statically valid.
522
+ if (this.needsGpioReadShim(program, ctx)) {
392
523
  lines.push(...emitGpioDevDispatcher(chip));
393
- lines.push('inline int __tc_gpio_read(uint32_t pin) { return gpio_pin_get_raw(__tc_gpio_dev(pin), pin); }', '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');
524
+ 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)); }');
525
+ }
526
+ // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
527
+ // lowering, so they stay gated on it.
528
+ if (program && programUsesSafety(program)) {
529
+ 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');
394
530
  }
395
531
  return lines;
396
532
  }
@@ -780,6 +916,61 @@ export class ZephyrStrategy {
780
916
  apiReservedEnumGuard() {
781
917
  return '';
782
918
  }
919
+ // ── Interrupt safety ─────────────────────────────────────────────────────
920
+ // Zephyr ISRs run above thread context: anything that sleeps (k_msleep),
921
+ // pends, or takes a driver lock is illegal there (asserted by the kernel in
922
+ // debug builds; corrupts scheduler state otherwise). The names below are the
923
+ // IR-level callees cuttlefish's interrupt-analysis pass matches (the same
924
+ // keys ArduinoStrategy uses; timing.delay/delay_microseconds hal-ops are
925
+ // mapped back to the bare names by the analyzer itself).
926
+ isrUnsafeOperations() {
927
+ return new Map([
928
+ ['delay', {
929
+ 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)',
930
+ severity: 'warning',
931
+ }],
932
+ ['delayMicroseconds', {
933
+ reason: 'delayMicroseconds() busy-waits the CPU for the full delay, stalling every lower-priority interrupt and the scheduler for its duration',
934
+ severity: 'warning',
935
+ }],
936
+ ['console.log', {
937
+ 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',
938
+ severity: 'info',
939
+ }],
940
+ ['console.error', {
941
+ 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',
942
+ severity: 'info',
943
+ }],
944
+ ['console.warn', {
945
+ 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',
946
+ severity: 'info',
947
+ }],
948
+ ['I2C0', {
949
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
950
+ severity: 'warning',
951
+ }],
952
+ ['I2C1', {
953
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
954
+ severity: 'warning',
955
+ }],
956
+ ['SPI0', {
957
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
958
+ severity: 'warning',
959
+ }],
960
+ ['SPI1', {
961
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
962
+ severity: 'warning',
963
+ }],
964
+ ['UART0', {
965
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
966
+ severity: 'info',
967
+ }],
968
+ ['UART1', {
969
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
970
+ severity: 'info',
971
+ }],
972
+ ]);
973
+ }
783
974
  ambientTypeDeclarations() {
784
975
  // Preferences is the only HAL surface the framework lowers that is used as
785
976
  // a bare global (the HAL Preferences class is exported, but the canonical
@@ -954,6 +1145,45 @@ export class ZephyrStrategy {
954
1145
  ]);
955
1146
  }
956
1147
  generateNativePolyfills(program, ctx) {
1148
+ // wiring_compat (digitalRead/HIGH/LOW macros + the __tc_gpio_read forward
1149
+ // declaration) is emitted only when something reads a pin: user
1150
+ // digitalRead() calls, the @typecad/safety voter, or the UI runtime
1151
+ // header's unconditional digitalRead() poll (init-press-input.ts — the
1152
+ // loop body is dead when no pin watchers are configured but must
1153
+ // compile). needsGpioReadShim defaults to true without analysis so
1154
+ // capability queries keep seeing it.
1155
+ const wiringCompat = {
1156
+ // Wiring-compatibility shims for symbols the UI runtime header
1157
+ // references unconditionally (e.g. init-press-input.ts polls pin
1158
+ // watchers via digitalRead/HIGH/LOW even when none are configured —
1159
+ // the loop body is dead but must compile). Zephyr lowers GPIO through
1160
+ // its __tc_gpio_* helpers (defined in shimLines); these macros route
1161
+ // the Wiring tokens to them.
1162
+ kind: 'polyfill',
1163
+ id: 'wiring_compat',
1164
+ domain: 'standard',
1165
+ requiredIncludes: [],
1166
+ forwardDeclarations: [
1167
+ // Forward-declared so the digitalRead macro (below) can reference it
1168
+ // before the shim block defines the body. The shim emits the full
1169
+ // definition via gpio_pin_get_raw.
1170
+ 'int __tc_gpio_read(int pin);',
1171
+ ],
1172
+ helperStructs: [],
1173
+ helperFunctions: [],
1174
+ shimMacros: [
1175
+ '#ifndef HIGH',
1176
+ '#define HIGH 1',
1177
+ '#endif',
1178
+ '#ifndef LOW',
1179
+ '#define LOW 0',
1180
+ '#endif',
1181
+ '#ifndef digitalRead',
1182
+ '#define digitalRead(pin) __tc_gpio_read(pin)',
1183
+ '#endif',
1184
+ ],
1185
+ dependencies: [],
1186
+ };
957
1187
  const polyfills = [
958
1188
  {
959
1189
  kind: 'polyfill',
@@ -968,38 +1198,7 @@ export class ZephyrStrategy {
968
1198
  shimMacros: [],
969
1199
  dependencies: [],
970
1200
  },
971
- {
972
- // Wiring-compatibility shims for symbols the UI runtime header
973
- // references unconditionally (e.g. init-press-input.ts polls pin
974
- // watchers via digitalRead/HIGH/LOW even when none are configured —
975
- // the loop body is dead but must compile). Zephyr lowers GPIO through
976
- // its __tc_gpio_* helpers (defined in shimLines); these macros route
977
- // the Wiring tokens to them.
978
- kind: 'polyfill',
979
- id: 'wiring_compat',
980
- domain: 'standard',
981
- requiredIncludes: [],
982
- forwardDeclarations: [
983
- // Forward-declared so the digitalRead macro (below) can reference it
984
- // before the shim block defines the body. The shim emits the full
985
- // definition via gpio_pin_get_raw.
986
- 'int __tc_gpio_read(int pin);',
987
- ],
988
- helperStructs: [],
989
- helperFunctions: [],
990
- shimMacros: [
991
- '#ifndef HIGH',
992
- '#define HIGH 1',
993
- '#endif',
994
- '#ifndef LOW',
995
- '#define LOW 0',
996
- '#endif',
997
- '#ifndef digitalRead',
998
- '#define digitalRead(pin) __tc_gpio_read(pin)',
999
- '#endif',
1000
- ],
1001
- dependencies: [],
1002
- },
1201
+ ...(this.needsGpioReadShim(program, ctx) ? [wiringCompat] : []),
1003
1202
  {
1004
1203
  // STL-free string-method polyfills. String methods (.toUpperCase(),
1005
1204
  // .includes(), .substring(), …) lower at IR level to __tc_* helpers for
@@ -1098,7 +1297,11 @@ struct __tc_StaticArray {
1098
1297
  id: 'async_runtime',
1099
1298
  domain: 'embedded',
1100
1299
  requiredIncludes: [],
1101
- forwardDeclarations: [],
1300
+ // Polyfill definitions emit before shimLines, but the runtime's
1301
+ // timer bodies call millis() (defined in shimLines) — declare it
1302
+ // first so the polyfill compiles even for programs whose source
1303
+ // has no explicit timing call.
1304
+ forwardDeclarations: ['unsigned long millis();'],
1102
1305
  helperStructs: [generateStaticAsyncRuntime(8, this.getAsyncRuntimeConfig().waitForPinEdge)],
1103
1306
  helperFunctions: [],
1104
1307
  shimMacros: [],
@@ -1184,11 +1387,12 @@ struct __tc_StaticArray {
1184
1387
  }
1185
1388
  // ── Strategy-owned display/touch adapter seam ────────────────────────────
1186
1389
  // Zephyr owns its display + touch adapters: the UI display adapter bridges
1187
- // the in-tree CuttlefishGFX class to Zephyr's display_write() API (see
1188
- // src/display/ui-adapter.ts), and the FT6336U touch adapter drives the I2C
1189
- // controller via Zephyr's i2c API (src/display/touch-adapter.ts). Both live
1190
- // in this package so cuttlefish carries no Zephyr/Wiring-specific display or
1191
- // touch knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1390
+ // the in-tree CuttlefishGFX class to the panel (per-controller init + wire
1391
+ // format, see src/display/ui-adapter.ts), and the touch adapters drive the
1392
+ // FT6336U (I2C capacitive) and XPT2046 (SPI resistive) controllers via
1393
+ // Zephyr's bus APIs (src/display/touch-adapter.ts). Both live in this
1394
+ // package so cuttlefish carries no Zephyr/Wiring-specific display or touch
1395
+ // knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1192
1396
  providesDisplayAdapter() { return true; }
1193
1397
  resolveDisplayAdapter(display) {
1194
1398
  const code = zephyrDisplayAdapterGenerator(display);
@@ -1201,22 +1405,10 @@ struct __tc_StaticArray {
1201
1405
  // Named display-profile registry: maps config `profile` values (e.g.
1202
1406
  // "st7796-zephyr") to the shared DisplayProfile shape so transpile.ts can
1203
1407
  // resolve them per-framework. The Zephyr profiles are DT-binding descriptors;
1204
- // they're mapped to the shared shape (driver/width/height/colorFormat/
1205
- // rotation) the profile resolver expects.
1408
+ // BUILT_IN_PROFILES (display/profiles.ts) is the single DT-binding
1409
+ // shared-shape mapping, shared with the preview's registry loader.
1206
1410
  getProfileRegistry() {
1207
- const m = new Map();
1208
- for (const [name, p] of Object.entries(ZEPHYR_DISPLAY_PROFILES)) {
1209
- m.set(name, {
1210
- driver: p.driver,
1211
- width: p.width,
1212
- height: p.height,
1213
- nativeWidth: p.nativeWidth,
1214
- nativeHeight: p.nativeHeight,
1215
- colorFormat: p.colorFormat,
1216
- rotation: p.rotation ?? 1,
1217
- });
1218
- }
1219
- return m;
1411
+ return new Map(Object.entries(BUILT_IN_PROFILES));
1220
1412
  }
1221
1413
  colorFormat() {
1222
1414
  return 'rgb565';