@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
@@ -10,22 +10,25 @@
10
10
  // a SDK root is discovered. See west-discover.ts / west-spawn.ts.
11
11
  //
12
12
  // The board target is carried via frameworkData.buildTarget (populated as
13
- // ToolchainOptions.buildTarget by the cuttlefish CLI), defaulting to the
13
+ // ToolchainOptions.buildTarget by the typecad-hal CLI), defaulting to the
14
14
  // framework's canonical MVP target (xiao_ble).
15
15
  //
16
16
  // Mirrors framework-esp32/src/toolchain/index.ts structure: projectRoot derived
17
17
  // from outputDir, prepare is a no-op (scaffold happens in compile when the
18
18
  // target is known), GCC errors parsed via the shared parseCompileErrors helper.
19
19
  // ---------------------------------------------------------------------------
20
- import { spawnSync } from 'node:child_process';
20
+ import { spawn, spawnSync } from 'node:child_process';
21
+ import { connect as netConnect } from 'node:net';
21
22
  import { basename, delimiter, dirname, join } from 'node:path';
22
23
  import { readdirSync, readFileSync, mkdirSync, rmSync, existsSync, writeFileSync } from 'node:fs';
23
24
  import { parseCompileErrors } from '@typecad/cuttlefish/api/shared';
24
25
  import { scaffoldZephyrProject, writeIfChanged, appendLibraryOverlayFragments } from './scaffold.js';
25
26
  import { parseZephyrDts, asBuiltJson } from '../as-built.js';
27
+ import { stampBuildSbom } from '../sbom.js';
26
28
  import { westSpawn, buildEnv } from './west-spawn.js';
27
29
  import { discoverWest } from './west-discover.js';
28
- import { writeDebugConfig, resolveDebugLocations } from './debug-config.js';
30
+ import { writeDebugConfig, resolveDebugLocations, debugArtifactsNeedRewrite, DEBUG_SERVER_PORT, DEBUG_SERVER_TCL_PORT } from './debug-config.js';
31
+ import { readRunnersFacts } from './runners.js';
29
32
  import { bossacTouchReset } from './bossac-touch.js';
30
33
  import { ZephyrStrategy } from '../strategy.js';
31
34
  import { generateOverlay } from '../dt-config/overlay.js';
@@ -34,7 +37,7 @@ import { NO_BOARD_CHIP } from '../chips/index.js';
34
37
  import { resolveChipFromBoard } from '../chips/resolve.js';
35
38
  import { pwmDtAliasToken } from '../lowering/pwm.js';
36
39
  import { detectZephyrVersion, checkZephyrCompat, resolveBoardTarget } from './compat.js';
37
- import { DEFAULT_ZEPHYR_DISPLAY_PROFILE } from '../display/profiles.js';
40
+ import { DEFAULT_ZEPHYR_DISPLAY_PROFILE, profileFromEmittedSource } from '../display/profiles.js';
38
41
  /**
39
42
  * Resolve the chip for a build the same way the strategy does at emit time —
40
43
  * from the board constants the transpile persisted next to the emitted
@@ -194,6 +197,36 @@ function scanPwmUsedPins(src, chip) {
194
197
  }
195
198
  return pins;
196
199
  }
200
+ export function scanMatrix(src) {
201
+ // The on_key lowering's comment marker carries the construction lists.
202
+ const m = src.match(/cuttlefish-matrix: rows=([\d,]*) cols=([\d,]*)/);
203
+ if (!m)
204
+ return undefined;
205
+ const rows = m[1] ? m[1].split(',').filter(Boolean).map(Number) : [];
206
+ const cols = m[2] ? m[2].split(',').filter(Boolean).map(Number) : [];
207
+ if (rows.length === 0 || cols.length === 0)
208
+ return undefined;
209
+ return { rows, cols };
210
+ }
211
+ export function scanHid(src) {
212
+ // The shim's report buffer names are the presence signal.
213
+ if (src.includes('__tc_mouse_report'))
214
+ return 'mouse';
215
+ if (src.includes('__tc_kb_report'))
216
+ return 'keyboard';
217
+ return undefined;
218
+ }
219
+ export function scanStrips(src) {
220
+ // The shim's buffer declaration is the presence signal:
221
+ // `static struct led_rgb __tc_strip<N>_buf[<COUNT>];`
222
+ const out = new Map();
223
+ for (const m of src.matchAll(/__tc_strip(\d+)_buf\[(\d+)\]/g)) {
224
+ const idx = Number(m[1]);
225
+ const count = Number(m[2]);
226
+ out.set(idx, Math.max(out.get(idx) ?? 0, count));
227
+ }
228
+ return [...out.entries()].map(([index, count]) => ({ index, count }));
229
+ }
197
230
  /**
198
231
  * Bus controller indexes the emitted sources actually reference — the shim
199
232
  * declares one `__tc_<bus><N>_dev` state block per used instance (gated by
@@ -228,13 +261,23 @@ export function scanSpiTargets(src) {
228
261
  }
229
262
  export function scanSensorParts(src) {
230
263
  const out = new Map();
264
+ // 1-Wire sensors: the stem carries the data pin (w1 master's GPIO).
265
+ for (const m of src.matchAll(/__tc_sensor_([a-z0-9_]+)_w1_p(\d+)_dev\b/g)) {
266
+ out.set(`${m[1]}|w1|${m[2]}`, { part: m[1], busIndex: 0, port: parseInt(m[2], 10), busKind: 'w1', spiHz: 0, spiMode: 0, alertPin: -1, resolution: 12 });
267
+ }
231
268
  for (const m of src.matchAll(/__tc_sensor_([a-z0-9_]+)_(i2c|spi)(\d+)_(0x[0-9a-f]+|cs[0-9]+)_dev\b/g)) {
232
269
  const port = m[4].startsWith('0x') ? parseInt(m[4], 16) : parseInt(m[4].slice(2), 10);
233
- const ref = { part: m[1], busIndex: parseInt(m[3], 10), port, busKind: m[2], spiHz: 0, spiMode: 0, alertPin: -1 };
270
+ const ref = { part: m[1], busIndex: parseInt(m[3], 10), port, busKind: m[2], spiHz: 0, spiMode: 0, alertPin: -1, resolution: 12 };
234
271
  out.set(`${ref.part}|${ref.busKind}${ref.busIndex}|${ref.port}`, ref);
235
272
  }
236
273
  // Construction facts ride the state block's config comment — merge by
237
274
  // nodelabel so the scanner stays the single source for the overlay.
275
+ for (const m of src.matchAll(/tc-sensor-cfg: tc_([a-z0-9_]+)_w1_p(\d+) res=(\d+)/g)) {
276
+ const key = `${m[1]}|w1|${m[2]}`;
277
+ const existing = out.get(key);
278
+ if (existing)
279
+ existing.resolution = parseInt(m[3], 10);
280
+ }
238
281
  for (const m of src.matchAll(/tc-sensor-cfg: tc_([a-z0-9_]+)_(i2c|spi)(\d+)_(0x[0-9a-f]+|cs[0-9]+) hz=(\d+) mode=(\d+) alert=(-?\d+)/g)) {
239
282
  const port = m[4].startsWith('0x') ? parseInt(m[4], 16) : parseInt(m[4].slice(2), 10);
240
283
  const key = `${m[1]}|${m[2]}${m[3]}|${port}`;
@@ -255,12 +298,12 @@ function targetFromOptions(o) {
255
298
  const fcTarget = o.frameworkConfig?.target;
256
299
  const board = o.buildTarget ?? fcTarget;
257
300
  if (!board) {
258
- throw new Error('No build target: set board: in cuttlefish.config.ts (or frameworkData.buildTarget for custom-board projects).');
301
+ throw new Error('No build target: set board: in typecad-hal.config.ts (or frameworkData.buildTarget for custom-board projects).');
259
302
  }
260
303
  return board;
261
304
  }
262
305
  /**
263
- * Derive the Zephyr project root from the cuttlefish-emitted source path.
306
+ * Derive the Zephyr project root from the typecad-hal-emitted source path.
264
307
  *
265
308
  * Cuttlefish emits `src/main.cpp` under the output dir. The CLI passes
266
309
  * `sourcePath` = full path to `main.cpp` and `outputDir` = its parent (`src/`).
@@ -280,6 +323,23 @@ export function projectRootFromOptions(o) {
280
323
  */
281
324
  const BUILD_TIMEOUT_MS = 600_000;
282
325
  const FLASH_TIMEOUT_MS = 120_000;
326
+ /**
327
+ * The board a cached build dir was configured for (CMakeCache.txt's
328
+ * BOARD:STRING — the exact value passed to `west build -b`), or undefined
329
+ * when no cache exists. compile() compares it against the requested board
330
+ * and nukes the dir on mismatch: `west build`'s --pristine=auto covers
331
+ * cmake/config churn, NOT a board switch — west aborts with "refusing to
332
+ * proceed without --force", and cuttlefish doesn't forward that flag.
333
+ */
334
+ export function cachedBuildBoard(buildDir) {
335
+ try {
336
+ return readFileSync(join(buildDir, 'CMakeCache.txt'), 'utf-8')
337
+ .match(/^BOARD:STRING=(.+)$/m)?.[1]?.trim() || undefined;
338
+ }
339
+ catch {
340
+ return undefined; // no build dir / unreadable cache — treat as fresh
341
+ }
342
+ }
283
343
  export function resolveProbeMethod(zc, chip, purpose = 'flash') {
284
344
  const probe = zc?.probe;
285
345
  const runner = zc?.runner;
@@ -287,7 +347,7 @@ export function resolveProbeMethod(zc, chip, purpose = 'flash') {
287
347
  if (probe && runner) {
288
348
  return {
289
349
  ok: false,
290
- error: `cuttlefish.config.ts sets both zephyr.probe ('${probe}') and zephyr.runner ('${runner}'). ` +
350
+ error: `typecad-hal.config.ts sets both zephyr.probe ('${probe}') and zephyr.runner ('${runner}'). ` +
291
351
  `They are two ways to choose the probe method — remove one.`,
292
352
  };
293
353
  }
@@ -385,7 +445,7 @@ function openocdProbeSession(buildDir, zc, chip, commands) {
385
445
  let cfgArgs;
386
446
  let sessionCfg;
387
447
  if (cfgLines && cfgLines.length > 0) {
388
- sessionCfg = join(buildDir, 'cuttlefish-probe.cfg');
448
+ sessionCfg = join(buildDir, 'typecad-hal-probe.cfg');
389
449
  }
390
450
  else {
391
451
  // The board target's qualifier ('blackpill_f411ce/stm32f411xe' →
@@ -612,11 +672,13 @@ export const Toolchain = {
612
672
  const usesDisplay = uses('display_write') || uses('display_init')
613
673
  || uses('display_fill_rect') || uses('__tc_display_dev')
614
674
  || uses('CuttlefishDisplayTarget');
615
- // Both registered Zephyr display profiles use dtLabel 'display0', so the
616
- // default profile's overlay block (&display0 { status="okay" }) is correct
617
- // for either driver. Thread a non-default profile here only if a future
618
- // board carries a display node under a different nodelabel.
619
- const displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
675
+ // Recover the exact display profile from the marker the adapter stamped
676
+ // into the emitted source (the registry is the single source of truth).
677
+ // Fall back to the default profile only when a display is used but no
678
+ // marker matched (e.g. hand-written source).
679
+ const displayProfile = usesDisplay
680
+ ? (profileFromEmittedSource(src) ?? DEFAULT_ZEPHYR_DISPLAY_PROFILE)
681
+ : undefined;
620
682
  // Touch controller kind comes from which DT nodelabel the emitted adapter
621
683
  // references (FT6336U on I2C, XPT2046 on the display's SPI bus).
622
684
  const usesTouch = uses('ft6336u') || uses('touch_');
@@ -635,12 +697,29 @@ export const Toolchain = {
635
697
  spiTargets: spiTargetParts,
636
698
  usesSpi: uses('spi_') || uses('__tc_spi') || spiTargetParts.length > 0,
637
699
  usesUart: uses('__tc_uart'),
638
- usesUsb: uses('__tc_usb'),
700
+ usesUsb: uses('__tc_usb0'),
639
701
  usesPwm: uses('pwm_'),
640
702
  usesAdc: uses('adc_'),
641
703
  adcReadPins: scanAdcReadPins(src, chip),
642
704
  dacWritePins: scanDacWritePins(src),
643
705
  pwmUsedPins: scanPwmUsedPins(src, chip),
706
+ usesStrip: uses('led_strip'),
707
+ strips: scanStrips(src),
708
+ usesClock: uses('__tc_rtc'),
709
+ usesCan: uses('can_'),
710
+ canLoopback: uses('CAN_MODE_LOOPBACK'),
711
+ usesI2s: uses('i2s_'),
712
+ clockShimCounter: chip.hwtimer && chip.hwtimer.controllers.length > 0
713
+ ? {
714
+ label: chip.hwtimer.controllers[0].nodeLabel,
715
+ ...(chip.hwtimer.controllers[0].counterParent ? { parent: chip.hwtimer.controllers[0].counterParent } : {}),
716
+ }
717
+ : undefined,
718
+ usesHid: uses('__tc_hid_'),
719
+ hidProtocol: scanHid(src),
720
+ usesMatrix: uses('__tc_matrix'),
721
+ matrix: scanMatrix(src),
722
+ usesPower: uses('sys_poweroff'),
644
723
  i2cUsedInstances: scanUsedBusInstances(src, chip.i2c?.controllers, 'i2c'),
645
724
  spiUsedInstances: scanUsedBusInstances(src, chip.spi?.controllers, 'spi'),
646
725
  uartUsedInstances: scanUsedBusInstances(src, chip.uart?.controllers, 'uart'),
@@ -704,7 +783,8 @@ export const Toolchain = {
704
783
  const isGdbDebug = o.debug === true && debugMode === 'gdb';
705
784
  const zc = o.zephyrConfig;
706
785
  const userKconfig = zc?.kconfig;
707
- const configChanged = scaffoldZephyrProject(projectRoot, isGdbDebug, userKconfig, o.psram);
786
+ const traceCfg = zc?.trace;
787
+ const configChanged = scaffoldZephyrProject(projectRoot, isGdbDebug, userKconfig, o.psram, traceCfg);
708
788
  // Regenerate the DT overlay for the ACTUAL target board. prepare() writes
709
789
  // it for the default board (the real target is unknown until compile), so
710
790
  // the <default>.overlay it wrote does not match `west build -b <board>`.
@@ -714,7 +794,7 @@ export const Toolchain = {
714
794
  // Custom-board generation: an MCU-only target (no board package) has no
715
795
  // upstream Zephyr board — generate one under boards/typecad/<name>/ from
716
796
  // the chip's silicon data. Opt-in via `zephyr.customBoard: true` in
717
- // cuttlefish.config.ts; the board takes its name from the build target.
797
+ // typecad-hal.config.ts; the board takes its name from the build target.
718
798
  // Idempotent — regenerated on every compile, before the overlay pass.
719
799
  if (zc?.customBoard === true) {
720
800
  const generated = generateCustomBoard(projectRoot, chip, board.split('/')[0]);
@@ -738,28 +818,22 @@ export const Toolchain = {
738
818
  const usesDisplay = uses('display_write') || uses('display_init')
739
819
  || uses('display_fill_rect') || uses('__tc_display_dev')
740
820
  || uses('CuttlefishDisplayTarget');
741
- // Derive the display dimensions from the emitted adapter code
742
- // (display_width/height return the profile's w/h). This ensures the DT
743
- // overlay's width/height match the panel the adapter targets, not the
744
- // default profile — critical for drivers like ST7796S that initialize
745
- // the panel geometry from the DT node.
746
- let displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
747
- if (usesDisplay) {
748
- const wMatch = src.match(/display_width\(\)\s*\{\s*return\s+(\d+)\s*;\s*\}/);
749
- const hMatch = src.match(/display_height\(\)\s*\{\s*return\s+(\d+)\s*;\s*\}/);
750
- if (wMatch && hMatch) {
751
- displayProfile = {
752
- ...DEFAULT_ZEPHYR_DISPLAY_PROFILE,
753
- width: parseInt(wMatch[1], 10),
754
- height: parseInt(hMatch[1], 10),
755
- };
756
- }
757
- }
821
+ // Recover the exact display profile from the marker the adapter emitted
822
+ // (`typecad-display-profile: <driver>`), so the DT overlay's compatible
823
+ // string, geometry, and rotation match the panel the adapter targets.
824
+ // The width/height regex this replaces could produce franken-profiles —
825
+ // e.g. an ST7796S build (480x320) on the default ILI9341 profile's
826
+ // controller/compatible, which emitted an ilitek,ili9341 DT node for a
827
+ // panel the ST7796S adapter drives.
828
+ const displayProfile = usesDisplay
829
+ ? (profileFromEmittedSource(src) ?? DEFAULT_ZEPHYR_DISPLAY_PROFILE)
830
+ : undefined;
758
831
  // Extract display pin wiring (cs/dc/rst/spiFrequency/spiPins) from the
759
832
  // config display section so the DT overlay wires the MIPI DBI bridge to
760
833
  // the correct GPIOs + SPI bus pins.
761
834
  const dispCfg = o.display;
762
835
  const spiPins = (dispCfg?.spiPins ?? undefined);
836
+ const i2cPins = (dispCfg?.i2cPins ?? undefined);
763
837
  const wiring = dispCfg
764
838
  ? {
765
839
  cs: typeof dispCfg.cs === 'number' ? dispCfg.cs : undefined,
@@ -769,8 +843,12 @@ export const Toolchain = {
769
843
  sck: typeof spiPins?.sck === 'number' ? spiPins.sck : undefined,
770
844
  mosi: typeof spiPins?.mosi === 'number' ? spiPins.mosi : undefined,
771
845
  miso: typeof spiPins?.miso === 'number' ? spiPins.miso : undefined,
846
+ sda: typeof i2cPins?.sda === 'number' ? i2cPins.sda : undefined,
847
+ scl: typeof i2cPins?.scl === 'number' ? i2cPins.scl : undefined,
772
848
  backlightPin: typeof dispCfg.backlightPin === 'number' ? dispCfg.backlightPin : undefined,
773
849
  tearingEffectPin: typeof dispCfg.tearingEffectPin === 'number' ? dispCfg.tearingEffectPin : undefined,
850
+ busyPin: typeof dispCfg.busyPin === 'number' ? dispCfg.busyPin : undefined,
851
+ address: typeof dispCfg.address === 'number' ? dispCfg.address : undefined,
774
852
  }
775
853
  : undefined;
776
854
  // Extract touch pin wiring from the config display.touch section so the
@@ -822,13 +900,30 @@ export const Toolchain = {
822
900
  spiTargets: spiTargetParts,
823
901
  usesSpi: uses('spi_') || uses('__tc_spi') || spiTargetParts.length > 0,
824
902
  usesUart: uses('__tc_uart'),
825
- usesUsb: uses('__tc_usb'),
903
+ usesUsb: uses('__tc_usb0'),
826
904
  usesPwm: uses('pwm_'),
827
905
  usesAdc: uses('adc_'),
828
906
  adcReadPins: scanAdcReadPins(src, chip),
829
907
  usesDac: uses('dac_') || uses('__tc_dac'),
830
908
  dacWritePins: scanDacWritePins(src),
831
909
  pwmUsedPins: scanPwmUsedPins(src, chip),
910
+ usesStrip: uses('led_strip'),
911
+ strips: scanStrips(src),
912
+ usesHid: uses('__tc_hid_'),
913
+ hidProtocol: scanHid(src),
914
+ usesMatrix: uses('__tc_matrix'),
915
+ matrix: scanMatrix(src),
916
+ usesPower: uses('sys_poweroff'),
917
+ usesClock: uses('__tc_rtc'),
918
+ usesCan: uses('can_'),
919
+ canLoopback: uses('CAN_MODE_LOOPBACK'),
920
+ usesI2s: uses('i2s_'),
921
+ clockShimCounter: chip.hwtimer && chip.hwtimer.controllers.length > 0
922
+ ? {
923
+ label: chip.hwtimer.controllers[0].nodeLabel,
924
+ ...(chip.hwtimer.controllers[0].counterParent ? { parent: chip.hwtimer.controllers[0].counterParent } : {}),
925
+ }
926
+ : undefined,
832
927
  i2cUsedInstances: scanUsedBusInstances(src, chip.i2c?.controllers, 'i2c'),
833
928
  spiUsedInstances: scanUsedBusInstances(src, chip.spi?.controllers, 'spi'),
834
929
  uartUsedInstances: scanUsedBusInstances(src, chip.uart?.controllers, 'uart'),
@@ -891,10 +986,15 @@ export const Toolchain = {
891
986
  // .ninja_deps, after which every ninja run fails with `dependency cycle`.
892
987
  // Plain source edits never reconfigure CMake, so they cannot trigger it —
893
988
  // and the retry after the spawn below self-heals any path that still does.
894
- // Board switches need no nuke here: `west build` is --pristine=auto by
895
- // default and recreates the dir itself when -b <board> mismatches the
896
- // cached board.
897
- if (configChanged) {
989
+ // Board switches DO need a nuke: `west build`'s --pristine=auto covers
990
+ // cmake/config churn, not a -b <board> mismatch — west aborts with
991
+ // "refusing to proceed without --force" and cuttlefish doesn't forward
992
+ // that flag, so the user would be stuck deleting the dir by hand. The
993
+ // cache names the board it was configured for (BOARD:STRING); detect the
994
+ // mismatch and apply west's own suggested remedy automatically.
995
+ const cachedBoard = cachedBuildBoard(buildDir);
996
+ const boardChanged = Boolean(cachedBoard && cachedBoard !== board);
997
+ if (configChanged || boardChanged) {
898
998
  try {
899
999
  rmSync(buildDir, { recursive: true, force: true });
900
1000
  }
@@ -915,7 +1015,7 @@ export const Toolchain = {
915
1015
  }
916
1016
  }
917
1017
  catch { /* no overlay — let Zephyr auto-detect or build without one */ }
918
- // Append user cmake args from cuttlefish.config.ts zephyr.cmakeArgs.
1018
+ // Append user cmake args from typecad-hal.config.ts zephyr.cmakeArgs.
919
1019
  const userCmakeArgs = zc?.cmakeArgs;
920
1020
  if (userCmakeArgs && userCmakeArgs.length > 0) {
921
1021
  if (!buildArgs.includes('--'))
@@ -941,36 +1041,39 @@ export const Toolchain = {
941
1041
  const stdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
942
1042
  const stderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
943
1043
  const output = stdout + stderr + (pristineRetry
944
- ? '\n[cuttlefish] dependency cycle detected in the cached build dir — retried with a pristine build'
1044
+ ? '\n[typecad-hal] dependency cycle detected in the cached build dir — retried with a pristine build'
945
1045
  : '');
946
1046
  // Prefix the build log with how west was resolved, for transparency.
947
1047
  const header = `Using west via ${inv.install.source}` +
948
1048
  (inv.install.zephyrBase ? ` (ZEPHYR_BASE=${inv.install.zephyrBase})` : '') + '\n';
949
- // After a successful build in gdb mode (--debug on a probe-capable target),
950
- // write the VS Code launch.json + tasks.json + gdb-script artifacts so F5
951
- // attaches GDB to the chip's debug probe. Non-fatal on failure — a missing
952
- // artifact doesn't block the build. Mirrors the deleted framework-esp32
953
- // toolchain compile() debug-config wiring.
954
- if (result.status === 0 && isGdbDebug) {
1049
+ // After a successful build on a gdb-capable board, keep the VS Code debug
1050
+ // artifacts current: always under --debug, or on a plain build when they
1051
+ // need it (still in create-time starter shape, an outDir rename moved the
1052
+ // app root, or a pre-west self-managed-server entry). Non-fatal on
1053
+ // failure — a missing artifact doesn't block the build.
1054
+ if (result.status === 0 && debugMode === 'gdb') {
955
1055
  try {
956
1056
  const { workspaceRoot, appRel } = resolveDebugLocations(projectRoot);
957
- writeDebugConfig({
958
- projectRoot,
959
- workspaceRoot,
960
- appRel,
961
- target: board,
962
- buildDir,
963
- sourceMapPath: join(dirname(o.sourcePath), `${basename(o.sourcePath)}.thcppmap.json`),
964
- });
1057
+ if (isGdbDebug
1058
+ || debugArtifactsNeedRewrite(workspaceRoot, appRel, readRunnersFacts(buildDir)?.gdb?.replace(/\\/g, '/'))) {
1059
+ writeDebugConfig({
1060
+ projectRoot,
1061
+ workspaceRoot,
1062
+ appRel,
1063
+ target: board,
1064
+ buildDir,
1065
+ sourceMapPath: join(dirname(o.sourcePath), `${basename(o.sourcePath)}.thcppmap.json`),
1066
+ });
1067
+ }
965
1068
  }
966
1069
  catch (e) {
967
- console.warn(`[cuttlefish] gdb debug config generation failed: ${e.message}`);
1070
+ console.warn(`[typecad-hal] gdb debug config generation failed: ${e.message}`);
968
1071
  }
969
1072
  }
970
1073
  // As-built snapshot: after a successful build, the resolved devicetree
971
1074
  // at <buildDir>/zephyr/zephyr.dts carries the board's pinctrl labels —
972
1075
  // the STABLE name grammar, immune to vendor macro churn. Harvest its
973
- // routes into .cuttlefish/as-built.json; the next build's board-module
1076
+ // routes into .typecad-hal/as-built.json; the next build's board-module
974
1077
  // generation merges them per-pin over the catalog harvest (build wins,
975
1078
  // silently when they agree). One-build freshness lag on first setup,
976
1079
  // self-maintaining after. Best-effort — a missing/unparseable artifact
@@ -982,14 +1085,14 @@ export const Toolchain = {
982
1085
  const facts = parseZephyrDts(dtsText);
983
1086
  const total = facts.adc.length + facts.pwm.length + facts.dac.length;
984
1087
  if (total > 0) {
985
- // Write beside the project's board module — the .cuttlefish dir the
1088
+ // Write beside the project's board module — the .typecad-hal dir the
986
1089
  // config loader reads from, discovered by walking up to the
987
1090
  // generated board.json (the scaffold root and the config root are
988
1091
  // different dirs in the standard layout: src/out vs project root).
989
- let cfDir = join(projectRoot, '.cuttlefish');
1092
+ let cfDir = join(projectRoot, '.typecad-hal');
990
1093
  for (let dir = projectRoot;; dir = dirname(dir)) {
991
- if (existsSync(join(dir, '.cuttlefish', 'board.json'))) {
992
- cfDir = join(dir, '.cuttlefish');
1094
+ if (existsSync(join(dir, '.typecad-hal', 'board.json'))) {
1095
+ cfDir = join(dir, '.typecad-hal');
993
1096
  break;
994
1097
  }
995
1098
  const parent = dirname(dir);
@@ -1002,6 +1105,18 @@ export const Toolchain = {
1002
1105
  }
1003
1106
  catch { /* best-effort snapshot — nothing to harvest or unreadable */ }
1004
1107
  }
1108
+ // SBOM stamp: after a successful build, record what the binary is made of
1109
+ // — kernel + linked west modules with commit SHAs + the hashed firmware
1110
+ // artifact (CycloneDX 1.6, next to zephyr.bin) — so `typecad-hal sbom
1111
+ // --check` can gate releases on the record still matching the build
1112
+ // (EU CRA essential-requirements support). Best-effort: a west-list or
1113
+ // hashing failure never blocks the build.
1114
+ if (result.status === 0) {
1115
+ try {
1116
+ stampBuildSbom({ buildDir, board, projectRoot });
1117
+ }
1118
+ catch { /* best-effort SBOM stamp */ }
1119
+ }
1005
1120
  return {
1006
1121
  success: result.status === 0,
1007
1122
  output: header + output,
@@ -1018,6 +1133,22 @@ export const Toolchain = {
1018
1133
  if (!probe.ok) {
1019
1134
  return { success: false, output: `-- west flash: ${probe.error}` };
1020
1135
  }
1136
+ // Flashing over a probe needs it EXCLUSIVE: a debug server still bound to
1137
+ // the gdb port (live session or an orphan whose wrapper died — a VS Code
1138
+ // window reload kills task terminals without killing their children on
1139
+ // Windows) makes openocd fail with LIBUSB_ERROR_ACCESS before any retry
1140
+ // logic can help. Reclaim it up front — it is ours by convention.
1141
+ {
1142
+ const holder = portOwnerPid(DEBUG_SERVER_PORT);
1143
+ if (holder !== undefined && holder !== process.pid) {
1144
+ console.log(`-- west flash: stopping debug server (pid ${holder}) — flashing needs exclusive probe access`);
1145
+ killPidTree(holder);
1146
+ try {
1147
+ rmSync(join(projectRoot, '.typecad-hal', 'debug-server.pid'), { force: true });
1148
+ }
1149
+ catch { /* already gone */ }
1150
+ }
1151
+ }
1021
1152
  // BOSSA bootloader boards with touch-reset data: open the app's console
1022
1153
  // port at 1200 baud (the firmware's USB shim reboots into the
1023
1154
  // bootloader), wait for the bootloader identity, and flash THAT port.
@@ -1035,7 +1166,7 @@ export const Toolchain = {
1035
1166
  if (uploadRequiresPort(flashRunner) && !flashPort) {
1036
1167
  return {
1037
1168
  success: false,
1038
- output: `-- upload requires a port for ${flashRunner} flashing. Set --port <port> on the command line (or the CUTTLEFISH_PORT env var).`,
1169
+ output: `-- upload requires a port for ${flashRunner} flashing. Set --port <port> on the command line (or the TYPECAD_HAL_PORT env var).`,
1039
1170
  };
1040
1171
  }
1041
1172
  if (flashRunner === 'bossac' && flashPort && chip.usb?.touchReset) {
@@ -1172,6 +1303,14 @@ export const Toolchain = {
1172
1303
  // python we spawn resolves pyserial from the same venv). Falls back to the
1173
1304
  // process env when no install is discovered.
1174
1305
  const env = install ? buildEnv(install) : process.env;
1306
+ // Post-flash re-enumeration: the chip resets when `--upload` finishes, the
1307
+ // OS tears the serial device object down and re-creates it, and miniterm's
1308
+ // single open can land inside that window (a FileNotFoundError even though
1309
+ // the board never unplugged). Wait for the port to (re)appear first.
1310
+ if (!waitForSerialPort(py, env, projectRootFromOptions(o), o.port)) {
1311
+ process.exitCode = 1;
1312
+ return;
1313
+ }
1175
1314
  spawnSync(py, ['-m', 'serial.tools.miniterm', o.port, String(baud)], {
1176
1315
  cwd: projectRootFromOptions(o),
1177
1316
  env,
@@ -1206,4 +1345,301 @@ export const Toolchain = {
1206
1345
  });
1207
1346
  spawnSync(inv.command, inv.args, inv.options);
1208
1347
  },
1348
+ debugServer(o, action) {
1349
+ const projectRoot = projectRootFromOptions(o);
1350
+ const buildDir = join(projectRoot, 'build');
1351
+ const pidFile = join(projectRoot, '.typecad-hal', 'debug-server.pid');
1352
+ if (action === 'stop') {
1353
+ stopDebugServer(pidFile);
1354
+ return;
1355
+ }
1356
+ // start: wrap `west debugserver` as a long-running foreground process (the
1357
+ // VS Code background task owns this process; postDebugTask runs `stop`).
1358
+ if (!existsSync(join(buildDir, 'zephyr', 'runners.yaml'))) {
1359
+ console.error(`! No build at ${buildDir} — run 'npm run compile' (or F5's preLaunchTask) first.`);
1360
+ process.exitCode = 1;
1361
+ return;
1362
+ }
1363
+ const stale = readStaleServerPid(pidFile);
1364
+ if (stale !== undefined) {
1365
+ // Already running (pid alive): just re-emit the ready marker so the
1366
+ // task's problem matcher completes immediately.
1367
+ console.log(`[typecad-hal] west debugserver already running (pid ${stale})`);
1368
+ console.log(`TYPECAD_HAL: debug server ready on ${DEBUG_SERVER_PORT}`);
1369
+ return;
1370
+ }
1371
+ try {
1372
+ rmSync(pidFile, { force: true });
1373
+ }
1374
+ catch { /* already gone */ }
1375
+ // A wrapper/west that died without cleanup can leave openocd bound to
1376
+ // the gdb port with a stale pidfile — reclaim it or the new server
1377
+ // cannot bind (and gdb would attach to the orphan).
1378
+ const orphan = portOwnerPid(DEBUG_SERVER_PORT);
1379
+ if (orphan !== undefined && orphan !== process.pid) {
1380
+ console.log(`[typecad-hal] reclaiming orphaned debug server on :${DEBUG_SERVER_PORT} (pid ${orphan})`);
1381
+ killPidTree(orphan);
1382
+ }
1383
+ // Runner + quirk parity with flash (resolveProbeMethod, same as `west
1384
+ // debug`): an explicit zephyr.probe selects the runner; either way, an
1385
+ // srst-based openocd cfg behind an unwired NRST (the probeRunnerQuirks
1386
+ // condition, read from the board's own method data) makes `reset init`
1387
+ // time out — the IDE's post-attach reset would hang the session. Apply
1388
+ // the core-reset override server-side unless the config's runnerArgs
1389
+ // already carry it (the create flow bakes it in).
1390
+ const board = targetFromOptions(o);
1391
+ const chip = chipForBuild(projectRoot, board);
1392
+ const zc = o.zephyrConfig;
1393
+ const probe = resolveProbeMethod(zc, chip, 'debug');
1394
+ // The runner west will actually drive: the explicit choice, else the
1395
+ // board's declared debug-runner default from the build's runners.yaml.
1396
+ const runner = ((probe.ok && probe.runner) || undefined)
1397
+ ?? readRunnersFacts(buildDir)?.debugRunner;
1398
+ const isOcd = runner === undefined || runner === 'openocd' || runner.startsWith('openocd');
1399
+ if (!isOcd) {
1400
+ console.warn(`! debug-server: the IDE wiring (ready marker, quirk args) targets the ` +
1401
+ `openocd runner; this build's debug runner is '${runner}'. Starting it plain — ` +
1402
+ `the F5 session may not connect.`);
1403
+ }
1404
+ const serverArgs = ['debugserver', '-d', buildDir];
1405
+ if (probe.ok) {
1406
+ if (probe.runner)
1407
+ serverArgs.push('--runner', probe.runner);
1408
+ serverArgs.push(...probe.args);
1409
+ }
1410
+ if (isOcd) {
1411
+ serverArgs.push('--gdb-port', String(DEBUG_SERVER_PORT),
1412
+ // Pinned so the readiness poll below has a deterministic port.
1413
+ '--tcl-port', String(DEBUG_SERVER_TCL_PORT),
1414
+ // The board's own openocd.cfg may declare gdb-attach/gdb-detach
1415
+ // events (reset-on-attach for standalone sessions). Under an
1416
+ // IDE-managed session a stop event mid-initialization aborts
1417
+ // debugger setup, so neutralize them: west's --cmd-pre-init lands
1418
+ // AFTER the cfg files in the openocd command line, so these win.
1419
+ '--cmd-pre-init', '$_TARGETNAME configure -event gdb-attach {}', '--cmd-pre-init', '$_TARGETNAME configure -event gdb-detach {}');
1420
+ const method = zc?.probe
1421
+ ? chip.probeMethods?.find((m) => m.id === zc?.probe)
1422
+ : chip.probeMethods?.find((m) => m.debug !== false);
1423
+ const cfg = method?.debugCfg ?? [];
1424
+ const srst = cfg.some((l) => /reset_config\s+srst/.test(l));
1425
+ const connectAssert = cfg.some((l) => /connect_assert_srst/.test(l));
1426
+ if (method?.runner === 'openocd' && srst && !connectAssert
1427
+ && !(probe.ok && probe.args.includes('--cmd-pre-init=reset_config none'))) {
1428
+ serverArgs.push('--cmd-pre-init', 'reset_config none');
1429
+ }
1430
+ }
1431
+ const inv = westSpawn(serverArgs, { cwd: projectRoot });
1432
+ const child = spawn(inv.command, inv.args, {
1433
+ ...inv.options,
1434
+ stdio: ['ignore', 'pipe', 'pipe'],
1435
+ // Own process group on POSIX so `stop` can signal the whole tree.
1436
+ ...(process.platform !== 'win32' ? { detached: true } : {}),
1437
+ });
1438
+ // @types/node 26 types ChildProcess's on/once through the internal
1439
+ // InternalEventEmitter base. When `tsc -b` rechecks this package in the
1440
+ // same solution pass that rebuilds its project references, that
1441
+ // inheritance can fail to surface and the spawned child's type loses
1442
+ // .on/.once (TS2339) — while stream types, which COPY the event
1443
+ // signatures, keep working. Subscribe through a structural copy of the
1444
+ // one signature this file needs: the copy-don't-inherit prescription
1445
+ // @types/node itself applies to multi-level emitter classes. The `unknown`
1446
+ // hop keeps the cast legal in both resolution states.
1447
+ const childExit = child;
1448
+ mkdirSync(join(projectRoot, '.typecad-hal'), { recursive: true });
1449
+ writeFileSync(pidFile, String(child.pid), 'utf-8');
1450
+ console.log(`[typecad-hal] starting west debugserver (gdb on localhost:${DEBUG_SERVER_PORT})`);
1451
+ // A reader that goes away (closed task terminal, piped head) must not
1452
+ // take the server down with an EPIPE.
1453
+ process.stdout?.on?.('error', () => { });
1454
+ process.stderr?.on?.('error', () => { });
1455
+ child.stdout?.on('data', (d) => process.stdout.write(d));
1456
+ child.stderr?.on('data', (d) => process.stderr.write(d));
1457
+ // Ready = the TCL port accepts connections. NOT the gdb port: openocd's
1458
+ // gdb server takes ONE client, so a TCP probe there both logs
1459
+ // "attempted 'gdb' connection rejected" and can race the real gdb
1460
+ // connection for the slot. The tcl listener opens at the END of openocd
1461
+ // init (after the gdb listener and the startup halt) — a truer signal —
1462
+ // and probes there are inert.
1463
+ const deadline = Date.now() + DEBUG_SERVER_START_TIMEOUT_MS;
1464
+ const poll = () => {
1465
+ const sock = netConnect(DEBUG_SERVER_TCL_PORT, '127.0.0.1');
1466
+ sock.once('connect', () => {
1467
+ sock.destroy();
1468
+ console.log(`TYPECAD_HAL: debug server ready on ${DEBUG_SERVER_PORT}`);
1469
+ });
1470
+ sock.once('error', () => {
1471
+ sock.destroy();
1472
+ if (child.exitCode !== null)
1473
+ return; // server died — exit handler reports
1474
+ if (Date.now() > deadline) {
1475
+ console.error(`! west debugserver did not open :${DEBUG_SERVER_TCL_PORT} within `
1476
+ + `${DEBUG_SERVER_START_TIMEOUT_MS / 1000}s — see its output above.`);
1477
+ stopDebugServer(pidFile);
1478
+ process.exitCode = 1;
1479
+ return;
1480
+ }
1481
+ setTimeout(poll, 250);
1482
+ });
1483
+ };
1484
+ poll();
1485
+ childExit.on('exit', (code) => {
1486
+ try {
1487
+ rmSync(pidFile, { force: true });
1488
+ }
1489
+ catch { /* already gone */ }
1490
+ // Exit before ready: surface as a task failure (the debugger never
1491
+ // connects and VS Code reports the background task's non-zero exit).
1492
+ if (code !== null && code !== 0)
1493
+ process.exitCode = code;
1494
+ });
1495
+ const forwardSignal = () => {
1496
+ stopDebugServer(pidFile);
1497
+ childExit.once('exit', () => process.exit(0));
1498
+ setTimeout(() => process.exit(0), 1500).unref();
1499
+ };
1500
+ process.on('SIGINT', forwardSignal);
1501
+ process.on('SIGTERM', forwardSignal);
1502
+ },
1209
1503
  };
1504
+ /** How long `debug-server start` waits for the gdb port before failing. */
1505
+ const DEBUG_SERVER_START_TIMEOUT_MS = 45_000;
1506
+ /**
1507
+ * Read the pidfile and return the pid when that process is still alive,
1508
+ * undefined otherwise (no file, dead pid, or unparseable). Best-effort.
1509
+ */
1510
+ function readStaleServerPid(pidFile) {
1511
+ try {
1512
+ const pid = Number.parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
1513
+ if (!Number.isInteger(pid))
1514
+ return undefined;
1515
+ process.kill(pid, 0); // throws ESRCH when dead
1516
+ return pid;
1517
+ }
1518
+ catch {
1519
+ return undefined;
1520
+ }
1521
+ }
1522
+ /**
1523
+ * The pid of whatever process is LISTENING on the gdb port — the recovery
1524
+ * path for orphaned servers (the wrapper and west can die while openocd
1525
+ * survives, e.g. a killed task terminal; the pidfile is then stale but the
1526
+ * port stays bound and the next session would attach to the orphan).
1527
+ * Best-effort: netstat on Windows, lsof on POSIX; undefined when the port is
1528
+ * free or the platform tool is unavailable.
1529
+ */
1530
+ function portOwnerPid(port) {
1531
+ try {
1532
+ if (process.platform === 'win32') {
1533
+ const out = spawnSync('netstat', ['-ano', '-p', 'tcp'], { encoding: 'utf-8' });
1534
+ if (out.status !== 0)
1535
+ return undefined;
1536
+ for (const line of (out.stdout ?? '').split(/\r?\n/)) {
1537
+ const cols = line.trim().split(/\s+/);
1538
+ if (cols.length >= 5 && cols[3] === 'LISTENING'
1539
+ && cols[1].endsWith(`:${port}`)) {
1540
+ const pid = Number.parseInt(cols[4], 10);
1541
+ if (Number.isInteger(pid))
1542
+ return pid;
1543
+ }
1544
+ }
1545
+ return undefined;
1546
+ }
1547
+ const out = spawnSync('lsof', ['-ti', `tcp:${port}`], { encoding: 'utf-8' });
1548
+ if (out.status !== 0 || !out.stdout?.trim())
1549
+ return undefined;
1550
+ const pid = Number.parseInt(out.stdout.trim().split(/\s+/)[0], 10);
1551
+ return Number.isInteger(pid) ? pid : undefined;
1552
+ }
1553
+ catch {
1554
+ return undefined;
1555
+ }
1556
+ }
1557
+ /** Kill a pid tree (Windows: taskkill /T; POSIX: the process group). */
1558
+ function killPidTree(pid) {
1559
+ if (process.platform === 'win32') {
1560
+ // /T: the whole tree (the pid may be the micromamba/west wrapper; openocd
1561
+ // is its grandchild). /F: force — the server has no stdin to close.
1562
+ spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { encoding: 'utf-8' });
1563
+ }
1564
+ else {
1565
+ try {
1566
+ process.kill(-pid, 'SIGTERM'); // the detached process group
1567
+ }
1568
+ catch {
1569
+ try {
1570
+ process.kill(pid, 'SIGTERM');
1571
+ }
1572
+ catch { /* already gone */ }
1573
+ }
1574
+ }
1575
+ }
1576
+ /** Kill the debug server tree (micromamba/west → openocd) and drop the pidfile.
1577
+ * Falls back to the gdb-port owner when the recorded pid is already dead —
1578
+ * that orphan would otherwise serve stale sessions forever. */
1579
+ function stopDebugServer(pidFile) {
1580
+ const pid = readStaleServerPid(pidFile) ?? portOwnerPid(DEBUG_SERVER_PORT);
1581
+ if (pid === undefined) {
1582
+ try {
1583
+ rmSync(pidFile, { force: true });
1584
+ }
1585
+ catch { /* already gone */ }
1586
+ console.log('[typecad-hal] debug server not running');
1587
+ return;
1588
+ }
1589
+ killPidTree(pid);
1590
+ try {
1591
+ rmSync(pidFile, { force: true });
1592
+ }
1593
+ catch { /* already gone */ }
1594
+ console.log(`[typecad-hal] debug server stopped (pid ${pid})`);
1595
+ }
1596
+ // -- serial monitor: post-flash re-enumeration wait --------------------------
1597
+ /** How long `--monitor` waits for the port to (re)appear after a flash reset. */
1598
+ export const MONITOR_PORT_WAIT_MS = 8000;
1599
+ /**
1600
+ * The pyserial port wait run before miniterm (argv built by
1601
+ * serialPortWaitArgs). Polls list_ports — never OPENS the port, because
1602
+ * toggling DTR on an open can itself reset some boards — until the device
1603
+ * name matches case-insensitively or the timeout elapses. Exit codes:
1604
+ * 0 port present, 1 timeout (message printed to stderr), 2 pyserial
1605
+ * unavailable (the caller lets miniterm surface the real error instead of
1606
+ * reporting a bogus timeout).
1607
+ */
1608
+ const SERIAL_PORT_WAIT_PY = [
1609
+ 'import sys, time',
1610
+ 'try:',
1611
+ ' from serial.tools import list_ports',
1612
+ 'except Exception:',
1613
+ ' sys.exit(2)',
1614
+ 'port, timeout = sys.argv[1], float(sys.argv[2])',
1615
+ 'def present():',
1616
+ ' return any(d.device.lower() == port.lower() for d in list_ports.comports())',
1617
+ 'if not present():',
1618
+ " print('waiting for %s to re-enumerate (the board resets after flashing)...' % port, flush=True)",
1619
+ ' end = time.time() + timeout',
1620
+ ' while not present():',
1621
+ ' if time.time() >= end:',
1622
+ " print('%s did not come back within %ds - some boards re-enumerate under a different port name' % (port, int(timeout)), file=sys.stderr, flush=True)",
1623
+ ' sys.exit(1)',
1624
+ ' time.sleep(0.25)',
1625
+ ].join('\n');
1626
+ /** argv for the pyserial port wait — exported for the toolchain unit tests. */
1627
+ export function serialPortWaitArgs(port, timeoutMs) {
1628
+ return ['-c', SERIAL_PORT_WAIT_PY, port, String(timeoutMs / 1000)];
1629
+ }
1630
+ /**
1631
+ * Wait for `port` to be listed by the venv's pyserial before miniterm opens
1632
+ * it once. True = proceed to miniterm (port present, or the wait itself was
1633
+ * best-effort-skipped — a missing python/pyserial lets miniterm show the
1634
+ * underlying failure); false = the port never came back (message printed).
1635
+ */
1636
+ function waitForSerialPort(py, env, cwd, port) {
1637
+ const res = spawnSync(py, serialPortWaitArgs(port, MONITOR_PORT_WAIT_MS), {
1638
+ cwd,
1639
+ env,
1640
+ stdio: 'inherit',
1641
+ });
1642
+ if (res.error || res.status === null || res.status === 2)
1643
+ return true;
1644
+ return res.status === 0;
1645
+ }