@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
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'))
@@ -135,10 +150,11 @@ export class ZephyrStrategy {
135
150
  // program-analysis usesStdString detector doesn't see types generated by
136
151
  // the BLE lowering layer — so without forcing <string> here, any BLE server
137
152
  // with a Utf8 characteristic fails to compile ('std::string does not name a
138
- // type'). Uses <string>, not <string.h>: the latter is the C flat-string
139
- // header (already included for the shim's strncpy/strcmp).
153
+ // type'). <cstdlib>/<cstring> (not <stdlib.h>/<string.h>) back the shim's
154
+ // strtol/strcmp/strncpy the same AUTOSAR-compliant spelling the HTTP,
155
+ // MQTT, and Preferences paths below already use.
140
156
  if (uses('usesBle'))
141
- inc.push('<stdlib.h>', '<string.h>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
157
+ inc.push('<cstdlib>', '<cstring>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
142
158
  // Display: the analyzer's usesDisplay flag (set by display.* hal-ops) drives
143
159
  // this include. When ctx.analysis is absent (capability query), uses()
144
160
  // defaults to true so a real build never strips it.
@@ -290,22 +306,28 @@ export class ZephyrStrategy {
290
306
  lines.push(...adcInitLines(chip));
291
307
  if (uses('usesPWM') && chip.pwm)
292
308
  lines.push(...pwmInitLines(chip));
309
+ if (uses('usesDAC') && chip.dac)
310
+ lines.push(...dacInitLines(chip));
311
+ if (uses('usesHwtimer') && chip.hwtimer)
312
+ lines.push(...hwtimerInitLines(chip));
293
313
  if (uses('usesInterrupts'))
294
314
  lines.push(...interruptInitLines(chip));
295
315
  if (uses('usesWDT') && chip.wdt)
296
316
  lines.push(...wdtInitLines(chip));
297
317
  if (uses('usesBle'))
298
318
  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()) {
319
+ // Display runtime (rect/text renderer): the DIRECT-call display path (user
320
+ // code calling screen.display.fillRect etc., no @typecad/ui). Emitted only
321
+ // when the program uses display.* but is NOT a UI program the UI display
322
+ // adapter (emitted by cuttlefish's emitUIRuntime, solely under entryHasUI())
323
+ // defines the same display_init symbol, so emitting both would collide.
324
+ // `providesDisplayAdapter()` is a static capability (always true here) and
325
+ // does NOT track whether the adapter is actually emitted for THIS build, so
326
+ // the per-program UI signal (entryHasUI) is the correct gate. Without this,
327
+ // a direct display.* program has no definition for display_init/
328
+ // display_fill_rect/draw_rect/draw_text/flush (the gfx runtime was
329
+ // previously dead code).
330
+ if (uses('usesDisplay') && !entryHasUI()) {
309
331
  const rt = buildDisplayRuntime(this._displayState.profile);
310
332
  lines.push(...rt.stateLines);
311
333
  lines.push(rt.fontTable);
@@ -319,6 +341,8 @@ export class ZephyrStrategy {
319
341
  lines.push(...mqttInitLines());
320
342
  if (uses('usesPreferences'))
321
343
  lines.push(...preferencesInitLines());
344
+ if (uses('usesFS'))
345
+ lines.push(...fsInitLines());
322
346
  if (uses('usesRandom'))
323
347
  lines.push(...randomInitLines());
324
348
  lines.push('#endif // CUTTLEFISH_SHIM_DEFINED');
@@ -354,20 +378,27 @@ export class ZephyrStrategy {
354
378
  // separate translation unit when generateHeaderFile() splits them into the
355
379
  // header.
356
380
  lines.push('extern void setup(void);', 'extern void loop(void);', '', 'int main(void) {', ' setup();', ' for (;;) {', ' loop();', ' k_msleep(1);', ' }', ' return 0;', '}');
357
- // Safety shims: when the program uses @typecad/safety, provide __tc_gpio_read
358
- // / __tc_gpio_write backed by the raw controller (a best-effort read that
359
- // does not depend on a pin having a DT spec). __tc_delay_us uses k_busy_wait.
381
+ // GPIO read shim: the wiring_compat polyfill routes the UI runtime
382
+ // header's unconditional digitalRead() poll (init-press-input.ts) to
383
+ // __tc_gpio_read, so the definition must NOT be gated on @typecad/safety.
384
+ // The signature is `int` to match wiring_compat's forward declaration —
385
+ // a uint32_t definition alongside it would leave the declared int
386
+ // overload undefined (int wins overload resolution for small integer
387
+ // arguments).
360
388
  //
361
- // The pin is a RUNTIME value here (safety's voter passes whatever pin it
362
- // was handed), so the controller cannot be baked in as a single DT_NODELABEL
363
- // on a multi-controller SoC (ESP32-S3: pins 0–31 → gpio0, 32–48 → gpio1).
364
- // Emit a tiny __tc_gpio_dev(pin) dispatcher that resolves the owning
365
- // controller's device per pin; single-controller SoCs collapse it to a
366
- // one-liner. Each DT_NODELABEL is still compile-time-resolved per branch, so
367
- // it is always statically valid.
389
+ // The pin is a RUNTIME value here (the UI pin-watch table and safety's
390
+ // voter pass whatever pin they were handed), so the controller cannot be
391
+ // baked in as a single DT_NODELABEL on a multi-controller SoC (ESP32-S3:
392
+ // pins 0–31 → gpio0, 32–48 → gpio1). Emit a tiny __tc_gpio_dev(pin)
393
+ // dispatcher that resolves the owning controller's device per pin;
394
+ // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
395
+ // still compile-time-resolved per branch, so it is always statically valid.
396
+ lines.push(...emitGpioDevDispatcher(chip));
397
+ lines.push('inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), static_cast<gpio_pin_t>(pin)); }');
398
+ // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
399
+ // lowering, so they stay gated on it.
368
400
  if (program && programUsesSafety(program)) {
369
- lines.push(...emitGpioDevDispatcher(chip));
370
- 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');
401
+ lines.push('inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }', '#ifndef __TC_DELAY_US_DEFINED', '#define __TC_DELAY_US_DEFINED', 'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }', '#endif');
371
402
  }
372
403
  return lines;
373
404
  }
@@ -384,6 +415,8 @@ export class ZephyrStrategy {
384
415
  const outputPins = new Set();
385
416
  const adcReadPins = new Set();
386
417
  const interruptPins = new Set();
418
+ const dacPins = new Set();
419
+ const hwtimerInstances = new Set();
387
420
  let usesWifiOps = false;
388
421
  let usesHttpOps = false;
389
422
  let usesMqttOps = false;
@@ -404,6 +437,16 @@ export class ZephyrStrategy {
404
437
  if (op.operation === 'interrupt.attach' && typeof op.pin === 'number') {
405
438
  interruptPins.add(op.pin);
406
439
  }
440
+ if (op.operation === 'dac.write' && typeof op.pin === 'number') {
441
+ dacPins.add(op.pin);
442
+ }
443
+ if (typeof op.operation === 'string' && op.operation.startsWith('hwtimer.')) {
444
+ const inst = typeof op.instance === 'number'
445
+ ? op.instance
446
+ : parseInt(String(op.instance), 10);
447
+ if (!isNaN(inst))
448
+ hwtimerInstances.add(inst);
449
+ }
407
450
  if (typeof op.operation === 'string' && op.operation.startsWith('wifi.')) {
408
451
  usesWifiOps = true;
409
452
  }
@@ -464,6 +507,55 @@ export class ZephyrStrategy {
464
507
  });
465
508
  }
466
509
  }
510
+ // ── DAC pin validity ────────────────────────────────────────────────────
511
+ // dac.write resolves a HAL pin to a channel via the chip descriptor's
512
+ // dac.channels map. A pin not in that map lowers to a comment (silent
513
+ // no-op), and a chip without a `dac` entry (nRF52840, ESP32-S3) has no DAC
514
+ // at all. Flag either case so the user gets a clear message instead of a
515
+ // pin that silently does nothing.
516
+ if (dacPins.size > 0) {
517
+ const dacChannels = new Set((chip.dac?.channels ?? []).map((c) => c.pin));
518
+ for (const pin of dacPins) {
519
+ if (!dacChannels.has(pin)) {
520
+ diags.push({
521
+ severity: 'error',
522
+ code: 'zephyr-dac-pin-unavailable',
523
+ message: `GPIO ${pin} is not a DAC channel on ${chip.id} and cannot be driven with dac.write.`,
524
+ hint: dacChannels.size > 0
525
+ ? `Use a DAC-capable pin. On ${chip.id}: ${[...dacChannels].sort((x, y) => x - y).join(', ')}.`
526
+ : `${chip.id} has no DAC. Use an esp32_devkitc target (ESP32 DAC on GPIO25/26).`,
527
+ source: program.fileName,
528
+ });
529
+ }
530
+ }
531
+ }
532
+ // ── Hardware-timer instance validity ────────────────────────────────────
533
+ // hwtimer.* resolves the instance index to a counter device via the chip
534
+ // descriptor's hwtimer.controllers. A chip without that entry (or an
535
+ // out-of-range instance) lowers to a comment — flag it so the user knows
536
+ // the timer will never fire.
537
+ if (hwtimerInstances.size > 0) {
538
+ const controllerCount = chip.hwtimer?.controllers.length ?? 0;
539
+ for (const inst of hwtimerInstances) {
540
+ if (controllerCount === 0) {
541
+ diags.push({
542
+ severity: 'error',
543
+ code: 'zephyr-hwtimer-unavailable',
544
+ message: `Hardware timer instance ${inst} is used but ${chip.id} exposes no free counter device.`,
545
+ hint: `${chip.id} declares no hwtimer.controllers. Use a target with a free counter (e.g. nRF RTC1).`,
546
+ source: program.fileName,
547
+ });
548
+ }
549
+ else if (inst < 0 || inst >= controllerCount) {
550
+ diags.push({
551
+ severity: 'error',
552
+ code: 'zephyr-hwtimer-instance-out-of-range',
553
+ message: `Hardware timer instance ${inst} is out of range on ${chip.id} (0..${controllerCount - 1}).`,
554
+ source: program.fileName,
555
+ });
556
+ }
557
+ }
558
+ }
467
559
  // ── WiFi target validity ────────────────────────────────────────────────
468
560
  // WiFi ops require a chip with a WiFi radio. The ESP32-S3 descriptor sets
469
561
  // wifi.supported; the XIAO nRF52840 omits it (no radio). Flag wifi usage on
@@ -592,6 +684,18 @@ export class ZephyrStrategy {
592
684
  v = v.replace(/\bundefined\b/g, 'CUTTLEFISH_UNDEFINED');
593
685
  v = v.replace(/\bnull\b/g, 'CUTTLEFISH_UNDEFINED');
594
686
  }
687
+ // String-method lowering is shared across targets (see string-method-
688
+ // registry). Zephyr's strings are const char*, so the __tc_* helpers this
689
+ // rewrites to (defined by the string_methods polyfill) take const char*.
690
+ // includes/startsWith lower to inline strstr/strncmp (matching framework-
691
+ // arduino); everything else → a __tc_* helper call.
692
+ v = applyStringMethodRewrites(v, {
693
+ wrapReceiverFor: new Set(['indexOf']),
694
+ special: {
695
+ includes: (recv, args) => `(strstr(${recv}, ${args[0]}) != NULL)`,
696
+ startsWith: (recv, args) => `(strncmp(${recv}, ${args[0]}, strlen(${args[0]})) == 0)`,
697
+ },
698
+ });
595
699
  return v;
596
700
  }
597
701
  nullValue() {
@@ -684,6 +788,61 @@ export class ZephyrStrategy {
684
788
  apiReservedEnumGuard() {
685
789
  return '';
686
790
  }
791
+ // ── Interrupt safety ─────────────────────────────────────────────────────
792
+ // Zephyr ISRs run above thread context: anything that sleeps (k_msleep),
793
+ // pends, or takes a driver lock is illegal there (asserted by the kernel in
794
+ // debug builds; corrupts scheduler state otherwise). The names below are the
795
+ // IR-level callees cuttlefish's interrupt-analysis pass matches (the same
796
+ // keys ArduinoStrategy uses; timing.delay/delay_microseconds hal-ops are
797
+ // mapped back to the bare names by the analyzer itself).
798
+ isrUnsafeOperations() {
799
+ return new Map([
800
+ ['delay', {
801
+ 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)',
802
+ severity: 'warning',
803
+ }],
804
+ ['delayMicroseconds', {
805
+ reason: 'delayMicroseconds() busy-waits the CPU for the full delay, stalling every lower-priority interrupt and the scheduler for its duration',
806
+ severity: 'warning',
807
+ }],
808
+ ['console.log', {
809
+ 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',
810
+ severity: 'info',
811
+ }],
812
+ ['console.error', {
813
+ 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',
814
+ severity: 'info',
815
+ }],
816
+ ['console.warn', {
817
+ 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',
818
+ severity: 'info',
819
+ }],
820
+ ['I2C0', {
821
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
822
+ severity: 'warning',
823
+ }],
824
+ ['I2C1', {
825
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
826
+ severity: 'warning',
827
+ }],
828
+ ['SPI0', {
829
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
830
+ severity: 'warning',
831
+ }],
832
+ ['SPI1', {
833
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
834
+ severity: 'warning',
835
+ }],
836
+ ['UART0', {
837
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
838
+ severity: 'info',
839
+ }],
840
+ ['UART1', {
841
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
842
+ severity: 'info',
843
+ }],
844
+ ]);
845
+ }
687
846
  ambientTypeDeclarations() {
688
847
  // Preferences is the only HAL surface the framework lowers that is used as
689
848
  // a bare global (the HAL Preferences class is exported, but the canonical
@@ -840,15 +999,22 @@ export class ZephyrStrategy {
840
999
  };
841
1000
  }
842
1001
  // ── 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.
1002
+ // Zephyr is a no-STL target (hasVector/hasString = false), so array/string
1003
+ // literals lower to __tc_StaticArray / const char* and string methods lower
1004
+ // to __tc_* helpers both need STL-free definitions emitted here (there is
1005
+ // no shared-runtime fallback; the pipeline sources 100% of polyfills from
1006
+ // generateNativePolyfills). Mirrors framework-arduino's AVR polyfills.
846
1007
  nativePolyfills() {
847
1008
  // cuttlefish_halt: always (the runtime header may reference it).
1009
+ // string_methods / static_array: STL-free array + string helpers a no-STL
1010
+ // target needs (mutated/struct array literals + any string method).
848
1011
  // timer_methods: k_timer/k_work pool for setInterval/setTimeout (gated on
849
1012
  // timerCallCount at emit time in generateNativePolyfills).
850
1013
  // async_runtime: heap-free static Promise/microtask runtime (no STL needed).
851
- return new Set(['cuttlefish_halt', 'wiring_compat', 'timer_methods', 'async_runtime']);
1014
+ return new Set([
1015
+ 'cuttlefish_halt', 'wiring_compat', 'string_methods', 'static_array',
1016
+ 'timer_methods', 'async_runtime',
1017
+ ]);
852
1018
  }
853
1019
  generateNativePolyfills(program, ctx) {
854
1020
  const polyfills = [
@@ -897,6 +1063,75 @@ export class ZephyrStrategy {
897
1063
  ],
898
1064
  dependencies: [],
899
1065
  },
1066
+ {
1067
+ // STL-free string-method polyfills. String methods (.toUpperCase(),
1068
+ // .includes(), .substring(), …) lower at IR level to __tc_* helpers for
1069
+ // every target; this supplies their definitions. Minimal-libc friendly:
1070
+ // only <cstring> primitives (no <cctype> — case conversion is inline
1071
+ // ASCII so the polyfill is self-contained). Mirrors framework-arduino.
1072
+ kind: 'polyfill',
1073
+ id: 'string_methods',
1074
+ domain: 'embedded',
1075
+ requiredIncludes: ['<cstring>'],
1076
+ forwardDeclarations: [],
1077
+ helperStructs: [],
1078
+ helperFunctions: [`
1079
+ // TypeCAD string method polyfills (Zephyr, minimal-libc).
1080
+ #ifndef CUTTLEFISH_STR_BUF_SIZE
1081
+ #define CUTTLEFISH_STR_BUF_SIZE 64
1082
+ #endif
1083
+ 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; }
1084
+ 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; }
1085
+ 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; }
1086
+ 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; }
1087
+ 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; }
1088
+ const char* __tc_substring1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
1089
+ const char* __tc_slice2(const char* s, int start, int end) { return __tc_substring2(s, start, end); }
1090
+ const char* __tc_slice1(const char* s, int start) { return __tc_substring2(s, start, strlen(s)); }
1091
+ 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; }
1092
+ 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]; }
1093
+ int __tc_charCodeAt(const char* s, int idx) { return static_cast<int>(static_cast<unsigned char>(s[idx])); }
1094
+ int __tc_indexOf(const char* s, const char* needle) { const char* p = strstr(s, needle); return p ? static_cast<int>(p - s) : -1; }
1095
+ `],
1096
+ shimMacros: [],
1097
+ dependencies: [],
1098
+ },
1099
+ {
1100
+ // STL-free fixed-size array wrapper. Mutated/struct-element array
1101
+ // literals and array methods (.push/.pop/.map/.filter) lower to
1102
+ // __tc_StaticArray<T,N>; this supplies the template. Idempotent guard
1103
+ // so a redefinition is a no-op. Mirrors framework-arduino.
1104
+ kind: 'polyfill',
1105
+ id: 'static_array',
1106
+ domain: 'embedded',
1107
+ requiredIncludes: [],
1108
+ forwardDeclarations: [],
1109
+ helperStructs: [],
1110
+ helperFunctions: [`
1111
+ #ifndef __TC_STATIC_ARRAY_DEFINED
1112
+ #define __TC_STATIC_ARRAY_DEFINED
1113
+ template<typename T, int N>
1114
+ struct __tc_StaticArray {
1115
+ T data[N];
1116
+ int _size;
1117
+ __tc_StaticArray() : _size(0) {}
1118
+ int length() const { return _size; }
1119
+ int size() const { return _size; }
1120
+ void push(T val) { if (_size < N) data[_size++] = val; }
1121
+ T pop() { return (_size > 0) ? data[--_size] : T(); }
1122
+ int indexOf(T val) const { for (int i = 0; i < _size; i++) if (data[i] == val) return i; return -1; }
1123
+ T& operator[](int i) { return data[i]; }
1124
+ const T& operator[](int i) const { return data[i]; }
1125
+ T* begin() { return &data[0]; }
1126
+ T* end() { return &data[_size]; }
1127
+ const T* begin() const { return &data[0]; }
1128
+ const T* end() const { return &data[_size]; }
1129
+ };
1130
+ #endif
1131
+ `],
1132
+ shimMacros: [],
1133
+ dependencies: [],
1134
+ },
900
1135
  ];
901
1136
  // Worker-offload runtime (Phase 1). Emitted only when the program uses
902
1137
  // worker.* ops, backed by the Zephyr primitives in worker-backing.ts
@@ -926,7 +1161,11 @@ export class ZephyrStrategy {
926
1161
  id: 'async_runtime',
927
1162
  domain: 'embedded',
928
1163
  requiredIncludes: [],
929
- forwardDeclarations: [],
1164
+ // Polyfill definitions emit before shimLines, but the runtime's
1165
+ // timer bodies call millis() (defined in shimLines) — declare it
1166
+ // first so the polyfill compiles even for programs whose source
1167
+ // has no explicit timing call.
1168
+ forwardDeclarations: ['unsigned long millis();'],
930
1169
  helperStructs: [generateStaticAsyncRuntime(8, this.getAsyncRuntimeConfig().waitForPinEdge)],
931
1170
  helperFunctions: [],
932
1171
  shimMacros: [],
@@ -1012,11 +1251,12 @@ export class ZephyrStrategy {
1012
1251
  }
1013
1252
  // ── Strategy-owned display/touch adapter seam ────────────────────────────
1014
1253
  // Zephyr owns its display + touch adapters: the UI display adapter bridges
1015
- // the in-tree CuttlefishGFX class to Zephyr's display_write() API (see
1016
- // src/display/ui-adapter.ts), and the FT6336U touch adapter drives the I2C
1017
- // controller via Zephyr's i2c API (src/display/touch-adapter.ts). Both live
1018
- // in this package so cuttlefish carries no Zephyr/Wiring-specific display or
1019
- // touch knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1254
+ // the in-tree CuttlefishGFX class to the panel (per-controller init + wire
1255
+ // format, see src/display/ui-adapter.ts), and the touch adapters drive the
1256
+ // FT6336U (I2C capacitive) and XPT2046 (SPI resistive) controllers via
1257
+ // Zephyr's bus APIs (src/display/touch-adapter.ts). Both live in this
1258
+ // package so cuttlefish carries no Zephyr/Wiring-specific display or touch
1259
+ // knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1020
1260
  providesDisplayAdapter() { return true; }
1021
1261
  resolveDisplayAdapter(display) {
1022
1262
  const code = zephyrDisplayAdapterGenerator(display);
@@ -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;