@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.
- 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/doctor.d.ts +6 -0
- package/dist/doctor.js +48 -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 +101 -0
- package/dist/framework.manifest.js +536 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +17 -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/compat.d.ts +38 -0
- package/dist/toolchain/compat.js +133 -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 +444 -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/doctor.ts +56 -0
- package/src/dt-config/kconfig.ts +308 -0
- package/src/dt-config/overlay.ts +293 -0
- package/src/framework.manifest.ts +558 -0
- package/src/index.ts +29 -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/compat.ts +145 -0
- package/src/toolchain/debug-config.ts +399 -0
- package/src/toolchain/index.ts +482 -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,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;
|