@typecad/framework-zephyr 1.0.0-alpha.16 → 1.0.0-alpha.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/LICENSE +202 -21
  2. package/README.md +49 -87
  3. package/dist/as-built.d.ts +2 -2
  4. package/dist/as-built.js +2 -2
  5. package/dist/audit.d.ts +111 -0
  6. package/dist/audit.js +416 -0
  7. package/dist/boardgen.d.ts +1 -9
  8. package/dist/boardgen.js +288 -47
  9. package/dist/chips/resolve.js +20 -0
  10. package/dist/chips/types.d.ts +22 -1
  11. package/dist/debug-codegen.js +1 -1
  12. package/dist/display/bindings.d.ts +55 -0
  13. package/dist/display/bindings.js +316 -0
  14. package/dist/display/gfx.d.ts +2 -3
  15. package/dist/display/gfx.js +166 -154
  16. package/dist/display/index.js +20 -1
  17. package/dist/display/mipi-dbi-host.d.ts +9 -0
  18. package/dist/display/mipi-dbi-host.js +174 -0
  19. package/dist/display/profiles.d.ts +109 -4
  20. package/dist/display/profiles.js +270 -7
  21. package/dist/display/touch-adapter.js +119 -49
  22. package/dist/display/ui-adapter-eink.d.ts +2 -0
  23. package/dist/display/ui-adapter-eink.js +4 -0
  24. package/dist/display/ui-adapter-gray.d.ts +8 -0
  25. package/dist/display/ui-adapter-gray.js +170 -0
  26. package/dist/display/ui-adapter-mono.d.ts +13 -0
  27. package/dist/display/ui-adapter-mono.js +230 -0
  28. package/dist/display/ui-adapter-native.d.ts +10 -0
  29. package/dist/display/ui-adapter-native.js +295 -0
  30. package/dist/display/ui-adapter-shared.d.ts +11 -0
  31. package/dist/display/ui-adapter-shared.js +122 -0
  32. package/dist/display/ui-adapter.js +510 -558
  33. package/dist/doctor.js +4 -4
  34. package/dist/dt-config/custom-board.js +2 -2
  35. package/dist/dt-config/kconfig.d.ts +62 -1
  36. package/dist/dt-config/kconfig.js +141 -38
  37. package/dist/dt-config/overlay.d.ts +15 -2
  38. package/dist/dt-config/overlay.js +433 -18
  39. package/dist/framework.manifest.d.ts +10 -4
  40. package/dist/framework.manifest.js +133 -17
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +14 -6
  43. package/dist/licenses.d.ts +2 -2
  44. package/dist/licenses.js +13 -92
  45. package/dist/lowering/can.d.ts +25 -0
  46. package/dist/lowering/can.js +97 -0
  47. package/dist/lowering/clock.d.ts +17 -0
  48. package/dist/lowering/clock.js +58 -0
  49. package/dist/lowering/fs.js +1 -1
  50. package/dist/lowering/gpio.js +0 -32
  51. package/dist/lowering/hid.d.ts +27 -0
  52. package/dist/lowering/hid.js +244 -0
  53. package/dist/lowering/http.js +264 -32
  54. package/dist/lowering/i2c.d.ts +8 -0
  55. package/dist/lowering/i2c.js +137 -5
  56. package/dist/lowering/i2s.d.ts +27 -0
  57. package/dist/lowering/i2s.js +98 -0
  58. package/dist/lowering/index.d.ts +9 -1
  59. package/dist/lowering/index.js +25 -1
  60. package/dist/lowering/interrupts.js +6 -0
  61. package/dist/lowering/matrix.d.ts +15 -0
  62. package/dist/lowering/matrix.js +63 -0
  63. package/dist/lowering/mqtt.js +110 -8
  64. package/dist/lowering/power.d.ts +3 -2
  65. package/dist/lowering/power.js +20 -45
  66. package/dist/lowering/pwm.js +25 -0
  67. package/dist/lowering/sensor.d.ts +2 -2
  68. package/dist/lowering/sensor.js +8 -4
  69. package/dist/lowering/strip.d.ts +16 -0
  70. package/dist/lowering/strip.js +70 -0
  71. package/dist/lowering/thread.js +5 -1
  72. package/dist/lowering/trace.d.ts +44 -0
  73. package/dist/lowering/trace.js +239 -0
  74. package/dist/lowering/uart.js +6 -1
  75. package/dist/lowering/usb.d.ts +3 -1
  76. package/dist/lowering/usb.js +16 -13
  77. package/dist/lowering/wdt.js +2 -29
  78. package/dist/sbom.d.ts +181 -0
  79. package/dist/sbom.js +901 -0
  80. package/dist/sdk/board-catalog-sync.d.ts +1 -3
  81. package/dist/sdk/board-catalog-sync.js +4 -10
  82. package/dist/strategy.d.ts +82 -46
  83. package/dist/strategy.js +639 -182
  84. package/dist/tmp-probe.d.ts +2 -0
  85. package/dist/tmp-probe.js +9 -0
  86. package/dist/toolchain/debug-config.d.ts +50 -90
  87. package/dist/toolchain/debug-config.js +239 -510
  88. package/dist/toolchain/env-check.d.ts +1 -3
  89. package/dist/toolchain/env-check.js +2 -7
  90. package/dist/toolchain/index.d.ts +28 -3
  91. package/dist/toolchain/index.js +498 -62
  92. package/dist/toolchain/runners.d.ts +16 -0
  93. package/dist/toolchain/runners.js +75 -0
  94. package/dist/toolchain/scaffold.d.ts +5 -2
  95. package/dist/toolchain/scaffold.js +79 -15
  96. package/dist/toolchain/west-discover.d.ts +6 -0
  97. package/dist/toolchain/west-discover.js +36 -13
  98. package/dist/toolchain/west-spawn.js +8 -2
  99. package/dist/west-inventory.d.ts +25 -0
  100. package/dist/west-inventory.js +97 -0
  101. package/installer/README.md +328 -328
  102. package/installer/install.sh +2 -2
  103. package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
  104. package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
  105. package/installer/templates/project/.vscode/settings.json +1 -1
  106. package/installer/templates/project/README.md +2 -2
  107. package/package.json +5 -5
  108. package/src/as-built.ts +206 -206
  109. package/src/audit.ts +529 -0
  110. package/src/boardgen.ts +265 -50
  111. package/src/chips/resolve.ts +21 -0
  112. package/src/chips/types.ts +10 -1
  113. package/src/display/bindings.ts +347 -0
  114. package/src/display/gfx.ts +318 -306
  115. package/src/display/index.ts +87 -70
  116. package/src/display/mipi-dbi-host.ts +183 -0
  117. package/src/display/profiles.ts +458 -139
  118. package/src/display/touch-adapter.ts +119 -49
  119. package/src/display/ui-adapter-eink.ts +13 -0
  120. package/src/display/ui-adapter-gray.ts +178 -0
  121. package/src/display/ui-adapter-mono.ts +238 -0
  122. package/src/display/ui-adapter-native.ts +304 -0
  123. package/src/display/ui-adapter-shared.ts +125 -0
  124. package/src/display/ui-adapter.ts +732 -781
  125. package/src/doctor.ts +4 -4
  126. package/src/dt-config/custom-board.ts +2 -2
  127. package/src/dt-config/kconfig.ts +647 -505
  128. package/src/dt-config/overlay.ts +1475 -1058
  129. package/src/framework.manifest.ts +659 -535
  130. package/src/index.ts +16 -6
  131. package/src/licenses.ts +346 -425
  132. package/src/lowering/can.ts +140 -0
  133. package/src/lowering/clock.ts +91 -0
  134. package/src/lowering/fs.ts +135 -135
  135. package/src/lowering/gpio.ts +0 -33
  136. package/src/lowering/hid.ts +261 -0
  137. package/src/lowering/http.ts +264 -32
  138. package/src/lowering/i2c.ts +142 -5
  139. package/src/lowering/i2s.ts +143 -0
  140. package/src/lowering/index.ts +18 -1
  141. package/src/lowering/interrupts.ts +6 -0
  142. package/src/lowering/matrix.ts +70 -0
  143. package/src/lowering/mqtt.ts +109 -8
  144. package/src/lowering/power.ts +41 -0
  145. package/src/lowering/pwm.ts +192 -167
  146. package/src/lowering/sensor.ts +159 -155
  147. package/src/lowering/strip.ts +81 -0
  148. package/src/lowering/thread.ts +5 -1
  149. package/src/lowering/trace.ts +270 -0
  150. package/src/lowering/uart.ts +6 -1
  151. package/src/lowering/usb.ts +16 -13
  152. package/src/lowering/wdt.ts +2 -25
  153. package/src/sbom.ts +1117 -0
  154. package/src/sdk/board-catalog-sync.ts +4 -25
  155. package/src/strategy.ts +2680 -2309
  156. package/src/toolchain/debug-config.ts +262 -522
  157. package/src/toolchain/env-check.ts +279 -285
  158. package/src/toolchain/index.ts +1792 -1359
  159. package/src/toolchain/runners.ts +80 -0
  160. package/src/toolchain/scaffold.ts +355 -296
  161. package/src/toolchain/west-discover.ts +35 -13
  162. package/src/toolchain/west-spawn.ts +174 -168
  163. package/src/west-inventory.ts +102 -0
  164. package/dist/async/timer-polyfill.d.ts +0 -10
  165. package/dist/async/timer-polyfill.js +0 -95
  166. package/dist/chips/board-overrides.d.ts +0 -7
  167. package/dist/chips/board-overrides.js +0 -11
  168. package/dist/chips/esp32.d.ts +0 -2
  169. package/dist/chips/esp32.js +0 -71
  170. package/dist/chips/esp32s3.d.ts +0 -2
  171. package/dist/chips/esp32s3.js +0 -103
  172. package/dist/chips/soc/.d.ts +0 -2
  173. package/dist/chips/soc/.js +0 -129
  174. package/dist/chips/soc/esp32.d.ts +0 -2
  175. package/dist/chips/soc/esp32.js +0 -120
  176. package/dist/chips/soc/esp32c3.d.ts +0 -2
  177. package/dist/chips/soc/esp32c3.js +0 -90
  178. package/dist/chips/soc/esp32c6.d.ts +0 -2
  179. package/dist/chips/soc/esp32c6.js +0 -109
  180. package/dist/chips/soc/esp32s3.d.ts +0 -2
  181. package/dist/chips/soc/esp32s3.js +0 -189
  182. package/dist/chips/soc/index.d.ts +0 -2
  183. package/dist/chips/soc/index.js +0 -23
  184. package/dist/chips/soc/nrf52840.d.ts +0 -2
  185. package/dist/chips/soc/nrf52840.js +0 -130
  186. package/dist/chips/soc/rp2040.d.ts +0 -2
  187. package/dist/chips/soc/rp2040.js +0 -141
  188. package/dist/chips/soc/rp2350a.d.ts +0 -2
  189. package/dist/chips/soc/rp2350a.js +0 -145
  190. package/dist/chips/soc/samd21g18a.d.ts +0 -2
  191. package/dist/chips/soc/samd21g18a.js +0 -143
  192. package/dist/chips/soc/stm32f411xe.d.ts +0 -2
  193. package/dist/chips/soc/stm32f411xe.js +0 -251
  194. package/dist/chips/xiao-ble.d.ts +0 -2
  195. package/dist/chips/xiao-ble.js +0 -100
  196. package/dist/lowering/pulse.d.ts +0 -7
  197. package/dist/lowering/pulse.js +0 -51
  198. package/dist/lowering/tone.d.ts +0 -10
  199. package/dist/lowering/tone.js +0 -63
  200. package/dist/lowering/worker-backing.d.ts +0 -14
  201. package/dist/lowering/worker-backing.js +0 -79
  202. package/dist/lowering/worker.d.ts +0 -6
  203. package/dist/lowering/worker.js +0 -14
  204. package/dist/sdk/board-data.generated.d.ts +0 -2
  205. package/dist/sdk/board-data.generated.js +0 -4
  206. package/dist/sdk/catalog-walker.d.ts +0 -90
  207. package/dist/sdk/catalog-walker.js +0 -682
  208. package/dist/sdk/dts-reader.d.ts +0 -83
  209. package/dist/sdk/dts-reader.js +0 -596
  210. package/src/debug-codegen.ts +0 -207
@@ -1,27 +1,37 @@
1
1
  // ---------------------------------------------------------------------------
2
- // debug-config.ts — VS Code / GDB debug artifact generation for `--debug`
2
+ // debug-config.ts — west-driven VS Code debug artifacts (cortex-debug external
3
+ // server mode).
3
4
  //
4
- // After a successful gdb-mode build, writes the artifacts that let the user
5
- // press F5 in VS Code and attach GDB to the running Zephyr target.
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
- // Uses the cortex-debug extension (NOT the ESP-IDF gdbtarget adapter) so the
8
- // debug session is self-contained. cortex-debug starts OpenOCD as a child
9
- // process via `servertype: "openocd"`; the preLaunch task handles only the
10
- // build + flash step. After attach we issue `monitor reset init`, set a
11
- // temporary hardware breakpoint at main(), and continue — this ensures the
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
- // All generators are deterministic + idempotent so toggling --debug does not
15
- // churn the tree.
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, readdirSync } from 'node:fs';
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 { resolveChipFromBoard } from '../chips/resolve.js';
22
- import { resolveProbeMethod } from './index.js';
23
- import { loadCuttlefishConfig } from '@typecad/cuttlefish/config-loader';
24
- import type { ZephyrProbeMethod } from '../chips/types.js';
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
- * Resolve the GDB binary path for the target from the build cache, falling
43
- * back to a filesystem scan of known Zephyr SDK locations when no build
44
- * exists yet (the create-time starter artifacts path). Zephyr records
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 cuttlefish project root (the
229
- // nearest ancestor containing cuttlefish.config.ts). Fall back to projectRoot
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, 'cuttlefish.config.ts'))) {
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
- matchKey: string,
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
- doc = JSON.parse(readFileSync(filePath, 'utf-8'));
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]) ? (doc[arrayKey] as T[]) : [];
263
- const idx = arr.findIndex((e) => e[matchKey] === entry[matchKey]);
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) + '\n', 'utf-8');
113
+ writeFileSync(filePath, `${JSON.stringify(doc, null, 2)}\n`, 'utf-8');
269
114
  }
270
115
 
271
116
  /**
272
- * Generate the GDB Python frame-filter that rewrites cuttlefish's hoisted
273
- * lambda frame names (`${prefix}_isr_N`) into readable `<lambda> @ file:line`
274
- * in the call stack. Only emitted when the source map references `_isr_N`
275
- * symbols; otherwise returns null (launch.json omits the `source` initCommand).
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 generateGdbScript(sourceMapPath?: string): string | null {
281
- if (!sourceMapPath || !existsSync(sourceMapPath)) return null;
282
- let mapText = '';
123
+ export function debugArtifactsNeedRewrite(
124
+ workspaceRoot: string,
125
+ appRel: string,
126
+ currentGdb?: string,
127
+ ): boolean {
128
+ let doc: unknown;
283
129
  try {
284
- mapText = readFileSync(sourceMapPath, 'utf-8');
130
+ doc = JSON.parse(readFileSync(join(workspaceRoot, '.vscode', 'launch.json'), 'utf-8'));
285
131
  } catch {
286
- return null;
132
+ return false; // no launch.json (or unreadable) — plain builds leave it absent
287
133
  }
288
- // Only emit when the emitted code contains hoisted-lambda symbols.
289
- if (!/_isr_\d+/.test(mapText)) return null;
290
-
291
- // A GDB Python frame-filter. Registered via the launch.json initCommand
292
- // `source <path>` so GDB auto-loads it on attach.
293
- return [
294
- '# Auto-generated by @typecad/framework-zephyr. GDB frame-filter that',
295
- '# rewrites cuttlefish hoisted-lambda frame names (*_isr_N) into readable',
296
- '# <lambda> form so the VS Code call stack is legible.',
297
- 'import gdb',
298
- 'import re',
299
- '',
300
- 'class CuttlefishLambdaFilter:',
301
- ' def __init__(self):',
302
- ' self.name = "cuttlefish_lambda"',
303
- ' self.priority = 100',
304
- ' self.enabled = True',
305
- '',
306
- ' def filter(self, frame_iter):',
307
- ' isr_re = re.compile(r"(.*)_isr_(\\d+)")',
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 for an ESP32-S3 (built-in USB-JTAG).
327
- * `openOcdCfgRel` is the workspace-relative path to the generated
328
- * .cuttlefish/openocd.cfg, passed to cortex-debug's configFiles.
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
- openOcdCfgRel: string,
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
- // OpenOCD cfg relative to the workspace root so cortex-debug can pass it
341
- // via the -f flag. Must be a list; cortex-debug prepends -f per entry.
342
- const configFiles = [`\${workspaceFolder}/${openOcdCfgRel}`];
343
-
344
- // GDB path resolved from the Zephyr SDK build cache (CMakeCache.txt).
345
- const gdbPath = resolveGdbPath(o.buildDir, o.target);
346
-
347
- // OpenOCD binary path — the Espressif fork (openocd-esp32) is required for
348
- // the esp_usb_jtag adapter. cortex-debug's `serverpath` tells it where to
349
- // find the binary (not on PATH by default).
350
- const openocdPath = resolveOpenOcdPath(o.target);
351
-
352
- // Post-attach commands executed after GDB connects to the OpenOCD gdbserver.
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
- ...(isEsp32Target ? [
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
- ...(isEsp32Target ? ['c'] : []),
191
+ ...(isXtensa ? ['c'] : []),
399
192
  ];
400
193
  if (gdbScriptRel) {
401
194
  postAttachCommands.splice(1, 0, `source ${gdbScriptRel}`);
402
195
  }
403
196
 
404
- // Probe-method-driven server shape: jlink-runner methods use cortex-debug's
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). The server
415
- // controller's attachCommands() just halts the target, then our
416
- // postAttachCommands reset it, set a HW breakpoint at main(), and
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
- ...(servertype === 'openocd' ? { configFiles } : {}),
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: 'cuttlefish: build + flash (debug)',
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: 'cuttlefish: build + flash (debug)',
218
+ label: DEBUG_BUILD_TASK,
447
219
  type: 'shell',
448
- command: 'npx cuttlefish build --compile --upload --debug',
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 adapter speed the S3's built-in USB-Serial-JTAG runs stably at.
457
- *
458
- * The interface cfg (esp_usb_jtag.cfg) defaults to 40000 (40 MHz), the chip
459
- * max. The USB-Serial-JTAG peripheral is a software bitq adapter that bit-bangs
460
- * JTAG over USB bulk transfers; at 40 MHz it can't keep up, drops transfers
461
- * (LIBUSB_ERROR_IO / "missing data from bitq interface"), and the reset/halt
462
- * sequence silently fails — leaving the target running with no breakpoint set
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
- const OPENOCD_ADAPTER_SPEED = 4000;
468
-
469
- /**
470
- * Build the OpenOCD cfg content. Sources the board's own openocd.cfg (which
471
- * sets ESP_RTOS Zephyr + ESP_ONLYCPU + the esp_usb_jtag driver + the esp32s3
472
- * target) then overrides the adapter speed AFTER the driver loads — the order
473
- * that a bare `-c "adapter speed N"` cannot guarantee (OpenOCD applies -c args
474
- * in command-line order relative to -f, and Cortex-Debug injects its own
475
- * helper/RTOS tcl, so the override can land before the driver exists or be
476
- * re-defaulted). Putting it in the cfg, after the source, is deterministic.
477
- */
478
- /**
479
- * Drop the gdb-attach/gdb-detach target-event blocks from a cfg line list.
480
- * Board cfgs use them for standalone openocd sessions (reset on attach,
481
- * resume on detach), but under VS Code they fire DURING cortex-debug's
482
- * initialization — the unexpected stop event aborts the session setup
483
- * ("Program stopped, probably due to a reset and/or halt issued by
484
- * debugger") and the toolbar never enables. The IDE owns the lifecycle.
485
- */
486
- function stripGdbEventBlocks(lines: readonly string[]): string[] {
487
- const out: string[] = [];
488
- let skipping = false;
489
- let depth = 0;
490
- for (const line of lines) {
491
- if (!skipping && /configure\s+-event\s+gdb-(attach|detach)/.test(line)) {
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
- function buildOpenOcdCfg(method?: ZephyrProbeMethod, target?: string): string {
506
- const header = [
507
- '# Auto-generated by @typecad/framework-zephyr. Do not edit — regenerate',
508
- '# with `cuttlefish build --debug`.',
509
- ];
510
- // Zephyr RTOS awareness (thread names in the call stack) — appended after
511
- // the target source on ARM targets; the ESP32 board cfgs already carry
512
- // ESP_RTOS Zephyr.
513
- const isEsp32 = (target ?? '').split('/')[0].startsWith('esp32');
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 all gdb-mode debug artifacts for the given target. Called from the
557
- * toolchain compile() after a successful build when debugMode === 'gdb'.
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 resolveDebugProbeMethod(
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
- const cuttlefishDir = join(o.projectRoot, '.cuttlefish');
597
- mkdirSync(cuttlefishDir, { recursive: true });
281
+ mkdirSync(vscodeDir, { recursive: true });
598
282
 
599
- // The selected probe method (if any) drives the debug server shape:
600
- // openocd cfg source/quirks, cortex-debug servertype, wire interface.
601
- const method = resolveDebugProbeMethod(o.projectRoot, o.workspaceRoot);
602
-
603
- // openocd.cfg — written first so its relative path can be wired into the
604
- // cortex-debug configFiles. cortex-debug starts OpenOCD as a child process
605
- // and passes this file via -f.
606
- const openOcdCfgPath = join(cuttlefishDir, 'openocd.cfg');
607
- writeFileSync(openOcdCfgPath, buildOpenOcdCfg(method, o.target), 'utf-8');
608
- const openOcdCfgRel = `${o.appRel}/.cuttlefish/openocd.cfg`;
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
- // launch.json — merge the cortex-debug config by name.
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(cuttlefishDir, '.cuttlefish-gdb.py');
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}/.cuttlefish/.cuttlefish-gdb.py`;
305
+ gdbScriptRel = `${o.appRel}/.typecad-hal/.typecad-hal-gdb.py`;
617
306
  }
618
307
 
619
- const launchConfig = buildLaunchConfig(o, gdbScriptRel, openOcdCfgRel, method);
308
+ const launchConfig = buildLaunchConfig(o, gdbScriptRel, gdbPath);
620
309
  mergeJsonArrayEntry(join(vscodeDir, 'launch.json'), 'configurations', 'name', launchConfig);
621
310
 
622
- // tasks.json — merge the build+flash task by label. OpenOCD is managed by
623
- // cortex-debug so the task is a simple synchronous build step.
624
- const task = buildTask(o);
625
- mergeJsonArrayEntry(join(vscodeDir, 'tasks.json'), 'tasks', 'label', task);
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 a `cuttlefish create` scaffold produces, relative to the
630
- * project root: the scaffold fixes entry `./src/main.ts` + outDir `./out`, and
631
- * the CLI resolves output.outDir against the ENTRY's directory (cli.ts), so
632
- * the emitted app root — and therefore the ELF, build dir, and .cuttlefish/
633
- * debug artifacts — always lands at `src/out`. Keep in sync with
634
- * generateProjectConfig in @typecad/cuttlefish create/templates.ts.
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. Called by the cuttlefish `create` flow
640
- * (via the package's `writeProjectDebugArtifacts` export) so a fresh project
641
- * has a working F5 before any build exists:
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
- * The launch.json's preLaunchTask runs `cuttlefish build --compile --upload
644
- * --debug`, which builds + flashes AND rewrites this same launch entry (merged
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 cuttlefish project root (contains cuttlefish.config.ts). */
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 projectRoot = join(workspaceRoot, STARTER_APP_REL);
662
- writeDebugConfig({
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: STARTER_APP_REL,
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
- '.vscode/launch.json',
673
- '.vscode/tasks.json',
674
- `${STARTER_APP_REL}/.cuttlefish/openocd.cfg`,
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
  }