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

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/dist/chips/esp32.js +12 -0
  2. package/dist/chips/types.d.ts +30 -0
  3. package/dist/chips/xiao-ble.js +6 -0
  4. package/dist/display/gfx.d.ts +12 -3
  5. package/dist/display/gfx.js +130 -17
  6. package/dist/display/profiles.d.ts +25 -0
  7. package/dist/display/profiles.js +30 -0
  8. package/dist/display/touch-adapter.d.ts +3 -4
  9. package/dist/display/touch-adapter.js +119 -16
  10. package/dist/display/ui-adapter.js +308 -139
  11. package/dist/doctor.d.ts +3 -3
  12. package/dist/doctor.js +56 -29
  13. package/dist/dt-config/kconfig.d.ts +8 -1
  14. package/dist/dt-config/kconfig.js +40 -5
  15. package/dist/dt-config/overlay.d.ts +18 -1
  16. package/dist/dt-config/overlay.js +124 -26
  17. package/dist/framework.manifest.d.ts +29 -28
  18. package/dist/framework.manifest.js +46 -17
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +5 -0
  21. package/dist/licenses.d.ts +59 -0
  22. package/dist/licenses.js +347 -0
  23. package/dist/lowering/ble.js +3 -1
  24. package/dist/lowering/dac.d.ts +15 -0
  25. package/dist/lowering/dac.js +69 -0
  26. package/dist/lowering/fs.d.ts +16 -0
  27. package/dist/lowering/fs.js +121 -0
  28. package/dist/lowering/hwtimer.d.ts +15 -0
  29. package/dist/lowering/hwtimer.js +84 -0
  30. package/dist/lowering/index.d.ts +4 -1
  31. package/dist/lowering/index.js +12 -3
  32. package/dist/strategy.d.ts +4 -0
  33. package/dist/strategy.js +275 -35
  34. package/dist/toolchain/compat.js +10 -1
  35. package/dist/toolchain/env-check.d.ts +93 -0
  36. package/dist/toolchain/env-check.js +190 -0
  37. package/dist/toolchain/index.js +39 -9
  38. package/dist/toolchain/scaffold.js +7 -2
  39. package/dist/toolchain/west-discover.d.ts +11 -3
  40. package/dist/toolchain/west-discover.js +84 -7
  41. package/dist/toolchain/west-spawn.js +15 -0
  42. package/package.json +4 -4
  43. package/src/chips/esp32.ts +12 -0
  44. package/src/chips/types.ts +29 -0
  45. package/src/chips/xiao-ble.ts +6 -0
  46. package/src/display/gfx.ts +135 -19
  47. package/src/display/profiles.ts +53 -0
  48. package/src/display/touch-adapter.ts +119 -15
  49. package/src/display/ui-adapter.ts +311 -139
  50. package/src/doctor.ts +77 -56
  51. package/src/dt-config/kconfig.ts +45 -6
  52. package/src/dt-config/overlay.ts +159 -29
  53. package/src/framework.manifest.ts +47 -17
  54. package/src/index.ts +6 -0
  55. package/src/licenses.ts +425 -0
  56. package/src/lowering/ble.ts +3 -1
  57. package/src/lowering/dac.ts +82 -0
  58. package/src/lowering/fs.ts +127 -0
  59. package/src/lowering/hwtimer.ts +101 -0
  60. package/src/lowering/index.ts +9 -2
  61. package/src/strategy.ts +271 -35
  62. package/src/toolchain/compat.ts +154 -145
  63. package/src/toolchain/env-check.ts +285 -0
  64. package/src/toolchain/index.ts +40 -9
  65. package/src/toolchain/scaffold.ts +7 -2
  66. package/src/toolchain/west-discover.ts +92 -9
  67. package/src/toolchain/west-spawn.ts +15 -0
@@ -0,0 +1,101 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Hardware-timer lowering — Zephyr counter API
3
+ //
4
+ // HAL `hwtimer.*` (instance → frequency/overflow/start/stop) maps onto Zephyr's
5
+ // counter driver (<zephyr/drivers/counter.h>). A chip declares the counter
6
+ // devices it exposes (e.g. nRF RTC1 — RTC0 is kernel-owned) via
7
+ // `hwtimer.controllers[instance].nodeLabel`.
8
+ //
9
+ // Frequency model: a Zephyr counter has a fixed clock; the HAL
10
+ // set_frequency(hz) is realized as a top value of counter_freq/hz with an alarm
11
+ // callback (the on_overflow handler). start() arms both the top value and the
12
+ // callback so the call order (setFrequency → onOverflow → start, or any
13
+ // permutation) is handled uniformly.
14
+ //
15
+ // Targets without a free counter omit `hwtimer`; usage lowers to a comment and
16
+ // profileDiagnostics flags it. The JS setInterval/setTimeout polyfill
17
+ // (k_timer) is a separate surface and is unaffected.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
21
+ import type { ZephyrChipDescriptor } from '../chips/types.js';
22
+
23
+ /** Per-instance emitted symbol stems. */
24
+ function devVar(instance: number): string {
25
+ return `__tc_hw_dev_${instance}`;
26
+ }
27
+ function hzVar(instance: number): string {
28
+ return `__tc_hw_hz_${instance}`;
29
+ }
30
+ function cbVar(instance: number): string {
31
+ return `__tc_hw_cb_${instance}`;
32
+ }
33
+
34
+ /**
35
+ * Emit the per-instance counter device handles + frequency/callback state.
36
+ * Called from shimLines when the program uses hardware timers.
37
+ */
38
+ export function hwtimerInitLines(chip: ZephyrChipDescriptor): string[] {
39
+ const controllers = chip.hwtimer?.controllers ?? [];
40
+ if (controllers.length === 0) return [];
41
+ const lines: string[] = ['// CUTTLEFISH_HWTIMER_BEGIN'];
42
+ controllers.forEach((c, i) => {
43
+ lines.push(
44
+ `static const struct device* ${devVar(i)} = DEVICE_DT_GET(DT_NODELABEL(${c.nodeLabel}));`,
45
+ `static uint32_t ${hzVar(i)} = 1;`,
46
+ `static counter_top_callback_t ${cbVar(i)} = NULL;`,
47
+ );
48
+ });
49
+ lines.push('// CUTTLEFISH_HWTIMER_END');
50
+ return lines;
51
+ }
52
+
53
+ /**
54
+ * Resolve a HAL hwtimer.* op to Zephyr C++.
55
+ * Returns `{ code }` for statement ops.
56
+ */
57
+ export function lowerHwtimer(
58
+ op: HALOpIR,
59
+ chip: ZephyrChipDescriptor,
60
+ ): { code?: string; expression?: string } {
61
+ const o = op as any;
62
+ const controllers = chip.hwtimer?.controllers ?? [];
63
+
64
+ // No counter on this target. Comment + (in profileDiagnostics) a clear error.
65
+ if (controllers.length === 0) {
66
+ return { code: `/* hwtimer instance ${o.instance}: no counter device on ${chip.id} */` };
67
+ }
68
+
69
+ const instance = typeof o.instance === 'number' ? o.instance : parseInt(String(o.instance), 10);
70
+ if (isNaN(instance) || instance < 0 || instance >= controllers.length) {
71
+ return { code: `/* hwtimer instance ${o.instance}: out of range on ${chip.id} */` };
72
+ }
73
+
74
+ switch (op.operation) {
75
+ case 'hwtimer.set_frequency':
76
+ return { code: `${hzVar(instance)} = ${o.hz};` };
77
+ case 'hwtimer.on_overflow':
78
+ return { code: `${cbVar(instance)} = (${o.handler});` };
79
+ case 'hwtimer.start':
80
+ // Arm the top value (counter_freq / desired_hz) + the overflow callback,
81
+ // then start the counter. Doing both here handles any call order — the
82
+ // HAL typical sequence (setFrequency → onOverflow → start) and permutations.
83
+ return {
84
+ code: [
85
+ `{`,
86
+ ` uint32_t __f = counter_get_frequency(${devVar(instance)});`,
87
+ ` uint32_t __top = __f ? (__f / ${hzVar(instance)}) : 0U;`,
88
+ ` if (__top > 0U) { (void)counter_set_top_value(${devVar(instance)}, __top, ${cbVar(instance)}, NULL); }`,
89
+ ` (void)counter_start(${devVar(instance)});`,
90
+ `}`,
91
+ ].join(' '),
92
+ };
93
+ case 'hwtimer.stop':
94
+ return { code: `(void)counter_stop(${devVar(instance)});` };
95
+ default:
96
+ throw new Error(
97
+ `framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
98
+ `Open an issue or use rawCpp() to emit it manually.`,
99
+ );
100
+ }
101
+ }
@@ -3,8 +3,8 @@
3
3
  //
4
4
  // Routes a HALOpIR to the per-category lowering module by category prefix
5
5
  // (e.g. 'gpio.write' → lowerGpio). Returns undefined for categories the
6
- // framework does not lower (display/fs/mdns/ota/rmt/dac/
7
- // hwtimer/capacitive/temp/espnow/crypto/i2s/twai/usb/eth/pcnt/mcpwm — see
6
+ // framework does not lower (display/mdns/ota/rmt/
7
+ // capacitive/temp/espnow/crypto/i2s/twai/usb/eth/pcnt/mcpwm — see
8
8
  // the manifest), so the transpiler falls back and the manifest validator
9
9
  // cross-checks the unsupported categories. (board.* is registered below but is
10
10
  // a dead-letter — its values are constant-folded at IR-build time.)
@@ -38,11 +38,15 @@ import { lowerMqtt } from './mqtt.js';
38
38
  import { lowerPreferences } from './preferences.js';
39
39
  import { lowerBoard } from './board.js';
40
40
  import { lowerRandom } from './random.js';
41
+ import { lowerDac } from './dac.js';
42
+ import { lowerFs } from './fs.js';
43
+ import { lowerHwtimer } from './hwtimer.js';
41
44
 
42
45
  export {
43
46
  lowerGpio, lowerTiming, lowerAdc, lowerPwm, lowerI2c, lowerSpi, lowerUart,
44
47
  lowerInterrupt, lowerWdt, lowerPower, lowerTone, lowerPulseOrShift, lowerBle,
45
48
  lowerWifi, lowerHttp, lowerMqtt, lowerPreferences, lowerBoard, lowerRandom,
49
+ lowerDac, lowerFs, lowerHwtimer,
46
50
  };
47
51
 
48
52
  /**
@@ -58,11 +62,13 @@ export function lowerHalOp(
58
62
  if (op.operation.startsWith('timing.')) return lowerTiming(op);
59
63
  if (op.operation.startsWith('adc.')) return lowerAdc(op, chip);
60
64
  if (op.operation.startsWith('pwm.')) return lowerPwm(op, chip);
65
+ if (op.operation.startsWith('dac.')) return lowerDac(op, chip);
61
66
  if (op.operation.startsWith('i2c.')) return lowerI2c(op, chip);
62
67
  if (op.operation.startsWith('spi.')) return lowerSpi(op, chip);
63
68
  if (op.operation.startsWith('uart.')) return lowerUart(op);
64
69
  if (op.operation.startsWith('interrupt.')) return lowerInterrupt(op, chip);
65
70
  if (op.operation.startsWith('wdt.')) return lowerWdt(op);
71
+ if (op.operation.startsWith('hwtimer.')) return lowerHwtimer(op, chip);
66
72
  if (op.operation.startsWith('power.')) return lowerPower(op);
67
73
  if (op.operation.startsWith('tone.')) return lowerTone(op, chip);
68
74
  // pulse.* and shift.* share a bit-bang lowering module.
@@ -74,6 +80,7 @@ export function lowerHalOp(
74
80
  if (op.operation.startsWith('http.')) return lowerHttp(op);
75
81
  if (op.operation.startsWith('mqtt.')) return lowerMqtt(op);
76
82
  if (op.operation.startsWith('preferences.')) return lowerPreferences(op);
83
+ if (op.operation.startsWith('fs.')) return lowerFs(op);
77
84
  // board.resolve is constant-folded at IR-build time; lowerBoard is the
78
85
  // dead-letter reached only on an unresolvable path.
79
86
  if (op.operation.startsWith('board.')) return lowerBoard(op);
package/src/strategy.ts CHANGED
@@ -36,7 +36,9 @@ import type {
36
36
  } from '@typecad/cuttlefish/api/shared';
37
37
  import { DEFAULT_STDLIB_SUPPORT } from '@typecad/cuttlefish/api/shared';
38
38
  import { buildWorkerRuntimePolyfill } from '@typecad/cuttlefish/api/shared';
39
+ import { applyStringMethodRewrites } from '@typecad/cuttlefish/api/shared';
39
40
  import { programUsesSafety } from '@typecad/cuttlefish/api';
41
+ import { entryHasUI } from '@typecad/cuttlefish/ui-hook';
40
42
  import { chipForTarget, setActiveChip, getActiveChip } from './chips/index.js';
41
43
  import { resolveChipFromBoard } from './chips/resolve.js';
42
44
  import { emitGpioDevDispatcher } from './chips/controllers.js';
@@ -44,6 +46,9 @@ import { lowerHalOp } from './lowering/index.js';
44
46
  import { buildZephyrWorkerBacking } from './lowering/worker-backing.js';
45
47
  import { adcInitLines } from './lowering/adc.js';
46
48
  import { pwmInitLines } from './lowering/pwm.js';
49
+ import { dacInitLines } from './lowering/dac.js';
50
+ import { fsInitLines } from './lowering/fs.js';
51
+ import { hwtimerInitLines } from './lowering/hwtimer.js';
47
52
  import { i2cInitLines } from './lowering/i2c.js';
48
53
  import { spiInitLines } from './lowering/spi.js';
49
54
  import { uartInitLines } from './lowering/uart.js';
@@ -137,6 +142,13 @@ export class ZephyrStrategy implements PlatformStrategy {
137
142
  if (isPrintf && !inc.includes('<zephyr/drivers/uart.h>')) inc.push('<zephyr/drivers/uart.h>');
138
143
  if (uses('usesADC')) inc.push('<zephyr/drivers/adc.h>');
139
144
  if (uses('usesPWM')) inc.push('<zephyr/drivers/pwm.h>');
145
+ if (uses('usesDAC')) inc.push('<zephyr/drivers/dac.h>');
146
+ // Filesystem: littlefs on the storage partition. <cstring> backs the
147
+ // shim's strlen; the storage/flash_map + fs/littlefs headers carry the
148
+ // FIXED_PARTITION_ID macro + FS_LITTLEFS_DECLARE_DEFAULT_CONFIG the shim uses.
149
+ if (uses('usesFS')) inc.push('<zephyr/fs/fs.h>', '<zephyr/fs/littlefs.h>', '<zephyr/storage/flash_map.h>', '<cstring>');
150
+ // Hardware timers via the counter driver.
151
+ if (uses('usesHwtimer')) inc.push('<zephyr/drivers/counter.h>');
140
152
  if (uses('usesWDT')) inc.push('<zephyr/drivers/watchdog.h>');
141
153
  if (uses('usesPower')) inc.push('<zephyr/pm/pm.h>', '<zephyr/pm/state.h>', '<zephyr/pm/policy.h>');
142
154
  // BLE: the bt_* GATT API + the flat-string headers the shim uses. <string>
@@ -145,9 +157,10 @@ export class ZephyrStrategy implements PlatformStrategy {
145
157
  // program-analysis usesStdString detector doesn't see types generated by
146
158
  // the BLE lowering layer — so without forcing <string> here, any BLE server
147
159
  // with a Utf8 characteristic fails to compile ('std::string does not name a
148
- // type'). Uses <string>, not <string.h>: the latter is the C flat-string
149
- // header (already included for the shim's strncpy/strcmp).
150
- 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>');
160
+ // type'). <cstdlib>/<cstring> (not <stdlib.h>/<string.h>) back the shim's
161
+ // strtol/strcmp/strncpy the same AUTOSAR-compliant spelling the HTTP,
162
+ // MQTT, and Preferences paths below already use.
163
+ if (uses('usesBle')) inc.push('<cstdlib>', '<cstring>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
151
164
  // Display: the analyzer's usesDisplay flag (set by display.* hal-ops) drives
152
165
  // this include. When ctx.analysis is absent (capability query), uses()
153
166
  // defaults to true so a real build never strips it.
@@ -300,19 +313,23 @@ export class ZephyrStrategy implements PlatformStrategy {
300
313
  }
301
314
  if (uses('usesADC') && chip.adc) lines.push(...adcInitLines(chip));
302
315
  if (uses('usesPWM') && chip.pwm) lines.push(...pwmInitLines(chip));
316
+ if (uses('usesDAC') && chip.dac) lines.push(...dacInitLines(chip));
317
+ if (uses('usesHwtimer') && chip.hwtimer) lines.push(...hwtimerInitLines(chip));
303
318
  if (uses('usesInterrupts')) lines.push(...interruptInitLines(chip));
304
319
  if (uses('usesWDT') && chip.wdt) lines.push(...wdtInitLines(chip));
305
320
  if (uses('usesBle')) lines.push(...bleInitLines());
306
- // Display runtime (minimal rect/text renderer): gated on the analyzer's
307
- // usesDisplay flag (set by display.* hal-ops). This is the DIRECT-call
308
- // display path (user code calling screen.display.fillRect etc.). It is
309
- // SUPPRESSED when the UI display adapter is active — the adapter seam
310
- // (providesDisplayAdapter/resolveDisplayAdapter) emits its own display_*
311
- // runtime that drives the UI rendering pipeline via CuttlefishGFX, and the
312
- // two define the same symbols (display_init, __tc_display_line) so emitting
313
- // both causes redefinition errors. The adapter is active for the
314
- // strategy-owned drivers (ili9341-zephyr, st7796-zephyr).
315
- if (uses('usesDisplay') && !this.providesDisplayAdapter()) {
321
+ // Display runtime (rect/text renderer): the DIRECT-call display path (user
322
+ // code calling screen.display.fillRect etc., no @typecad/ui). Emitted only
323
+ // when the program uses display.* but is NOT a UI program the UI display
324
+ // adapter (emitted by cuttlefish's emitUIRuntime, solely under entryHasUI())
325
+ // defines the same display_init symbol, so emitting both would collide.
326
+ // `providesDisplayAdapter()` is a static capability (always true here) and
327
+ // does NOT track whether the adapter is actually emitted for THIS build, so
328
+ // the per-program UI signal (entryHasUI) is the correct gate. Without this,
329
+ // a direct display.* program has no definition for display_init/
330
+ // display_fill_rect/draw_rect/draw_text/flush (the gfx runtime was
331
+ // previously dead code).
332
+ if (uses('usesDisplay') && !entryHasUI()) {
316
333
  const rt = buildDisplayRuntime(this._displayState.profile);
317
334
  lines.push(...rt.stateLines);
318
335
  lines.push(rt.fontTable);
@@ -322,6 +339,7 @@ export class ZephyrStrategy implements PlatformStrategy {
322
339
  if (uses('usesHttp')) lines.push(...httpInitLines());
323
340
  if (uses('usesMqtt')) lines.push(...mqttInitLines());
324
341
  if (uses('usesPreferences')) lines.push(...preferencesInitLines());
342
+ if (uses('usesFS')) lines.push(...fsInitLines());
325
343
  if (uses('usesRandom')) lines.push(...randomInitLines());
326
344
 
327
345
  lines.push('#endif // CUTTLEFISH_SHIM_DEFINED');
@@ -390,21 +408,29 @@ export class ZephyrStrategy implements PlatformStrategy {
390
408
  '}',
391
409
  );
392
410
 
393
- // Safety shims: when the program uses @typecad/safety, provide __tc_gpio_read
394
- // / __tc_gpio_write backed by the raw controller (a best-effort read that
395
- // does not depend on a pin having a DT spec). __tc_delay_us uses k_busy_wait.
411
+ // GPIO read shim: the wiring_compat polyfill routes the UI runtime
412
+ // header's unconditional digitalRead() poll (init-press-input.ts) to
413
+ // __tc_gpio_read, so the definition must NOT be gated on @typecad/safety.
414
+ // The signature is `int` to match wiring_compat's forward declaration —
415
+ // a uint32_t definition alongside it would leave the declared int
416
+ // overload undefined (int wins overload resolution for small integer
417
+ // arguments).
396
418
  //
397
- // The pin is a RUNTIME value here (safety's voter passes whatever pin it
398
- // was handed), so the controller cannot be baked in as a single DT_NODELABEL
399
- // on a multi-controller SoC (ESP32-S3: pins 0–31 → gpio0, 32–48 → gpio1).
400
- // Emit a tiny __tc_gpio_dev(pin) dispatcher that resolves the owning
401
- // controller's device per pin; single-controller SoCs collapse it to a
402
- // one-liner. Each DT_NODELABEL is still compile-time-resolved per branch, so
403
- // it is always statically valid.
419
+ // The pin is a RUNTIME value here (the UI pin-watch table and safety's
420
+ // voter pass whatever pin they were handed), so the controller cannot be
421
+ // baked in as a single DT_NODELABEL on a multi-controller SoC (ESP32-S3:
422
+ // pins 0–31 → gpio0, 32–48 → gpio1). Emit a tiny __tc_gpio_dev(pin)
423
+ // dispatcher that resolves the owning controller's device per pin;
424
+ // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
425
+ // still compile-time-resolved per branch, so it is always statically valid.
426
+ lines.push(...emitGpioDevDispatcher(chip));
427
+ lines.push(
428
+ 'inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), static_cast<gpio_pin_t>(pin)); }',
429
+ );
430
+ // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
431
+ // lowering, so they stay gated on it.
404
432
  if (program && programUsesSafety(program)) {
405
- lines.push(...emitGpioDevDispatcher(chip));
406
433
  lines.push(
407
- 'inline int __tc_gpio_read(uint32_t pin) { return gpio_pin_get_raw(__tc_gpio_dev(pin), pin); }',
408
434
  'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }',
409
435
  '#ifndef __TC_DELAY_US_DEFINED',
410
436
  '#define __TC_DELAY_US_DEFINED',
@@ -429,6 +455,8 @@ export class ZephyrStrategy implements PlatformStrategy {
429
455
  const outputPins = new Set<number>();
430
456
  const adcReadPins = new Set<number>();
431
457
  const interruptPins = new Set<number>();
458
+ const dacPins = new Set<number>();
459
+ const hwtimerInstances = new Set<number>();
432
460
  let usesWifiOps = false;
433
461
  let usesHttpOps = false;
434
462
  let usesMqttOps = false;
@@ -449,6 +477,15 @@ export class ZephyrStrategy implements PlatformStrategy {
449
477
  if (op.operation === 'interrupt.attach' && typeof op.pin === 'number') {
450
478
  interruptPins.add(op.pin);
451
479
  }
480
+ if (op.operation === 'dac.write' && typeof op.pin === 'number') {
481
+ dacPins.add(op.pin);
482
+ }
483
+ if (typeof op.operation === 'string' && op.operation.startsWith('hwtimer.')) {
484
+ const inst = typeof op.instance === 'number'
485
+ ? op.instance
486
+ : parseInt(String(op.instance), 10);
487
+ if (!isNaN(inst)) hwtimerInstances.add(inst);
488
+ }
452
489
  if (typeof op.operation === 'string' && op.operation.startsWith('wifi.')) {
453
490
  usesWifiOps = true;
454
491
  }
@@ -510,6 +547,56 @@ export class ZephyrStrategy implements PlatformStrategy {
510
547
  }
511
548
  }
512
549
 
550
+ // ── DAC pin validity ────────────────────────────────────────────────────
551
+ // dac.write resolves a HAL pin to a channel via the chip descriptor's
552
+ // dac.channels map. A pin not in that map lowers to a comment (silent
553
+ // no-op), and a chip without a `dac` entry (nRF52840, ESP32-S3) has no DAC
554
+ // at all. Flag either case so the user gets a clear message instead of a
555
+ // pin that silently does nothing.
556
+ if (dacPins.size > 0) {
557
+ const dacChannels = new Set((chip.dac?.channels ?? []).map((c) => c.pin));
558
+ for (const pin of dacPins) {
559
+ if (!dacChannels.has(pin)) {
560
+ diags.push({
561
+ severity: 'error',
562
+ code: 'zephyr-dac-pin-unavailable',
563
+ message: `GPIO ${pin} is not a DAC channel on ${chip.id} and cannot be driven with dac.write.`,
564
+ hint: dacChannels.size > 0
565
+ ? `Use a DAC-capable pin. On ${chip.id}: ${[...dacChannels].sort((x, y) => x - y).join(', ')}.`
566
+ : `${chip.id} has no DAC. Use an esp32_devkitc target (ESP32 DAC on GPIO25/26).`,
567
+ source: program.fileName,
568
+ });
569
+ }
570
+ }
571
+ }
572
+
573
+ // ── Hardware-timer instance validity ────────────────────────────────────
574
+ // hwtimer.* resolves the instance index to a counter device via the chip
575
+ // descriptor's hwtimer.controllers. A chip without that entry (or an
576
+ // out-of-range instance) lowers to a comment — flag it so the user knows
577
+ // the timer will never fire.
578
+ if (hwtimerInstances.size > 0) {
579
+ const controllerCount = chip.hwtimer?.controllers.length ?? 0;
580
+ for (const inst of hwtimerInstances) {
581
+ if (controllerCount === 0) {
582
+ diags.push({
583
+ severity: 'error',
584
+ code: 'zephyr-hwtimer-unavailable',
585
+ message: `Hardware timer instance ${inst} is used but ${chip.id} exposes no free counter device.`,
586
+ hint: `${chip.id} declares no hwtimer.controllers. Use a target with a free counter (e.g. nRF RTC1).`,
587
+ source: program.fileName,
588
+ });
589
+ } else if (inst < 0 || inst >= controllerCount) {
590
+ diags.push({
591
+ severity: 'error',
592
+ code: 'zephyr-hwtimer-instance-out-of-range',
593
+ message: `Hardware timer instance ${inst} is out of range on ${chip.id} (0..${controllerCount - 1}).`,
594
+ source: program.fileName,
595
+ });
596
+ }
597
+ }
598
+ }
599
+
513
600
  // ── WiFi target validity ────────────────────────────────────────────────
514
601
  // WiFi ops require a chip with a WiFi radio. The ESP32-S3 descriptor sets
515
602
  // wifi.supported; the XIAO nRF52840 omits it (no radio). Flag wifi usage on
@@ -656,6 +743,18 @@ export class ZephyrStrategy implements PlatformStrategy {
656
743
  v = v.replace(/\bundefined\b/g, 'CUTTLEFISH_UNDEFINED');
657
744
  v = v.replace(/\bnull\b/g, 'CUTTLEFISH_UNDEFINED');
658
745
  }
746
+ // String-method lowering is shared across targets (see string-method-
747
+ // registry). Zephyr's strings are const char*, so the __tc_* helpers this
748
+ // rewrites to (defined by the string_methods polyfill) take const char*.
749
+ // includes/startsWith lower to inline strstr/strncmp (matching framework-
750
+ // arduino); everything else → a __tc_* helper call.
751
+ v = applyStringMethodRewrites(v, {
752
+ wrapReceiverFor: new Set(['indexOf']),
753
+ special: {
754
+ includes: (recv, args) => `(strstr(${recv}, ${args[0]}) != NULL)`,
755
+ startsWith: (recv, args) => `(strncmp(${recv}, ${args[0]}, strlen(${args[0]})) == 0)`,
756
+ },
757
+ });
659
758
  return v;
660
759
  }
661
760
 
@@ -770,6 +869,62 @@ export class ZephyrStrategy implements PlatformStrategy {
770
869
  return '';
771
870
  }
772
871
 
872
+ // ── Interrupt safety ─────────────────────────────────────────────────────
873
+ // Zephyr ISRs run above thread context: anything that sleeps (k_msleep),
874
+ // pends, or takes a driver lock is illegal there (asserted by the kernel in
875
+ // debug builds; corrupts scheduler state otherwise). The names below are the
876
+ // IR-level callees cuttlefish's interrupt-analysis pass matches (the same
877
+ // keys ArduinoStrategy uses; timing.delay/delay_microseconds hal-ops are
878
+ // mapped back to the bare names by the analyzer itself).
879
+ isrUnsafeOperations(): Map<string, { reason: string; severity: 'warning' | 'info' }> {
880
+ return new Map<string, { reason: string; severity: 'warning' | 'info' }>([
881
+ ['delay', {
882
+ 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)',
883
+ severity: 'warning',
884
+ }],
885
+ ['delayMicroseconds', {
886
+ reason: 'delayMicroseconds() busy-waits the CPU for the full delay, stalling every lower-priority interrupt and the scheduler for its duration',
887
+ severity: 'warning',
888
+ }],
889
+ ['console.log', {
890
+ 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',
891
+ severity: 'info',
892
+ }],
893
+ ['console.error', {
894
+ 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',
895
+ severity: 'info',
896
+ }],
897
+ ['console.warn', {
898
+ 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',
899
+ severity: 'info',
900
+ }],
901
+ ['I2C0', {
902
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
903
+ severity: 'warning',
904
+ }],
905
+ ['I2C1', {
906
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
907
+ severity: 'warning',
908
+ }],
909
+ ['SPI0', {
910
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
911
+ severity: 'warning',
912
+ }],
913
+ ['SPI1', {
914
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
915
+ severity: 'warning',
916
+ }],
917
+ ['UART0', {
918
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
919
+ severity: 'info',
920
+ }],
921
+ ['UART1', {
922
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
923
+ severity: 'info',
924
+ }],
925
+ ]);
926
+ }
927
+
773
928
  ambientTypeDeclarations(): string[] {
774
929
  // Preferences is the only HAL surface the framework lowers that is used as
775
930
  // a bare global (the HAL Preferences class is exported, but the canonical
@@ -962,16 +1117,23 @@ export class ZephyrStrategy implements PlatformStrategy {
962
1117
  }
963
1118
 
964
1119
  // ── Polyfills ───────────────────────────────────────────────────────────
965
- // No native polyfills for the MVP the shared runtime's string/array
966
- // polyfills are pulled in when a program uses them. cuttlefish_halt is the
967
- // one symbol the runtime header may reference; supply it as a halt loop.
1120
+ // Zephyr is a no-STL target (hasVector/hasString = false), so array/string
1121
+ // literals lower to __tc_StaticArray / const char* and string methods lower
1122
+ // to __tc_* helpers both need STL-free definitions emitted here (there is
1123
+ // no shared-runtime fallback; the pipeline sources 100% of polyfills from
1124
+ // generateNativePolyfills). Mirrors framework-arduino's AVR polyfills.
968
1125
 
969
1126
  nativePolyfills(): Set<string> {
970
1127
  // cuttlefish_halt: always (the runtime header may reference it).
1128
+ // string_methods / static_array: STL-free array + string helpers a no-STL
1129
+ // target needs (mutated/struct array literals + any string method).
971
1130
  // timer_methods: k_timer/k_work pool for setInterval/setTimeout (gated on
972
1131
  // timerCallCount at emit time in generateNativePolyfills).
973
1132
  // async_runtime: heap-free static Promise/microtask runtime (no STL needed).
974
- return new Set<string>(['cuttlefish_halt', 'wiring_compat', 'timer_methods', 'async_runtime']);
1133
+ return new Set<string>([
1134
+ 'cuttlefish_halt', 'wiring_compat', 'string_methods', 'static_array',
1135
+ 'timer_methods', 'async_runtime',
1136
+ ]);
975
1137
  }
976
1138
 
977
1139
  generateNativePolyfills(program?: ProgramIR, ctx?: PlatformContext): RuntimePolyfillIR[] {
@@ -1021,6 +1183,75 @@ export class ZephyrStrategy implements PlatformStrategy {
1021
1183
  ],
1022
1184
  dependencies: [],
1023
1185
  },
1186
+ {
1187
+ // STL-free string-method polyfills. String methods (.toUpperCase(),
1188
+ // .includes(), .substring(), …) lower at IR level to __tc_* helpers for
1189
+ // every target; this supplies their definitions. Minimal-libc friendly:
1190
+ // only <cstring> primitives (no <cctype> — case conversion is inline
1191
+ // ASCII so the polyfill is self-contained). Mirrors framework-arduino.
1192
+ kind: 'polyfill',
1193
+ id: 'string_methods',
1194
+ domain: 'embedded' as const,
1195
+ requiredIncludes: ['<cstring>'],
1196
+ forwardDeclarations: [],
1197
+ helperStructs: [],
1198
+ helperFunctions: [`
1199
+ // TypeCAD string method polyfills (Zephyr, minimal-libc).
1200
+ #ifndef CUTTLEFISH_STR_BUF_SIZE
1201
+ #define CUTTLEFISH_STR_BUF_SIZE 64
1202
+ #endif
1203
+ bool __tc_endsWith(const char* s, const char* suffix) { int sl = strlen(s), tl = strlen(suffix); return sl >= tl && strcmp(s + sl - tl, suffix) == 0; }
1204
+ const char* __tc_toUpperCase(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; for (char* p = b; *p; p++) { if (*p >= 'a' && *p <= 'z') { *p = static_cast<char>(*p - 32); } } return b; }
1205
+ const char* __tc_toLowerCase(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; for (char* p = b; *p; p++) { if (*p >= 'A' && *p <= 'Z') { *p = static_cast<char>(*p + 32); } } return b; }
1206
+ const char* __tc_trim(const char* s) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; while (*s == ' ' || *s == '\\t' || *s == '\\n' || *s == '\\r') s++; int len = strlen(s); while (len > 0 && (s[len-1] == ' ' || s[len-1] == '\\t' || s[len-1] == '\\n' || s[len-1] == '\\r')) len--; int cplen = len < CUTTLEFISH_STR_BUF_SIZE - 1 ? len : CUTTLEFISH_STR_BUF_SIZE - 1; strncpy(b, s, cplen); b[cplen] = '\\0'; return b; }
1207
+ const char* __tc_substring2(const char* s, int start, int end) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; int slen = strlen(s); if (start < 0) start = 0; if (end > slen) end = slen; if (end < start) end = start; int len = end - start; if (len >= CUTTLEFISH_STR_BUF_SIZE) len = CUTTLEFISH_STR_BUF_SIZE - 1; strncpy(b, s + start, len); b[len] = '\\0'; return b; }
1208
+ const char* __tc_substring1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
1209
+ const char* __tc_slice2(const char* s, int start, int end) { return __tc_substring2(s, start, end); }
1210
+ const char* __tc_slice1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
1211
+ const char* __tc_replace(const char* s, const char* old, const char* repl) { static char buf[2][CUTTLEFISH_STR_BUF_SIZE]; static uint8_t slot = 0; slot ^= 1; char* b = buf[slot]; const char* pos = strstr(s, old); if (!pos) { strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; return b; } int beforeLen = static_cast<int>(pos - s); int oldLen = static_cast<int>(strlen(old)); int replLen = static_cast<int>(strlen(repl)); if (beforeLen + replLen + static_cast<int>(strlen(pos + oldLen)) >= CUTTLEFISH_STR_BUF_SIZE) { strncpy(b, s, CUTTLEFISH_STR_BUF_SIZE - 1); b[CUTTLEFISH_STR_BUF_SIZE - 1] = '\\0'; return b; } memcpy(b, s, beforeLen); memcpy(b + beforeLen, repl, replLen); strcpy(b + beforeLen + replLen, pos + oldLen); return b; }
1212
+ const char* __tc_charAt(const char* s, int idx) { static char buf[2][2]; static uint8_t slot = 0; slot ^= 1; buf[slot][0] = s[idx]; buf[slot][1] = '\\0'; return buf[slot]; }
1213
+ int __tc_charCodeAt(const char* s, int idx) { return static_cast<int>(static_cast<unsigned char>(s[idx])); }
1214
+ int __tc_indexOf(const char* s, const char* needle) { const char* p = strstr(s, needle); return p ? static_cast<int>(p - s) : -1; }
1215
+ `],
1216
+ shimMacros: [],
1217
+ dependencies: [],
1218
+ },
1219
+ {
1220
+ // STL-free fixed-size array wrapper. Mutated/struct-element array
1221
+ // literals and array methods (.push/.pop/.map/.filter) lower to
1222
+ // __tc_StaticArray<T,N>; this supplies the template. Idempotent guard
1223
+ // so a redefinition is a no-op. Mirrors framework-arduino.
1224
+ kind: 'polyfill',
1225
+ id: 'static_array',
1226
+ domain: 'embedded' as const,
1227
+ requiredIncludes: [],
1228
+ forwardDeclarations: [],
1229
+ helperStructs: [],
1230
+ helperFunctions: [`
1231
+ #ifndef __TC_STATIC_ARRAY_DEFINED
1232
+ #define __TC_STATIC_ARRAY_DEFINED
1233
+ template<typename T, int N>
1234
+ struct __tc_StaticArray {
1235
+ T data[N];
1236
+ int _size;
1237
+ __tc_StaticArray() : _size(0) {}
1238
+ int length() const { return _size; }
1239
+ int size() const { return _size; }
1240
+ void push(T val) { if (_size < N) data[_size++] = val; }
1241
+ T pop() { return (_size > 0) ? data[--_size] : T(); }
1242
+ int indexOf(T val) const { for (int i = 0; i < _size; i++) if (data[i] == val) return i; return -1; }
1243
+ T& operator[](int i) { return data[i]; }
1244
+ const T& operator[](int i) const { return data[i]; }
1245
+ T* begin() { return &data[0]; }
1246
+ T* end() { return &data[_size]; }
1247
+ const T* begin() const { return &data[0]; }
1248
+ const T* end() const { return &data[_size]; }
1249
+ };
1250
+ #endif
1251
+ `],
1252
+ shimMacros: [],
1253
+ dependencies: [],
1254
+ },
1024
1255
  ];
1025
1256
 
1026
1257
  // Worker-offload runtime (Phase 1). Emitted only when the program uses
@@ -1052,7 +1283,11 @@ export class ZephyrStrategy implements PlatformStrategy {
1052
1283
  id: 'async_runtime',
1053
1284
  domain: 'embedded',
1054
1285
  requiredIncludes: [],
1055
- forwardDeclarations: [],
1286
+ // Polyfill definitions emit before shimLines, but the runtime's
1287
+ // timer bodies call millis() (defined in shimLines) — declare it
1288
+ // first so the polyfill compiles even for programs whose source
1289
+ // has no explicit timing call.
1290
+ forwardDeclarations: ['unsigned long millis();'],
1056
1291
  helperStructs: [generateStaticAsyncRuntime(8, this.getAsyncRuntimeConfig().waitForPinEdge)],
1057
1292
  helperFunctions: [],
1058
1293
  shimMacros: [],
@@ -1159,11 +1394,12 @@ export class ZephyrStrategy implements PlatformStrategy {
1159
1394
 
1160
1395
  // ── Strategy-owned display/touch adapter seam ────────────────────────────
1161
1396
  // Zephyr owns its display + touch adapters: the UI display adapter bridges
1162
- // the in-tree CuttlefishGFX class to Zephyr's display_write() API (see
1163
- // src/display/ui-adapter.ts), and the FT6336U touch adapter drives the I2C
1164
- // controller via Zephyr's i2c API (src/display/touch-adapter.ts). Both live
1165
- // in this package so cuttlefish carries no Zephyr/Wiring-specific display or
1166
- // touch knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1397
+ // the in-tree CuttlefishGFX class to the panel (per-controller init + wire
1398
+ // format, see src/display/ui-adapter.ts), and the touch adapters drive the
1399
+ // FT6336U (I2C capacitive) and XPT2046 (SPI resistive) controllers via
1400
+ // Zephyr's bus APIs (src/display/touch-adapter.ts). Both live in this
1401
+ // package so cuttlefish carries no Zephyr/Wiring-specific display or touch
1402
+ // knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1167
1403
 
1168
1404
  providesDisplayAdapter(): boolean { return true; }
1169
1405