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

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 (56) 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.js +13 -0
  7. package/dist/display/ui-adapter.js +6 -0
  8. package/dist/doctor.d.ts +3 -3
  9. package/dist/doctor.js +56 -29
  10. package/dist/dt-config/kconfig.d.ts +3 -0
  11. package/dist/dt-config/kconfig.js +18 -0
  12. package/dist/dt-config/overlay.js +5 -0
  13. package/dist/framework.manifest.js +37 -14
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +5 -0
  16. package/dist/licenses.d.ts +59 -0
  17. package/dist/licenses.js +347 -0
  18. package/dist/lowering/dac.d.ts +15 -0
  19. package/dist/lowering/dac.js +69 -0
  20. package/dist/lowering/fs.d.ts +16 -0
  21. package/dist/lowering/fs.js +121 -0
  22. package/dist/lowering/hwtimer.d.ts +15 -0
  23. package/dist/lowering/hwtimer.js +84 -0
  24. package/dist/lowering/index.d.ts +4 -1
  25. package/dist/lowering/index.js +12 -3
  26. package/dist/strategy.js +186 -14
  27. package/dist/toolchain/compat.js +10 -1
  28. package/dist/toolchain/env-check.d.ts +93 -0
  29. package/dist/toolchain/env-check.js +190 -0
  30. package/dist/toolchain/scaffold.js +3 -0
  31. package/dist/toolchain/west-discover.d.ts +11 -3
  32. package/dist/toolchain/west-discover.js +80 -6
  33. package/dist/toolchain/west-spawn.js +15 -0
  34. package/package.json +4 -4
  35. package/src/chips/esp32.ts +12 -0
  36. package/src/chips/types.ts +29 -0
  37. package/src/chips/xiao-ble.ts +6 -0
  38. package/src/display/gfx.ts +135 -19
  39. package/src/display/profiles.ts +13 -0
  40. package/src/display/ui-adapter.ts +5 -0
  41. package/src/doctor.ts +77 -56
  42. package/src/dt-config/kconfig.ts +19 -0
  43. package/src/dt-config/overlay.ts +5 -0
  44. package/src/framework.manifest.ts +38 -14
  45. package/src/index.ts +6 -0
  46. package/src/licenses.ts +425 -0
  47. package/src/lowering/dac.ts +82 -0
  48. package/src/lowering/fs.ts +127 -0
  49. package/src/lowering/hwtimer.ts +101 -0
  50. package/src/lowering/index.ts +9 -2
  51. package/src/strategy.ts +180 -14
  52. package/src/toolchain/compat.ts +154 -145
  53. package/src/toolchain/env-check.ts +285 -0
  54. package/src/toolchain/scaffold.ts +3 -0
  55. package/src/toolchain/west-discover.ts +88 -8
  56. package/src/toolchain/west-spawn.ts +15 -0
@@ -0,0 +1,84 @@
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
+ /** Per-instance emitted symbol stems. */
20
+ function devVar(instance) {
21
+ return `__tc_hw_dev_${instance}`;
22
+ }
23
+ function hzVar(instance) {
24
+ return `__tc_hw_hz_${instance}`;
25
+ }
26
+ function cbVar(instance) {
27
+ return `__tc_hw_cb_${instance}`;
28
+ }
29
+ /**
30
+ * Emit the per-instance counter device handles + frequency/callback state.
31
+ * Called from shimLines when the program uses hardware timers.
32
+ */
33
+ export function hwtimerInitLines(chip) {
34
+ const controllers = chip.hwtimer?.controllers ?? [];
35
+ if (controllers.length === 0)
36
+ return [];
37
+ const lines = ['// CUTTLEFISH_HWTIMER_BEGIN'];
38
+ controllers.forEach((c, i) => {
39
+ lines.push(`static const struct device* ${devVar(i)} = DEVICE_DT_GET(DT_NODELABEL(${c.nodeLabel}));`, `static uint32_t ${hzVar(i)} = 1;`, `static counter_top_callback_t ${cbVar(i)} = NULL;`);
40
+ });
41
+ lines.push('// CUTTLEFISH_HWTIMER_END');
42
+ return lines;
43
+ }
44
+ /**
45
+ * Resolve a HAL hwtimer.* op to Zephyr C++.
46
+ * Returns `{ code }` for statement ops.
47
+ */
48
+ export function lowerHwtimer(op, chip) {
49
+ const o = op;
50
+ const controllers = chip.hwtimer?.controllers ?? [];
51
+ // No counter on this target. Comment + (in profileDiagnostics) a clear error.
52
+ if (controllers.length === 0) {
53
+ return { code: `/* hwtimer instance ${o.instance}: no counter device on ${chip.id} */` };
54
+ }
55
+ const instance = typeof o.instance === 'number' ? o.instance : parseInt(String(o.instance), 10);
56
+ if (isNaN(instance) || instance < 0 || instance >= controllers.length) {
57
+ return { code: `/* hwtimer instance ${o.instance}: out of range on ${chip.id} */` };
58
+ }
59
+ switch (op.operation) {
60
+ case 'hwtimer.set_frequency':
61
+ return { code: `${hzVar(instance)} = ${o.hz};` };
62
+ case 'hwtimer.on_overflow':
63
+ return { code: `${cbVar(instance)} = (${o.handler});` };
64
+ case 'hwtimer.start':
65
+ // Arm the top value (counter_freq / desired_hz) + the overflow callback,
66
+ // then start the counter. Doing both here handles any call order — the
67
+ // HAL typical sequence (setFrequency → onOverflow → start) and permutations.
68
+ return {
69
+ code: [
70
+ `{`,
71
+ ` uint32_t __f = counter_get_frequency(${devVar(instance)});`,
72
+ ` uint32_t __top = __f ? (__f / ${hzVar(instance)}) : 0U;`,
73
+ ` if (__top > 0U) { (void)counter_set_top_value(${devVar(instance)}, __top, ${cbVar(instance)}, NULL); }`,
74
+ ` (void)counter_start(${devVar(instance)});`,
75
+ `}`,
76
+ ].join(' '),
77
+ };
78
+ case 'hwtimer.stop':
79
+ return { code: `(void)counter_stop(${devVar(instance)});` };
80
+ default:
81
+ throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
82
+ `Open an issue or use rawCpp() to emit it manually.`);
83
+ }
84
+ }
@@ -18,7 +18,10 @@ import { lowerMqtt } from './mqtt.js';
18
18
  import { lowerPreferences } from './preferences.js';
19
19
  import { lowerBoard } from './board.js';
20
20
  import { lowerRandom } from './random.js';
21
- export { lowerGpio, lowerTiming, lowerAdc, lowerPwm, lowerI2c, lowerSpi, lowerUart, lowerInterrupt, lowerWdt, lowerPower, lowerTone, lowerPulseOrShift, lowerBle, lowerWifi, lowerHttp, lowerMqtt, lowerPreferences, lowerBoard, lowerRandom, };
21
+ import { lowerDac } from './dac.js';
22
+ import { lowerFs } from './fs.js';
23
+ import { lowerHwtimer } from './hwtimer.js';
24
+ export { lowerGpio, lowerTiming, lowerAdc, lowerPwm, lowerI2c, lowerSpi, lowerUart, lowerInterrupt, lowerWdt, lowerPower, lowerTone, lowerPulseOrShift, lowerBle, lowerWifi, lowerHttp, lowerMqtt, lowerPreferences, lowerBoard, lowerRandom, lowerDac, lowerFs, lowerHwtimer, };
22
25
  /**
23
26
  * Lower a HAL op to Zephyr C++. Returns undefined for unsupported categories
24
27
  * (mirrors lowerHalOp in framework-esp32/src/lowering/index.ts).
@@ -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.)
@@ -36,7 +36,10 @@ import { lowerMqtt } from './mqtt.js';
36
36
  import { lowerPreferences } from './preferences.js';
37
37
  import { lowerBoard } from './board.js';
38
38
  import { lowerRandom } from './random.js';
39
- export { lowerGpio, lowerTiming, lowerAdc, lowerPwm, lowerI2c, lowerSpi, lowerUart, lowerInterrupt, lowerWdt, lowerPower, lowerTone, lowerPulseOrShift, lowerBle, lowerWifi, lowerHttp, lowerMqtt, lowerPreferences, lowerBoard, lowerRandom, };
39
+ import { lowerDac } from './dac.js';
40
+ import { lowerFs } from './fs.js';
41
+ import { lowerHwtimer } from './hwtimer.js';
42
+ export { lowerGpio, lowerTiming, lowerAdc, lowerPwm, lowerI2c, lowerSpi, lowerUart, lowerInterrupt, lowerWdt, lowerPower, lowerTone, lowerPulseOrShift, lowerBle, lowerWifi, lowerHttp, lowerMqtt, lowerPreferences, lowerBoard, lowerRandom, lowerDac, lowerFs, lowerHwtimer, };
40
43
  /**
41
44
  * Lower a HAL op to Zephyr C++. Returns undefined for unsupported categories
42
45
  * (mirrors lowerHalOp in framework-esp32/src/lowering/index.ts).
@@ -51,6 +54,8 @@ export function lowerHalOp(op) {
51
54
  return lowerAdc(op, chip);
52
55
  if (op.operation.startsWith('pwm.'))
53
56
  return lowerPwm(op, chip);
57
+ if (op.operation.startsWith('dac.'))
58
+ return lowerDac(op, chip);
54
59
  if (op.operation.startsWith('i2c.'))
55
60
  return lowerI2c(op, chip);
56
61
  if (op.operation.startsWith('spi.'))
@@ -61,6 +66,8 @@ export function lowerHalOp(op) {
61
66
  return lowerInterrupt(op, chip);
62
67
  if (op.operation.startsWith('wdt.'))
63
68
  return lowerWdt(op);
69
+ if (op.operation.startsWith('hwtimer.'))
70
+ return lowerHwtimer(op, chip);
64
71
  if (op.operation.startsWith('power.'))
65
72
  return lowerPower(op);
66
73
  if (op.operation.startsWith('tone.'))
@@ -80,6 +87,8 @@ export function lowerHalOp(op) {
80
87
  return lowerMqtt(op);
81
88
  if (op.operation.startsWith('preferences.'))
82
89
  return lowerPreferences(op);
90
+ if (op.operation.startsWith('fs.'))
91
+ return lowerFs(op);
83
92
  // board.resolve is constant-folded at IR-build time; lowerBoard is the
84
93
  // dead-letter reached only on an unresolvable path.
85
94
  if (op.operation.startsWith('board.'))
package/dist/strategy.js CHANGED
@@ -15,7 +15,9 @@
15
15
  // repository root) and are not subject to the license of this tool source.
16
16
  // ---------------------------------------------------------------------------
17
17
  import { buildWorkerRuntimePolyfill } from '@typecad/cuttlefish/api/shared';
18
+ import { applyStringMethodRewrites } from '@typecad/cuttlefish/api/shared';
18
19
  import { programUsesSafety } from '@typecad/cuttlefish/api';
20
+ import { entryHasUI } from '@typecad/cuttlefish/ui-hook';
19
21
  import { chipForTarget, setActiveChip } from './chips/index.js';
20
22
  import { resolveChipFromBoard } from './chips/resolve.js';
21
23
  import { emitGpioDevDispatcher } from './chips/controllers.js';
@@ -23,6 +25,9 @@ import { lowerHalOp } from './lowering/index.js';
23
25
  import { buildZephyrWorkerBacking } from './lowering/worker-backing.js';
24
26
  import { adcInitLines } from './lowering/adc.js';
25
27
  import { pwmInitLines } from './lowering/pwm.js';
28
+ import { dacInitLines } from './lowering/dac.js';
29
+ import { fsInitLines } from './lowering/fs.js';
30
+ import { hwtimerInitLines } from './lowering/hwtimer.js';
26
31
  import { i2cInitLines } from './lowering/i2c.js';
27
32
  import { spiInitLines } from './lowering/spi.js';
28
33
  import { uartInitLines } from './lowering/uart.js';
@@ -125,6 +130,16 @@ export class ZephyrStrategy {
125
130
  inc.push('<zephyr/drivers/adc.h>');
126
131
  if (uses('usesPWM'))
127
132
  inc.push('<zephyr/drivers/pwm.h>');
133
+ if (uses('usesDAC'))
134
+ inc.push('<zephyr/drivers/dac.h>');
135
+ // Filesystem: littlefs on the storage partition. <cstring> backs the
136
+ // shim's strlen; the storage/flash_map + fs/littlefs headers carry the
137
+ // FIXED_PARTITION_ID macro + FS_LITTLEFS_DECLARE_DEFAULT_CONFIG the shim uses.
138
+ if (uses('usesFS'))
139
+ inc.push('<zephyr/fs/fs.h>', '<zephyr/fs/littlefs.h>', '<zephyr/storage/flash_map.h>', '<cstring>');
140
+ // Hardware timers via the counter driver.
141
+ if (uses('usesHwtimer'))
142
+ inc.push('<zephyr/drivers/counter.h>');
128
143
  if (uses('usesWDT'))
129
144
  inc.push('<zephyr/drivers/watchdog.h>');
130
145
  if (uses('usesPower'))
@@ -290,22 +305,28 @@ export class ZephyrStrategy {
290
305
  lines.push(...adcInitLines(chip));
291
306
  if (uses('usesPWM') && chip.pwm)
292
307
  lines.push(...pwmInitLines(chip));
308
+ if (uses('usesDAC') && chip.dac)
309
+ lines.push(...dacInitLines(chip));
310
+ if (uses('usesHwtimer') && chip.hwtimer)
311
+ lines.push(...hwtimerInitLines(chip));
293
312
  if (uses('usesInterrupts'))
294
313
  lines.push(...interruptInitLines(chip));
295
314
  if (uses('usesWDT') && chip.wdt)
296
315
  lines.push(...wdtInitLines(chip));
297
316
  if (uses('usesBle'))
298
317
  lines.push(...bleInitLines());
299
- // Display runtime (minimal rect/text renderer): gated on the analyzer's
300
- // usesDisplay flag (set by display.* hal-ops). This is the DIRECT-call
301
- // display path (user code calling screen.display.fillRect etc.). It is
302
- // SUPPRESSED when the UI display adapter is active — the adapter seam
303
- // (providesDisplayAdapter/resolveDisplayAdapter) emits its own display_*
304
- // runtime that drives the UI rendering pipeline via CuttlefishGFX, and the
305
- // two define the same symbols (display_init, __tc_display_line) so emitting
306
- // both causes redefinition errors. The adapter is active for the
307
- // strategy-owned drivers (ili9341-zephyr, st7796-zephyr).
308
- if (uses('usesDisplay') && !this.providesDisplayAdapter()) {
318
+ // Display runtime (rect/text renderer): the DIRECT-call display path (user
319
+ // code calling screen.display.fillRect etc., no @typecad/ui). Emitted only
320
+ // when the program uses display.* but is NOT a UI program the UI display
321
+ // adapter (emitted by cuttlefish's emitUIRuntime, solely under entryHasUI())
322
+ // defines the same display_init symbol, so emitting both would collide.
323
+ // `providesDisplayAdapter()` is a static capability (always true here) and
324
+ // does NOT track whether the adapter is actually emitted for THIS build, so
325
+ // the per-program UI signal (entryHasUI) is the correct gate. Without this,
326
+ // a direct display.* program has no definition for display_init/
327
+ // display_fill_rect/draw_rect/draw_text/flush (the gfx runtime was
328
+ // previously dead code).
329
+ if (uses('usesDisplay') && !entryHasUI()) {
309
330
  const rt = buildDisplayRuntime(this._displayState.profile);
310
331
  lines.push(...rt.stateLines);
311
332
  lines.push(rt.fontTable);
@@ -319,6 +340,8 @@ export class ZephyrStrategy {
319
340
  lines.push(...mqttInitLines());
320
341
  if (uses('usesPreferences'))
321
342
  lines.push(...preferencesInitLines());
343
+ if (uses('usesFS'))
344
+ lines.push(...fsInitLines());
322
345
  if (uses('usesRandom'))
323
346
  lines.push(...randomInitLines());
324
347
  lines.push('#endif // CUTTLEFISH_SHIM_DEFINED');
@@ -384,6 +407,8 @@ export class ZephyrStrategy {
384
407
  const outputPins = new Set();
385
408
  const adcReadPins = new Set();
386
409
  const interruptPins = new Set();
410
+ const dacPins = new Set();
411
+ const hwtimerInstances = new Set();
387
412
  let usesWifiOps = false;
388
413
  let usesHttpOps = false;
389
414
  let usesMqttOps = false;
@@ -404,6 +429,16 @@ export class ZephyrStrategy {
404
429
  if (op.operation === 'interrupt.attach' && typeof op.pin === 'number') {
405
430
  interruptPins.add(op.pin);
406
431
  }
432
+ if (op.operation === 'dac.write' && typeof op.pin === 'number') {
433
+ dacPins.add(op.pin);
434
+ }
435
+ if (typeof op.operation === 'string' && op.operation.startsWith('hwtimer.')) {
436
+ const inst = typeof op.instance === 'number'
437
+ ? op.instance
438
+ : parseInt(String(op.instance), 10);
439
+ if (!isNaN(inst))
440
+ hwtimerInstances.add(inst);
441
+ }
407
442
  if (typeof op.operation === 'string' && op.operation.startsWith('wifi.')) {
408
443
  usesWifiOps = true;
409
444
  }
@@ -464,6 +499,55 @@ export class ZephyrStrategy {
464
499
  });
465
500
  }
466
501
  }
502
+ // ── DAC pin validity ────────────────────────────────────────────────────
503
+ // dac.write resolves a HAL pin to a channel via the chip descriptor's
504
+ // dac.channels map. A pin not in that map lowers to a comment (silent
505
+ // no-op), and a chip without a `dac` entry (nRF52840, ESP32-S3) has no DAC
506
+ // at all. Flag either case so the user gets a clear message instead of a
507
+ // pin that silently does nothing.
508
+ if (dacPins.size > 0) {
509
+ const dacChannels = new Set((chip.dac?.channels ?? []).map((c) => c.pin));
510
+ for (const pin of dacPins) {
511
+ if (!dacChannels.has(pin)) {
512
+ diags.push({
513
+ severity: 'error',
514
+ code: 'zephyr-dac-pin-unavailable',
515
+ message: `GPIO ${pin} is not a DAC channel on ${chip.id} and cannot be driven with dac.write.`,
516
+ hint: dacChannels.size > 0
517
+ ? `Use a DAC-capable pin. On ${chip.id}: ${[...dacChannels].sort((x, y) => x - y).join(', ')}.`
518
+ : `${chip.id} has no DAC. Use an esp32_devkitc target (ESP32 DAC on GPIO25/26).`,
519
+ source: program.fileName,
520
+ });
521
+ }
522
+ }
523
+ }
524
+ // ── Hardware-timer instance validity ────────────────────────────────────
525
+ // hwtimer.* resolves the instance index to a counter device via the chip
526
+ // descriptor's hwtimer.controllers. A chip without that entry (or an
527
+ // out-of-range instance) lowers to a comment — flag it so the user knows
528
+ // the timer will never fire.
529
+ if (hwtimerInstances.size > 0) {
530
+ const controllerCount = chip.hwtimer?.controllers.length ?? 0;
531
+ for (const inst of hwtimerInstances) {
532
+ if (controllerCount === 0) {
533
+ diags.push({
534
+ severity: 'error',
535
+ code: 'zephyr-hwtimer-unavailable',
536
+ message: `Hardware timer instance ${inst} is used but ${chip.id} exposes no free counter device.`,
537
+ hint: `${chip.id} declares no hwtimer.controllers. Use a target with a free counter (e.g. nRF RTC1).`,
538
+ source: program.fileName,
539
+ });
540
+ }
541
+ else if (inst < 0 || inst >= controllerCount) {
542
+ diags.push({
543
+ severity: 'error',
544
+ code: 'zephyr-hwtimer-instance-out-of-range',
545
+ message: `Hardware timer instance ${inst} is out of range on ${chip.id} (0..${controllerCount - 1}).`,
546
+ source: program.fileName,
547
+ });
548
+ }
549
+ }
550
+ }
467
551
  // ── WiFi target validity ────────────────────────────────────────────────
468
552
  // WiFi ops require a chip with a WiFi radio. The ESP32-S3 descriptor sets
469
553
  // wifi.supported; the XIAO nRF52840 omits it (no radio). Flag wifi usage on
@@ -592,6 +676,18 @@ export class ZephyrStrategy {
592
676
  v = v.replace(/\bundefined\b/g, 'CUTTLEFISH_UNDEFINED');
593
677
  v = v.replace(/\bnull\b/g, 'CUTTLEFISH_UNDEFINED');
594
678
  }
679
+ // String-method lowering is shared across targets (see string-method-
680
+ // registry). Zephyr's strings are const char*, so the __tc_* helpers this
681
+ // rewrites to (defined by the string_methods polyfill) take const char*.
682
+ // includes/startsWith lower to inline strstr/strncmp (matching framework-
683
+ // arduino); everything else → a __tc_* helper call.
684
+ v = applyStringMethodRewrites(v, {
685
+ wrapReceiverFor: new Set(['indexOf']),
686
+ special: {
687
+ includes: (recv, args) => `(strstr(${recv}, ${args[0]}) != NULL)`,
688
+ startsWith: (recv, args) => `(strncmp(${recv}, ${args[0]}, strlen(${args[0]})) == 0)`,
689
+ },
690
+ });
595
691
  return v;
596
692
  }
597
693
  nullValue() {
@@ -840,15 +936,22 @@ export class ZephyrStrategy {
840
936
  };
841
937
  }
842
938
  // ── Polyfills ───────────────────────────────────────────────────────────
843
- // No native polyfills for the MVP the shared runtime's string/array
844
- // polyfills are pulled in when a program uses them. cuttlefish_halt is the
845
- // one symbol the runtime header may reference; supply it as a halt loop.
939
+ // Zephyr is a no-STL target (hasVector/hasString = false), so array/string
940
+ // literals lower to __tc_StaticArray / const char* and string methods lower
941
+ // to __tc_* helpers both need STL-free definitions emitted here (there is
942
+ // no shared-runtime fallback; the pipeline sources 100% of polyfills from
943
+ // generateNativePolyfills). Mirrors framework-arduino's AVR polyfills.
846
944
  nativePolyfills() {
847
945
  // cuttlefish_halt: always (the runtime header may reference it).
946
+ // string_methods / static_array: STL-free array + string helpers a no-STL
947
+ // target needs (mutated/struct array literals + any string method).
848
948
  // timer_methods: k_timer/k_work pool for setInterval/setTimeout (gated on
849
949
  // timerCallCount at emit time in generateNativePolyfills).
850
950
  // async_runtime: heap-free static Promise/microtask runtime (no STL needed).
851
- return new Set(['cuttlefish_halt', 'wiring_compat', 'timer_methods', 'async_runtime']);
951
+ return new Set([
952
+ 'cuttlefish_halt', 'wiring_compat', 'string_methods', 'static_array',
953
+ 'timer_methods', 'async_runtime',
954
+ ]);
852
955
  }
853
956
  generateNativePolyfills(program, ctx) {
854
957
  const polyfills = [
@@ -897,6 +1000,75 @@ export class ZephyrStrategy {
897
1000
  ],
898
1001
  dependencies: [],
899
1002
  },
1003
+ {
1004
+ // STL-free string-method polyfills. String methods (.toUpperCase(),
1005
+ // .includes(), .substring(), …) lower at IR level to __tc_* helpers for
1006
+ // every target; this supplies their definitions. Minimal-libc friendly:
1007
+ // only <cstring> primitives (no <cctype> — case conversion is inline
1008
+ // ASCII so the polyfill is self-contained). Mirrors framework-arduino.
1009
+ kind: 'polyfill',
1010
+ id: 'string_methods',
1011
+ domain: 'embedded',
1012
+ requiredIncludes: ['<cstring>'],
1013
+ forwardDeclarations: [],
1014
+ helperStructs: [],
1015
+ helperFunctions: [`
1016
+ // TypeCAD string method polyfills (Zephyr, minimal-libc).
1017
+ #ifndef CUTTLEFISH_STR_BUF_SIZE
1018
+ #define CUTTLEFISH_STR_BUF_SIZE 64
1019
+ #endif
1020
+ 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; }
1021
+ 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; }
1022
+ 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; }
1023
+ 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; }
1024
+ 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; }
1025
+ const char* __tc_substring1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
1026
+ const char* __tc_slice2(const char* s, int start, int end) { return __tc_substring2(s, start, end); }
1027
+ const char* __tc_slice1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
1028
+ 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; }
1029
+ 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]; }
1030
+ int __tc_charCodeAt(const char* s, int idx) { return static_cast<int>(static_cast<unsigned char>(s[idx])); }
1031
+ int __tc_indexOf(const char* s, const char* needle) { const char* p = strstr(s, needle); return p ? static_cast<int>(p - s) : -1; }
1032
+ `],
1033
+ shimMacros: [],
1034
+ dependencies: [],
1035
+ },
1036
+ {
1037
+ // STL-free fixed-size array wrapper. Mutated/struct-element array
1038
+ // literals and array methods (.push/.pop/.map/.filter) lower to
1039
+ // __tc_StaticArray<T,N>; this supplies the template. Idempotent guard
1040
+ // so a redefinition is a no-op. Mirrors framework-arduino.
1041
+ kind: 'polyfill',
1042
+ id: 'static_array',
1043
+ domain: 'embedded',
1044
+ requiredIncludes: [],
1045
+ forwardDeclarations: [],
1046
+ helperStructs: [],
1047
+ helperFunctions: [`
1048
+ #ifndef __TC_STATIC_ARRAY_DEFINED
1049
+ #define __TC_STATIC_ARRAY_DEFINED
1050
+ template<typename T, int N>
1051
+ struct __tc_StaticArray {
1052
+ T data[N];
1053
+ int _size;
1054
+ __tc_StaticArray() : _size(0) {}
1055
+ int length() const { return _size; }
1056
+ int size() const { return _size; }
1057
+ void push(T val) { if (_size < N) data[_size++] = val; }
1058
+ T pop() { return (_size > 0) ? data[--_size] : T(); }
1059
+ int indexOf(T val) const { for (int i = 0; i < _size; i++) if (data[i] == val) return i; return -1; }
1060
+ T& operator[](int i) { return data[i]; }
1061
+ const T& operator[](int i) const { return data[i]; }
1062
+ T* begin() { return &data[0]; }
1063
+ T* end() { return &data[_size]; }
1064
+ const T* begin() const { return &data[0]; }
1065
+ const T* end() const { return &data[_size]; }
1066
+ };
1067
+ #endif
1068
+ `],
1069
+ shimMacros: [],
1070
+ dependencies: [],
1071
+ },
900
1072
  ];
901
1073
  // Worker-offload runtime (Phase 1). Emitted only when the program uses
902
1074
  // worker.* ops, backed by the Zephyr primitives in worker-backing.ts
@@ -15,6 +15,7 @@
15
15
  import { readFileSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
17
  import manifest from '../framework.manifest.js';
18
+ import { discoverWest } from './west-discover.js';
18
19
  // ── minimal semver ──────────────────────────────────────────────────────────
19
20
  /** Parse "4.3.99" / "v4.3" / "4.3.99-rc1" → [4, 3, 99]. Undefined if unparseable. */
20
21
  export function parseVersion(v) {
@@ -70,7 +71,15 @@ export function satisfiesRange(version, range) {
70
71
  * and a bare "4.3.99".
71
72
  */
72
73
  export function detectZephyrVersion() {
73
- const base = process.env.ZEPHYR_BASE;
74
+ let base = process.env.ZEPHYR_BASE;
75
+ if (!base) {
76
+ // Fall back to the discovered west install's zephyrBase — covers the
77
+ // micromamba env from @typecad/zephyr-installer WITHOUT activation.
78
+ // (micromamba run sets ZEPHYR_BASE only inside the west subprocess; this
79
+ // makes the compat check work in the parent cuttlefish process too.)
80
+ const install = discoverWest();
81
+ base = install?.zephyrBase;
82
+ }
74
83
  if (!base)
75
84
  return undefined;
76
85
  let content;
@@ -0,0 +1,93 @@
1
+ import { type WestInstall } from './west-discover.js';
2
+ import { type CompatStatus } from './compat.js';
3
+ /**
4
+ * Raw west facts gathered from discovery + a `west --version` probe. Mirrors
5
+ * ArduinoCliProbeData: `westFound` is true when a usable west install was
6
+ * discovered (discovery itself probes responsiveness).
7
+ */
8
+ export interface WestProbeData {
9
+ /** A usable west install was discovered. */
10
+ westFound: boolean;
11
+ /** west version string if the `--version` probe parsed one, e.g. "1.3.0". */
12
+ westVersion: string | undefined;
13
+ /** Which discovery strategy found west, for surfacing to the user. */
14
+ source: WestInstall['source'] | undefined;
15
+ /** Effective ZEPHYR_BASE (env var, else a base discovery surfaced). */
16
+ zephyrBase: string | undefined;
17
+ }
18
+ /**
19
+ * Test-injection seam for checkZephyrEnv. Mirrors Arduino's
20
+ * CheckArduinoEnvOptions.fakeProbe so tests never spawn a real west/python.
21
+ */
22
+ export interface CheckZephyrEnvOptions {
23
+ /** FOR TESTS ONLY: skip the real probe and use this data directly. */
24
+ fakeWestProbe?: WestProbeData;
25
+ /** FOR TESTS ONLY: override the board-existence lookup. */
26
+ fakeBoardExists?: (boardId: string, zephyrBase: string | undefined) => boolean | undefined;
27
+ }
28
+ export interface ZephyrEnvCheck {
29
+ /** west (the Zephyr build tool) was discovered and responsive. */
30
+ westFound: boolean;
31
+ /** west version string if known, e.g. "1.3.0". */
32
+ westVersion: string | undefined;
33
+ /** Discovery strategy that found west, for display. */
34
+ westSource: WestInstall['source'] | undefined;
35
+ /** Effective ZEPHYR_BASE (env var, else a discovered base). */
36
+ zephyrBase: string | undefined;
37
+ /** Detected Zephyr RTOS version from $ZEPHYR_BASE/VERSION, if readable. */
38
+ zephyrVersion: string | undefined;
39
+ /** Declared supported range (manifest.compat.zephyr), if any. */
40
+ compatRange: string | undefined;
41
+ /** Result of the compat-range check against the detected version. */
42
+ compatStatus: CompatStatus;
43
+ /** Raw board target from cuttlefish.config.ts, if configured. */
44
+ buildTarget: string | undefined;
45
+ /** buildTarget normalized for the installed Zephyr version (may equal it). */
46
+ resolvedBoardTarget: string | undefined;
47
+ /** Does the resolved board exist in the checkout? undefined = undetermined. */
48
+ boardTargetSupported: boolean | undefined;
49
+ }
50
+ export type ZephyrEnvOk = {
51
+ ok: true;
52
+ check: ZephyrEnvCheck;
53
+ };
54
+ export type ZephyrEnvFailure = {
55
+ ok: false;
56
+ reason: 'west-not-found' | 'zephyr-out-of-range' | 'board-not-supported';
57
+ check: ZephyrEnvCheck;
58
+ /** Human-readable lines ready to print. */
59
+ messages: string[];
60
+ /** Exact remediation hint, when applicable. */
61
+ fixCommand: string | undefined;
62
+ };
63
+ export type ZephyrEnvResult = ZephyrEnvOk | ZephyrEnvFailure;
64
+ /** Clear the west-probe cache (for tests). Also resets discovery cache. */
65
+ export declare function resetWestProbeCacheForTest(): void;
66
+ /**
67
+ * Gather west facts: discover a usable install, then run `west --version`
68
+ * through it to capture the version. Memoized for the process lifetime (west
69
+ * installs don't move). Never throws — returns westFound:false on any failure.
70
+ */
71
+ export declare function probeWestEnv(): WestProbeData;
72
+ /**
73
+ * Does `boardId` exist as a board directory in the Zephyr checkout? Checks the
74
+ * HWMv2 vendor layout used by Zephyr 4.x: $ZEPHYR_BASE/boards/<vendor>/<boardId>.
75
+ * Returns true/false when determinable; undefined when the base is unknown or
76
+ * the boards/ tree can't be read (so callers never fail on an inconclusive
77
+ * lookup — they just skip the board check).
78
+ */
79
+ export declare function boardExistsInCheckout(boardId: string, zephyrBase: string | undefined): boolean | undefined;
80
+ /**
81
+ * Verify the environment can build for `buildTarget`. Cheap and
82
+ * side-effect-free: discovers west, reads the Zephyr version, checks the compat
83
+ * range, and — when a target is configured — verifies the board exists in the
84
+ * checkout. Reports what (if anything) is wrong.
85
+ *
86
+ * - If `buildTarget` is undefined/empty, the board check is skipped (not a
87
+ * failure), mirroring Arduino's no-FQBN path.
88
+ * - Never installs anything. Never mutates the user environment.
89
+ * - Never throws — always returns a result. Callers decide how to react.
90
+ *
91
+ * `options` is for-test only (injects fake probe data / board lookup).
92
+ */
93
+ export declare function checkZephyrEnv(buildTarget?: string, options?: CheckZephyrEnvOptions): ZephyrEnvResult;