@typecad/framework-zephyr 1.0.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/dist/async/timer-polyfill.d.ts +10 -0
  4. package/dist/async/timer-polyfill.js +95 -0
  5. package/dist/chips/controllers.d.ts +21 -0
  6. package/dist/chips/controllers.js +66 -0
  7. package/dist/chips/esp32.d.ts +2 -0
  8. package/dist/chips/esp32.js +59 -0
  9. package/dist/chips/esp32s3.d.ts +2 -0
  10. package/dist/chips/esp32s3.js +54 -0
  11. package/dist/chips/index.d.ts +15 -0
  12. package/dist/chips/index.js +45 -0
  13. package/dist/chips/resolve.d.ts +9 -0
  14. package/dist/chips/resolve.js +121 -0
  15. package/dist/chips/types.d.ts +154 -0
  16. package/dist/chips/types.js +14 -0
  17. package/dist/chips/xiao-ble.d.ts +2 -0
  18. package/dist/chips/xiao-ble.js +61 -0
  19. package/dist/debug-codegen.d.ts +33 -0
  20. package/dist/debug-codegen.js +162 -0
  21. package/dist/display/gfx.d.ts +17 -0
  22. package/dist/display/gfx.js +171 -0
  23. package/dist/display/index.d.ts +20 -0
  24. package/dist/display/index.js +53 -0
  25. package/dist/display/profiles.d.ts +25 -0
  26. package/dist/display/profiles.js +41 -0
  27. package/dist/display/touch-adapter.d.ts +9 -0
  28. package/dist/display/touch-adapter.js +95 -0
  29. package/dist/display/ui-adapter.d.ts +21 -0
  30. package/dist/display/ui-adapter.js +534 -0
  31. package/dist/dt-config/kconfig.d.ts +27 -0
  32. package/dist/dt-config/kconfig.js +283 -0
  33. package/dist/dt-config/overlay.d.ts +35 -0
  34. package/dist/dt-config/overlay.js +246 -0
  35. package/dist/framework.manifest.d.ts +98 -0
  36. package/dist/framework.manifest.js +525 -0
  37. package/dist/index.d.ts +5 -0
  38. package/dist/index.js +12 -0
  39. package/dist/lowering/adc.d.ts +19 -0
  40. package/dist/lowering/adc.js +76 -0
  41. package/dist/lowering/ble.d.ts +20 -0
  42. package/dist/lowering/ble.js +419 -0
  43. package/dist/lowering/board.d.ts +9 -0
  44. package/dist/lowering/board.js +33 -0
  45. package/dist/lowering/gpio.d.ts +12 -0
  46. package/dist/lowering/gpio.js +123 -0
  47. package/dist/lowering/http.d.ts +23 -0
  48. package/dist/lowering/http.js +482 -0
  49. package/dist/lowering/i2c.d.ts +15 -0
  50. package/dist/lowering/i2c.js +114 -0
  51. package/dist/lowering/index.d.ts +29 -0
  52. package/dist/lowering/index.js +93 -0
  53. package/dist/lowering/interrupts.d.ts +21 -0
  54. package/dist/lowering/interrupts.js +91 -0
  55. package/dist/lowering/mqtt.d.ts +19 -0
  56. package/dist/lowering/mqtt.js +366 -0
  57. package/dist/lowering/power.d.ts +9 -0
  58. package/dist/lowering/power.js +64 -0
  59. package/dist/lowering/preferences.d.ts +23 -0
  60. package/dist/lowering/preferences.js +345 -0
  61. package/dist/lowering/pulse.d.ts +7 -0
  62. package/dist/lowering/pulse.js +82 -0
  63. package/dist/lowering/pwm.d.ts +15 -0
  64. package/dist/lowering/pwm.js +59 -0
  65. package/dist/lowering/random.d.ts +18 -0
  66. package/dist/lowering/random.js +129 -0
  67. package/dist/lowering/spi.d.ts +17 -0
  68. package/dist/lowering/spi.js +113 -0
  69. package/dist/lowering/timing.d.ts +9 -0
  70. package/dist/lowering/timing.js +63 -0
  71. package/dist/lowering/tone.d.ts +10 -0
  72. package/dist/lowering/tone.js +50 -0
  73. package/dist/lowering/uart.d.ts +15 -0
  74. package/dist/lowering/uart.js +102 -0
  75. package/dist/lowering/util.d.ts +4 -0
  76. package/dist/lowering/util.js +12 -0
  77. package/dist/lowering/wdt.d.ts +15 -0
  78. package/dist/lowering/wdt.js +80 -0
  79. package/dist/lowering/wifi.d.ts +21 -0
  80. package/dist/lowering/wifi.js +447 -0
  81. package/dist/lowering/worker-backing.d.ts +14 -0
  82. package/dist/lowering/worker-backing.js +79 -0
  83. package/dist/lowering/worker.d.ts +6 -0
  84. package/dist/lowering/worker.js +14 -0
  85. package/dist/strategy.d.ts +168 -0
  86. package/dist/strategy.js +1094 -0
  87. package/dist/toolchain/debug-config.d.ts +82 -0
  88. package/dist/toolchain/debug-config.js +359 -0
  89. package/dist/toolchain/index.d.ts +73 -0
  90. package/dist/toolchain/index.js +427 -0
  91. package/dist/toolchain/scaffold.d.ts +15 -0
  92. package/dist/toolchain/scaffold.js +196 -0
  93. package/dist/toolchain/west-discover.d.ts +40 -0
  94. package/dist/toolchain/west-discover.js +203 -0
  95. package/dist/toolchain/west-spawn.d.ts +39 -0
  96. package/dist/toolchain/west-spawn.js +117 -0
  97. package/package.json +82 -0
  98. package/src/async/timer-polyfill.ts +107 -0
  99. package/src/chips/controllers.ts +74 -0
  100. package/src/chips/esp32.ts +62 -0
  101. package/src/chips/esp32s3.ts +57 -0
  102. package/src/chips/index.ts +54 -0
  103. package/src/chips/resolve.ts +146 -0
  104. package/src/chips/types.ts +163 -0
  105. package/src/chips/xiao-ble.ts +64 -0
  106. package/src/debug-codegen.ts +207 -0
  107. package/src/display/gfx.ts +190 -0
  108. package/src/display/index.ts +70 -0
  109. package/src/display/profiles.ts +63 -0
  110. package/src/display/touch-adapter.ts +100 -0
  111. package/src/display/ui-adapter.ts +558 -0
  112. package/src/dt-config/kconfig.ts +308 -0
  113. package/src/dt-config/overlay.ts +293 -0
  114. package/src/framework.manifest.ts +545 -0
  115. package/src/index.ts +23 -0
  116. package/src/lowering/adc.ts +102 -0
  117. package/src/lowering/ble.ts +426 -0
  118. package/src/lowering/board.ts +38 -0
  119. package/src/lowering/gpio.ts +148 -0
  120. package/src/lowering/http.ts +488 -0
  121. package/src/lowering/i2c.ts +126 -0
  122. package/src/lowering/index.ts +86 -0
  123. package/src/lowering/interrupts.ts +114 -0
  124. package/src/lowering/mqtt.ts +371 -0
  125. package/src/lowering/power.ts +70 -0
  126. package/src/lowering/preferences.ts +354 -0
  127. package/src/lowering/pulse.ts +93 -0
  128. package/src/lowering/pwm.ts +74 -0
  129. package/src/lowering/random.ts +135 -0
  130. package/src/lowering/spi.ts +124 -0
  131. package/src/lowering/timing.ts +72 -0
  132. package/src/lowering/tone.ts +60 -0
  133. package/src/lowering/uart.ts +111 -0
  134. package/src/lowering/util.ts +12 -0
  135. package/src/lowering/wdt.ts +84 -0
  136. package/src/lowering/wifi.ts +452 -0
  137. package/src/lowering/worker-backing.ts +94 -0
  138. package/src/lowering/worker.ts +17 -0
  139. package/src/strategy.ts +1265 -0
  140. package/src/toolchain/debug-config.ts +399 -0
  141. package/src/toolchain/index.ts +459 -0
  142. package/src/toolchain/scaffold.ts +196 -0
  143. package/src/toolchain/west-discover.ts +233 -0
  144. package/src/toolchain/west-spawn.ts +141 -0
@@ -0,0 +1,82 @@
1
+ export interface DebugConfigOptions {
2
+ /** Absolute path to the Zephyr project root (contains CMakeLists.txt + src/). */
3
+ projectRoot: string;
4
+ /** Absolute path to the workspace root (the cuttlefish config dir — the folder VS Code has open). */
5
+ workspaceRoot: string;
6
+ /** The Zephyr app dir relative to the workspace root (e.g. 'src/out'). */
7
+ sketchRel: string;
8
+ /** The Zephyr board id (e.g. 'esp32s3_devkitc'). */
9
+ target: string;
10
+ /** Absolute path to the Zephyr build dir (<projectRoot>/build) — read for the SDK/GDB path. */
11
+ buildDir: string;
12
+ /** Absolute path to the emitted source map (*.thcppmap.json), if any. */
13
+ sourceMapPath?: string;
14
+ }
15
+ /**
16
+ * Resolve the GDB binary path for the target from the build cache. Zephyr
17
+ * records ZEPHYR_SDK_INSTALL_DIR in CMakeCache.txt at configure time, and the
18
+ * xtensa GDB lives at <sdk>/xtensa-espressif_esp32s3_zephyr-elf/bin/... (note
19
+ * the Zephyr-SDK naming, distinct from the ESP-IDF xtensa-esp32s3-elf-gdb).
20
+ *
21
+ * Returns the absolute gdb path on success, or undefined (the launch.json then
22
+ * omits gdbPath and relies on Cortex-Debug's default resolution).
23
+ */
24
+ export declare function resolveGdbPath(buildDir: string, target: string): string | undefined;
25
+ /**
26
+ * Resolve the Espressif OpenOCD binary path. The esp32s3 needs the Espressif
27
+ * OpenOCD fork (openocd-esp32) — not the Zephyr SDK's openocd and not a
28
+ * generic/GDB-stub build — because only it carries the Xtensa + esp_usb_jtag
29
+ * support. It is NOT on PATH by default, so Cortex-Debug must be pointed at it
30
+ * explicitly or it fails with `spawn openocd.exe ENOENT`.
31
+ *
32
+ * Discovery order:
33
+ * 1. ESPRESSIF_TOOLCHAIN_PATH env (the var board.cmake reads) — if set, its
34
+ * openocd-esp32/bin/openocd.exe.
35
+ * 2. The standard ESP-IDF install layout: ~/.espressif/tools/openocd-esp32/
36
+ * <version>/openocd-esp32/bin/openocd.exe. Pick the newest version dir.
37
+ * Returns undefined if not found (the launch.json then omits openOCDPath and
38
+ * Cortex-Debug falls back to PATH / its openocdPath setting).
39
+ */
40
+ export declare function resolveOpenOcdPath(): string | undefined;
41
+ /**
42
+ * Resolve where to write the VS Code debug artifacts and how to express paths
43
+ * in them.
44
+ *
45
+ * VS Code reads `.vscode/` from the folder the user has OPENED — and for a
46
+ * cuttlefish project that is almost always the **cuttlefish project root**
47
+ * (the directory containing `cuttlefish.config.ts`), NOT the git repo root.
48
+ * The toolchain's `projectRoot` is the Zephyr *app* dir (e.g.
49
+ * `<projectRoot>/src/out`), which sits below the cuttlefish config dir. So we
50
+ * walk up from `projectRoot` to the nearest `cuttlefish.config.ts` and treat
51
+ * THAT as the workspace root. This makes F5 work when a user opens the project
52
+ * folder directly, and keeps launch.json paths relative to it.
53
+ *
54
+ * Returns { workspaceRoot, sketchRel } where workspaceRoot is the cuttlefish
55
+ * project root (the `.vscode/` target) and sketchRel is the Zephyr app dir
56
+ * (`projectRoot`) relative to it (e.g. 'src/out').
57
+ */
58
+ export declare function resolveDebugLocations(projectRoot: string): {
59
+ workspaceRoot: string;
60
+ sketchRel: string;
61
+ };
62
+ /**
63
+ * Generate the GDB Python frame-filter that rewrites cuttlefish's hoisted
64
+ * lambda frame names (`${prefix}_isr_N`) into readable `<lambda> @ file:line`
65
+ * in the call stack. Only emitted when the source map references `_isr_N`
66
+ * symbols; otherwise returns null (launch.json omits the `source` initCommand).
67
+ *
68
+ * Ported verbatim from the deleted framework-esp32/toolchain/gdb-script.ts —
69
+ * the filter is cuttlefish-internal (lambda hoisting is framework-agnostic).
70
+ */
71
+ export declare function generateGdbScript(sourceMapPath?: string): string | null;
72
+ /**
73
+ * Write all gdb-mode debug artifacts for the given target. Called from the
74
+ * toolchain compile() after a successful build when debugMode === 'gdb'.
75
+ *
76
+ * Writes (all idempotent):
77
+ * <workspaceRoot>/.vscode/launch.json (cortex-debug config)
78
+ * <workspaceRoot>/.vscode/tasks.json (build + flash preLaunch task)
79
+ * <projectRoot>/.cuttlefish/openocd.cfg (OpenOCD cfg, adapter speed override)
80
+ * <projectRoot>/.cuttlefish/.cuttlefish-gdb.py (lambda frame filter, conditional)
81
+ */
82
+ export declare function writeDebugConfig(o: DebugConfigOptions): void;
@@ -0,0 +1,359 @@
1
+ // ---------------------------------------------------------------------------
2
+ // debug-config.ts — VS Code / GDB debug artifact generation for `--debug`
3
+ //
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.
6
+ //
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 setup(), and continue — this ensures the
12
+ // breakpoint is deferred until the bootloader maps the app flash region.
13
+ //
14
+ // All generators are deterministic + idempotent so toggling --debug does not
15
+ // churn the tree.
16
+ // ---------------------------------------------------------------------------
17
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
18
+ import { join, resolve, dirname, relative } from 'node:path';
19
+ /**
20
+ * Resolve the GDB binary path for the target from the build cache. Zephyr
21
+ * records ZEPHYR_SDK_INSTALL_DIR in CMakeCache.txt at configure time, and the
22
+ * xtensa GDB lives at <sdk>/xtensa-espressif_esp32s3_zephyr-elf/bin/... (note
23
+ * the Zephyr-SDK naming, distinct from the ESP-IDF xtensa-esp32s3-elf-gdb).
24
+ *
25
+ * Returns the absolute gdb path on success, or undefined (the launch.json then
26
+ * omits gdbPath and relies on Cortex-Debug's default resolution).
27
+ */
28
+ export function resolveGdbPath(buildDir, target) {
29
+ const cachePath = join(buildDir, 'CMakeCache.txt');
30
+ if (!existsSync(cachePath))
31
+ return undefined;
32
+ let sdk = '';
33
+ try {
34
+ const cache = readFileSync(cachePath, 'utf-8');
35
+ const m = cache.match(/^ZEPHYR_SDK_INSTALL_DIR:PATH=(.+)$/m);
36
+ if (m)
37
+ sdk = m[1].trim();
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ if (!sdk)
43
+ return undefined;
44
+ // The Zephyr SDK toolchain dir is target-specific. For esp32s3 it is
45
+ // xtensa-espressif_esp32s3_zephyr-elf (verified against zephyr-sdk-0.17.4).
46
+ const gdbName = 'xtensa-espressif_esp32s3_zephyr-elf-gdb.exe';
47
+ const gdbPath = join(sdk, 'xtensa-espressif_esp32s3_zephyr-elf', 'bin', gdbName);
48
+ return existsSync(gdbPath) ? gdbPath.replace(/\\/g, '/') : undefined;
49
+ }
50
+ /**
51
+ * Resolve the Espressif OpenOCD binary path. The esp32s3 needs the Espressif
52
+ * OpenOCD fork (openocd-esp32) — not the Zephyr SDK's openocd and not a
53
+ * generic/GDB-stub build — because only it carries the Xtensa + esp_usb_jtag
54
+ * support. It is NOT on PATH by default, so Cortex-Debug must be pointed at it
55
+ * explicitly or it fails with `spawn openocd.exe ENOENT`.
56
+ *
57
+ * Discovery order:
58
+ * 1. ESPRESSIF_TOOLCHAIN_PATH env (the var board.cmake reads) — if set, its
59
+ * openocd-esp32/bin/openocd.exe.
60
+ * 2. The standard ESP-IDF install layout: ~/.espressif/tools/openocd-esp32/
61
+ * <version>/openocd-esp32/bin/openocd.exe. Pick the newest version dir.
62
+ * Returns undefined if not found (the launch.json then omits openOCDPath and
63
+ * Cortex-Debug falls back to PATH / its openocdPath setting).
64
+ */
65
+ export function resolveOpenOcdPath() {
66
+ const candidates = [];
67
+ // 1. ESPRESSIF_TOOLCHAIN_PATH env
68
+ const envPath = process.env.ESPRESSIF_TOOLCHAIN_PATH;
69
+ if (envPath) {
70
+ candidates.push(join(envPath, 'openocd-esp32', 'bin', 'openocd.exe'));
71
+ }
72
+ // 2. ~/.espressif/tools/openocd-esp32/<version>/openocd-esp32/bin/openocd.exe
73
+ const home = process.env.USERPROFILE || process.env.HOME;
74
+ if (home) {
75
+ const base = join(home, '.espressif', 'tools', 'openocd-esp32');
76
+ let versions = [];
77
+ try {
78
+ versions = readdirSync(base).filter((v) => existsSync(join(base, v, 'openocd-esp32', 'bin', 'openocd.exe')));
79
+ }
80
+ catch {
81
+ // dir absent
82
+ }
83
+ // newest version last — sort then reverse so the highest wins on match.
84
+ versions.sort().reverse();
85
+ for (const v of versions) {
86
+ candidates.push(join(base, v, 'openocd-esp32', 'bin', 'openocd.exe'));
87
+ }
88
+ }
89
+ for (const c of candidates) {
90
+ if (existsSync(c))
91
+ return resolve(c).replace(/\\/g, '/');
92
+ }
93
+ return undefined;
94
+ }
95
+ /**
96
+ * Resolve where to write the VS Code debug artifacts and how to express paths
97
+ * in them.
98
+ *
99
+ * VS Code reads `.vscode/` from the folder the user has OPENED — and for a
100
+ * cuttlefish project that is almost always the **cuttlefish project root**
101
+ * (the directory containing `cuttlefish.config.ts`), NOT the git repo root.
102
+ * The toolchain's `projectRoot` is the Zephyr *app* dir (e.g.
103
+ * `<projectRoot>/src/out`), which sits below the cuttlefish config dir. So we
104
+ * walk up from `projectRoot` to the nearest `cuttlefish.config.ts` and treat
105
+ * THAT as the workspace root. This makes F5 work when a user opens the project
106
+ * folder directly, and keeps launch.json paths relative to it.
107
+ *
108
+ * Returns { workspaceRoot, sketchRel } where workspaceRoot is the cuttlefish
109
+ * project root (the `.vscode/` target) and sketchRel is the Zephyr app dir
110
+ * (`projectRoot`) relative to it (e.g. 'src/out').
111
+ */
112
+ export function resolveDebugLocations(projectRoot) {
113
+ // Walk up from the Zephyr app dir to find the cuttlefish project root (the
114
+ // nearest ancestor containing cuttlefish.config.ts). Fall back to projectRoot
115
+ // itself if none is found (single-dir project where the app sits at root).
116
+ let workspaceRoot = resolve(projectRoot);
117
+ let dir = resolve(projectRoot);
118
+ for (let i = 0; i < 20; i++) {
119
+ if (existsSync(join(dir, 'cuttlefish.config.ts'))) {
120
+ workspaceRoot = dir;
121
+ break;
122
+ }
123
+ const parent = dirname(dir);
124
+ if (parent === dir)
125
+ break;
126
+ dir = parent;
127
+ }
128
+ const sketchRel = relative(workspaceRoot, resolve(projectRoot)).replace(/\\/g, '/');
129
+ return { workspaceRoot, sketchRel };
130
+ }
131
+ /** Read-merge-write a JSON file, adding/replacing a single config by a key. */
132
+ function mergeJsonArrayEntry(filePath, arrayKey, matchKey, entry) {
133
+ let doc = {};
134
+ if (existsSync(filePath)) {
135
+ try {
136
+ doc = JSON.parse(readFileSync(filePath, 'utf-8'));
137
+ }
138
+ catch {
139
+ // malformed — start fresh
140
+ }
141
+ }
142
+ const arr = Array.isArray(doc[arrayKey]) ? doc[arrayKey] : [];
143
+ const idx = arr.findIndex((e) => e[matchKey] === entry[matchKey]);
144
+ if (idx >= 0)
145
+ arr[idx] = entry;
146
+ else
147
+ arr.push(entry);
148
+ doc[arrayKey] = arr;
149
+ mkdirSync(dirname(filePath), { recursive: true });
150
+ writeFileSync(filePath, JSON.stringify(doc, null, 2) + '\n', 'utf-8');
151
+ }
152
+ /**
153
+ * Generate the GDB Python frame-filter that rewrites cuttlefish's hoisted
154
+ * lambda frame names (`${prefix}_isr_N`) into readable `<lambda> @ file:line`
155
+ * in the call stack. Only emitted when the source map references `_isr_N`
156
+ * symbols; otherwise returns null (launch.json omits the `source` initCommand).
157
+ *
158
+ * Ported verbatim from the deleted framework-esp32/toolchain/gdb-script.ts —
159
+ * the filter is cuttlefish-internal (lambda hoisting is framework-agnostic).
160
+ */
161
+ export function generateGdbScript(sourceMapPath) {
162
+ if (!sourceMapPath || !existsSync(sourceMapPath))
163
+ return null;
164
+ let mapText = '';
165
+ try {
166
+ mapText = readFileSync(sourceMapPath, 'utf-8');
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ // Only emit when the emitted code contains hoisted-lambda symbols.
172
+ if (!/_isr_\d+/.test(mapText))
173
+ return null;
174
+ // A GDB Python frame-filter. Registered via the launch.json initCommand
175
+ // `source <path>` so GDB auto-loads it on attach.
176
+ return [
177
+ '# Auto-generated by @typecad/framework-zephyr. GDB frame-filter that',
178
+ '# rewrites cuttlefish hoisted-lambda frame names (*_isr_N) into readable',
179
+ '# <lambda> form so the VS Code call stack is legible.',
180
+ 'import gdb',
181
+ 'import re',
182
+ '',
183
+ 'class CuttlefishLambdaFilter:',
184
+ ' def __init__(self):',
185
+ ' self.name = "cuttlefish_lambda"',
186
+ ' self.priority = 100',
187
+ ' self.enabled = True',
188
+ '',
189
+ ' def filter(self, frame_iter):',
190
+ ' isr_re = re.compile(r"(.*)_isr_(\\d+)")',
191
+ ' return (CuttlefishFrame(f) for f in frame_iter)',
192
+ '',
193
+ 'class CuttlefishFrame:',
194
+ ' def __init__(self, frame):',
195
+ ' self.frame = frame',
196
+ ' def __getattr__(self, name):',
197
+ ' val = getattr(self.frame, name)',
198
+ ' if name == "function":',
199
+ ' m = isr_re.match(val)',
200
+ ' if m: return "<lambda>"',
201
+ ' return val',
202
+ '',
203
+ 'gdb.frame_filters[CuttlefishLambdaFilter().name] = CuttlefishLambdaFilter()',
204
+ '',
205
+ ].join('\n');
206
+ }
207
+ /**
208
+ * Build the cortex-debug launch.json config for an ESP32-S3 (built-in USB-JTAG).
209
+ * `openOcdCfgRel` is the workspace-relative path to the generated
210
+ * .cuttlefish/openocd.cfg, passed to cortex-debug's configFiles.
211
+ */
212
+ function buildLaunchConfig(o, gdbScriptRel, openOcdCfgRel) {
213
+ // The ELF is at <projectRoot>/build/zephyr/zephyr.elf (Zephyr's standard
214
+ // build output). cortex-debug uses `executable` (not `program`).
215
+ const executable = `\${workspaceFolder}/${o.sketchRel}/build/zephyr/zephyr.elf`;
216
+ // OpenOCD cfg relative to the workspace root so cortex-debug can pass it
217
+ // via the -f flag. Must be a list; cortex-debug prepends -f per entry.
218
+ const configFiles = [`\${workspaceFolder}/${openOcdCfgRel}`];
219
+ // GDB path resolved from the Zephyr SDK build cache (CMakeCache.txt).
220
+ const gdbPath = resolveGdbPath(o.buildDir, o.target);
221
+ // OpenOCD binary path — the Espressif fork (openocd-esp32) is required for
222
+ // the esp_usb_jtag adapter. cortex-debug's `serverpath` tells it where to
223
+ // find the binary (not on PATH by default).
224
+ const openocdPath = resolveOpenOcdPath();
225
+ // Post-attach commands executed after GDB connects to the OpenOCD gdbserver.
226
+ // set mem inaccessible-by-default off — suppresses "Cannot access memory"
227
+ // errors caused by overlapping Xtensa memory regions (flash-mapped
228
+ // 0x4200xxxx isn't accessible until the bootloader runs).
229
+ // mem 0x42000000 0x44000000 ro cache — tells GDB the app flash region IS
230
+ // read-only so -break-insert uses hw breakpoints, not sw breakpoints
231
+ // (which would fail with "Cannot access memory at 0x4200xxxx").
232
+ // monitor reset init — reset target + halt (bootloader maps flash)
233
+ // thb setup — temporary HW breakpoint at setup()
234
+ // c — continue; bootloader maps flash, breaks at setup()
235
+ //
236
+ // Paths use forward slashes — ${workspaceFolder} on Windows produces
237
+ // backslashes that GDB interprets as escape sequences (\t → tab, etc.).
238
+ const ws = o.workspaceRoot.replace(/\\/g, '/');
239
+ const postAttachCommands = [
240
+ `set directories ${ws}`,
241
+ 'set remote hardware-watchpoint-limit 2',
242
+ 'set remote hardware-breakpoint-limit 2',
243
+ 'set mem inaccessible-by-default off',
244
+ 'mem 0x42000000 0x44000000 ro cache',
245
+ 'monitor reset init',
246
+ 'thb setup',
247
+ 'c',
248
+ ];
249
+ if (gdbScriptRel) {
250
+ postAttachCommands.splice(1, 0, `source ${ws}/${gdbScriptRel}`);
251
+ }
252
+ const cfg = {
253
+ name: 'TypeCAD Debug (Zephyr, ESP32-S3)',
254
+ type: 'cortex-debug',
255
+ // Attach mode: no download (the ELF is already flashed). The server
256
+ // controller's attachCommands() just halts the target, then our
257
+ // postAttachCommands reset it, set a HW breakpoint at setup(), and
258
+ // continue. HW breakpoints use debug registers and work before the
259
+ // bootloader maps the app flash region.
260
+ request: 'attach',
261
+ cwd: '${workspaceFolder}',
262
+ executable,
263
+ servertype: 'openocd',
264
+ configFiles,
265
+ interface: 'jtag',
266
+ ...(gdbPath ? { gdbPath } : {}),
267
+ ...(openocdPath ? { serverpath: openocdPath } : {}),
268
+ postAttachCommands,
269
+ preLaunchTask: 'cuttlefish: build + flash (debug)',
270
+ };
271
+ // openOcdCfgRel is consumed by configFiles above. Referenced here only to
272
+ // keep the signature honest.
273
+ void openOcdCfgRel;
274
+ return cfg;
275
+ }
276
+ /**
277
+ * Build the tasks.json entry: rebuild + flash only. cortex-debug starts
278
+ * OpenOCD as a child process (servertype: "openocd"), so the preLaunch task
279
+ * just needs to build and upload — no isBackground / OpenOCD wrapping.
280
+ */
281
+ function buildTask(o) {
282
+ return {
283
+ label: 'cuttlefish: build + flash (debug)',
284
+ type: 'shell',
285
+ command: 'npx cuttlefish build --compile --upload --debug',
286
+ options: { cwd: `\${workspaceFolder}/${o.sketchRel}` },
287
+ group: { kind: 'build', isDefault: false },
288
+ problemMatcher: [],
289
+ };
290
+ }
291
+ /**
292
+ * The adapter speed the S3's built-in USB-Serial-JTAG runs stably at.
293
+ *
294
+ * The interface cfg (esp_usb_jtag.cfg) defaults to 40000 (40 MHz), the chip
295
+ * max. The USB-Serial-JTAG peripheral is a software bitq adapter that bit-bangs
296
+ * JTAG over USB bulk transfers; at 40 MHz it can't keep up, drops transfers
297
+ * (LIBUSB_ERROR_IO / "missing data from bitq interface"), and the reset/halt
298
+ * sequence silently fails — leaving the target running with no breakpoint set
299
+ * (the "debugger starts but never stops / buttons don't work" symptom).
300
+ *
301
+ * 4000 (4 MHz) is the empirically-stable speed for this peripheral.
302
+ */
303
+ const OPENOCD_ADAPTER_SPEED = 4000;
304
+ /**
305
+ * Build the OpenOCD cfg content. Sources the board's own openocd.cfg (which
306
+ * sets ESP_RTOS Zephyr + ESP_ONLYCPU + the esp_usb_jtag driver + the esp32s3
307
+ * target) then overrides the adapter speed AFTER the driver loads — the order
308
+ * that a bare `-c "adapter speed N"` cannot guarantee (OpenOCD applies -c args
309
+ * in command-line order relative to -f, and Cortex-Debug injects its own
310
+ * helper/RTOS tcl, so the override can land before the driver exists or be
311
+ * re-defaulted). Putting it in the cfg, after the source, is deterministic.
312
+ */
313
+ function buildOpenOcdCfg() {
314
+ return [
315
+ '# Auto-generated by @typecad/framework-zephyr. Do not edit — regenerate',
316
+ '# with `cuttlefish build --debug`. Sources the board cfg (which loads the',
317
+ '# esp_usb_jtag adapter driver + esp32s3 target + ESP_RTOS Zephyr) then',
318
+ '# overrides the adapter speed to a USB-JTAG-stable value AFTER the driver',
319
+ '# is loaded. See debug-config.ts for the rationale.',
320
+ 'source [find board/esp32s3-builtin.cfg]',
321
+ `adapter speed ${OPENOCD_ADAPTER_SPEED}`,
322
+ '',
323
+ ].join('\n');
324
+ }
325
+ /**
326
+ * Write all gdb-mode debug artifacts for the given target. Called from the
327
+ * toolchain compile() after a successful build when debugMode === 'gdb'.
328
+ *
329
+ * Writes (all idempotent):
330
+ * <workspaceRoot>/.vscode/launch.json (cortex-debug config)
331
+ * <workspaceRoot>/.vscode/tasks.json (build + flash preLaunch task)
332
+ * <projectRoot>/.cuttlefish/openocd.cfg (OpenOCD cfg, adapter speed override)
333
+ * <projectRoot>/.cuttlefish/.cuttlefish-gdb.py (lambda frame filter, conditional)
334
+ */
335
+ export function writeDebugConfig(o) {
336
+ const vscodeDir = join(o.workspaceRoot, '.vscode');
337
+ const cuttlefishDir = join(o.projectRoot, '.cuttlefish');
338
+ mkdirSync(cuttlefishDir, { recursive: true });
339
+ // openocd.cfg — written first so its relative path can be wired into the
340
+ // cortex-debug configFiles. cortex-debug starts OpenOCD as a child process
341
+ // and passes this file via -f.
342
+ const openOcdCfgPath = join(cuttlefishDir, 'openocd.cfg');
343
+ writeFileSync(openOcdCfgPath, buildOpenOcdCfg(), 'utf-8');
344
+ const openOcdCfgRel = `${o.sketchRel}/.cuttlefish/openocd.cfg`;
345
+ // launch.json — merge the cortex-debug config by name.
346
+ const gdbScript = generateGdbScript(o.sourceMapPath);
347
+ let gdbScriptRel;
348
+ if (gdbScript) {
349
+ const scriptPath = join(cuttlefishDir, '.cuttlefish-gdb.py');
350
+ writeFileSync(scriptPath, gdbScript, 'utf-8');
351
+ gdbScriptRel = `${o.sketchRel}/.cuttlefish/.cuttlefish-gdb.py`;
352
+ }
353
+ const launchConfig = buildLaunchConfig(o, gdbScriptRel, openOcdCfgRel);
354
+ mergeJsonArrayEntry(join(vscodeDir, 'launch.json'), 'configurations', 'name', launchConfig);
355
+ // tasks.json — merge the build+flash task by label. OpenOCD is managed by
356
+ // cortex-debug so the task is a simple synchronous build step.
357
+ const task = buildTask(o);
358
+ mergeJsonArrayEntry(join(vscodeDir, 'tasks.json'), 'tasks', 'label', task);
359
+ }
@@ -0,0 +1,73 @@
1
+ import type { ToolchainOptions, CompileResult, UploadResult } from '@typecad/cuttlefish/api/shared';
2
+ /**
3
+ * Derive the Zephyr project root from the cuttlefish-emitted source path.
4
+ *
5
+ * Cuttlefish emits `src/main.cpp` under the output dir. The CLI passes
6
+ * `sourcePath` = full path to `main.cpp` and `outputDir` = its parent (`src/`).
7
+ * For Zephyr, the project root is the parent of `src/` — one level above
8
+ * `outputDir`. Detect that shape and adjust; otherwise fall back to `outputDir`.
9
+ */
10
+ export declare function projectRootFromOptions(o: ToolchainOptions): string;
11
+ /**
12
+ * Build the `west flash` argument list for a board.
13
+ *
14
+ * Runner selection: each board's board.cmake declares a sensible default flash
15
+ * runner for its hardware (xiao_ble → nrfutil, esp32* → esptool), and `west
16
+ * flash` resolves it automatically. The framework only intervenes where the
17
+ * board default needs an argument it can't infer:
18
+ * - An explicit `zephyr.runner` (from cuttlefish.config.ts) always wins.
19
+ * - ESP32 boards forward the port via `--esp-device` (esptool reads the
20
+ * device from it); board.cmake still picks the runner.
21
+ * - Every other board trusts the board.cmake default. Previously this forced
22
+ * `--runner nrfjprog` for every non-ESP32 target, which broke boards whose
23
+ * default is not nrfjprog (xiao_ble defaults to nrfutil) and required
24
+ * Nordic J-Link tools that a USB-bootloader board does not have.
25
+ *
26
+ * Exported (pure) so the runner-selection contract is unit-testable without
27
+ * spawning west.
28
+ */
29
+ export declare function buildFlashArgs(buildDir: string, board: string, userRunner: string | undefined, port: string | undefined): string[];
30
+ /**
31
+ * Classify a `west flash` result as success/failure.
32
+ *
33
+ * west's exit status is authoritative except for one known race in the uf2
34
+ * runner on Windows: the UF2 bootloader reboots to run new firmware the instant
35
+ * the file copy completes, unmounting the USB-MSC drive before `shutil.copy`'s
36
+ * trailing `copymode`/chmod runs. That raises `OSError: [WinError 433] A
37
+ * device which does not exist was specified` and makes west exit non-zero —
38
+ * even though the firmware copied and flashed correctly (the LED blinks).
39
+ *
40
+ * The copy starting is logged ("Copying UF2 file to '<drive>'"); WinError 433
41
+ * during `copymode` after that point proves the data write finished and the
42
+ * drive only vanished on the metadata step. Treat that exact signature as
43
+ * success so the upload isn't reported as a failure. Genuine uf2 failures
44
+ * (no partition found, write errors before the copy) still surface as failures.
45
+ *
46
+ * Exported (pure) so the classification is unit-testable without spawning west.
47
+ */
48
+ export declare function classifyUploadResult(runner: string | undefined, status: number | null, output: string): boolean;
49
+ /**
50
+ * Cleanse the `west flash` output shown to the user.
51
+ *
52
+ * When classifyUploadResult has decided a non-zero west exit was the benign UF2
53
+ * race (firmware copied, drive unmounted on the trailing chmod), the raw output
54
+ * is a wall of Python traceback that reads like a hard failure. Drop everything
55
+ * after the "Copying UF2 file to" line — i.e. the entire traceback — so a
56
+ * successful flash reads as a success (the framework's ✓ Done follows). Non-race
57
+ * output is returned untouched; genuine errors stay fully visible for diagnosis.
58
+ *
59
+ * Exported (pure) so the cleansing is unit-testable without spawning west.
60
+ */
61
+ export declare function cleanseUploadOutput(runner: string | undefined, status: number | null, output: string): string;
62
+ /**
63
+ * FrameworkToolchain for Zephyr. Spec §3.5 (mirror of the ESP-IDF toolchain).
64
+ * The target board is carried via frameworkData.buildTarget; scaffolding
65
+ * happens at compile time when the target is known.
66
+ */
67
+ export declare const Toolchain: {
68
+ prepare(outputDir: string, entryPoint: string): void;
69
+ compile(o: ToolchainOptions): CompileResult;
70
+ upload(o: ToolchainOptions): UploadResult;
71
+ monitor(o: ToolchainOptions): void;
72
+ debug(o: ToolchainOptions): void;
73
+ };