@typecad/framework-zephyr 1.0.0-alpha.8
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.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/async/timer-polyfill.d.ts +10 -0
- package/dist/async/timer-polyfill.js +95 -0
- package/dist/chips/controllers.d.ts +21 -0
- package/dist/chips/controllers.js +66 -0
- package/dist/chips/esp32.d.ts +2 -0
- package/dist/chips/esp32.js +59 -0
- package/dist/chips/esp32s3.d.ts +2 -0
- package/dist/chips/esp32s3.js +54 -0
- package/dist/chips/index.d.ts +15 -0
- package/dist/chips/index.js +45 -0
- package/dist/chips/resolve.d.ts +9 -0
- package/dist/chips/resolve.js +121 -0
- package/dist/chips/types.d.ts +154 -0
- package/dist/chips/types.js +14 -0
- package/dist/chips/xiao-ble.d.ts +2 -0
- package/dist/chips/xiao-ble.js +61 -0
- package/dist/debug-codegen.d.ts +33 -0
- package/dist/debug-codegen.js +162 -0
- package/dist/display/gfx.d.ts +17 -0
- package/dist/display/gfx.js +171 -0
- package/dist/display/index.d.ts +20 -0
- package/dist/display/index.js +53 -0
- package/dist/display/profiles.d.ts +25 -0
- package/dist/display/profiles.js +41 -0
- package/dist/display/touch-adapter.d.ts +9 -0
- package/dist/display/touch-adapter.js +95 -0
- package/dist/display/ui-adapter.d.ts +21 -0
- package/dist/display/ui-adapter.js +534 -0
- package/dist/dt-config/kconfig.d.ts +27 -0
- package/dist/dt-config/kconfig.js +283 -0
- package/dist/dt-config/overlay.d.ts +35 -0
- package/dist/dt-config/overlay.js +246 -0
- package/dist/framework.manifest.d.ts +98 -0
- package/dist/framework.manifest.js +525 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +12 -0
- package/dist/lowering/adc.d.ts +19 -0
- package/dist/lowering/adc.js +76 -0
- package/dist/lowering/ble.d.ts +20 -0
- package/dist/lowering/ble.js +419 -0
- package/dist/lowering/board.d.ts +9 -0
- package/dist/lowering/board.js +33 -0
- package/dist/lowering/gpio.d.ts +12 -0
- package/dist/lowering/gpio.js +123 -0
- package/dist/lowering/http.d.ts +23 -0
- package/dist/lowering/http.js +482 -0
- package/dist/lowering/i2c.d.ts +15 -0
- package/dist/lowering/i2c.js +114 -0
- package/dist/lowering/index.d.ts +29 -0
- package/dist/lowering/index.js +93 -0
- package/dist/lowering/interrupts.d.ts +21 -0
- package/dist/lowering/interrupts.js +91 -0
- package/dist/lowering/mqtt.d.ts +19 -0
- package/dist/lowering/mqtt.js +366 -0
- package/dist/lowering/power.d.ts +9 -0
- package/dist/lowering/power.js +64 -0
- package/dist/lowering/preferences.d.ts +23 -0
- package/dist/lowering/preferences.js +345 -0
- package/dist/lowering/pulse.d.ts +7 -0
- package/dist/lowering/pulse.js +82 -0
- package/dist/lowering/pwm.d.ts +15 -0
- package/dist/lowering/pwm.js +59 -0
- package/dist/lowering/random.d.ts +18 -0
- package/dist/lowering/random.js +129 -0
- package/dist/lowering/spi.d.ts +17 -0
- package/dist/lowering/spi.js +113 -0
- package/dist/lowering/timing.d.ts +9 -0
- package/dist/lowering/timing.js +63 -0
- package/dist/lowering/tone.d.ts +10 -0
- package/dist/lowering/tone.js +50 -0
- package/dist/lowering/uart.d.ts +15 -0
- package/dist/lowering/uart.js +102 -0
- package/dist/lowering/util.d.ts +4 -0
- package/dist/lowering/util.js +12 -0
- package/dist/lowering/wdt.d.ts +15 -0
- package/dist/lowering/wdt.js +80 -0
- package/dist/lowering/wifi.d.ts +21 -0
- package/dist/lowering/wifi.js +447 -0
- package/dist/lowering/worker-backing.d.ts +14 -0
- package/dist/lowering/worker-backing.js +79 -0
- package/dist/lowering/worker.d.ts +6 -0
- package/dist/lowering/worker.js +14 -0
- package/dist/strategy.d.ts +168 -0
- package/dist/strategy.js +1094 -0
- package/dist/toolchain/debug-config.d.ts +82 -0
- package/dist/toolchain/debug-config.js +359 -0
- package/dist/toolchain/index.d.ts +73 -0
- package/dist/toolchain/index.js +427 -0
- package/dist/toolchain/scaffold.d.ts +15 -0
- package/dist/toolchain/scaffold.js +196 -0
- package/dist/toolchain/west-discover.d.ts +40 -0
- package/dist/toolchain/west-discover.js +203 -0
- package/dist/toolchain/west-spawn.d.ts +39 -0
- package/dist/toolchain/west-spawn.js +117 -0
- package/package.json +82 -0
- package/src/async/timer-polyfill.ts +107 -0
- package/src/chips/controllers.ts +74 -0
- package/src/chips/esp32.ts +62 -0
- package/src/chips/esp32s3.ts +57 -0
- package/src/chips/index.ts +54 -0
- package/src/chips/resolve.ts +146 -0
- package/src/chips/types.ts +163 -0
- package/src/chips/xiao-ble.ts +64 -0
- package/src/debug-codegen.ts +207 -0
- package/src/display/gfx.ts +190 -0
- package/src/display/index.ts +70 -0
- package/src/display/profiles.ts +63 -0
- package/src/display/touch-adapter.ts +100 -0
- package/src/display/ui-adapter.ts +558 -0
- package/src/dt-config/kconfig.ts +308 -0
- package/src/dt-config/overlay.ts +293 -0
- package/src/framework.manifest.ts +545 -0
- package/src/index.ts +23 -0
- package/src/lowering/adc.ts +102 -0
- package/src/lowering/ble.ts +426 -0
- package/src/lowering/board.ts +38 -0
- package/src/lowering/gpio.ts +148 -0
- package/src/lowering/http.ts +488 -0
- package/src/lowering/i2c.ts +126 -0
- package/src/lowering/index.ts +86 -0
- package/src/lowering/interrupts.ts +114 -0
- package/src/lowering/mqtt.ts +371 -0
- package/src/lowering/power.ts +70 -0
- package/src/lowering/preferences.ts +354 -0
- package/src/lowering/pulse.ts +93 -0
- package/src/lowering/pwm.ts +74 -0
- package/src/lowering/random.ts +135 -0
- package/src/lowering/spi.ts +124 -0
- package/src/lowering/timing.ts +72 -0
- package/src/lowering/tone.ts +60 -0
- package/src/lowering/uart.ts +111 -0
- package/src/lowering/util.ts +12 -0
- package/src/lowering/wdt.ts +84 -0
- package/src/lowering/wifi.ts +452 -0
- package/src/lowering/worker-backing.ts +94 -0
- package/src/lowering/worker.ts +17 -0
- package/src/strategy.ts +1265 -0
- package/src/toolchain/debug-config.ts +399 -0
- package/src/toolchain/index.ts +459 -0
- package/src/toolchain/scaffold.ts +196 -0
- package/src/toolchain/west-discover.ts +233 -0
- package/src/toolchain/west-spawn.ts +141 -0
|
@@ -0,0 +1,459 @@
|
|
|
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
|
+
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { basename, dirname, join } from 'node:path';
|
|
23
|
+
import { readdirSync, readFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
24
|
+
import type { ToolchainOptions, CompileResult, UploadResult } from '@typecad/cuttlefish/api/shared';
|
|
25
|
+
import { parseCompileErrors } from '@typecad/cuttlefish/api/shared';
|
|
26
|
+
import { scaffoldZephyrProject, writeIfChanged } from './scaffold.js';
|
|
27
|
+
import { westSpawn, buildEnv } from './west-spawn.js';
|
|
28
|
+
import { discoverWest } from './west-discover.js';
|
|
29
|
+
import { writeDebugConfig, resolveDebugLocations } from './debug-config.js';
|
|
30
|
+
import { ZephyrStrategy } from '../strategy.js';
|
|
31
|
+
import { generateOverlay, type DisplayWiring, type TouchWiring } from '../dt-config/overlay.js';
|
|
32
|
+
import { chipForTarget } from '../chips/index.js';
|
|
33
|
+
import { DEFAULT_ZEPHYR_DISPLAY_PROFILE } from '../display/profiles.js';
|
|
34
|
+
|
|
35
|
+
/** Default board target — the framework's MVP canonical board. */
|
|
36
|
+
const DEFAULT_BOARD = 'xiao_ble';
|
|
37
|
+
|
|
38
|
+
function targetFromOptions(o: ToolchainOptions): string {
|
|
39
|
+
// The cuttlefish CLI populates ToolchainOptions.buildTarget from
|
|
40
|
+
// config.frameworkData.buildTarget. Accept frameworkData.target as an alias.
|
|
41
|
+
const fcTarget = (o.frameworkConfig?.target as string | undefined);
|
|
42
|
+
return (o.buildTarget as string | undefined) ?? fcTarget ?? DEFAULT_BOARD;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Derive the Zephyr project root from the cuttlefish-emitted source path.
|
|
47
|
+
*
|
|
48
|
+
* Cuttlefish emits `src/main.cpp` under the output dir. The CLI passes
|
|
49
|
+
* `sourcePath` = full path to `main.cpp` and `outputDir` = its parent (`src/`).
|
|
50
|
+
* For Zephyr, the project root is the parent of `src/` — one level above
|
|
51
|
+
* `outputDir`. Detect that shape and adjust; otherwise fall back to `outputDir`.
|
|
52
|
+
*/
|
|
53
|
+
export function projectRootFromOptions(o: ToolchainOptions): string {
|
|
54
|
+
const outDir = o.outputDir;
|
|
55
|
+
if (basename(outDir) === 'src') {
|
|
56
|
+
return dirname(outDir);
|
|
57
|
+
}
|
|
58
|
+
return outDir;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* west build timeout. Zephyr's first build fetches the toolchain modules and
|
|
63
|
+
* configures CMake/Ninja, which can take several minutes; allow generous headroom.
|
|
64
|
+
*/
|
|
65
|
+
const BUILD_TIMEOUT_MS = 600_000;
|
|
66
|
+
const FLASH_TIMEOUT_MS = 120_000;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build the `west flash` argument list for a board.
|
|
70
|
+
*
|
|
71
|
+
* Runner selection: each board's board.cmake declares a sensible default flash
|
|
72
|
+
* runner for its hardware (xiao_ble → nrfutil, esp32* → esptool), and `west
|
|
73
|
+
* flash` resolves it automatically. The framework only intervenes where the
|
|
74
|
+
* board default needs an argument it can't infer:
|
|
75
|
+
* - An explicit `zephyr.runner` (from cuttlefish.config.ts) always wins.
|
|
76
|
+
* - ESP32 boards forward the port via `--esp-device` (esptool reads the
|
|
77
|
+
* device from it); board.cmake still picks the runner.
|
|
78
|
+
* - Every other board trusts the board.cmake default. Previously this forced
|
|
79
|
+
* `--runner nrfjprog` for every non-ESP32 target, which broke boards whose
|
|
80
|
+
* default is not nrfjprog (xiao_ble defaults to nrfutil) and required
|
|
81
|
+
* Nordic J-Link tools that a USB-bootloader board does not have.
|
|
82
|
+
*
|
|
83
|
+
* Exported (pure) so the runner-selection contract is unit-testable without
|
|
84
|
+
* spawning west.
|
|
85
|
+
*/
|
|
86
|
+
export function buildFlashArgs(
|
|
87
|
+
buildDir: string,
|
|
88
|
+
board: string,
|
|
89
|
+
userRunner: string | undefined,
|
|
90
|
+
port: string | undefined,
|
|
91
|
+
): string[] {
|
|
92
|
+
const args = ['flash', '-d', buildDir];
|
|
93
|
+
if (userRunner) {
|
|
94
|
+
args.push('--runner', userRunner);
|
|
95
|
+
}
|
|
96
|
+
if (port && board.startsWith('esp32')) {
|
|
97
|
+
args.push('--esp-device', port);
|
|
98
|
+
}
|
|
99
|
+
return args;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Classify a `west flash` result as success/failure.
|
|
104
|
+
*
|
|
105
|
+
* west's exit status is authoritative except for one known race in the uf2
|
|
106
|
+
* runner on Windows: the UF2 bootloader reboots to run new firmware the instant
|
|
107
|
+
* the file copy completes, unmounting the USB-MSC drive before `shutil.copy`'s
|
|
108
|
+
* trailing `copymode`/chmod runs. That raises `OSError: [WinError 433] A
|
|
109
|
+
* device which does not exist was specified` and makes west exit non-zero —
|
|
110
|
+
* even though the firmware copied and flashed correctly (the LED blinks).
|
|
111
|
+
*
|
|
112
|
+
* The copy starting is logged ("Copying UF2 file to '<drive>'"); WinError 433
|
|
113
|
+
* during `copymode` after that point proves the data write finished and the
|
|
114
|
+
* drive only vanished on the metadata step. Treat that exact signature as
|
|
115
|
+
* success so the upload isn't reported as a failure. Genuine uf2 failures
|
|
116
|
+
* (no partition found, write errors before the copy) still surface as failures.
|
|
117
|
+
*
|
|
118
|
+
* Exported (pure) so the classification is unit-testable without spawning west.
|
|
119
|
+
*/
|
|
120
|
+
export function classifyUploadResult(
|
|
121
|
+
runner: string | undefined,
|
|
122
|
+
status: number | null,
|
|
123
|
+
output: string,
|
|
124
|
+
): boolean {
|
|
125
|
+
if (status === 0) return true;
|
|
126
|
+
if (isUf2DriveVanishRace(output)) return runner === 'uf2';
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Whether `output` carries the benign UF2 copymode/WinError-433 race signature
|
|
132
|
+
* (see classifyUploadResult). Centralized so classify + cleanse share one match.
|
|
133
|
+
*/
|
|
134
|
+
function isUf2DriveVanishRace(output: string): boolean {
|
|
135
|
+
return /Copying UF2 file to/.test(output)
|
|
136
|
+
&& /WinError 433/.test(output)
|
|
137
|
+
&& /copymode/.test(output);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Cleanse the `west flash` output shown to the user.
|
|
142
|
+
*
|
|
143
|
+
* When classifyUploadResult has decided a non-zero west exit was the benign UF2
|
|
144
|
+
* race (firmware copied, drive unmounted on the trailing chmod), the raw output
|
|
145
|
+
* is a wall of Python traceback that reads like a hard failure. Drop everything
|
|
146
|
+
* after the "Copying UF2 file to" line — i.e. the entire traceback — so a
|
|
147
|
+
* successful flash reads as a success (the framework's ✓ Done follows). Non-race
|
|
148
|
+
* output is returned untouched; genuine errors stay fully visible for diagnosis.
|
|
149
|
+
*
|
|
150
|
+
* Exported (pure) so the cleansing is unit-testable without spawning west.
|
|
151
|
+
*/
|
|
152
|
+
export function cleanseUploadOutput(
|
|
153
|
+
runner: string | undefined,
|
|
154
|
+
status: number | null,
|
|
155
|
+
output: string,
|
|
156
|
+
): string {
|
|
157
|
+
if (status === 0) return output;
|
|
158
|
+
if (runner === 'uf2' && isUf2DriveVanishRace(output)) {
|
|
159
|
+
// Keep everything west printed up to and including "Copying UF2 file to",
|
|
160
|
+
// then stop — everything after that is the drive-vanish traceback.
|
|
161
|
+
const upto = output.match(/[\s\S]*Copying UF2 file to[^\n]*/);
|
|
162
|
+
const head = upto ? upto[0] : '-- west flash: using runner uf2';
|
|
163
|
+
return head;
|
|
164
|
+
}
|
|
165
|
+
return output;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* FrameworkToolchain for Zephyr. Spec §3.5 (mirror of the ESP-IDF toolchain).
|
|
171
|
+
* The target board is carried via frameworkData.buildTarget; scaffolding
|
|
172
|
+
* happens at compile time when the target is known.
|
|
173
|
+
*/
|
|
174
|
+
export const Toolchain = {
|
|
175
|
+
prepare(outputDir: string, entryPoint: string): void {
|
|
176
|
+
// Write the DT overlay for the default board (the real target is known at
|
|
177
|
+
// compile time; prepare runs before compile, so use the default board id).
|
|
178
|
+
// The overlay is additive and idempotent; compile re-runs prepare-equivalent
|
|
179
|
+
// logic in scaffold via the usage scan. Mirrors how Arduino's library
|
|
180
|
+
// resolution is a pre-build artifact step.
|
|
181
|
+
const projectRoot = basename(outputDir) === 'src' ? dirname(outputDir) : outputDir;
|
|
182
|
+
const board = DEFAULT_BOARD;
|
|
183
|
+
const chip = chipForTarget(board);
|
|
184
|
+
// Scan the emitted source for usage tokens (same authoritative signal the
|
|
185
|
+
// scaffold uses). entryPoint is the path to main.cpp; its dir is src/.
|
|
186
|
+
const srcDir = dirname(entryPoint);
|
|
187
|
+
let src = '';
|
|
188
|
+
try {
|
|
189
|
+
for (const name of readdirSync(srcDir)) {
|
|
190
|
+
if (name.endsWith('.cpp') || name.endsWith('.c')) {
|
|
191
|
+
src += readFileSync(join(srcDir, name), 'utf8');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} catch { /* src may not exist yet on first prepare */ }
|
|
195
|
+
const uses = (t: string): boolean => src.includes(t);
|
|
196
|
+
// Display usage tokens: the minimal GFX runtime (display_write/_fill_rect)
|
|
197
|
+
// and the UI display adapter (display_init / __tc_display_dev /
|
|
198
|
+
// DEVICE_DT_GET on the display nodelabel). Both paths need the DT overlay
|
|
199
|
+
// to enable the display node.
|
|
200
|
+
const usesDisplay = uses('display_write') || uses('display_init')
|
|
201
|
+
|| uses('display_fill_rect') || uses('__tc_display_dev')
|
|
202
|
+
|| uses('CuttlefishDisplayTarget');
|
|
203
|
+
// Both registered Zephyr display profiles use dtLabel 'display0', so the
|
|
204
|
+
// default profile's overlay block (&display0 { status="okay" }) is correct
|
|
205
|
+
// for either driver. Thread a non-default profile here only if a future
|
|
206
|
+
// board carries a display node under a different nodelabel.
|
|
207
|
+
const displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
|
|
208
|
+
const overlay = generateOverlay(chip, {
|
|
209
|
+
usesI2c: uses('i2c_'),
|
|
210
|
+
usesSpi: uses('spi_'),
|
|
211
|
+
usesUart: uses('uart_'),
|
|
212
|
+
usesDisplay,
|
|
213
|
+
usesTouch: uses('ft6336u') || uses('touch_'),
|
|
214
|
+
}, displayProfile);
|
|
215
|
+
const overlayDir = join(projectRoot, 'boards');
|
|
216
|
+
mkdirSync(overlayDir, { recursive: true });
|
|
217
|
+
writeIfChanged(join(overlayDir, `${board}.overlay`), overlay);
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
compile(o: ToolchainOptions): CompileResult {
|
|
221
|
+
const projectRoot = projectRootFromOptions(o);
|
|
222
|
+
const board = targetFromOptions(o);
|
|
223
|
+
const debugMode = new ZephyrStrategy().debugMode(board);
|
|
224
|
+
const isGdbDebug = o.debug === true && debugMode === 'gdb';
|
|
225
|
+
const zc = o.zephyrConfig as Record<string, unknown> | undefined;
|
|
226
|
+
const userKconfig = zc?.kconfig as Record<string, string> | undefined;
|
|
227
|
+
const configChanged = scaffoldZephyrProject(projectRoot, isGdbDebug, userKconfig, o.psram);
|
|
228
|
+
|
|
229
|
+
// Regenerate the DT overlay for the ACTUAL target board. prepare() writes
|
|
230
|
+
// it for the default board (the real target is unknown until compile), so
|
|
231
|
+
// the <default>.overlay it wrote does not match `west build -b <board>`.
|
|
232
|
+
// Zephyr auto-detects boards/<board>.overlay under APPLICATION_CONFIG_DIR.
|
|
233
|
+
try {
|
|
234
|
+
const chip = chipForTarget(board);
|
|
235
|
+
const srcDir = join(projectRoot, 'src');
|
|
236
|
+
let src = '';
|
|
237
|
+
try {
|
|
238
|
+
for (const name of readdirSync(srcDir)) {
|
|
239
|
+
if (name.endsWith('.cpp') || name.endsWith('.c')) {
|
|
240
|
+
src += readFileSync(join(srcDir, name), 'utf-8');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch { /* src may not exist */ }
|
|
244
|
+
const uses = (t: string): boolean => src.includes(t);
|
|
245
|
+
const usesDisplay = uses('display_write') || uses('display_init')
|
|
246
|
+
|| uses('display_fill_rect') || uses('__tc_display_dev')
|
|
247
|
+
|| uses('CuttlefishDisplayTarget');
|
|
248
|
+
// Derive the display dimensions from the emitted adapter code
|
|
249
|
+
// (display_width/height return the profile's w/h). This ensures the DT
|
|
250
|
+
// overlay's width/height match the panel the adapter targets, not the
|
|
251
|
+
// default profile — critical for drivers like ST7796S that initialize
|
|
252
|
+
// the panel geometry from the DT node.
|
|
253
|
+
let displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
|
|
254
|
+
if (usesDisplay) {
|
|
255
|
+
const wMatch = src.match(/display_width\(\)\s*\{\s*return\s+(\d+)\s*;\s*\}/);
|
|
256
|
+
const hMatch = src.match(/display_height\(\)\s*\{\s*return\s+(\d+)\s*;\s*\}/);
|
|
257
|
+
if (wMatch && hMatch) {
|
|
258
|
+
displayProfile = {
|
|
259
|
+
...DEFAULT_ZEPHYR_DISPLAY_PROFILE,
|
|
260
|
+
width: parseInt(wMatch[1], 10),
|
|
261
|
+
height: parseInt(hMatch[1], 10),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Extract display pin wiring (cs/dc/rst/spiFrequency/spiPins) from the
|
|
266
|
+
// config display section so the DT overlay wires the MIPI DBI bridge to
|
|
267
|
+
// the correct GPIOs + SPI bus pins.
|
|
268
|
+
const dispCfg = o.display as Record<string, unknown> | undefined;
|
|
269
|
+
const spiPins = (dispCfg?.spiPins ?? undefined) as
|
|
270
|
+
{ sck?: unknown; mosi?: unknown; miso?: unknown } | undefined;
|
|
271
|
+
const wiring: DisplayWiring | undefined = dispCfg
|
|
272
|
+
? {
|
|
273
|
+
cs: typeof dispCfg.cs === 'number' ? dispCfg.cs : undefined,
|
|
274
|
+
dc: typeof dispCfg.dc === 'number' ? dispCfg.dc : undefined,
|
|
275
|
+
rst: typeof dispCfg.rst === 'number' ? dispCfg.rst : undefined,
|
|
276
|
+
spiFrequency: typeof dispCfg.spiFrequency === 'number' ? dispCfg.spiFrequency : undefined,
|
|
277
|
+
sck: typeof spiPins?.sck === 'number' ? spiPins.sck : undefined,
|
|
278
|
+
mosi: typeof spiPins?.mosi === 'number' ? spiPins.mosi : undefined,
|
|
279
|
+
miso: typeof spiPins?.miso === 'number' ? spiPins.miso : undefined,
|
|
280
|
+
backlightPin: typeof dispCfg.backlightPin === 'number' ? dispCfg.backlightPin : undefined,
|
|
281
|
+
}
|
|
282
|
+
: undefined;
|
|
283
|
+
// Extract touch pin wiring (irq/resetPin/sda/scl) from the config
|
|
284
|
+
// display.touch section so the DT overlay wires the I2C bus + touch node.
|
|
285
|
+
const touchCfg = dispCfg?.touch as Record<string, unknown> | undefined;
|
|
286
|
+
const touchWiring: TouchWiring | undefined = touchCfg
|
|
287
|
+
? {
|
|
288
|
+
irq: typeof touchCfg.irq === 'number' ? touchCfg.irq : undefined,
|
|
289
|
+
resetPin: typeof touchCfg.resetPin === 'number' ? touchCfg.resetPin : undefined,
|
|
290
|
+
sda: typeof touchCfg.sda === 'number' ? touchCfg.sda : undefined,
|
|
291
|
+
scl: typeof touchCfg.scl === 'number' ? touchCfg.scl : undefined,
|
|
292
|
+
}
|
|
293
|
+
: undefined;
|
|
294
|
+
const overlay = generateOverlay(chip, {
|
|
295
|
+
usesI2c: uses('i2c_'),
|
|
296
|
+
usesSpi: uses('spi_'),
|
|
297
|
+
usesUart: uses('uart_'),
|
|
298
|
+
usesDisplay,
|
|
299
|
+
usesTouch: uses('ft6336u') || uses('touch_'),
|
|
300
|
+
psram: o.psram,
|
|
301
|
+
}, displayProfile, wiring, touchWiring);
|
|
302
|
+
const overlayDir = join(projectRoot, 'boards');
|
|
303
|
+
mkdirSync(overlayDir, { recursive: true });
|
|
304
|
+
// Write the board-specific overlay (the one west loads). Zephyr looks for
|
|
305
|
+
// boards/<board_id>.overlay under APPLICATION_CONFIG_DIR — use the bare
|
|
306
|
+
// board id (before any hardware-qualifier suffix, e.g. 'esp32_devkitc'
|
|
307
|
+
// not the full 'esp32_devkitc/esp32/procpu' target string).
|
|
308
|
+
const boardId = board.split('/')[0];
|
|
309
|
+
writeIfChanged(join(overlayDir, `${boardId}.overlay`), overlay);
|
|
310
|
+
} catch { /* best-effort overlay regen; the build surfaces DT errors */ }
|
|
311
|
+
|
|
312
|
+
// Use a stable build dir so incremental builds reuse the Ninja graph.
|
|
313
|
+
// west defaults to <projectRoot>/build.
|
|
314
|
+
const buildDir = join(projectRoot, 'build');
|
|
315
|
+
|
|
316
|
+
// Nuke the build dir whenever a previous build exists. Zephyr's gen_offset
|
|
317
|
+
// flow (offsets.h is generated FROM offsets.c.obj, while gen_offset.h makes
|
|
318
|
+
// offsets.c include offsets.h) leaves a permanent `offsets.h ->
|
|
319
|
+
// offsets.c.obj -> offsets.h` cycle in the .ninja_deps log after the first
|
|
320
|
+
// incremental pass — ninja then fails every later build with `dependency
|
|
321
|
+
// cycle` even when nothing changed. This is a known Zephyr-on-Windows
|
|
322
|
+
// issue; the reliable fix is a pristine build dir per build. Also nukes
|
|
323
|
+
// when prj.conf/CMakeLists/overlay changed, so Kconfig symbols and
|
|
324
|
+
// generated headers never diverge from a cached graph.
|
|
325
|
+
if (configChanged || existsSync(join(buildDir, 'zephyr', 'zephyr.bin'))) {
|
|
326
|
+
try { rmSync(buildDir, { recursive: true, force: true }); } catch { /* may not exist */ }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const buildArgs = ['build', '-b', board, '-d', buildDir, projectRoot];
|
|
330
|
+
// Explicitly pass the generated DT overlay. Zephyr's auto-detection of
|
|
331
|
+
// boards/<board>.overlay fails for hardware-qualified targets (e.g.
|
|
332
|
+
// esp32_devkitc/esp32/procpu) because the FILE_SUFFIX matching doesn't
|
|
333
|
+
// resolve — passing -DDTC_OVERLAY_FILE forces it unconditionally.
|
|
334
|
+
const boardId = board.split('/')[0];
|
|
335
|
+
const overlayPath = join(projectRoot, 'boards', `${boardId}.overlay`);
|
|
336
|
+
try {
|
|
337
|
+
if (readFileSync(overlayPath, 'utf-8').length > 0) {
|
|
338
|
+
// CMake parses backslashes as escapes — use forward slashes so the
|
|
339
|
+
// Windows path survives the -D argument intact.
|
|
340
|
+
buildArgs.push('--', `-DDTC_OVERLAY_FILE=${overlayPath.replace(/\\/g, '/')}`);
|
|
341
|
+
}
|
|
342
|
+
} catch { /* no overlay — let Zephyr auto-detect or build without one */ }
|
|
343
|
+
// Append user cmake args from cuttlefish.config.ts zephyr.cmakeArgs.
|
|
344
|
+
const userCmakeArgs = zc?.cmakeArgs as string[] | undefined;
|
|
345
|
+
if (userCmakeArgs && userCmakeArgs.length > 0) {
|
|
346
|
+
if (!buildArgs.includes('--')) buildArgs.push('--');
|
|
347
|
+
buildArgs.push(...userCmakeArgs);
|
|
348
|
+
}
|
|
349
|
+
const inv = westSpawn(
|
|
350
|
+
buildArgs,
|
|
351
|
+
{ cwd: projectRoot, encoding: 'utf-8', timeout: BUILD_TIMEOUT_MS },
|
|
352
|
+
);
|
|
353
|
+
const result = spawnSync(inv.command, inv.args, inv.options);
|
|
354
|
+
|
|
355
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
|
|
356
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
|
|
357
|
+
const output = stdout + stderr;
|
|
358
|
+
// Prefix the build log with how west was resolved, for transparency.
|
|
359
|
+
const header = `Using west via ${inv.install.source}` +
|
|
360
|
+
(inv.install.zephyrBase ? ` (ZEPHYR_BASE=${inv.install.zephyrBase})` : '') + '\n';
|
|
361
|
+
|
|
362
|
+
// After a successful build in gdb mode (--debug on a probe-capable target),
|
|
363
|
+
// write the VS Code launch.json + tasks.json + gdb-script artifacts so F5
|
|
364
|
+
// attaches GDB to the chip's debug probe. Non-fatal on failure — a missing
|
|
365
|
+
// artifact doesn't block the build. Mirrors the deleted framework-esp32
|
|
366
|
+
// toolchain compile() debug-config wiring.
|
|
367
|
+
if (result.status === 0 && isGdbDebug) {
|
|
368
|
+
try {
|
|
369
|
+
const { workspaceRoot, sketchRel } = resolveDebugLocations(projectRoot);
|
|
370
|
+
writeDebugConfig({
|
|
371
|
+
projectRoot,
|
|
372
|
+
workspaceRoot,
|
|
373
|
+
sketchRel,
|
|
374
|
+
target: board,
|
|
375
|
+
buildDir,
|
|
376
|
+
sourceMapPath: join(dirname(o.sourcePath), `${basename(o.sourcePath)}.thcppmap.json`),
|
|
377
|
+
});
|
|
378
|
+
} catch (e) {
|
|
379
|
+
console.warn(`[cuttlefish] gdb debug config generation failed: ${(e as Error).message}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
success: result.status === 0,
|
|
385
|
+
output: header + output,
|
|
386
|
+
errors: parseCompileErrors(output, o.sourcePath),
|
|
387
|
+
};
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
upload(o: ToolchainOptions): UploadResult {
|
|
391
|
+
const projectRoot = projectRootFromOptions(o);
|
|
392
|
+
const buildDir = join(projectRoot, 'build');
|
|
393
|
+
const board = targetFromOptions(o);
|
|
394
|
+
const zc = o.zephyrConfig as Record<string, unknown> | undefined;
|
|
395
|
+
const runner = zc?.runner as string | undefined;
|
|
396
|
+
const args = buildFlashArgs(buildDir, board, runner, o.port);
|
|
397
|
+
|
|
398
|
+
const inv = westSpawn(args, {
|
|
399
|
+
cwd: projectRoot,
|
|
400
|
+
encoding: 'utf-8',
|
|
401
|
+
timeout: FLASH_TIMEOUT_MS,
|
|
402
|
+
});
|
|
403
|
+
const result = spawnSync(inv.command, inv.args, inv.options);
|
|
404
|
+
|
|
405
|
+
const fstdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
|
|
406
|
+
const fstderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
|
|
407
|
+
const raw = fstdout + fstderr;
|
|
408
|
+
return {
|
|
409
|
+
success: classifyUploadResult(runner, result.status, raw),
|
|
410
|
+
output: cleanseUploadOutput(runner, result.status, raw),
|
|
411
|
+
};
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
monitor(o: ToolchainOptions): void {
|
|
415
|
+
// Serial monitor over USB-CDC. Zephyr does NOT ship a `west serial`
|
|
416
|
+
// subcommand (it's not a real west command — invoking it errors with
|
|
417
|
+
// "unknown command"). The discovered west install's venv carries pyserial,
|
|
418
|
+
// so run its bundled miniterm directly: `python -m serial.tools.miniterm`.
|
|
419
|
+
// That is the same cross-platform terminal pyserial provides in ESP-IDF's
|
|
420
|
+
// idf.py monitor, and it inherits stdio so Ctrl+C exits cleanly.
|
|
421
|
+
if (!o.port) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
'A serial port is required to monitor. Pass --port <COMx/ttyX>.',
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
// ESP32 USB-CDC console runs at 115200 (the Zephyr ESP32 board default).
|
|
427
|
+
// The CLI's generic default of 9600 is wrong for this target; honor an
|
|
428
|
+
// explicit --baud / config.console.baudRate when given, else 115200.
|
|
429
|
+
const baud = o.baud ?? 115200;
|
|
430
|
+
const install = discoverWest();
|
|
431
|
+
const py = install?.pythonExecutable ?? process.env.PYTHON ?? 'python';
|
|
432
|
+
// Reuse west-spawn's env builder (prepends the venv bin dir to PATH so the
|
|
433
|
+
// python we spawn resolves pyserial from the same venv). Falls back to the
|
|
434
|
+
// process env when no install is discovered.
|
|
435
|
+
const env = install ? buildEnv(install) : process.env;
|
|
436
|
+
spawnSync(py, ['-m', 'serial.tools.miniterm', o.port, String(baud)], {
|
|
437
|
+
cwd: projectRootFromOptions(o),
|
|
438
|
+
env,
|
|
439
|
+
stdio: 'inherit',
|
|
440
|
+
});
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
debug(o: ToolchainOptions): void {
|
|
444
|
+
// Launch an interactive GDB session for the last build. `west debug`
|
|
445
|
+
// auto-resolves the runner (openocd for esp32s3, nrfjprog/jlink for nRF)
|
|
446
|
+
// and the GDB binary from the build dir's CMakeCache/board.cmake — no
|
|
447
|
+
// hand-authored gdbinit needed. Inherits stdio so GDB runs interactively.
|
|
448
|
+
// (Not invoked by the standard build/compile flow; powers an explicit
|
|
449
|
+
// debug-attach entry point for terminal-driven debugging without VS Code.)
|
|
450
|
+
const projectRoot = projectRootFromOptions(o);
|
|
451
|
+
const buildDir = join(projectRoot, 'build');
|
|
452
|
+
const inv = westSpawn(['debug', '-d', buildDir], {
|
|
453
|
+
cwd: projectRoot,
|
|
454
|
+
encoding: 'utf-8',
|
|
455
|
+
stdio: 'inherit',
|
|
456
|
+
});
|
|
457
|
+
spawnSync(inv.command, inv.args, inv.options);
|
|
458
|
+
},
|
|
459
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Zephyr project scaffolding
|
|
3
|
+
//
|
|
4
|
+
// Writes the CMakeLists.txt + prj.conf around the cuttlefish-emitted src/main.cpp
|
|
5
|
+
// so `west build` has a valid Zephyr application. Idempotent: overwrites the
|
|
6
|
+
// generated files only when their content changes (avoids invalidating the
|
|
7
|
+
// Ninja incremental build's mtime-based dependency tracking).
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
import { writeFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { resolveKconfigFragments, type KconfigUsage } from '../dt-config/kconfig.js';
|
|
13
|
+
|
|
14
|
+
/** Write a file only if the content differs from the existing file.
|
|
15
|
+
* Returns true when the file was written (content changed or file was new). */
|
|
16
|
+
export function writeIfChanged(filePath: string, content: string): boolean {
|
|
17
|
+
if (existsSync(filePath)) {
|
|
18
|
+
try {
|
|
19
|
+
if (readFileSync(filePath, 'utf8') === content) return false;
|
|
20
|
+
} catch {
|
|
21
|
+
// Read failed — fall through to write.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
writeFileSync(filePath, content);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Concatenate all emitted source under src/ so the scaffold can detect which
|
|
30
|
+
* peripherals the program actually uses. The cuttlefish lowering emits
|
|
31
|
+
* well-known driver API tokens (adc_read, spi_transceive, bt_*, …), so scanning
|
|
32
|
+
* the post-transpile source is an authoritative usage signal — and it keeps the
|
|
33
|
+
* scaffold self-contained (no need to thread analysis through the toolchain
|
|
34
|
+
* contract). Returns '' when no sources exist yet (first prepare call).
|
|
35
|
+
*/
|
|
36
|
+
function readEmittedSources(srcDir: string): string {
|
|
37
|
+
if (!existsSync(srcDir)) return '';
|
|
38
|
+
let out = '';
|
|
39
|
+
for (const name of readdirSync(srcDir)) {
|
|
40
|
+
if (name.endsWith('.cpp') || name.endsWith('.c')) {
|
|
41
|
+
try {
|
|
42
|
+
out += readFileSync(join(srcDir, name), 'utf8');
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore unreadable files
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Emit the Zephyr application skeleton around the generated src/main.cpp.
|
|
53
|
+
*
|
|
54
|
+
* Layout written:
|
|
55
|
+
* <projectRoot>/
|
|
56
|
+
* CMakeLists.txt (find_package(Zephyr), target_sources app ← src/*.cpp)
|
|
57
|
+
* prj.conf (CONFIG_* for GPIO + C++ + libc)
|
|
58
|
+
* src/main.cpp (owned by cuttlefish's emit pipeline — NOT touched here)
|
|
59
|
+
*
|
|
60
|
+
* Idempotent. Mirrors scaffoldEspIdfProject's writeIfChanged discipline.
|
|
61
|
+
*/
|
|
62
|
+
export function scaffoldZephyrProject(projectRoot: string, debug = false, userKconfig?: Record<string, string>, psram?: 'opi' | 'quad'): boolean {
|
|
63
|
+
const srcDir = join(projectRoot, 'src');
|
|
64
|
+
if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
// Detect which peripherals the program actually uses by scanning the emitted
|
|
67
|
+
// source. The cuttlefish lowering emits well-known driver API tokens, so this
|
|
68
|
+
// is authoritative. Usage-gating the Kconfig symbols keeps a GPIO-only
|
|
69
|
+
// program from pulling in (and linking) stacks it doesn't need — notably
|
|
70
|
+
// NimBLE (CONFIG_BT), whose Espressif prebuilt blobs are not always present
|
|
71
|
+
// in a workspace. The symbol set itself lives in resolveKconfigFragments
|
|
72
|
+
// (dt-config/kconfig.ts), unit-tested separately.
|
|
73
|
+
const src = readEmittedSources(srcDir);
|
|
74
|
+
// Boundary-anchored token scan: a bare `src.includes('power_')` would match
|
|
75
|
+
// `tx_power_dbm` (emitted by the WiFi shim) and flip CONFIG_PM on for a
|
|
76
|
+
// WiFi-only program — on the ESP32-S3 that spins the PM soft-off retry loop
|
|
77
|
+
// forever and starves the app. Anchor each `<prefix>_` token at a leading
|
|
78
|
+
// word boundary so it matches the intended driver/HAL symbol, not a suffix.
|
|
79
|
+
const uses = (token: string): boolean => {
|
|
80
|
+
if (token.endsWith('_')) {
|
|
81
|
+
return new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`).test(src);
|
|
82
|
+
}
|
|
83
|
+
return src.includes(token);
|
|
84
|
+
};
|
|
85
|
+
const usage: KconfigUsage = {
|
|
86
|
+
usesAdc: uses('adc_'),
|
|
87
|
+
usesPwm: uses('pwm_'),
|
|
88
|
+
usesI2c: uses('i2c_'),
|
|
89
|
+
usesSpi: uses('spi_'),
|
|
90
|
+
usesUart: uses('uart_'),
|
|
91
|
+
usesWdt: uses('wdt_'),
|
|
92
|
+
usesBle: uses('bt_') || uses('bt_gatt') || uses('bt_le_'),
|
|
93
|
+
usesDisplay: uses('display_write') || uses('display_init') || uses('display_fill_rect') || uses('__tc_display_dev') || uses('CuttlefishDisplayTarget'),
|
|
94
|
+
usesTouch: uses('ft6336u') || uses('touch_'),
|
|
95
|
+
// Power tokens: k_sleep + pm_state_force / PM_STATE_* (what power.ts emits).
|
|
96
|
+
// The old `power_` token matched nothing the power HAL emits and collides
|
|
97
|
+
// with tx_power_dbm — removed.
|
|
98
|
+
usesPower: uses('pm_') || uses('k_sleep') || uses('PM_STATE_'),
|
|
99
|
+
usesWifi: uses('wifi_') || uses('net_mgmt') || uses('conn_mgr'),
|
|
100
|
+
// HTTP: the __tc_http_* shim + http_client_req + getaddrinfo. The '_'-anchored
|
|
101
|
+
// 'http_' token matches __tc_http_* and http_client_req (the core HTTP lib
|
|
102
|
+
// symbol), mirroring how wifi_ detects the wifi shim. An http-only program
|
|
103
|
+
// still pulls the networking stack even without usesWifi.
|
|
104
|
+
usesHttp: uses('http_') || uses('__tc_http') || uses('http_client_req'),
|
|
105
|
+
// MQTT: the __tc_mqtt_* shim + mqtt_connect/mqtt_publish/mqtt_subscribe.
|
|
106
|
+
usesMqtt: uses('mqtt_') || uses('__tc_mqtt') || uses('mqtt_connect'),
|
|
107
|
+
// Preferences: the __tc_prefs_* shim + the settings_* API the shim calls.
|
|
108
|
+
// settings_load/save_one/delete + SETTINGS_STATIC_HANDLER_DEFINE all emit
|
|
109
|
+
// `settings_` symbols; __tc_prefs catches the typed accessors (put_int etc.
|
|
110
|
+
// template into __tc_prefs_put<...>, which keeps the __tc_prefs token).
|
|
111
|
+
usesPreferences: uses('settings_') || uses('__tc_prefs'),
|
|
112
|
+
// Random: the __tc_rand_* shim + the sys_rand_get entropy tap it seeds from.
|
|
113
|
+
usesRandom: uses('__tc_rand') || uses('sys_rand_get'),
|
|
114
|
+
psram,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
let changed = false;
|
|
118
|
+
|
|
119
|
+
// ── Root CMakeLists.txt ─────────────────────────────────────────────────
|
|
120
|
+
// The canonical Zephyr CMake application. GLOB src/*.cpp so future multi-
|
|
121
|
+
// file emits are picked up automatically; cuttlefish owns the file contents.
|
|
122
|
+
const cmakeLists = [
|
|
123
|
+
'# Auto-generated by @typecad/framework-zephyr from cuttlefish.config.ts.',
|
|
124
|
+
'cmake_minimum_required(VERSION 3.20.0)',
|
|
125
|
+
'',
|
|
126
|
+
'find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})',
|
|
127
|
+
'',
|
|
128
|
+
'project(zephyr_app)',
|
|
129
|
+
'',
|
|
130
|
+
'# Collect cuttlefish-emitted sources.',
|
|
131
|
+
'file(GLOB app_sources src/*.cpp src/*.c)',
|
|
132
|
+
'',
|
|
133
|
+
'target_sources(app PRIVATE ${app_sources})',
|
|
134
|
+
// When PSRAM is configured, define BOARD_HAS_PSRAM so the UI runtime's
|
|
135
|
+
// PSRAM canvas allocator (ui_create_canvas_best) is compiled in.
|
|
136
|
+
...(psram ? ['', '# PSRAM enabled: activate the runtime PSRAM canvas paths.', 'target_compile_definitions(app PRIVATE BOARD_HAS_PSRAM)', ''] : ['']),
|
|
137
|
+
].join('\n');
|
|
138
|
+
if (writeIfChanged(join(projectRoot, 'CMakeLists.txt'), cmakeLists)) changed = true;
|
|
139
|
+
|
|
140
|
+
// ── prj.conf ────────────────────────────────────────────────────────────
|
|
141
|
+
// Driver Kconfig symbols are usage-gated on the emitted source: only a
|
|
142
|
+
// peripheral whose driver API the program actually references is enabled.
|
|
143
|
+
// Zephyr's Kconfig treats an enabled-but-unused driver as harmless, BUT
|
|
144
|
+
// several driver stacks pull in large/specific dependencies — notably
|
|
145
|
+
// NimBLE (CONFIG_BT) needs Espressif prebuilt blobs that are not always
|
|
146
|
+
// fetched (`west blobs fetch hal_espressif`). Usage-gating keeps a GPIO-only
|
|
147
|
+
// blink from requiring those, and shrinks the link for every program. The
|
|
148
|
+
// core GPIO driver and C++ support stay unconditional. Symbol selection is
|
|
149
|
+
// delegated to resolveKconfigFragments (unit-tested in dt-config/kconfig).
|
|
150
|
+
const symbols = resolveKconfigFragments(usage, debug);
|
|
151
|
+
|
|
152
|
+
const prjConf: string[] = [
|
|
153
|
+
'# Auto-generated by @typecad/framework-zephyr from cuttlefish.config.ts.',
|
|
154
|
+
'# Edit in cuttlefish.config.ts (frameworkData), not here.',
|
|
155
|
+
'# Driver symbols are usage-gated on the emitted source — only peripherals',
|
|
156
|
+
'# the program references are enabled.',
|
|
157
|
+
'',
|
|
158
|
+
'# Route printf/stdout to the console UART (needed by the @typecad/expect',
|
|
159
|
+
'# test runner protocol, which uses printf via __tc_print/__tc_println).',
|
|
160
|
+
'CONFIG_STDOUT_CONSOLE=y',
|
|
161
|
+
'CONFIG_PRINTK=y',
|
|
162
|
+
'',
|
|
163
|
+
];
|
|
164
|
+
// Emit a section header before the BT block when present.
|
|
165
|
+
let btHeaderEmitted = false;
|
|
166
|
+
let wifiHeaderEmitted = false;
|
|
167
|
+
for (const [sym, val] of symbols) {
|
|
168
|
+
if (sym === 'CONFIG_BT' && !btHeaderEmitted) {
|
|
169
|
+
prjConf.push('', '# Bluetooth (NimBLE peripheral).');
|
|
170
|
+
btHeaderEmitted = true;
|
|
171
|
+
}
|
|
172
|
+
if (sym === 'CONFIG_WIFI' && !wifiHeaderEmitted) {
|
|
173
|
+
prjConf.push('', '# WiFi / networking (conn_mgr + esp32 wifi driver).');
|
|
174
|
+
wifiHeaderEmitted = true;
|
|
175
|
+
}
|
|
176
|
+
// Skip auto-detected symbols that the user explicitly overrides
|
|
177
|
+
// in cuttlefish.config.ts zephyr.kconfig — the user value is
|
|
178
|
+
// emitted in the User Kconfig section below and takes precedence.
|
|
179
|
+
if (userKconfig && userKconfig.hasOwnProperty(sym)) continue;
|
|
180
|
+
prjConf.push(`${sym}=${val}`);
|
|
181
|
+
}
|
|
182
|
+
// Emit user-specified Kconfig from cuttlefish.config.ts zephyr.kconfig.
|
|
183
|
+
// These override any matching auto-detected symbol (skipped above).
|
|
184
|
+
if (userKconfig) {
|
|
185
|
+
prjConf.push('', '# User Kconfig (cuttlefish.config.ts → zephyr.kconfig).');
|
|
186
|
+
for (const [sym, val] of Object.entries(userKconfig)) {
|
|
187
|
+
prjConf.push(`${sym}=${val}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// CONFIG_BT_DEVICE_NAME is a string value not produced by the resolver — add
|
|
191
|
+
// it after the BT block when BLE is used (parity with the previous inline form).
|
|
192
|
+
if (usage.usesBle) prjConf.push('CONFIG_BT_DEVICE_NAME="TypeCAD"');
|
|
193
|
+
prjConf.push('');
|
|
194
|
+
if (writeIfChanged(join(projectRoot, 'prj.conf'), prjConf.join('\n'))) changed = true;
|
|
195
|
+
return changed;
|
|
196
|
+
}
|