@typecad/framework-zephyr 1.0.0-alpha.16 → 1.0.0-alpha.18
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 +202 -21
- package/README.md +49 -87
- package/dist/as-built.d.ts +2 -2
- package/dist/as-built.js +2 -2
- package/dist/boardgen.d.ts +1 -9
- package/dist/boardgen.js +228 -45
- package/dist/chips/types.d.ts +1 -1
- package/dist/debug-codegen.js +1 -1
- package/dist/display/touch-adapter.js +1 -1
- package/dist/display/ui-adapter.js +549 -549
- package/dist/doctor.js +4 -4
- package/dist/dt-config/custom-board.js +2 -2
- package/dist/dt-config/kconfig.js +18 -12
- package/dist/dt-config/overlay.d.ts +2 -2
- package/dist/dt-config/overlay.js +2 -2
- package/dist/framework.manifest.d.ts +3 -3
- package/dist/framework.manifest.js +10 -7
- package/dist/index.js +5 -5
- package/dist/licenses.d.ts +2 -2
- package/dist/licenses.js +8 -8
- package/dist/lowering/fs.js +1 -1
- package/dist/lowering/gpio.js +0 -32
- package/dist/lowering/http.js +264 -32
- package/dist/lowering/i2c.js +0 -8
- package/dist/lowering/interrupts.js +6 -0
- package/dist/lowering/mqtt.js +110 -8
- package/dist/lowering/usb.js +11 -11
- package/dist/lowering/wdt.js +2 -29
- package/dist/sdk/board-catalog-sync.d.ts +1 -3
- package/dist/sdk/board-catalog-sync.js +4 -10
- package/dist/strategy.d.ts +22 -44
- package/dist/strategy.js +152 -155
- package/dist/tmp-probe.d.ts +2 -0
- package/dist/tmp-probe.js +9 -0
- package/dist/toolchain/debug-config.d.ts +50 -90
- package/dist/toolchain/debug-config.js +239 -510
- package/dist/toolchain/env-check.d.ts +1 -3
- package/dist/toolchain/env-check.js +2 -7
- package/dist/toolchain/index.d.ts +16 -2
- package/dist/toolchain/index.js +383 -35
- package/dist/toolchain/runners.d.ts +16 -0
- package/dist/toolchain/runners.js +75 -0
- package/dist/toolchain/scaffold.d.ts +1 -1
- package/dist/toolchain/scaffold.js +12 -12
- package/dist/toolchain/west-discover.d.ts +6 -0
- package/dist/toolchain/west-discover.js +36 -13
- package/dist/toolchain/west-spawn.js +8 -2
- package/installer/README.md +328 -328
- package/installer/install.sh +2 -2
- package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
- package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
- package/installer/templates/project/.vscode/settings.json +1 -1
- package/installer/templates/project/README.md +2 -2
- package/package.json +5 -5
- package/src/as-built.ts +206 -206
- package/src/boardgen.ts +214 -48
- package/src/chips/types.ts +567 -567
- package/src/display/touch-adapter.ts +204 -204
- package/src/display/ui-adapter.ts +781 -781
- package/src/doctor.ts +4 -4
- package/src/dt-config/custom-board.ts +2 -2
- package/src/dt-config/kconfig.ts +18 -12
- package/src/dt-config/overlay.ts +1058 -1058
- package/src/framework.manifest.ts +538 -535
- package/src/index.ts +5 -5
- package/src/licenses.ts +425 -425
- package/src/lowering/fs.ts +135 -135
- package/src/lowering/gpio.ts +0 -33
- package/src/lowering/http.ts +264 -32
- package/src/lowering/i2c.ts +0 -9
- package/src/lowering/interrupts.ts +6 -0
- package/src/lowering/mqtt.ts +109 -8
- package/src/lowering/usb.ts +221 -221
- package/src/lowering/wdt.ts +2 -25
- package/src/sdk/board-catalog-sync.ts +4 -25
- package/src/strategy.ts +2265 -2309
- package/src/toolchain/debug-config.ts +262 -522
- package/src/toolchain/env-check.ts +279 -285
- package/src/toolchain/index.ts +1703 -1359
- package/src/toolchain/runners.ts +80 -0
- package/src/toolchain/scaffold.ts +296 -296
- package/src/toolchain/west-discover.ts +35 -13
- package/src/toolchain/west-spawn.ts +174 -168
- package/dist/async/timer-polyfill.d.ts +0 -10
- package/dist/async/timer-polyfill.js +0 -95
- package/dist/chips/board-overrides.d.ts +0 -7
- package/dist/chips/board-overrides.js +0 -11
- package/dist/chips/esp32.d.ts +0 -2
- package/dist/chips/esp32.js +0 -71
- package/dist/chips/esp32s3.d.ts +0 -2
- package/dist/chips/esp32s3.js +0 -103
- package/dist/chips/soc/.d.ts +0 -2
- package/dist/chips/soc/.js +0 -129
- package/dist/chips/soc/esp32.d.ts +0 -2
- package/dist/chips/soc/esp32.js +0 -120
- package/dist/chips/soc/esp32c3.d.ts +0 -2
- package/dist/chips/soc/esp32c3.js +0 -90
- package/dist/chips/soc/esp32c6.d.ts +0 -2
- package/dist/chips/soc/esp32c6.js +0 -109
- package/dist/chips/soc/esp32s3.d.ts +0 -2
- package/dist/chips/soc/esp32s3.js +0 -189
- package/dist/chips/soc/index.d.ts +0 -2
- package/dist/chips/soc/index.js +0 -23
- package/dist/chips/soc/nrf52840.d.ts +0 -2
- package/dist/chips/soc/nrf52840.js +0 -130
- package/dist/chips/soc/rp2040.d.ts +0 -2
- package/dist/chips/soc/rp2040.js +0 -141
- package/dist/chips/soc/rp2350a.d.ts +0 -2
- package/dist/chips/soc/rp2350a.js +0 -145
- package/dist/chips/soc/samd21g18a.d.ts +0 -2
- package/dist/chips/soc/samd21g18a.js +0 -143
- package/dist/chips/soc/stm32f411xe.d.ts +0 -2
- package/dist/chips/soc/stm32f411xe.js +0 -251
- package/dist/chips/xiao-ble.d.ts +0 -2
- package/dist/chips/xiao-ble.js +0 -100
- package/dist/lowering/power.d.ts +0 -9
- package/dist/lowering/power.js +0 -60
- package/dist/lowering/pulse.d.ts +0 -7
- package/dist/lowering/pulse.js +0 -51
- package/dist/lowering/tone.d.ts +0 -10
- package/dist/lowering/tone.js +0 -63
- package/dist/lowering/worker-backing.d.ts +0 -14
- package/dist/lowering/worker-backing.js +0 -79
- package/dist/lowering/worker.d.ts +0 -6
- package/dist/lowering/worker.js +0 -14
- package/dist/sdk/board-data.generated.d.ts +0 -2
- package/dist/sdk/board-data.generated.js +0 -4
- package/dist/sdk/catalog-walker.d.ts +0 -90
- package/dist/sdk/catalog-walker.js +0 -682
- package/dist/sdk/dts-reader.d.ts +0 -83
- package/dist/sdk/dts-reader.js +0 -596
- package/src/debug-codegen.ts +0 -207
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// debug-config.ts — VS Code
|
|
2
|
+
// debug-config.ts — west-driven VS Code debug artifacts (cortex-debug external
|
|
3
|
+
// server mode).
|
|
3
4
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
5
|
+
// The board-specific debug knowledge lives in Zephyr, not here: after any
|
|
6
|
+
// successful build, `west debugserver` serves the GDB connection using the
|
|
7
|
+
// board's own runner configuration (resolved in build/zephyr/runners.yaml —
|
|
8
|
+
// the exact arch gdb, the openocd/jlink binaries and flags, the Zephyr RTOS
|
|
9
|
+
// awareness, adapter quirks). The launch.json written here just connects
|
|
10
|
+
// cortex-debug to that server (`servertype: 'external'` +
|
|
11
|
+
// `gdbTarget: localhost:<port>`).
|
|
6
12
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// breakpoint is deferred until the bootloader maps the app flash region.
|
|
13
|
+
// Lifecycle: F5 runs the preLaunchTask (build + flash, unchanged), the
|
|
14
|
+
// background task `typecad-hal: debug server (west)` starts
|
|
15
|
+
// `typecad-hal debug-server start` (which wraps `west debugserver` and prints
|
|
16
|
+
// a ready marker the task's problem matcher waits for), and VS Code runs the
|
|
17
|
+
// postDebugTask (`typecad-hal: stop debug server`) when the session ends.
|
|
13
18
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
19
|
+
// Everything here is best-effort: a failed artifact write warns and never
|
|
20
|
+
// fails the build (a project without launch.json still builds fine).
|
|
16
21
|
// ---------------------------------------------------------------------------
|
|
17
22
|
|
|
18
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync
|
|
23
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
19
24
|
import { join, resolve, dirname, relative } from 'node:path';
|
|
20
25
|
import { ZephyrStrategy } from '../strategy.js';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
import { readRunnersFacts } from './runners.js';
|
|
27
|
+
|
|
28
|
+
/** The gdb server port west's openocd/jlink runners default to. */
|
|
29
|
+
export const DEBUG_SERVER_PORT = 3333;
|
|
30
|
+
/** west's openocd tcl port — pinned in the debugserver invocation and used
|
|
31
|
+
* as the READINESS probe: its listener opens at the END of openocd's init
|
|
32
|
+
* (after the gdb listener and the startup halt), and probe connections
|
|
33
|
+
* there never consume the gdb server's single client slot. */
|
|
34
|
+
export const DEBUG_SERVER_TCL_PORT = 6333;
|
|
25
35
|
|
|
26
36
|
export interface DebugConfigOptions {
|
|
27
37
|
/** Absolute path to the Zephyr project root (contains CMakeLists.txt + src/). */
|
|
@@ -36,202 +46,31 @@ export interface DebugConfigOptions {
|
|
|
36
46
|
buildDir: string;
|
|
37
47
|
/** Absolute path to the emitted source map (*.thcppmap.json), if any. */
|
|
38
48
|
sourceMapPath?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Create-time best-effort gdb (from the SDK + the board's silicon) — used
|
|
51
|
+
* only when runners.yaml doesn't exist yet (pre-first-build). The first
|
|
52
|
+
* build's west-resolved gdb replaces it, and a mismatch triggers the
|
|
53
|
+
* artifact rewrite (debugArtifactsNeedRewrite).
|
|
54
|
+
*/
|
|
55
|
+
starterGdbPath?: string;
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
* ZEPHYR_SDK_INSTALL_DIR in CMakeCache.txt at configure time, and the
|
|
46
|
-
* xtensa GDB lives at <sdk>/xtensa-espressif_esp32s3_zephyr-elf/bin/... (note
|
|
47
|
-
* the Zephyr-SDK naming, distinct from the ESP-IDF xtensa-esp32s3-elf-gdb).
|
|
48
|
-
*
|
|
49
|
-
* Returns the absolute gdb path on success, or undefined (the launch.json then
|
|
50
|
-
* omits gdbPath and relies on Cortex-Debug's default resolution).
|
|
51
|
-
*/
|
|
52
|
-
export function resolveGdbPath(buildDir: string, target: string): string | undefined {
|
|
53
|
-
const cachePath = join(buildDir, 'CMakeCache.txt');
|
|
54
|
-
if (existsSync(cachePath)) {
|
|
55
|
-
try {
|
|
56
|
-
const cache = readFileSync(cachePath, 'utf-8');
|
|
57
|
-
const m = cache.match(/^ZEPHYR_SDK_INSTALL_DIR:PATH=(.+)$/m);
|
|
58
|
-
if (m) {
|
|
59
|
-
const fromCache = gdbPathFromSdkRoot(m[1].trim(), target);
|
|
60
|
-
if (fromCache) return fromCache;
|
|
61
|
-
}
|
|
62
|
-
} catch {
|
|
63
|
-
// unreadable cache — fall through to the SDK scan
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
// No build dir yet (project just created): probe known SDK locations.
|
|
67
|
-
for (const sdkRoot of discoverZephyrSdkRoots()) {
|
|
68
|
-
const p = gdbPathFromSdkRoot(sdkRoot, target);
|
|
69
|
-
if (p) return p;
|
|
70
|
-
}
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** The GDB location inside a Zephyr SDK root, per target architecture:
|
|
75
|
-
* ARM boards use the arm-zephyr-eabi toolchain (verified against
|
|
76
|
-
* zephyr-sdk-0.17.4), Espressif the xtensa-espressif_esp32s3_zephyr-elf one.
|
|
77
|
-
* Returns a forward-slash absolute path or undefined. */
|
|
78
|
-
export function gdbPathFromSdkRoot(sdkRoot: string, target?: string): string | undefined {
|
|
79
|
-
// No target given (legacy callers/tests): the historical Espresif default.
|
|
80
|
-
const boardId = (target ?? '').split('/')[0];
|
|
81
|
-
const isArm = boardId.length > 0 && !boardId.startsWith('esp32');
|
|
82
|
-
const toolchainDir = isArm ? 'arm-zephyr-eabi' : 'xtensa-espressif_esp32s3_zephyr-elf';
|
|
83
|
-
const gdbName = isArm ? 'arm-zephyr-eabi-gdb.exe' : 'xtensa-espressif_esp32s3_zephyr-elf-gdb.exe';
|
|
84
|
-
const gdbPath = join(sdkRoot, toolchainDir, 'bin', gdbName);
|
|
85
|
-
return existsSync(gdbPath) ? gdbPath.replace(/\\/g, '/') : undefined;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Compare two dotted version strings numerically (0.17.10 > 0.17.4). */
|
|
89
|
-
function compareSdkVersions(a: string, b: string): number {
|
|
90
|
-
const segsOf = (v: string): number[] => v.split('.').map((s) => parseInt(s, 10) || 0);
|
|
91
|
-
const aa = segsOf(a);
|
|
92
|
-
const bb = segsOf(b);
|
|
93
|
-
for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
|
|
94
|
-
const d = (aa[i] ?? 0) - (bb[i] ?? 0);
|
|
95
|
-
if (d !== 0) return d;
|
|
96
|
-
}
|
|
97
|
-
return 0;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Probe the well-known Zephyr SDK install locations, newest version first:
|
|
102
|
-
* 1. $ZEPHYR_SDK_INSTALL_DIR (the var board.cmake reads)
|
|
103
|
-
* 2. <MAMBA_ROOT_PREFIX | ~/micromamba>/zephyr-sdk/zephyr-sdk-<ver> — the
|
|
104
|
-
* the bundled Zephyr installer layout
|
|
105
|
-
* 3. ~/zephyr-sdk-<ver> — the standalone download layout
|
|
106
|
-
*
|
|
107
|
-
* Only roots that actually contain the esp32s3 GDB are useful to callers;
|
|
108
|
-
* this returns candidate roots (gdbPathFromSdkRoot does the existence check)
|
|
109
|
-
* so tests can inject home/env overrides.
|
|
110
|
-
*/
|
|
111
|
-
export function discoverZephyrSdkRoots(opts?: {
|
|
112
|
-
home?: string;
|
|
113
|
-
env?: Record<string, string | undefined>;
|
|
114
|
-
}): string[] {
|
|
115
|
-
const env = opts?.env ?? process.env;
|
|
116
|
-
const home = opts?.home ?? (env.USERPROFILE || env.HOME || '');
|
|
117
|
-
const scanned: string[] = [];
|
|
118
|
-
|
|
119
|
-
const versionedDirs = (base: string): string[] => {
|
|
120
|
-
try {
|
|
121
|
-
return readdirSync(base)
|
|
122
|
-
.filter((d) => existsSync(join(base, d)) && d.startsWith('zephyr-sdk-'))
|
|
123
|
-
.map((d) => join(base, d));
|
|
124
|
-
} catch {
|
|
125
|
-
return []; // dir absent
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
const mambaRoot = env.MAMBA_ROOT_PREFIX || (home ? join(home, 'micromamba') : '');
|
|
129
|
-
if (mambaRoot) scanned.push(...versionedDirs(join(mambaRoot, 'zephyr-sdk')));
|
|
130
|
-
if (home) scanned.push(...versionedDirs(home));
|
|
131
|
-
|
|
132
|
-
// Scanned roots newest version first; the env var stays pinned first
|
|
133
|
-
// (explicit user intent outranks any discovered location).
|
|
134
|
-
scanned.sort((a, b) => {
|
|
135
|
-
const va = a.match(/zephyr-sdk-([\d.]+)/)?.[1] ?? '';
|
|
136
|
-
const vb = b.match(/zephyr-sdk-([\d.]+)/)?.[1] ?? '';
|
|
137
|
-
return compareSdkVersions(vb, va);
|
|
138
|
-
});
|
|
139
|
-
const roots = env.ZEPHYR_SDK_INSTALL_DIR
|
|
140
|
-
? [env.ZEPHYR_SDK_INSTALL_DIR, ...scanned]
|
|
141
|
-
: scanned;
|
|
142
|
-
// De-duplicate (an env var may repeat a scan hit) preserving order.
|
|
143
|
-
return roots.filter((r, i) => roots.indexOf(r) === i);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Resolve the OpenOCD binary path. The esp32s3 needs the Espressif OpenOCD
|
|
148
|
-
* fork (openocd-esp32) — not the Zephyr SDK's openocd and not a
|
|
149
|
-
* generic/GDB-stub build — because only it carries the Xtensa + esp_usb_jtag
|
|
150
|
-
* support. ARM targets prefer the Zephyr SDK's own openocd (it ships the
|
|
151
|
-
* interface/target cfgs boards reference); the Espressif fork is only
|
|
152
|
-
* consulted for esp32 targets.
|
|
153
|
-
*
|
|
154
|
-
* Discovery order:
|
|
155
|
-
* 1. ESPRESSIF_TOOLCHAIN_PATH env (the var board.cmake reads) — esp32
|
|
156
|
-
* targets; its openocd-esp32/bin/openocd.exe.
|
|
157
|
-
* 2. The standard ESP-IDF install layout: ~/.espressif/tools/openocd-esp32/
|
|
158
|
-
* <version>/openocd-esp32/bin/openocd.exe (esp32 targets; newest first).
|
|
159
|
-
* 3. A Zephyr SDK root (discoverZephyrSdkRoots) — its
|
|
160
|
-
* tools/opt/openocd/bin/openocd(.exe), any target.
|
|
161
|
-
* Returns undefined if not found (the launch.json then omits openOCDPath and
|
|
162
|
-
* Cortex-Debug falls back to PATH / its openocdPath setting).
|
|
163
|
-
*/
|
|
164
|
-
export function resolveOpenOcdPath(target?: string): string | undefined {
|
|
165
|
-
const isEsp32Target = (target ?? '').split('/')[0].startsWith('esp32');
|
|
166
|
-
const candidates: string[] = [];
|
|
167
|
-
if (isEsp32Target) {
|
|
168
|
-
// 1. ESPRESSIF_TOOLCHAIN_PATH env
|
|
169
|
-
const envPath = process.env.ESPRESSIF_TOOLCHAIN_PATH;
|
|
170
|
-
if (envPath) {
|
|
171
|
-
candidates.push(join(envPath, 'openocd-esp32', 'bin', 'openocd.exe'));
|
|
172
|
-
}
|
|
173
|
-
// 2. ~/.espressif/tools/openocd-esp32/<version>/openocd-esp32/bin/openocd.exe
|
|
174
|
-
const home = process.env.USERPROFILE || process.env.HOME;
|
|
175
|
-
if (home) {
|
|
176
|
-
const base = join(home, '.espressif', 'tools', 'openocd-esp32');
|
|
177
|
-
let versions: string[] = [];
|
|
178
|
-
try {
|
|
179
|
-
versions = readdirSync(base).filter((v) =>
|
|
180
|
-
existsSync(join(base, v, 'openocd-esp32', 'bin', 'openocd.exe')),
|
|
181
|
-
);
|
|
182
|
-
} catch {
|
|
183
|
-
// dir absent
|
|
184
|
-
}
|
|
185
|
-
// newest version last — sort then reverse so the highest wins on match.
|
|
186
|
-
versions.sort().reverse();
|
|
187
|
-
for (const v of versions) {
|
|
188
|
-
candidates.push(join(base, v, 'openocd-esp32', 'bin', 'openocd.exe'));
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
// 3. The Zephyr SDK's openocd (ARM targets' interface/target cfgs ship
|
|
193
|
-
// with it — interface/stlink-dap.cfg, target/stm32f4x.cfg, ...). Layout
|
|
194
|
-
// differs by SDK generation: 1.0.x hosttools/openocd, 0.17.x
|
|
195
|
-
// tools/opt/openocd (when present).
|
|
196
|
-
const exe = process.platform === 'win32' ? 'openocd.exe' : 'openocd';
|
|
197
|
-
for (const sdkRoot of discoverZephyrSdkRoots()) {
|
|
198
|
-
candidates.push(join(sdkRoot, 'hosttools', 'openocd', 'bin', exe));
|
|
199
|
-
candidates.push(join(sdkRoot, 'tools', 'opt', 'openocd', 'bin', exe));
|
|
200
|
-
}
|
|
201
|
-
for (const c of candidates) {
|
|
202
|
-
if (existsSync(c)) return resolve(c).replace(/\\/g, '/');
|
|
203
|
-
}
|
|
204
|
-
return undefined;
|
|
205
|
-
}
|
|
58
|
+
/** Task labels (also the preLaunchTask/postDebugTask references in launch.json). */
|
|
59
|
+
export const DEBUG_BUILD_TASK = 'typecad-hal: build + flash (debug)';
|
|
60
|
+
export const DEBUG_SERVER_TASK = 'typecad-hal: debug server (west)';
|
|
61
|
+
export const DEBUG_SERVER_STOP_TASK = 'typecad-hal: stop debug server';
|
|
206
62
|
|
|
207
|
-
/**
|
|
208
|
-
* Resolve where to write the VS Code debug artifacts and how to express paths
|
|
209
|
-
* in them.
|
|
210
|
-
*
|
|
211
|
-
* VS Code reads `.vscode/` from the folder the user has OPENED — and for a
|
|
212
|
-
* cuttlefish project that is almost always the **cuttlefish project root**
|
|
213
|
-
* (the directory containing `cuttlefish.config.ts`), NOT the git repo root.
|
|
214
|
-
* The toolchain's `projectRoot` is the Zephyr *app* dir (e.g.
|
|
215
|
-
* `<projectRoot>/src/out`), which sits below the cuttlefish config dir. So we
|
|
216
|
-
* walk up from `projectRoot` to the nearest `cuttlefish.config.ts` and treat
|
|
217
|
-
* THAT as the workspace root. This makes F5 work when a user opens the project
|
|
218
|
-
* folder directly, and keeps launch.json paths relative to it.
|
|
219
|
-
*
|
|
220
|
-
* Returns { workspaceRoot, appRel } where workspaceRoot is the cuttlefish
|
|
221
|
-
* project root (the `.vscode/` target) and appRel is the Zephyr app dir
|
|
222
|
-
* (`projectRoot`) relative to it (e.g. 'src/out').
|
|
223
|
-
*/
|
|
224
63
|
export function resolveDebugLocations(projectRoot: string): {
|
|
225
64
|
workspaceRoot: string;
|
|
226
65
|
appRel: string;
|
|
227
66
|
} {
|
|
228
|
-
// Walk up from the Zephyr app dir to find the
|
|
229
|
-
// nearest ancestor containing
|
|
67
|
+
// Walk up from the Zephyr app dir to find the typecad-hal project root (the
|
|
68
|
+
// nearest ancestor containing typecad-hal.config.ts). Fall back to projectRoot
|
|
230
69
|
// itself if none is found (single-dir project where the app sits at root).
|
|
231
70
|
let workspaceRoot = resolve(projectRoot);
|
|
232
71
|
let dir = resolve(projectRoot);
|
|
233
72
|
for (let i = 0; i < 20; i++) {
|
|
234
|
-
if (existsSync(join(dir, '
|
|
73
|
+
if (existsSync(join(dir, 'typecad-hal.config.ts'))) {
|
|
235
74
|
workspaceRoot = dir;
|
|
236
75
|
break;
|
|
237
76
|
}
|
|
@@ -248,204 +87,137 @@ export function resolveDebugLocations(projectRoot: string): {
|
|
|
248
87
|
function mergeJsonArrayEntry<T extends Record<string, unknown>>(
|
|
249
88
|
filePath: string,
|
|
250
89
|
arrayKey: string,
|
|
251
|
-
|
|
90
|
+
nameKey: string,
|
|
252
91
|
entry: T,
|
|
253
92
|
): void {
|
|
254
93
|
let doc: Record<string, unknown> = {};
|
|
255
94
|
if (existsSync(filePath)) {
|
|
256
95
|
try {
|
|
257
|
-
|
|
96
|
+
const parsed: unknown = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
97
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
98
|
+
doc = parsed as Record<string, unknown>;
|
|
99
|
+
}
|
|
258
100
|
} catch {
|
|
259
101
|
// malformed — start fresh
|
|
260
102
|
}
|
|
261
103
|
}
|
|
262
|
-
const arr = Array.isArray(doc[arrayKey])
|
|
263
|
-
|
|
104
|
+
const arr = Array.isArray(doc[arrayKey])
|
|
105
|
+
? (doc[arrayKey] as Record<string, unknown>[])
|
|
106
|
+
: [];
|
|
107
|
+
const name = entry[nameKey] as string;
|
|
108
|
+
const idx = arr.findIndex((e) => e && e[nameKey] === name);
|
|
264
109
|
if (idx >= 0) arr[idx] = entry;
|
|
265
110
|
else arr.push(entry);
|
|
266
111
|
doc[arrayKey] = arr;
|
|
267
112
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
268
|
-
writeFileSync(filePath, JSON.stringify(doc, null, 2)
|
|
113
|
+
writeFileSync(filePath, `${JSON.stringify(doc, null, 2)}\n`, 'utf-8');
|
|
269
114
|
}
|
|
270
115
|
|
|
271
116
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* Ported verbatim from the deleted framework-esp32/toolchain/gdb-script.ts —
|
|
278
|
-
* the filter is cuttlefish-internal (lambda hoisting is framework-agnostic).
|
|
117
|
+
* Whether the workspace's gdb debug artifacts need rewriting: missing, still
|
|
118
|
+
* in an older (self-managed-server) shape, written before a build resolved
|
|
119
|
+
* the gdb path (the create-time starter omits gdbPath), or referencing an app
|
|
120
|
+
* root other than the current one (an outDir rename). Only OUR entries count
|
|
121
|
+
* (`TypeCAD Debug (Zephyr, …)`); user-authored ones are never judged.
|
|
279
122
|
*/
|
|
280
|
-
export function
|
|
281
|
-
|
|
282
|
-
|
|
123
|
+
export function debugArtifactsNeedRewrite(
|
|
124
|
+
workspaceRoot: string,
|
|
125
|
+
appRel: string,
|
|
126
|
+
currentGdb?: string,
|
|
127
|
+
): boolean {
|
|
128
|
+
let doc: unknown;
|
|
283
129
|
try {
|
|
284
|
-
|
|
130
|
+
doc = JSON.parse(readFileSync(join(workspaceRoot, '.vscode', 'launch.json'), 'utf-8'));
|
|
285
131
|
} catch {
|
|
286
|
-
return
|
|
132
|
+
return false; // no launch.json (or unreadable) — plain builds leave it absent
|
|
287
133
|
}
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
'
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
' return (CuttlefishFrame(f) for f in frame_iter)',
|
|
309
|
-
'',
|
|
310
|
-
'class CuttlefishFrame:',
|
|
311
|
-
' def __init__(self, frame):',
|
|
312
|
-
' self.frame = frame',
|
|
313
|
-
' def __getattr__(self, name):',
|
|
314
|
-
' val = getattr(self.frame, name)',
|
|
315
|
-
' if name == "function":',
|
|
316
|
-
' m = isr_re.match(val)',
|
|
317
|
-
' if m: return "<lambda>"',
|
|
318
|
-
' return val',
|
|
319
|
-
'',
|
|
320
|
-
'gdb.frame_filters[CuttlefishLambdaFilter().name] = CuttlefishLambdaFilter()',
|
|
321
|
-
'',
|
|
322
|
-
].join('\n');
|
|
134
|
+
const configs = (doc as { configurations?: unknown }).configurations;
|
|
135
|
+
if (!Array.isArray(configs)) return false;
|
|
136
|
+
const prefix = `\${workspaceFolder}/${appRel ? `${appRel}/` : ''}`;
|
|
137
|
+
for (const c of configs) {
|
|
138
|
+
const e = c as { name?: unknown; executable?: unknown; servertype?: unknown; gdbPath?: unknown };
|
|
139
|
+
if (typeof e.name !== 'string' || !e.name.startsWith('TypeCAD Debug (Zephyr,')) continue;
|
|
140
|
+
if (e.servertype !== 'external') return true; // pre-west (self-managed server) shape
|
|
141
|
+
if (e.gdbPath === undefined) return true; // create-time starter — upgrade on first build
|
|
142
|
+
if (currentGdb !== undefined && typeof e.gdbPath === 'string'
|
|
143
|
+
&& e.gdbPath.replace(/\\/g, '/') !== currentGdb) {
|
|
144
|
+
return true; // a wrong create-time silicon guess — west's answer wins
|
|
145
|
+
}
|
|
146
|
+
if (typeof e.executable === 'string'
|
|
147
|
+
&& e.executable.startsWith('${workspaceFolder}/')
|
|
148
|
+
&& !e.executable.startsWith(prefix)) {
|
|
149
|
+
return true; // outDir moved
|
|
150
|
+
}
|
|
151
|
+
return false; // our entry exists in the current shape — nothing to do
|
|
152
|
+
}
|
|
153
|
+
return false; // no our-entry (deleted launch entry): only --debug recreates
|
|
323
154
|
}
|
|
324
155
|
|
|
325
156
|
/**
|
|
326
|
-
* Build the cortex-debug launch.json config
|
|
327
|
-
* `
|
|
328
|
-
*
|
|
157
|
+
* Build the cortex-debug launch.json config connecting to the west-owned gdb
|
|
158
|
+
* server. `gdbPath` comes from the build's runners.yaml (west's own resolved
|
|
159
|
+
* arch gdb); undefined before the first build (create-time starter).
|
|
329
160
|
*/
|
|
330
161
|
function buildLaunchConfig(
|
|
331
162
|
o: DebugConfigOptions,
|
|
332
163
|
gdbScriptRel: string | undefined,
|
|
333
|
-
|
|
334
|
-
method?: ZephyrProbeMethod,
|
|
164
|
+
gdbPath: string | undefined,
|
|
335
165
|
): Record<string, unknown> {
|
|
336
|
-
// The ELF is at <projectRoot>/build/zephyr/zephyr.elf (Zephyr's standard
|
|
337
|
-
// build output). cortex-debug uses `executable` (not `program`).
|
|
338
166
|
const executable = `\${workspaceFolder}/${o.appRel}/build/zephyr/zephyr.elf`;
|
|
339
167
|
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
// set mem inaccessible-by-default off — suppresses "Cannot access memory"
|
|
354
|
-
// errors caused by overlapping Xtensa memory regions (flash-mapped
|
|
355
|
-
// 0x4200xxxx isn't accessible until the bootloader runs).
|
|
356
|
-
// mem 0x42000000 0x44000000 ro cache — tells GDB the app flash region IS
|
|
357
|
-
// read-only so -break-insert uses hw breakpoints, not sw breakpoints
|
|
358
|
-
// (which would fail with "Cannot access memory at 0x4200xxxx").
|
|
359
|
-
// monitor reset init — reset target + halt (bootloader maps flash)
|
|
360
|
-
// thb main — temporary HW breakpoint at main()
|
|
361
|
-
// c — continue; bootloader maps flash, breaks at main()
|
|
362
|
-
//
|
|
363
|
-
// The trailing `c` is ESP32-only. On instant-reset ARM targets the
|
|
364
|
-
// thb-main stop lands within milliseconds — WHILE cortex-debug is still
|
|
365
|
-
// chewing through this command list — and a stop event mid-initialization
|
|
366
|
-
// leaves the session half-started (toolbar never enables, user breakpoints
|
|
367
|
-
// never bind). The ESP32's bootloader takes hundreds of milliseconds, so
|
|
368
|
-
// its stop safely arrives after init completes. ARM keeps the pending
|
|
369
|
-
// thb (the first user Continue stops at main()) but hands the run/stop
|
|
370
|
-
// transition to cortex-debug.
|
|
371
|
-
//
|
|
372
|
-
// Paths in GDB commands must be RELATIVE (resolved against GDB's own
|
|
373
|
-
// working directory), never ${workspaceFolder} literals or absolute paths:
|
|
374
|
-
// - cortex-debug passes command strings through verbatim — no variable
|
|
375
|
-
// expansion in postAttachCommands (verified against its gdb.ts) — so a
|
|
376
|
-
// ${workspaceFolder} would reach GDB as a literal.
|
|
377
|
-
// - Even where VS Code expands it, on Windows the expansion carries
|
|
378
|
-
// backslashes that GDB reads as escape sequences (\t → tab, etc.).
|
|
379
|
-
// - An absolute path bakes the generating machine's checkout location
|
|
380
|
-
// into launch.json and breaks on every other clone/checkout.
|
|
381
|
-
// cortex-debug spawns GDB with cwd from the config's `cwd` field
|
|
382
|
-
// (${workspaceFolder} above), so a relative path IS the workspace root —
|
|
383
|
-
// `set directories .` adds exactly the directory the old absolute form did,
|
|
384
|
-
// and `source <rel>` finds the frame-filter script.
|
|
385
|
-
// The Xtensa flash-mapping commands are ESP32-specific (app flash isn't
|
|
386
|
-
// readable until the bootloader maps it); ARM targets drop them.
|
|
387
|
-
const isEsp32Target = o.target.split('/')[0].startsWith('esp32');
|
|
168
|
+
// The gdb binary names the architecture (xtensa-espressif_* / arm-zephyr-* /
|
|
169
|
+
// riscv64-*): the Xtensa flash-mapping commands are only correct there —
|
|
170
|
+
// app flash is unreadable until the ESP32 bootloader maps it, and the
|
|
171
|
+
// trailing `c` rides the bootloader's slow boot past cortex-debug's init.
|
|
172
|
+
const isXtensa = (gdbPath ?? '').includes('xtensa');
|
|
173
|
+
|
|
174
|
+
// Post-attach commands executed after GDB connects to the west-owned server.
|
|
175
|
+
// set directories . — source lookup root (gdb's cwd is the
|
|
176
|
+
// config's cwd = workspace root)
|
|
177
|
+
// monitor reset init — reset target + halt via the server
|
|
178
|
+
// thb main — temporary HW breakpoint at main()
|
|
179
|
+
// Paths must stay RELATIVE (cortex-debug passes commands verbatim; Windows
|
|
180
|
+
// backslash expansion would corrupt absolute paths).
|
|
388
181
|
const postAttachCommands = [
|
|
389
182
|
'set directories .',
|
|
390
183
|
'set remote hardware-watchpoint-limit 2',
|
|
391
184
|
'set remote hardware-breakpoint-limit 2',
|
|
392
|
-
...(
|
|
185
|
+
...(isXtensa ? [
|
|
393
186
|
'set mem inaccessible-by-default off',
|
|
394
187
|
'mem 0x42000000 0x44000000 ro cache',
|
|
395
188
|
] : []),
|
|
396
189
|
'monitor reset init',
|
|
397
190
|
'thb main',
|
|
398
|
-
...(
|
|
191
|
+
...(isXtensa ? ['c'] : []),
|
|
399
192
|
];
|
|
400
193
|
if (gdbScriptRel) {
|
|
401
194
|
postAttachCommands.splice(1, 0, `source ${gdbScriptRel}`);
|
|
402
195
|
}
|
|
403
196
|
|
|
404
|
-
|
|
405
|
-
// jlink server (needs the device name from the board table); openocd-runner
|
|
406
|
-
// methods keep the openocd server with the generated cfg file. Without a
|
|
407
|
-
// method (ESP32 builtin-JTAG targets), the historical default applies.
|
|
408
|
-
const servertype = method?.runner === 'jlink' ? 'jlink' : 'openocd';
|
|
409
|
-
const isEsp32 = o.target.split('/')[0].startsWith('esp32');
|
|
410
|
-
|
|
411
|
-
const cfg: Record<string, unknown> = {
|
|
197
|
+
return {
|
|
412
198
|
name: `TypeCAD Debug (Zephyr, ${o.target.split('/')[0]})`,
|
|
413
199
|
type: 'cortex-debug',
|
|
414
|
-
// Attach mode: no download (the ELF is already flashed
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
// continue. HW breakpoints use debug registers and work before the
|
|
418
|
-
// bootloader maps the app flash region.
|
|
200
|
+
// Attach mode: no download (the ELF is already flashed by the
|
|
201
|
+
// preLaunchTask's dependency chain). The server is west-owned;
|
|
202
|
+
// cortex-debug only connects.
|
|
419
203
|
request: 'attach',
|
|
420
204
|
cwd: '${workspaceFolder}',
|
|
421
205
|
executable,
|
|
422
|
-
servertype,
|
|
423
|
-
|
|
424
|
-
...(method?.debugDevice ? { device: method.debugDevice } : {}),
|
|
425
|
-
interface: method?.debugInterface ?? (isEsp32 ? 'jtag' : 'swd'),
|
|
206
|
+
servertype: 'external',
|
|
207
|
+
gdbTarget: `localhost:${DEBUG_SERVER_PORT}`,
|
|
426
208
|
...(gdbPath ? { gdbPath } : {}),
|
|
427
|
-
...(servertype === 'openocd' && openocdPath ? { serverpath: openocdPath } : {}),
|
|
428
209
|
postAttachCommands,
|
|
429
|
-
preLaunchTask:
|
|
210
|
+
preLaunchTask: DEBUG_SERVER_TASK,
|
|
211
|
+
postDebugTask: DEBUG_SERVER_STOP_TASK,
|
|
430
212
|
};
|
|
431
|
-
|
|
432
|
-
// openOcdCfgRel is consumed by configFiles above. Referenced here only to
|
|
433
|
-
// keep the signature honest.
|
|
434
|
-
void openOcdCfgRel;
|
|
435
|
-
|
|
436
|
-
return cfg;
|
|
437
213
|
}
|
|
438
214
|
|
|
439
|
-
/**
|
|
440
|
-
* Build the tasks.json entry: rebuild + flash only. cortex-debug starts
|
|
441
|
-
* OpenOCD as a child process (servertype: "openocd"), so the preLaunch task
|
|
442
|
-
* just needs to build and upload — no isBackground / OpenOCD wrapping.
|
|
443
|
-
*/
|
|
215
|
+
/** The build+flash task (preLaunchTask) — unchanged shape from the old flow. */
|
|
444
216
|
function buildTask(o: DebugConfigOptions): Record<string, unknown> {
|
|
445
217
|
return {
|
|
446
|
-
label:
|
|
218
|
+
label: DEBUG_BUILD_TASK,
|
|
447
219
|
type: 'shell',
|
|
448
|
-
command: 'npx
|
|
220
|
+
command: 'npx typecad-hal build --compile --upload --debug',
|
|
449
221
|
options: { cwd: `\${workspaceFolder}/${o.appRel}` },
|
|
450
222
|
group: { kind: 'build', isDefault: false },
|
|
451
223
|
problemMatcher: [],
|
|
@@ -453,224 +225,192 @@ function buildTask(o: DebugConfigOptions): Record<string, unknown> {
|
|
|
453
225
|
}
|
|
454
226
|
|
|
455
227
|
/**
|
|
456
|
-
* The
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
* (the "debugger starts but never stops / buttons don't work" symptom).
|
|
464
|
-
*
|
|
465
|
-
* 4000 (4 MHz) is the empirically-stable speed for this peripheral.
|
|
228
|
+
* The background task that is the F5 preLaunchTask — ONE task, no dependsOn:
|
|
229
|
+
* `debug-server start --flash` runs the whole build pipeline (transpile →
|
|
230
|
+
* compile → upload, with --debug) and then serves. VS Code releases the
|
|
231
|
+
* debug session to connect gdbTarget when the matcher's endsPattern (the
|
|
232
|
+
* ready marker) fires, while the task keeps serving. (A dependsOn chain was
|
|
233
|
+
* tried: VS Code awaits the whole dependency GROUP, and a background task
|
|
234
|
+
* that never exits never releases the session — F5 hung with no debug UI.)
|
|
466
235
|
*/
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
skipping = true;
|
|
493
|
-
depth = 0;
|
|
494
|
-
}
|
|
495
|
-
if (skipping) {
|
|
496
|
-
depth += (line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length;
|
|
497
|
-
if (depth <= 0 && (line.includes('}') || !line.includes('{'))) skipping = false;
|
|
498
|
-
continue;
|
|
499
|
-
}
|
|
500
|
-
out.push(line);
|
|
501
|
-
}
|
|
502
|
-
return out;
|
|
236
|
+
function serverTask(o: DebugConfigOptions): Record<string, unknown> {
|
|
237
|
+
return {
|
|
238
|
+
label: DEBUG_SERVER_TASK,
|
|
239
|
+
type: 'shell',
|
|
240
|
+
command: 'npx typecad-hal debug-server start --flash',
|
|
241
|
+
// NO_COLOR keeps chalk's ANSI codes out of the output — they break the
|
|
242
|
+
// background patterns below (the banner prints as ESC[36m⇳ Transpiling,
|
|
243
|
+
// and ^-anchored patterns never match past the escape byte). Same remedy
|
|
244
|
+
// as the scaffold's watch task.
|
|
245
|
+
options: { cwd: `\${workspaceFolder}/${o.appRel}`, env: { NO_COLOR: '1' } },
|
|
246
|
+
isBackground: true,
|
|
247
|
+
problemMatcher: {
|
|
248
|
+
owner: 'typecad-hal-debug-server',
|
|
249
|
+
// MUST never match a real line: matched lines become file-less
|
|
250
|
+
// problems (default severity: error), and VS Code then blocks F5 with
|
|
251
|
+
// "errors exist after running preLaunchTask". The sentinel literal
|
|
252
|
+
// cannot occur in pipeline or openocd output. (^$ was tried — it
|
|
253
|
+
// matches the blank lines openocd prints.)
|
|
254
|
+
pattern: { regexp: '__cuttlefish_never_matches__' },
|
|
255
|
+
background: {
|
|
256
|
+
beginsPattern: 'Transpiling',
|
|
257
|
+
endsPattern: `^TYPECAD_HAL: debug server ready on ${DEBUG_SERVER_PORT}`,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
};
|
|
503
261
|
}
|
|
504
262
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const rtosLine = isEsp32 ? [] : ['$_TARGETNAME configure -rtos Zephyr'];
|
|
515
|
-
if (method?.debugCfgSource && method.debugCfgSource.length > 0) {
|
|
516
|
-
// Probe-method-driven config: the board package's verified interface +
|
|
517
|
-
// target sources, then the method's quirk lines (e.g. reset_config for an
|
|
518
|
-
// unwired SRST) — the cfg-file form of the west runner args.
|
|
519
|
-
return [
|
|
520
|
-
...header,
|
|
521
|
-
`# Probe method '${method.id}' — from the board package's probeMethods table.`,
|
|
522
|
-
...method.debugCfgSource.map((src) => `source [find ${src}]`),
|
|
523
|
-
...stripGdbEventBlocks(method.debugCfg ?? []),
|
|
524
|
-
...rtosLine,
|
|
525
|
-
'',
|
|
526
|
-
].join('\n');
|
|
527
|
-
}
|
|
528
|
-
// Pack-derived methods (the board's own support/openocd.cfg, extracted by
|
|
529
|
-
// the board data pack) carry complete cfg lines — including their
|
|
530
|
-
// `source [find ...]` directives — verbatim in file order, minus the
|
|
531
|
-
// gdb-attach/detach events (IDE-managed lifecycle; see stripGdbEventBlocks).
|
|
532
|
-
if (method?.debugCfg && method.debugCfg.length > 0) {
|
|
533
|
-
return [
|
|
534
|
-
...header,
|
|
535
|
-
`# Probe method '${method.id}' — the board's own support/openocd.cfg,`,
|
|
536
|
-
`# extracted by the board data pack (gdb-attach/detach events removed —`,
|
|
537
|
-
`# the IDE owns the session lifecycle).`,
|
|
538
|
-
...stripGdbEventBlocks(method.debugCfg),
|
|
539
|
-
...rtosLine,
|
|
540
|
-
'',
|
|
541
|
-
].join('\n');
|
|
542
|
-
}
|
|
543
|
-
return [
|
|
544
|
-
...header,
|
|
545
|
-
'# Sources the board cfg (which loads the esp_usb_jtag adapter driver +',
|
|
546
|
-
'# esp32s3 target + ESP_RTOS Zephyr) then overrides the adapter speed to a',
|
|
547
|
-
'# USB-JTAG-stable value AFTER the driver is loaded. See debug-config.ts',
|
|
548
|
-
'# for the rationale.',
|
|
549
|
-
'source [find board/esp32s3-builtin.cfg]',
|
|
550
|
-
`adapter speed ${OPENOCD_ADAPTER_SPEED}`,
|
|
551
|
-
'',
|
|
552
|
-
].join('\n');
|
|
263
|
+
/** The postDebugTask — stops the west-owned server when the session ends. */
|
|
264
|
+
function serverStopTask(o: DebugConfigOptions): Record<string, unknown> {
|
|
265
|
+
return {
|
|
266
|
+
label: DEBUG_SERVER_STOP_TASK,
|
|
267
|
+
type: 'shell',
|
|
268
|
+
command: 'npx typecad-hal debug-server stop',
|
|
269
|
+
options: { cwd: `\${workspaceFolder}/${o.appRel}` },
|
|
270
|
+
problemMatcher: [],
|
|
271
|
+
};
|
|
553
272
|
}
|
|
554
273
|
|
|
555
274
|
/**
|
|
556
|
-
* Write
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
* Writes (all idempotent):
|
|
560
|
-
* <workspaceRoot>/.vscode/launch.json (cortex-debug config)
|
|
561
|
-
* <workspaceRoot>/.vscode/tasks.json (build + flash preLaunch task)
|
|
562
|
-
* <projectRoot>/.cuttlefish/openocd.cfg (OpenOCD cfg, adapter speed override)
|
|
563
|
-
* <projectRoot>/.cuttlefish/.cuttlefish-gdb.py (lambda frame filter, conditional)
|
|
564
|
-
*/
|
|
565
|
-
/**
|
|
566
|
-
* Resolve the debug-side probe method for a project: the board constants the
|
|
567
|
-
* transpile persists + the workspace config's zephyr.probe selection.
|
|
568
|
-
* Returns the method object (for cfg/servertype data) or undefined — ESP32
|
|
569
|
-
* targets and boards without tables fall back to the framework's default
|
|
570
|
-
* (esp_usb_jtag openocd) behavior.
|
|
275
|
+
* Write the debug artifacts: the cortex-debug launch entry (merged by name)
|
|
276
|
+
* and the three tasks (merged by label). Returns the workspace-relative
|
|
277
|
+
* paths written, for CLI reporting.
|
|
571
278
|
*/
|
|
572
|
-
function
|
|
573
|
-
projectRoot: string,
|
|
574
|
-
workspaceRoot: string,
|
|
575
|
-
): ZephyrProbeMethod | undefined {
|
|
576
|
-
try {
|
|
577
|
-
const bcPath = [
|
|
578
|
-
join(projectRoot, 'src', 'board-constants.json'),
|
|
579
|
-
join(projectRoot, 'board-constants.json'),
|
|
580
|
-
].find((p) => existsSync(p));
|
|
581
|
-
if (!bcPath) return undefined;
|
|
582
|
-
const raw = JSON.parse(readFileSync(bcPath, 'utf8')) as Record<string, string | number | boolean>;
|
|
583
|
-
const chip = resolveChipFromBoard(new Map(Object.entries(raw)));
|
|
584
|
-
if (!chip) return undefined;
|
|
585
|
-
const zc = loadCuttlefishConfig(workspaceRoot)?.zephyrConfig as Record<string, unknown> | undefined;
|
|
586
|
-
const probe = resolveProbeMethod(zc, chip, 'debug');
|
|
587
|
-
if (!probe.ok) return undefined;
|
|
588
|
-
return chip.probeMethods?.find((m) => m.id === (zc?.probe as string | undefined));
|
|
589
|
-
} catch {
|
|
590
|
-
return undefined;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
export function writeDebugConfig(o: DebugConfigOptions): void {
|
|
279
|
+
export function writeDebugConfig(o: DebugConfigOptions): string[] {
|
|
595
280
|
const vscodeDir = join(o.workspaceRoot, '.vscode');
|
|
596
|
-
|
|
597
|
-
mkdirSync(cuttlefishDir, { recursive: true });
|
|
281
|
+
mkdirSync(vscodeDir, { recursive: true });
|
|
598
282
|
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
283
|
+
// west's resolved facts — present after any successful build. Before the
|
|
284
|
+
// first build the launch entry is written without gdbPath; the next build
|
|
285
|
+
// upgrades it (debugArtifactsNeedRewrite).
|
|
286
|
+
//
|
|
287
|
+
// west records the `-py` (python-enabled) gdb variant; cortex-debug derives
|
|
288
|
+
// objdump/nm paths from the gdb path by name substitution, and the -py
|
|
289
|
+
// variants of those do not exist in the SDK (ENOENT noise, degraded symbol
|
|
290
|
+
// classification). Prefer the plain gdb sibling when it exists.
|
|
291
|
+
let gdbPath = readRunnersFacts(o.buildDir)?.gdb?.replace(/\\/g, '/');
|
|
292
|
+
if (gdbPath && /-py(\.exe)?$/i.test(gdbPath)) {
|
|
293
|
+
const plain = gdbPath.replace(/-py(\.exe)?$/i, '$1');
|
|
294
|
+
if (existsSync(plain)) gdbPath = plain;
|
|
295
|
+
}
|
|
296
|
+
gdbPath = gdbPath ?? o.starterGdbPath;
|
|
609
297
|
|
|
610
|
-
//
|
|
298
|
+
// gdb frame-filter script (lambda frames) — conditional on a source map.
|
|
611
299
|
const gdbScript = generateGdbScript(o.sourceMapPath);
|
|
612
300
|
let gdbScriptRel: string | undefined;
|
|
613
301
|
if (gdbScript) {
|
|
614
|
-
const scriptPath = join(
|
|
302
|
+
const scriptPath = join(o.projectRoot, '.typecad-hal', '.typecad-hal-gdb.py');
|
|
303
|
+
mkdirSync(dirname(scriptPath), { recursive: true });
|
|
615
304
|
writeFileSync(scriptPath, gdbScript, 'utf-8');
|
|
616
|
-
gdbScriptRel = `${o.appRel}/.
|
|
305
|
+
gdbScriptRel = `${o.appRel}/.typecad-hal/.typecad-hal-gdb.py`;
|
|
617
306
|
}
|
|
618
307
|
|
|
619
|
-
const launchConfig = buildLaunchConfig(o, gdbScriptRel,
|
|
308
|
+
const launchConfig = buildLaunchConfig(o, gdbScriptRel, gdbPath);
|
|
620
309
|
mergeJsonArrayEntry(join(vscodeDir, 'launch.json'), 'configurations', 'name', launchConfig);
|
|
621
310
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
311
|
+
const tasksPath = join(vscodeDir, 'tasks.json');
|
|
312
|
+
for (const task of [buildTask(o), serverTask(o), serverStopTask(o)]) {
|
|
313
|
+
mergeJsonArrayEntry(tasksPath, 'tasks', 'label', task);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return ['.vscode/launch.json', '.vscode/tasks.json'];
|
|
626
317
|
}
|
|
627
318
|
|
|
628
319
|
/**
|
|
629
|
-
* The Zephyr app dir
|
|
630
|
-
* project root: the scaffold fixes entry `./src/main.ts` +
|
|
631
|
-
* the CLI resolves output.outDir against the ENTRY's
|
|
632
|
-
* the emitted app root
|
|
633
|
-
*
|
|
634
|
-
*
|
|
320
|
+
* The Zephyr app dir the standard `typecad-hal create` scaffold produces,
|
|
321
|
+
* relative to the project root: the scaffold fixes entry `./src/main.ts` +
|
|
322
|
+
* outDir `./out`, and the CLI resolves output.outDir against the ENTRY's
|
|
323
|
+
* directory (cli.ts), so the emitted app root lands at `src/out`. The create
|
|
324
|
+
* flow passes its actual resolution (typecad-hal create's starterAppRel) so a
|
|
325
|
+
* non-standard scaffold still gets correct starter paths; this is only the
|
|
326
|
+
* default.
|
|
635
327
|
*/
|
|
636
328
|
const STARTER_APP_REL = 'src/out';
|
|
637
329
|
|
|
638
330
|
/**
|
|
639
|
-
* Create-time starter debug artifacts
|
|
640
|
-
* (
|
|
641
|
-
*
|
|
331
|
+
* Create-time starter debug artifacts: the same west-driven launch entry and
|
|
332
|
+
* tasks, minus gdbPath (no build exists yet — runners.yaml is absent). The
|
|
333
|
+
* first build upgrades the entry (debugArtifactsNeedRewrite fires on the
|
|
334
|
+
* missing gdbPath), so F5 after a compile is fully resolved.
|
|
642
335
|
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* by name) with the CMakeCache-resolved gdbPath — so the starter files upgrade
|
|
646
|
-
* themselves on the first debug build.
|
|
647
|
-
*
|
|
648
|
-
* No-ops (returns []) for targets without native GDB support (debugMode() !==
|
|
649
|
-
* 'gdb'); the gdb frame-filter script is skipped (no source map exists yet).
|
|
650
|
-
*
|
|
651
|
-
* Returns the workspace-relative paths written, for CLI reporting.
|
|
336
|
+
* No-ops (returns []) for targets without gdb debug facts (no debug-capable
|
|
337
|
+
* probe method in the board's table — printf instrumentation territory).
|
|
652
338
|
*/
|
|
653
339
|
export function writeProjectDebugArtifacts(o: {
|
|
654
|
-
/** Absolute path to the
|
|
340
|
+
/** Absolute path to the typecad-hal project root (contains typecad-hal.config.ts). */
|
|
655
341
|
workspaceRoot: string;
|
|
656
342
|
/** The Zephyr board id from the project config (frameworkData.buildTarget). */
|
|
657
343
|
buildTarget?: string;
|
|
344
|
+
/**
|
|
345
|
+
* The scaffolded app dir, workspace-relative with forward slashes.
|
|
346
|
+
* Defaults to the standard layout ('src/out'); typecad-hal create passes
|
|
347
|
+
* its config's actual entry + outDir resolution so a non-standard scaffold
|
|
348
|
+
* still gets correct starter paths.
|
|
349
|
+
*/
|
|
350
|
+
appRel?: string;
|
|
351
|
+
/**
|
|
352
|
+
* Create-time best-effort gdb from typecad-hal create (SDK + silicon) —
|
|
353
|
+
* see DebugConfigOptions.starterGdbPath. Plain JSON string.
|
|
354
|
+
*/
|
|
355
|
+
gdbPath?: string;
|
|
658
356
|
}): string[] {
|
|
659
357
|
if (new ZephyrStrategy().debugMode(o.buildTarget) !== 'gdb') return [];
|
|
660
358
|
const workspaceRoot = resolve(o.workspaceRoot);
|
|
661
|
-
const
|
|
662
|
-
|
|
359
|
+
const appRel = o.appRel ?? STARTER_APP_REL;
|
|
360
|
+
const projectRoot = join(workspaceRoot, appRel);
|
|
361
|
+
return writeDebugConfig({
|
|
663
362
|
projectRoot,
|
|
664
363
|
workspaceRoot,
|
|
665
|
-
appRel
|
|
364
|
+
appRel,
|
|
666
365
|
target: o.buildTarget ?? '',
|
|
667
|
-
// No build dir exists yet — resolveGdbPath falls back to probing known
|
|
668
|
-
// Zephyr SDK locations so gdbPath is still filled in when possible.
|
|
669
366
|
buildDir: join(projectRoot, 'build'),
|
|
367
|
+
...(o.gdbPath ? { starterGdbPath: o.gdbPath } : {}),
|
|
670
368
|
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// -- gdb frame-filter script -------------------------------------------------
|
|
372
|
+
|
|
373
|
+
export function generateGdbScript(sourceMapPath?: string): string | null {
|
|
374
|
+
if (!sourceMapPath || !existsSync(sourceMapPath)) return null;
|
|
375
|
+
let mapText = '';
|
|
376
|
+
try {
|
|
377
|
+
mapText = readFileSync(sourceMapPath, 'utf-8');
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
// Only emit when the emitted code contains hoisted-lambda symbols.
|
|
382
|
+
if (!/_isr_\d+/.test(mapText)) return null;
|
|
383
|
+
|
|
384
|
+
// A GDB Python frame-filter. Registered via the launch.json postAttachCommand
|
|
385
|
+
// `source <path>` so GDB auto-loads it on attach.
|
|
671
386
|
return [
|
|
672
|
-
'
|
|
673
|
-
'
|
|
674
|
-
|
|
675
|
-
|
|
387
|
+
'# Auto-generated by @typecad/framework-zephyr. GDB frame-filter that',
|
|
388
|
+
'# rewrites typecad-hal hoisted-lambda frame names (*_isr_N) into readable',
|
|
389
|
+
'# <lambda> form so the VS Code call stack is legible.',
|
|
390
|
+
'import gdb',
|
|
391
|
+
'import re',
|
|
392
|
+
'',
|
|
393
|
+
'class CuttlefishLambdaFilter:',
|
|
394
|
+
' def __init__(self):',
|
|
395
|
+
' self.name = "cuttlefish_lambda"',
|
|
396
|
+
' self.priority = 100',
|
|
397
|
+
' self.enabled = True',
|
|
398
|
+
'',
|
|
399
|
+
' def filter(self, frame_iter):',
|
|
400
|
+
' isr_re = re.compile(r"(.*)_isr_(\\d+)")',
|
|
401
|
+
' return (CuttlefishFrame(f) for f in frame_iter)',
|
|
402
|
+
'',
|
|
403
|
+
'class CuttlefishFrame:',
|
|
404
|
+
' def __init__(self, frame):',
|
|
405
|
+
' self.frame = frame',
|
|
406
|
+
' def __getattr__(self, name):',
|
|
407
|
+
' val = getattr(self.frame, name)',
|
|
408
|
+
' if name == "function":',
|
|
409
|
+
' m = isr_re.match(val)',
|
|
410
|
+
' if m: return "<lambda>"',
|
|
411
|
+
' return val',
|
|
412
|
+
'',
|
|
413
|
+
'gdb.frame_filters[CuttlefishLambdaFilter().name] = CuttlefishLambdaFilter()',
|
|
414
|
+
'',
|
|
415
|
+
].join('\n');
|
|
676
416
|
}
|