@typecad/framework-zephyr 1.0.0-alpha.10

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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/dist/async/timer-polyfill.d.ts +10 -0
  4. package/dist/async/timer-polyfill.js +95 -0
  5. package/dist/chips/controllers.d.ts +21 -0
  6. package/dist/chips/controllers.js +66 -0
  7. package/dist/chips/esp32.d.ts +2 -0
  8. package/dist/chips/esp32.js +59 -0
  9. package/dist/chips/esp32s3.d.ts +2 -0
  10. package/dist/chips/esp32s3.js +54 -0
  11. package/dist/chips/index.d.ts +15 -0
  12. package/dist/chips/index.js +45 -0
  13. package/dist/chips/resolve.d.ts +9 -0
  14. package/dist/chips/resolve.js +121 -0
  15. package/dist/chips/types.d.ts +154 -0
  16. package/dist/chips/types.js +14 -0
  17. package/dist/chips/xiao-ble.d.ts +2 -0
  18. package/dist/chips/xiao-ble.js +61 -0
  19. package/dist/debug-codegen.d.ts +33 -0
  20. package/dist/debug-codegen.js +162 -0
  21. package/dist/display/gfx.d.ts +17 -0
  22. package/dist/display/gfx.js +171 -0
  23. package/dist/display/index.d.ts +20 -0
  24. package/dist/display/index.js +53 -0
  25. package/dist/display/profiles.d.ts +25 -0
  26. package/dist/display/profiles.js +41 -0
  27. package/dist/display/touch-adapter.d.ts +9 -0
  28. package/dist/display/touch-adapter.js +95 -0
  29. package/dist/display/ui-adapter.d.ts +21 -0
  30. package/dist/display/ui-adapter.js +534 -0
  31. package/dist/doctor.d.ts +6 -0
  32. package/dist/doctor.js +48 -0
  33. package/dist/dt-config/kconfig.d.ts +27 -0
  34. package/dist/dt-config/kconfig.js +283 -0
  35. package/dist/dt-config/overlay.d.ts +35 -0
  36. package/dist/dt-config/overlay.js +246 -0
  37. package/dist/framework.manifest.d.ts +101 -0
  38. package/dist/framework.manifest.js +536 -0
  39. package/dist/index.d.ts +6 -0
  40. package/dist/index.js +17 -0
  41. package/dist/lowering/adc.d.ts +19 -0
  42. package/dist/lowering/adc.js +76 -0
  43. package/dist/lowering/ble.d.ts +20 -0
  44. package/dist/lowering/ble.js +419 -0
  45. package/dist/lowering/board.d.ts +9 -0
  46. package/dist/lowering/board.js +33 -0
  47. package/dist/lowering/gpio.d.ts +12 -0
  48. package/dist/lowering/gpio.js +123 -0
  49. package/dist/lowering/http.d.ts +23 -0
  50. package/dist/lowering/http.js +482 -0
  51. package/dist/lowering/i2c.d.ts +15 -0
  52. package/dist/lowering/i2c.js +114 -0
  53. package/dist/lowering/index.d.ts +29 -0
  54. package/dist/lowering/index.js +93 -0
  55. package/dist/lowering/interrupts.d.ts +21 -0
  56. package/dist/lowering/interrupts.js +91 -0
  57. package/dist/lowering/mqtt.d.ts +19 -0
  58. package/dist/lowering/mqtt.js +366 -0
  59. package/dist/lowering/power.d.ts +9 -0
  60. package/dist/lowering/power.js +64 -0
  61. package/dist/lowering/preferences.d.ts +23 -0
  62. package/dist/lowering/preferences.js +345 -0
  63. package/dist/lowering/pulse.d.ts +7 -0
  64. package/dist/lowering/pulse.js +82 -0
  65. package/dist/lowering/pwm.d.ts +15 -0
  66. package/dist/lowering/pwm.js +59 -0
  67. package/dist/lowering/random.d.ts +18 -0
  68. package/dist/lowering/random.js +129 -0
  69. package/dist/lowering/spi.d.ts +17 -0
  70. package/dist/lowering/spi.js +113 -0
  71. package/dist/lowering/timing.d.ts +9 -0
  72. package/dist/lowering/timing.js +63 -0
  73. package/dist/lowering/tone.d.ts +10 -0
  74. package/dist/lowering/tone.js +50 -0
  75. package/dist/lowering/uart.d.ts +15 -0
  76. package/dist/lowering/uart.js +102 -0
  77. package/dist/lowering/util.d.ts +4 -0
  78. package/dist/lowering/util.js +12 -0
  79. package/dist/lowering/wdt.d.ts +15 -0
  80. package/dist/lowering/wdt.js +80 -0
  81. package/dist/lowering/wifi.d.ts +21 -0
  82. package/dist/lowering/wifi.js +447 -0
  83. package/dist/lowering/worker-backing.d.ts +14 -0
  84. package/dist/lowering/worker-backing.js +79 -0
  85. package/dist/lowering/worker.d.ts +6 -0
  86. package/dist/lowering/worker.js +14 -0
  87. package/dist/strategy.d.ts +168 -0
  88. package/dist/strategy.js +1094 -0
  89. package/dist/toolchain/compat.d.ts +38 -0
  90. package/dist/toolchain/compat.js +133 -0
  91. package/dist/toolchain/debug-config.d.ts +82 -0
  92. package/dist/toolchain/debug-config.js +359 -0
  93. package/dist/toolchain/index.d.ts +73 -0
  94. package/dist/toolchain/index.js +444 -0
  95. package/dist/toolchain/scaffold.d.ts +15 -0
  96. package/dist/toolchain/scaffold.js +196 -0
  97. package/dist/toolchain/west-discover.d.ts +40 -0
  98. package/dist/toolchain/west-discover.js +203 -0
  99. package/dist/toolchain/west-spawn.d.ts +39 -0
  100. package/dist/toolchain/west-spawn.js +117 -0
  101. package/package.json +82 -0
  102. package/src/async/timer-polyfill.ts +107 -0
  103. package/src/chips/controllers.ts +74 -0
  104. package/src/chips/esp32.ts +62 -0
  105. package/src/chips/esp32s3.ts +57 -0
  106. package/src/chips/index.ts +54 -0
  107. package/src/chips/resolve.ts +146 -0
  108. package/src/chips/types.ts +163 -0
  109. package/src/chips/xiao-ble.ts +64 -0
  110. package/src/debug-codegen.ts +207 -0
  111. package/src/display/gfx.ts +190 -0
  112. package/src/display/index.ts +70 -0
  113. package/src/display/profiles.ts +63 -0
  114. package/src/display/touch-adapter.ts +100 -0
  115. package/src/display/ui-adapter.ts +558 -0
  116. package/src/doctor.ts +56 -0
  117. package/src/dt-config/kconfig.ts +308 -0
  118. package/src/dt-config/overlay.ts +293 -0
  119. package/src/framework.manifest.ts +558 -0
  120. package/src/index.ts +29 -0
  121. package/src/lowering/adc.ts +102 -0
  122. package/src/lowering/ble.ts +426 -0
  123. package/src/lowering/board.ts +38 -0
  124. package/src/lowering/gpio.ts +148 -0
  125. package/src/lowering/http.ts +488 -0
  126. package/src/lowering/i2c.ts +126 -0
  127. package/src/lowering/index.ts +86 -0
  128. package/src/lowering/interrupts.ts +114 -0
  129. package/src/lowering/mqtt.ts +371 -0
  130. package/src/lowering/power.ts +70 -0
  131. package/src/lowering/preferences.ts +354 -0
  132. package/src/lowering/pulse.ts +93 -0
  133. package/src/lowering/pwm.ts +74 -0
  134. package/src/lowering/random.ts +135 -0
  135. package/src/lowering/spi.ts +124 -0
  136. package/src/lowering/timing.ts +72 -0
  137. package/src/lowering/tone.ts +60 -0
  138. package/src/lowering/uart.ts +111 -0
  139. package/src/lowering/util.ts +12 -0
  140. package/src/lowering/wdt.ts +84 -0
  141. package/src/lowering/wifi.ts +452 -0
  142. package/src/lowering/worker-backing.ts +94 -0
  143. package/src/lowering/worker.ts +17 -0
  144. package/src/strategy.ts +1265 -0
  145. package/src/toolchain/compat.ts +145 -0
  146. package/src/toolchain/debug-config.ts +399 -0
  147. package/src/toolchain/index.ts +482 -0
  148. package/src/toolchain/scaffold.ts +196 -0
  149. package/src/toolchain/west-discover.ts +233 -0
  150. package/src/toolchain/west-spawn.ts +141 -0
@@ -0,0 +1,73 @@
1
+ import type { ToolchainOptions, CompileResult, UploadResult } from '@typecad/cuttlefish/api/shared';
2
+ /**
3
+ * Derive the Zephyr project root from the cuttlefish-emitted source path.
4
+ *
5
+ * Cuttlefish emits `src/main.cpp` under the output dir. The CLI passes
6
+ * `sourcePath` = full path to `main.cpp` and `outputDir` = its parent (`src/`).
7
+ * For Zephyr, the project root is the parent of `src/` — one level above
8
+ * `outputDir`. Detect that shape and adjust; otherwise fall back to `outputDir`.
9
+ */
10
+ export declare function projectRootFromOptions(o: ToolchainOptions): string;
11
+ /**
12
+ * Build the `west flash` argument list for a board.
13
+ *
14
+ * Runner selection: each board's board.cmake declares a sensible default flash
15
+ * runner for its hardware (xiao_ble → nrfutil, esp32* → esptool), and `west
16
+ * flash` resolves it automatically. The framework only intervenes where the
17
+ * board default needs an argument it can't infer:
18
+ * - An explicit `zephyr.runner` (from cuttlefish.config.ts) always wins.
19
+ * - ESP32 boards forward the port via `--esp-device` (esptool reads the
20
+ * device from it); board.cmake still picks the runner.
21
+ * - Every other board trusts the board.cmake default. Previously this forced
22
+ * `--runner nrfjprog` for every non-ESP32 target, which broke boards whose
23
+ * default is not nrfjprog (xiao_ble defaults to nrfutil) and required
24
+ * Nordic J-Link tools that a USB-bootloader board does not have.
25
+ *
26
+ * Exported (pure) so the runner-selection contract is unit-testable without
27
+ * spawning west.
28
+ */
29
+ export declare function buildFlashArgs(buildDir: string, board: string, userRunner: string | undefined, port: string | undefined): string[];
30
+ /**
31
+ * Classify a `west flash` result as success/failure.
32
+ *
33
+ * west's exit status is authoritative except for one known race in the uf2
34
+ * runner on Windows: the UF2 bootloader reboots to run new firmware the instant
35
+ * the file copy completes, unmounting the USB-MSC drive before `shutil.copy`'s
36
+ * trailing `copymode`/chmod runs. That raises `OSError: [WinError 433] A
37
+ * device which does not exist was specified` and makes west exit non-zero —
38
+ * even though the firmware copied and flashed correctly (the LED blinks).
39
+ *
40
+ * The copy starting is logged ("Copying UF2 file to '<drive>'"); WinError 433
41
+ * during `copymode` after that point proves the data write finished and the
42
+ * drive only vanished on the metadata step. Treat that exact signature as
43
+ * success so the upload isn't reported as a failure. Genuine uf2 failures
44
+ * (no partition found, write errors before the copy) still surface as failures.
45
+ *
46
+ * Exported (pure) so the classification is unit-testable without spawning west.
47
+ */
48
+ export declare function classifyUploadResult(runner: string | undefined, status: number | null, output: string): boolean;
49
+ /**
50
+ * Cleanse the `west flash` output shown to the user.
51
+ *
52
+ * When classifyUploadResult has decided a non-zero west exit was the benign UF2
53
+ * race (firmware copied, drive unmounted on the trailing chmod), the raw output
54
+ * is a wall of Python traceback that reads like a hard failure. Drop everything
55
+ * after the "Copying UF2 file to" line — i.e. the entire traceback — so a
56
+ * successful flash reads as a success (the framework's ✓ Done follows). Non-race
57
+ * output is returned untouched; genuine errors stay fully visible for diagnosis.
58
+ *
59
+ * Exported (pure) so the cleansing is unit-testable without spawning west.
60
+ */
61
+ export declare function cleanseUploadOutput(runner: string | undefined, status: number | null, output: string): string;
62
+ /**
63
+ * FrameworkToolchain for Zephyr. Spec §3.5 (mirror of the ESP-IDF toolchain).
64
+ * The target board is carried via frameworkData.buildTarget; scaffolding
65
+ * happens at compile time when the target is known.
66
+ */
67
+ export declare const Toolchain: {
68
+ prepare(outputDir: string, entryPoint: string): void;
69
+ compile(o: ToolchainOptions): CompileResult;
70
+ upload(o: ToolchainOptions): UploadResult;
71
+ monitor(o: ToolchainOptions): void;
72
+ debug(o: ToolchainOptions): void;
73
+ };
@@ -0,0 +1,444 @@
1
+ // ---------------------------------------------------------------------------
2
+ // FrameworkToolchain impl for Zephyr (west / CMake)
3
+ //
4
+ // compile() scaffolds the project (idempotent) then runs `west build -b <board>`.
5
+ // upload() runs `west flash`. monitor() runs a best-effort serial monitor.
6
+ //
7
+ // west resolution goes through westSpawn(), which finds a usable west without
8
+ // requiring the user to have activated the Zephyr Python venv — it prefers
9
+ // `<python> -m west` (robust cross-platform form) and injects ZEPHYR_BASE when
10
+ // a SDK root is discovered. See west-discover.ts / west-spawn.ts.
11
+ //
12
+ // The board target is carried via frameworkData.buildTarget (populated as
13
+ // ToolchainOptions.buildTarget by the cuttlefish CLI), defaulting to the
14
+ // framework's canonical MVP target (xiao_ble).
15
+ //
16
+ // Mirrors framework-esp32/src/toolchain/index.ts structure: projectRoot derived
17
+ // from outputDir, prepare is a no-op (scaffold happens in compile when the
18
+ // target is known), GCC errors parsed via the shared parseCompileErrors helper.
19
+ // ---------------------------------------------------------------------------
20
+ import { spawnSync } from 'node:child_process';
21
+ import { basename, dirname, join } from 'node:path';
22
+ import { readdirSync, readFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
23
+ import { parseCompileErrors } from '@typecad/cuttlefish/api/shared';
24
+ import { scaffoldZephyrProject, writeIfChanged } from './scaffold.js';
25
+ import { westSpawn, buildEnv } from './west-spawn.js';
26
+ import { discoverWest } from './west-discover.js';
27
+ import { writeDebugConfig, resolveDebugLocations } from './debug-config.js';
28
+ import { ZephyrStrategy } from '../strategy.js';
29
+ import { generateOverlay } from '../dt-config/overlay.js';
30
+ import { chipForTarget } from '../chips/index.js';
31
+ import { detectZephyrVersion, checkZephyrCompat, resolveBoardTarget } from './compat.js';
32
+ import { DEFAULT_ZEPHYR_DISPLAY_PROFILE } from '../display/profiles.js';
33
+ /** Default board target — the framework's MVP canonical board. */
34
+ const DEFAULT_BOARD = 'xiao_ble';
35
+ function targetFromOptions(o) {
36
+ // The cuttlefish CLI populates ToolchainOptions.buildTarget from
37
+ // config.frameworkData.buildTarget. Accept frameworkData.target as an alias.
38
+ const fcTarget = o.frameworkConfig?.target;
39
+ return o.buildTarget ?? fcTarget ?? DEFAULT_BOARD;
40
+ }
41
+ /**
42
+ * Derive the Zephyr project root from the cuttlefish-emitted source path.
43
+ *
44
+ * Cuttlefish emits `src/main.cpp` under the output dir. The CLI passes
45
+ * `sourcePath` = full path to `main.cpp` and `outputDir` = its parent (`src/`).
46
+ * For Zephyr, the project root is the parent of `src/` — one level above
47
+ * `outputDir`. Detect that shape and adjust; otherwise fall back to `outputDir`.
48
+ */
49
+ export function projectRootFromOptions(o) {
50
+ const outDir = o.outputDir;
51
+ if (basename(outDir) === 'src') {
52
+ return dirname(outDir);
53
+ }
54
+ return outDir;
55
+ }
56
+ /**
57
+ * west build timeout. Zephyr's first build fetches the toolchain modules and
58
+ * configures CMake/Ninja, which can take several minutes; allow generous headroom.
59
+ */
60
+ const BUILD_TIMEOUT_MS = 600_000;
61
+ const FLASH_TIMEOUT_MS = 120_000;
62
+ /**
63
+ * Build the `west flash` argument list for a board.
64
+ *
65
+ * Runner selection: each board's board.cmake declares a sensible default flash
66
+ * runner for its hardware (xiao_ble → nrfutil, esp32* → esptool), and `west
67
+ * flash` resolves it automatically. The framework only intervenes where the
68
+ * board default needs an argument it can't infer:
69
+ * - An explicit `zephyr.runner` (from cuttlefish.config.ts) always wins.
70
+ * - ESP32 boards forward the port via `--esp-device` (esptool reads the
71
+ * device from it); board.cmake still picks the runner.
72
+ * - Every other board trusts the board.cmake default. Previously this forced
73
+ * `--runner nrfjprog` for every non-ESP32 target, which broke boards whose
74
+ * default is not nrfjprog (xiao_ble defaults to nrfutil) and required
75
+ * Nordic J-Link tools that a USB-bootloader board does not have.
76
+ *
77
+ * Exported (pure) so the runner-selection contract is unit-testable without
78
+ * spawning west.
79
+ */
80
+ export function buildFlashArgs(buildDir, board, userRunner, port) {
81
+ const args = ['flash', '-d', buildDir];
82
+ if (userRunner) {
83
+ args.push('--runner', userRunner);
84
+ }
85
+ if (port && board.startsWith('esp32')) {
86
+ args.push('--esp-device', port);
87
+ }
88
+ return args;
89
+ }
90
+ /**
91
+ * Classify a `west flash` result as success/failure.
92
+ *
93
+ * west's exit status is authoritative except for one known race in the uf2
94
+ * runner on Windows: the UF2 bootloader reboots to run new firmware the instant
95
+ * the file copy completes, unmounting the USB-MSC drive before `shutil.copy`'s
96
+ * trailing `copymode`/chmod runs. That raises `OSError: [WinError 433] A
97
+ * device which does not exist was specified` and makes west exit non-zero —
98
+ * even though the firmware copied and flashed correctly (the LED blinks).
99
+ *
100
+ * The copy starting is logged ("Copying UF2 file to '<drive>'"); WinError 433
101
+ * during `copymode` after that point proves the data write finished and the
102
+ * drive only vanished on the metadata step. Treat that exact signature as
103
+ * success so the upload isn't reported as a failure. Genuine uf2 failures
104
+ * (no partition found, write errors before the copy) still surface as failures.
105
+ *
106
+ * Exported (pure) so the classification is unit-testable without spawning west.
107
+ */
108
+ export function classifyUploadResult(runner, status, output) {
109
+ if (status === 0)
110
+ return true;
111
+ if (isUf2DriveVanishRace(output))
112
+ return runner === 'uf2';
113
+ return false;
114
+ }
115
+ /**
116
+ * Whether `output` carries the benign UF2 copymode/WinError-433 race signature
117
+ * (see classifyUploadResult). Centralized so classify + cleanse share one match.
118
+ */
119
+ function isUf2DriveVanishRace(output) {
120
+ return /Copying UF2 file to/.test(output)
121
+ && /WinError 433/.test(output)
122
+ && /copymode/.test(output);
123
+ }
124
+ /**
125
+ * Cleanse the `west flash` output shown to the user.
126
+ *
127
+ * When classifyUploadResult has decided a non-zero west exit was the benign UF2
128
+ * race (firmware copied, drive unmounted on the trailing chmod), the raw output
129
+ * is a wall of Python traceback that reads like a hard failure. Drop everything
130
+ * after the "Copying UF2 file to" line — i.e. the entire traceback — so a
131
+ * successful flash reads as a success (the framework's ✓ Done follows). Non-race
132
+ * output is returned untouched; genuine errors stay fully visible for diagnosis.
133
+ *
134
+ * Exported (pure) so the cleansing is unit-testable without spawning west.
135
+ */
136
+ export function cleanseUploadOutput(runner, status, output) {
137
+ if (status === 0)
138
+ return output;
139
+ if (runner === 'uf2' && isUf2DriveVanishRace(output)) {
140
+ // Keep everything west printed up to and including "Copying UF2 file to",
141
+ // then stop — everything after that is the drive-vanish traceback.
142
+ const upto = output.match(/[\s\S]*Copying UF2 file to[^\n]*/);
143
+ const head = upto ? upto[0] : '-- west flash: using runner uf2';
144
+ return head;
145
+ }
146
+ return output;
147
+ }
148
+ /**
149
+ * FrameworkToolchain for Zephyr. Spec §3.5 (mirror of the ESP-IDF toolchain).
150
+ * The target board is carried via frameworkData.buildTarget; scaffolding
151
+ * happens at compile time when the target is known.
152
+ */
153
+ export const Toolchain = {
154
+ prepare(outputDir, entryPoint) {
155
+ // Write the DT overlay for the default board (the real target is known at
156
+ // compile time; prepare runs before compile, so use the default board id).
157
+ // The overlay is additive and idempotent; compile re-runs prepare-equivalent
158
+ // logic in scaffold via the usage scan. Mirrors how Arduino's library
159
+ // resolution is a pre-build artifact step.
160
+ const projectRoot = basename(outputDir) === 'src' ? dirname(outputDir) : outputDir;
161
+ const board = DEFAULT_BOARD;
162
+ const chip = chipForTarget(board);
163
+ // Scan the emitted source for usage tokens (same authoritative signal the
164
+ // scaffold uses). entryPoint is the path to main.cpp; its dir is src/.
165
+ const srcDir = dirname(entryPoint);
166
+ let src = '';
167
+ try {
168
+ for (const name of readdirSync(srcDir)) {
169
+ if (name.endsWith('.cpp') || name.endsWith('.c')) {
170
+ src += readFileSync(join(srcDir, name), 'utf8');
171
+ }
172
+ }
173
+ }
174
+ catch { /* src may not exist yet on first prepare */ }
175
+ const uses = (t) => src.includes(t);
176
+ // Display usage tokens: the minimal GFX runtime (display_write/_fill_rect)
177
+ // and the UI display adapter (display_init / __tc_display_dev /
178
+ // DEVICE_DT_GET on the display nodelabel). Both paths need the DT overlay
179
+ // to enable the display node.
180
+ const usesDisplay = uses('display_write') || uses('display_init')
181
+ || uses('display_fill_rect') || uses('__tc_display_dev')
182
+ || uses('CuttlefishDisplayTarget');
183
+ // Both registered Zephyr display profiles use dtLabel 'display0', so the
184
+ // default profile's overlay block (&display0 { status="okay" }) is correct
185
+ // for either driver. Thread a non-default profile here only if a future
186
+ // board carries a display node under a different nodelabel.
187
+ const displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
188
+ const overlay = generateOverlay(chip, {
189
+ usesI2c: uses('i2c_'),
190
+ usesSpi: uses('spi_'),
191
+ usesUart: uses('uart_'),
192
+ usesDisplay,
193
+ usesTouch: uses('ft6336u') || uses('touch_'),
194
+ }, displayProfile);
195
+ const overlayDir = join(projectRoot, 'boards');
196
+ mkdirSync(overlayDir, { recursive: true });
197
+ writeIfChanged(join(overlayDir, `${board}.overlay`), overlay);
198
+ },
199
+ compile(o) {
200
+ const projectRoot = projectRootFromOptions(o);
201
+ const rawBoard = targetFromOptions(o);
202
+ // Fail fast on an incompatible Zephyr (clear message vs. a cryptic west/
203
+ // CMake board error), then normalize the board target for the installed
204
+ // version — Zephyr 4.3+ rejects bare multi-core board names, so a stale
205
+ // config (esp32s3_devkitc) is rewritten to the qualified form
206
+ // (esp32s3_devkitc/esp32s3/procpu). See toolchain/compat.ts.
207
+ const zephyrVersion = detectZephyrVersion();
208
+ const compat = checkZephyrCompat(zephyrVersion);
209
+ if (compat.status === 'out-of-range') {
210
+ throw new Error(`Zephyr ${zephyrVersion} is outside the supported range (${compat.range}) for @typecad/framework-zephyr. ` +
211
+ `Set ZEPHYR_BASE to a compatible Zephyr checkout, or install one via '@typecad/zephyr-installer'.`);
212
+ }
213
+ if (compat.status === 'undetectable') {
214
+ console.warn(`! Could not detect the installed Zephyr version (is ZEPHYR_BASE set?). ` +
215
+ `Skipping compat check; declared range is ${compat.range}.`);
216
+ }
217
+ const board = resolveBoardTarget(rawBoard, zephyrVersion);
218
+ const debugMode = new ZephyrStrategy().debugMode(board);
219
+ const isGdbDebug = o.debug === true && debugMode === 'gdb';
220
+ const zc = o.zephyrConfig;
221
+ const userKconfig = zc?.kconfig;
222
+ const configChanged = scaffoldZephyrProject(projectRoot, isGdbDebug, userKconfig, o.psram);
223
+ // Regenerate the DT overlay for the ACTUAL target board. prepare() writes
224
+ // it for the default board (the real target is unknown until compile), so
225
+ // the <default>.overlay it wrote does not match `west build -b <board>`.
226
+ // Zephyr auto-detects boards/<board>.overlay under APPLICATION_CONFIG_DIR.
227
+ try {
228
+ const chip = chipForTarget(board);
229
+ const srcDir = join(projectRoot, 'src');
230
+ let src = '';
231
+ try {
232
+ for (const name of readdirSync(srcDir)) {
233
+ if (name.endsWith('.cpp') || name.endsWith('.c')) {
234
+ src += readFileSync(join(srcDir, name), 'utf-8');
235
+ }
236
+ }
237
+ }
238
+ catch { /* src may not exist */ }
239
+ const uses = (t) => src.includes(t);
240
+ const usesDisplay = uses('display_write') || uses('display_init')
241
+ || uses('display_fill_rect') || uses('__tc_display_dev')
242
+ || uses('CuttlefishDisplayTarget');
243
+ // Derive the display dimensions from the emitted adapter code
244
+ // (display_width/height return the profile's w/h). This ensures the DT
245
+ // overlay's width/height match the panel the adapter targets, not the
246
+ // default profile — critical for drivers like ST7796S that initialize
247
+ // the panel geometry from the DT node.
248
+ let displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
249
+ if (usesDisplay) {
250
+ const wMatch = src.match(/display_width\(\)\s*\{\s*return\s+(\d+)\s*;\s*\}/);
251
+ const hMatch = src.match(/display_height\(\)\s*\{\s*return\s+(\d+)\s*;\s*\}/);
252
+ if (wMatch && hMatch) {
253
+ displayProfile = {
254
+ ...DEFAULT_ZEPHYR_DISPLAY_PROFILE,
255
+ width: parseInt(wMatch[1], 10),
256
+ height: parseInt(hMatch[1], 10),
257
+ };
258
+ }
259
+ }
260
+ // Extract display pin wiring (cs/dc/rst/spiFrequency/spiPins) from the
261
+ // config display section so the DT overlay wires the MIPI DBI bridge to
262
+ // the correct GPIOs + SPI bus pins.
263
+ const dispCfg = o.display;
264
+ const spiPins = (dispCfg?.spiPins ?? undefined);
265
+ const wiring = dispCfg
266
+ ? {
267
+ cs: typeof dispCfg.cs === 'number' ? dispCfg.cs : undefined,
268
+ dc: typeof dispCfg.dc === 'number' ? dispCfg.dc : undefined,
269
+ rst: typeof dispCfg.rst === 'number' ? dispCfg.rst : undefined,
270
+ spiFrequency: typeof dispCfg.spiFrequency === 'number' ? dispCfg.spiFrequency : undefined,
271
+ sck: typeof spiPins?.sck === 'number' ? spiPins.sck : undefined,
272
+ mosi: typeof spiPins?.mosi === 'number' ? spiPins.mosi : undefined,
273
+ miso: typeof spiPins?.miso === 'number' ? spiPins.miso : undefined,
274
+ backlightPin: typeof dispCfg.backlightPin === 'number' ? dispCfg.backlightPin : undefined,
275
+ }
276
+ : undefined;
277
+ // Extract touch pin wiring (irq/resetPin/sda/scl) from the config
278
+ // display.touch section so the DT overlay wires the I2C bus + touch node.
279
+ const touchCfg = dispCfg?.touch;
280
+ const touchWiring = touchCfg
281
+ ? {
282
+ irq: typeof touchCfg.irq === 'number' ? touchCfg.irq : undefined,
283
+ resetPin: typeof touchCfg.resetPin === 'number' ? touchCfg.resetPin : undefined,
284
+ sda: typeof touchCfg.sda === 'number' ? touchCfg.sda : undefined,
285
+ scl: typeof touchCfg.scl === 'number' ? touchCfg.scl : undefined,
286
+ }
287
+ : undefined;
288
+ const overlay = generateOverlay(chip, {
289
+ usesI2c: uses('i2c_'),
290
+ usesSpi: uses('spi_'),
291
+ usesUart: uses('uart_'),
292
+ usesDisplay,
293
+ usesTouch: uses('ft6336u') || uses('touch_'),
294
+ psram: o.psram,
295
+ }, displayProfile, wiring, touchWiring);
296
+ const overlayDir = join(projectRoot, 'boards');
297
+ mkdirSync(overlayDir, { recursive: true });
298
+ // Write the board-specific overlay (the one west loads). Zephyr looks for
299
+ // boards/<board_id>.overlay under APPLICATION_CONFIG_DIR — use the bare
300
+ // board id (before any hardware-qualifier suffix, e.g. 'esp32_devkitc'
301
+ // not the full 'esp32_devkitc/esp32/procpu' target string).
302
+ const boardId = board.split('/')[0];
303
+ writeIfChanged(join(overlayDir, `${boardId}.overlay`), overlay);
304
+ }
305
+ catch { /* best-effort overlay regen; the build surfaces DT errors */ }
306
+ // Use a stable build dir so incremental builds reuse the Ninja graph.
307
+ // west defaults to <projectRoot>/build.
308
+ const buildDir = join(projectRoot, 'build');
309
+ // Nuke the build dir whenever a previous build exists. Zephyr's gen_offset
310
+ // flow (offsets.h is generated FROM offsets.c.obj, while gen_offset.h makes
311
+ // offsets.c include offsets.h) leaves a permanent `offsets.h ->
312
+ // offsets.c.obj -> offsets.h` cycle in the .ninja_deps log after the first
313
+ // incremental pass — ninja then fails every later build with `dependency
314
+ // cycle` even when nothing changed. This is a known Zephyr-on-Windows
315
+ // issue; the reliable fix is a pristine build dir per build. Also nukes
316
+ // when prj.conf/CMakeLists/overlay changed, so Kconfig symbols and
317
+ // generated headers never diverge from a cached graph.
318
+ if (configChanged || existsSync(join(buildDir, 'zephyr', 'zephyr.bin'))) {
319
+ try {
320
+ rmSync(buildDir, { recursive: true, force: true });
321
+ }
322
+ catch { /* may not exist */ }
323
+ }
324
+ const buildArgs = ['build', '-b', board, '-d', buildDir, projectRoot];
325
+ // Explicitly pass the generated DT overlay. Zephyr's auto-detection of
326
+ // boards/<board>.overlay fails for hardware-qualified targets (e.g.
327
+ // esp32_devkitc/esp32/procpu) because the FILE_SUFFIX matching doesn't
328
+ // resolve — passing -DDTC_OVERLAY_FILE forces it unconditionally.
329
+ const boardId = board.split('/')[0];
330
+ const overlayPath = join(projectRoot, 'boards', `${boardId}.overlay`);
331
+ try {
332
+ if (readFileSync(overlayPath, 'utf-8').length > 0) {
333
+ // CMake parses backslashes as escapes — use forward slashes so the
334
+ // Windows path survives the -D argument intact.
335
+ buildArgs.push('--', `-DDTC_OVERLAY_FILE=${overlayPath.replace(/\\/g, '/')}`);
336
+ }
337
+ }
338
+ catch { /* no overlay — let Zephyr auto-detect or build without one */ }
339
+ // Append user cmake args from cuttlefish.config.ts zephyr.cmakeArgs.
340
+ const userCmakeArgs = zc?.cmakeArgs;
341
+ if (userCmakeArgs && userCmakeArgs.length > 0) {
342
+ if (!buildArgs.includes('--'))
343
+ buildArgs.push('--');
344
+ buildArgs.push(...userCmakeArgs);
345
+ }
346
+ const inv = westSpawn(buildArgs, { cwd: projectRoot, encoding: 'utf-8', timeout: BUILD_TIMEOUT_MS });
347
+ const result = spawnSync(inv.command, inv.args, inv.options);
348
+ const stdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
349
+ const stderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
350
+ const output = stdout + stderr;
351
+ // Prefix the build log with how west was resolved, for transparency.
352
+ const header = `Using west via ${inv.install.source}` +
353
+ (inv.install.zephyrBase ? ` (ZEPHYR_BASE=${inv.install.zephyrBase})` : '') + '\n';
354
+ // After a successful build in gdb mode (--debug on a probe-capable target),
355
+ // write the VS Code launch.json + tasks.json + gdb-script artifacts so F5
356
+ // attaches GDB to the chip's debug probe. Non-fatal on failure — a missing
357
+ // artifact doesn't block the build. Mirrors the deleted framework-esp32
358
+ // toolchain compile() debug-config wiring.
359
+ if (result.status === 0 && isGdbDebug) {
360
+ try {
361
+ const { workspaceRoot, sketchRel } = resolveDebugLocations(projectRoot);
362
+ writeDebugConfig({
363
+ projectRoot,
364
+ workspaceRoot,
365
+ sketchRel,
366
+ target: board,
367
+ buildDir,
368
+ sourceMapPath: join(dirname(o.sourcePath), `${basename(o.sourcePath)}.thcppmap.json`),
369
+ });
370
+ }
371
+ catch (e) {
372
+ console.warn(`[cuttlefish] gdb debug config generation failed: ${e.message}`);
373
+ }
374
+ }
375
+ return {
376
+ success: result.status === 0,
377
+ output: header + output,
378
+ errors: parseCompileErrors(output, o.sourcePath),
379
+ };
380
+ },
381
+ upload(o) {
382
+ const projectRoot = projectRootFromOptions(o);
383
+ const buildDir = join(projectRoot, 'build');
384
+ const board = targetFromOptions(o);
385
+ const zc = o.zephyrConfig;
386
+ const runner = zc?.runner;
387
+ const args = buildFlashArgs(buildDir, board, runner, o.port);
388
+ const inv = westSpawn(args, {
389
+ cwd: projectRoot,
390
+ encoding: 'utf-8',
391
+ timeout: FLASH_TIMEOUT_MS,
392
+ });
393
+ const result = spawnSync(inv.command, inv.args, inv.options);
394
+ const fstdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
395
+ const fstderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
396
+ const raw = fstdout + fstderr;
397
+ return {
398
+ success: classifyUploadResult(runner, result.status, raw),
399
+ output: cleanseUploadOutput(runner, result.status, raw),
400
+ };
401
+ },
402
+ monitor(o) {
403
+ // Serial monitor over USB-CDC. Zephyr does NOT ship a `west serial`
404
+ // subcommand (it's not a real west command — invoking it errors with
405
+ // "unknown command"). The discovered west install's venv carries pyserial,
406
+ // so run its bundled miniterm directly: `python -m serial.tools.miniterm`.
407
+ // That is the same cross-platform terminal pyserial provides in ESP-IDF's
408
+ // idf.py monitor, and it inherits stdio so Ctrl+C exits cleanly.
409
+ if (!o.port) {
410
+ throw new Error('A serial port is required to monitor. Pass --port <COMx/ttyX>.');
411
+ }
412
+ // ESP32 USB-CDC console runs at 115200 (the Zephyr ESP32 board default).
413
+ // The CLI's generic default of 9600 is wrong for this target; honor an
414
+ // explicit --baud / config.console.baudRate when given, else 115200.
415
+ const baud = o.baud ?? 115200;
416
+ const install = discoverWest();
417
+ const py = install?.pythonExecutable ?? process.env.PYTHON ?? 'python';
418
+ // Reuse west-spawn's env builder (prepends the venv bin dir to PATH so the
419
+ // python we spawn resolves pyserial from the same venv). Falls back to the
420
+ // process env when no install is discovered.
421
+ const env = install ? buildEnv(install) : process.env;
422
+ spawnSync(py, ['-m', 'serial.tools.miniterm', o.port, String(baud)], {
423
+ cwd: projectRootFromOptions(o),
424
+ env,
425
+ stdio: 'inherit',
426
+ });
427
+ },
428
+ debug(o) {
429
+ // Launch an interactive GDB session for the last build. `west debug`
430
+ // auto-resolves the runner (openocd for esp32s3, nrfjprog/jlink for nRF)
431
+ // and the GDB binary from the build dir's CMakeCache/board.cmake — no
432
+ // hand-authored gdbinit needed. Inherits stdio so GDB runs interactively.
433
+ // (Not invoked by the standard build/compile flow; powers an explicit
434
+ // debug-attach entry point for terminal-driven debugging without VS Code.)
435
+ const projectRoot = projectRootFromOptions(o);
436
+ const buildDir = join(projectRoot, 'build');
437
+ const inv = westSpawn(['debug', '-d', buildDir], {
438
+ cwd: projectRoot,
439
+ encoding: 'utf-8',
440
+ stdio: 'inherit',
441
+ });
442
+ spawnSync(inv.command, inv.args, inv.options);
443
+ },
444
+ };
@@ -0,0 +1,15 @@
1
+ /** Write a file only if the content differs from the existing file.
2
+ * Returns true when the file was written (content changed or file was new). */
3
+ export declare function writeIfChanged(filePath: string, content: string): boolean;
4
+ /**
5
+ * Emit the Zephyr application skeleton around the generated src/main.cpp.
6
+ *
7
+ * Layout written:
8
+ * <projectRoot>/
9
+ * CMakeLists.txt (find_package(Zephyr), target_sources app ← src/*.cpp)
10
+ * prj.conf (CONFIG_* for GPIO + C++ + libc)
11
+ * src/main.cpp (owned by cuttlefish's emit pipeline — NOT touched here)
12
+ *
13
+ * Idempotent. Mirrors scaffoldEspIdfProject's writeIfChanged discipline.
14
+ */
15
+ export declare function scaffoldZephyrProject(projectRoot: string, debug?: boolean, userKconfig?: Record<string, string>, psram?: 'opi' | 'quad'): boolean;