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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) 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/boardgen.d.ts +1 -9
  6. package/dist/boardgen.js +228 -45
  7. package/dist/chips/types.d.ts +1 -1
  8. package/dist/debug-codegen.js +1 -1
  9. package/dist/display/touch-adapter.js +1 -1
  10. package/dist/display/ui-adapter.js +549 -549
  11. package/dist/doctor.js +4 -4
  12. package/dist/dt-config/custom-board.js +2 -2
  13. package/dist/dt-config/kconfig.js +18 -12
  14. package/dist/dt-config/overlay.d.ts +2 -2
  15. package/dist/dt-config/overlay.js +2 -2
  16. package/dist/framework.manifest.d.ts +3 -3
  17. package/dist/framework.manifest.js +10 -7
  18. package/dist/index.js +5 -5
  19. package/dist/licenses.d.ts +2 -2
  20. package/dist/licenses.js +8 -8
  21. package/dist/lowering/fs.js +1 -1
  22. package/dist/lowering/gpio.js +0 -32
  23. package/dist/lowering/http.js +264 -32
  24. package/dist/lowering/i2c.js +0 -8
  25. package/dist/lowering/interrupts.js +6 -0
  26. package/dist/lowering/mqtt.js +110 -8
  27. package/dist/lowering/usb.js +11 -11
  28. package/dist/lowering/wdt.js +2 -29
  29. package/dist/sdk/board-catalog-sync.d.ts +1 -3
  30. package/dist/sdk/board-catalog-sync.js +4 -10
  31. package/dist/strategy.d.ts +22 -44
  32. package/dist/strategy.js +152 -155
  33. package/dist/tmp-probe.d.ts +2 -0
  34. package/dist/tmp-probe.js +9 -0
  35. package/dist/toolchain/debug-config.d.ts +50 -90
  36. package/dist/toolchain/debug-config.js +239 -510
  37. package/dist/toolchain/env-check.d.ts +1 -3
  38. package/dist/toolchain/env-check.js +2 -7
  39. package/dist/toolchain/index.d.ts +16 -2
  40. package/dist/toolchain/index.js +383 -35
  41. package/dist/toolchain/runners.d.ts +16 -0
  42. package/dist/toolchain/runners.js +75 -0
  43. package/dist/toolchain/scaffold.d.ts +1 -1
  44. package/dist/toolchain/scaffold.js +12 -12
  45. package/dist/toolchain/west-discover.d.ts +6 -0
  46. package/dist/toolchain/west-discover.js +36 -13
  47. package/dist/toolchain/west-spawn.js +8 -2
  48. package/installer/README.md +328 -328
  49. package/installer/install.sh +2 -2
  50. package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
  51. package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
  52. package/installer/templates/project/.vscode/settings.json +1 -1
  53. package/installer/templates/project/README.md +2 -2
  54. package/package.json +5 -5
  55. package/src/as-built.ts +206 -206
  56. package/src/boardgen.ts +214 -48
  57. package/src/chips/types.ts +567 -567
  58. package/src/display/touch-adapter.ts +204 -204
  59. package/src/display/ui-adapter.ts +781 -781
  60. package/src/doctor.ts +4 -4
  61. package/src/dt-config/custom-board.ts +2 -2
  62. package/src/dt-config/kconfig.ts +18 -12
  63. package/src/dt-config/overlay.ts +1058 -1058
  64. package/src/framework.manifest.ts +538 -535
  65. package/src/index.ts +5 -5
  66. package/src/licenses.ts +425 -425
  67. package/src/lowering/fs.ts +135 -135
  68. package/src/lowering/gpio.ts +0 -33
  69. package/src/lowering/http.ts +264 -32
  70. package/src/lowering/i2c.ts +0 -9
  71. package/src/lowering/interrupts.ts +6 -0
  72. package/src/lowering/mqtt.ts +109 -8
  73. package/src/lowering/usb.ts +221 -221
  74. package/src/lowering/wdt.ts +2 -25
  75. package/src/sdk/board-catalog-sync.ts +4 -25
  76. package/src/strategy.ts +2265 -2309
  77. package/src/toolchain/debug-config.ts +262 -522
  78. package/src/toolchain/env-check.ts +279 -285
  79. package/src/toolchain/index.ts +1703 -1359
  80. package/src/toolchain/runners.ts +80 -0
  81. package/src/toolchain/scaffold.ts +296 -296
  82. package/src/toolchain/west-discover.ts +35 -13
  83. package/src/toolchain/west-spawn.ts +174 -168
  84. package/dist/async/timer-polyfill.d.ts +0 -10
  85. package/dist/async/timer-polyfill.js +0 -95
  86. package/dist/chips/board-overrides.d.ts +0 -7
  87. package/dist/chips/board-overrides.js +0 -11
  88. package/dist/chips/esp32.d.ts +0 -2
  89. package/dist/chips/esp32.js +0 -71
  90. package/dist/chips/esp32s3.d.ts +0 -2
  91. package/dist/chips/esp32s3.js +0 -103
  92. package/dist/chips/soc/.d.ts +0 -2
  93. package/dist/chips/soc/.js +0 -129
  94. package/dist/chips/soc/esp32.d.ts +0 -2
  95. package/dist/chips/soc/esp32.js +0 -120
  96. package/dist/chips/soc/esp32c3.d.ts +0 -2
  97. package/dist/chips/soc/esp32c3.js +0 -90
  98. package/dist/chips/soc/esp32c6.d.ts +0 -2
  99. package/dist/chips/soc/esp32c6.js +0 -109
  100. package/dist/chips/soc/esp32s3.d.ts +0 -2
  101. package/dist/chips/soc/esp32s3.js +0 -189
  102. package/dist/chips/soc/index.d.ts +0 -2
  103. package/dist/chips/soc/index.js +0 -23
  104. package/dist/chips/soc/nrf52840.d.ts +0 -2
  105. package/dist/chips/soc/nrf52840.js +0 -130
  106. package/dist/chips/soc/rp2040.d.ts +0 -2
  107. package/dist/chips/soc/rp2040.js +0 -141
  108. package/dist/chips/soc/rp2350a.d.ts +0 -2
  109. package/dist/chips/soc/rp2350a.js +0 -145
  110. package/dist/chips/soc/samd21g18a.d.ts +0 -2
  111. package/dist/chips/soc/samd21g18a.js +0 -143
  112. package/dist/chips/soc/stm32f411xe.d.ts +0 -2
  113. package/dist/chips/soc/stm32f411xe.js +0 -251
  114. package/dist/chips/xiao-ble.d.ts +0 -2
  115. package/dist/chips/xiao-ble.js +0 -100
  116. package/dist/lowering/power.d.ts +0 -9
  117. package/dist/lowering/power.js +0 -60
  118. package/dist/lowering/pulse.d.ts +0 -7
  119. package/dist/lowering/pulse.js +0 -51
  120. package/dist/lowering/tone.d.ts +0 -10
  121. package/dist/lowering/tone.js +0 -63
  122. package/dist/lowering/worker-backing.d.ts +0 -14
  123. package/dist/lowering/worker-backing.js +0 -79
  124. package/dist/lowering/worker.d.ts +0 -6
  125. package/dist/lowering/worker.js +0 -14
  126. package/dist/sdk/board-data.generated.d.ts +0 -2
  127. package/dist/sdk/board-data.generated.js +0 -4
  128. package/dist/sdk/catalog-walker.d.ts +0 -90
  129. package/dist/sdk/catalog-walker.js +0 -682
  130. package/dist/sdk/dts-reader.d.ts +0 -83
  131. package/dist/sdk/dts-reader.js +0 -596
  132. package/src/debug-codegen.ts +0 -207
@@ -40,7 +40,7 @@ export interface ZephyrEnvCheck {
40
40
  compatRange: string | undefined;
41
41
  /** Result of the compat-range check against the detected version. */
42
42
  compatStatus: CompatStatus;
43
- /** Raw board target from cuttlefish.config.ts, if configured. */
43
+ /** Raw board target from typecad-hal.config.ts, if configured. */
44
44
  buildTarget: string | undefined;
45
45
  /** buildTarget normalized for the installed Zephyr version (may equal it). */
46
46
  resolvedBoardTarget: string | undefined;
@@ -61,8 +61,6 @@ export type ZephyrEnvFailure = {
61
61
  fixCommand: string | undefined;
62
62
  };
63
63
  export type ZephyrEnvResult = ZephyrEnvOk | ZephyrEnvFailure;
64
- /** Clear the west-probe cache (for tests). Also resets discovery cache. */
65
- export declare function resetWestProbeCacheForTest(): void;
66
64
  /**
67
65
  * Gather west facts: discover a usable install, then run `west --version`
68
66
  * through it to capture the version. Memoized for the process lifetime (west
@@ -1,5 +1,5 @@
1
1
  // ---------------------------------------------------------------------------
2
- // Zephyr environment check — the shared detection behind `cuttlefish doctor`.
2
+ // Zephyr environment check — the shared detection behind `typecad-hal doctor`.
3
3
  //
4
4
  // Zephyr environment check: gather the impure
5
5
  // environment facts once (west presence + version, Zephyr version, board
@@ -21,16 +21,11 @@
21
21
  import { spawnSync } from 'node:child_process';
22
22
  import { existsSync, readdirSync } from 'node:fs';
23
23
  import { join } from 'node:path';
24
- import { discoverWest, resetWestDiscoveryCache } from './west-discover.js';
24
+ import { discoverWest } from './west-discover.js';
25
25
  import { westSpawn } from './west-spawn.js';
26
26
  import { detectZephyrVersion, checkZephyrCompat, resolveBoardTarget, } from './compat.js';
27
27
  // ---- west probe (impure; isolated + cached + overridable) ------------------
28
28
  let cachedProbe;
29
- /** Clear the west-probe cache (for tests). Also resets discovery cache. */
30
- export function resetWestProbeCacheForTest() {
31
- cachedProbe = undefined;
32
- resetWestDiscoveryCache();
33
- }
34
29
  /**
35
30
  * Gather west facts: discover a usable install, then run `west --version`
36
31
  * through it to capture the version. Memoized for the process lifetime (west
@@ -28,7 +28,7 @@ export interface ScannedSpiTarget {
28
28
  export declare function scanSpiTargets(src: string): ScannedSpiTarget[];
29
29
  export declare function scanSensorParts(src: string): ScannedSensorPart[];
30
30
  /**
31
- * Derive the Zephyr project root from the cuttlefish-emitted source path.
31
+ * Derive the Zephyr project root from the typecad-hal-emitted source path.
32
32
  *
33
33
  * Cuttlefish emits `src/main.cpp` under the output dir. The CLI passes
34
34
  * `sourcePath` = full path to `main.cpp` and `outputDir` = its parent (`src/`).
@@ -43,7 +43,7 @@ export declare function projectRootFromOptions(o: ToolchainOptions): string;
43
43
  * runner for its hardware (xiao_ble → nrfutil, esp32* → esptool), and `west
44
44
  * flash` resolves it automatically. The framework only intervenes where the
45
45
  * board default needs an argument it can't infer:
46
- * - An explicit `zephyr.runner` (from cuttlefish.config.ts) always wins.
46
+ * - An explicit `zephyr.runner` (from typecad-hal.config.ts) always wins.
47
47
  * - ESP32 boards forward the port via `--esp-device` (esptool reads the
48
48
  * device from it); board.cmake still picks the runner.
49
49
  * - Every other board trusts the board.cmake default. Previously this forced
@@ -78,6 +78,15 @@ export type ProbeResolution = {
78
78
  ok: false;
79
79
  error: string;
80
80
  };
81
+ /**
82
+ * The board a cached build dir was configured for (CMakeCache.txt's
83
+ * BOARD:STRING — the exact value passed to `west build -b`), or undefined
84
+ * when no cache exists. compile() compares it against the requested board
85
+ * and nukes the dir on mismatch: `west build`'s --pristine=auto covers
86
+ * cmake/config churn, NOT a board switch — west aborts with "refusing to
87
+ * proceed without --force", and cuttlefish doesn't forward that flag.
88
+ */
89
+ export declare function cachedBuildBoard(buildDir: string): string | undefined;
81
90
  export declare function resolveProbeMethod(zc: Record<string, unknown> | undefined, chip: ZephyrChipDescriptor, purpose?: 'flash' | 'debug'): ProbeResolution;
82
91
  /**
83
92
  * Run an openocd session against the board's probe config — the shared
@@ -197,4 +206,9 @@ export declare const Toolchain: {
197
206
  upload(o: ToolchainOptions): UploadResult;
198
207
  monitor(o: ToolchainOptions): void;
199
208
  debug(o: ToolchainOptions): void;
209
+ debugServer(o: ToolchainOptions, action: "start" | "stop"): void;
200
210
  };
211
+ /** How long `--monitor` waits for the port to (re)appear after a flash reset. */
212
+ export declare const MONITOR_PORT_WAIT_MS = 8000;
213
+ /** argv for the pyserial port wait — exported for the toolchain unit tests. */
214
+ export declare function serialPortWaitArgs(port: string, timeoutMs: number): string[];
@@ -10,14 +10,15 @@
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';
@@ -25,7 +26,8 @@ import { scaffoldZephyrProject, writeIfChanged, appendLibraryOverlayFragments }
25
26
  import { parseZephyrDts, asBuiltJson } from '../as-built.js';
26
27
  import { westSpawn, buildEnv } from './west-spawn.js';
27
28
  import { discoverWest } from './west-discover.js';
28
- import { writeDebugConfig, resolveDebugLocations } from './debug-config.js';
29
+ import { writeDebugConfig, resolveDebugLocations, debugArtifactsNeedRewrite, DEBUG_SERVER_PORT, DEBUG_SERVER_TCL_PORT } from './debug-config.js';
30
+ import { readRunnersFacts } from './runners.js';
29
31
  import { bossacTouchReset } from './bossac-touch.js';
30
32
  import { ZephyrStrategy } from '../strategy.js';
31
33
  import { generateOverlay } from '../dt-config/overlay.js';
@@ -255,12 +257,12 @@ function targetFromOptions(o) {
255
257
  const fcTarget = o.frameworkConfig?.target;
256
258
  const board = o.buildTarget ?? fcTarget;
257
259
  if (!board) {
258
- throw new Error('No build target: set board: in cuttlefish.config.ts (or frameworkData.buildTarget for custom-board projects).');
260
+ throw new Error('No build target: set board: in typecad-hal.config.ts (or frameworkData.buildTarget for custom-board projects).');
259
261
  }
260
262
  return board;
261
263
  }
262
264
  /**
263
- * Derive the Zephyr project root from the cuttlefish-emitted source path.
265
+ * Derive the Zephyr project root from the typecad-hal-emitted source path.
264
266
  *
265
267
  * Cuttlefish emits `src/main.cpp` under the output dir. The CLI passes
266
268
  * `sourcePath` = full path to `main.cpp` and `outputDir` = its parent (`src/`).
@@ -280,6 +282,23 @@ export function projectRootFromOptions(o) {
280
282
  */
281
283
  const BUILD_TIMEOUT_MS = 600_000;
282
284
  const FLASH_TIMEOUT_MS = 120_000;
285
+ /**
286
+ * The board a cached build dir was configured for (CMakeCache.txt's
287
+ * BOARD:STRING — the exact value passed to `west build -b`), or undefined
288
+ * when no cache exists. compile() compares it against the requested board
289
+ * and nukes the dir on mismatch: `west build`'s --pristine=auto covers
290
+ * cmake/config churn, NOT a board switch — west aborts with "refusing to
291
+ * proceed without --force", and cuttlefish doesn't forward that flag.
292
+ */
293
+ export function cachedBuildBoard(buildDir) {
294
+ try {
295
+ return readFileSync(join(buildDir, 'CMakeCache.txt'), 'utf-8')
296
+ .match(/^BOARD:STRING=(.+)$/m)?.[1]?.trim() || undefined;
297
+ }
298
+ catch {
299
+ return undefined; // no build dir / unreadable cache — treat as fresh
300
+ }
301
+ }
283
302
  export function resolveProbeMethod(zc, chip, purpose = 'flash') {
284
303
  const probe = zc?.probe;
285
304
  const runner = zc?.runner;
@@ -287,7 +306,7 @@ export function resolveProbeMethod(zc, chip, purpose = 'flash') {
287
306
  if (probe && runner) {
288
307
  return {
289
308
  ok: false,
290
- error: `cuttlefish.config.ts sets both zephyr.probe ('${probe}') and zephyr.runner ('${runner}'). ` +
309
+ error: `typecad-hal.config.ts sets both zephyr.probe ('${probe}') and zephyr.runner ('${runner}'). ` +
291
310
  `They are two ways to choose the probe method — remove one.`,
292
311
  };
293
312
  }
@@ -385,7 +404,7 @@ function openocdProbeSession(buildDir, zc, chip, commands) {
385
404
  let cfgArgs;
386
405
  let sessionCfg;
387
406
  if (cfgLines && cfgLines.length > 0) {
388
- sessionCfg = join(buildDir, 'cuttlefish-probe.cfg');
407
+ sessionCfg = join(buildDir, 'typecad-hal-probe.cfg');
389
408
  }
390
409
  else {
391
410
  // The board target's qualifier ('blackpill_f411ce/stm32f411xe' →
@@ -714,7 +733,7 @@ export const Toolchain = {
714
733
  // Custom-board generation: an MCU-only target (no board package) has no
715
734
  // upstream Zephyr board — generate one under boards/typecad/<name>/ from
716
735
  // 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.
736
+ // typecad-hal.config.ts; the board takes its name from the build target.
718
737
  // Idempotent — regenerated on every compile, before the overlay pass.
719
738
  if (zc?.customBoard === true) {
720
739
  const generated = generateCustomBoard(projectRoot, chip, board.split('/')[0]);
@@ -891,10 +910,15 @@ export const Toolchain = {
891
910
  // .ninja_deps, after which every ninja run fails with `dependency cycle`.
892
911
  // Plain source edits never reconfigure CMake, so they cannot trigger it —
893
912
  // 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) {
913
+ // Board switches DO need a nuke: `west build`'s --pristine=auto covers
914
+ // cmake/config churn, not a -b <board> mismatch — west aborts with
915
+ // "refusing to proceed without --force" and cuttlefish doesn't forward
916
+ // that flag, so the user would be stuck deleting the dir by hand. The
917
+ // cache names the board it was configured for (BOARD:STRING); detect the
918
+ // mismatch and apply west's own suggested remedy automatically.
919
+ const cachedBoard = cachedBuildBoard(buildDir);
920
+ const boardChanged = Boolean(cachedBoard && cachedBoard !== board);
921
+ if (configChanged || boardChanged) {
898
922
  try {
899
923
  rmSync(buildDir, { recursive: true, force: true });
900
924
  }
@@ -915,7 +939,7 @@ export const Toolchain = {
915
939
  }
916
940
  }
917
941
  catch { /* no overlay — let Zephyr auto-detect or build without one */ }
918
- // Append user cmake args from cuttlefish.config.ts zephyr.cmakeArgs.
942
+ // Append user cmake args from typecad-hal.config.ts zephyr.cmakeArgs.
919
943
  const userCmakeArgs = zc?.cmakeArgs;
920
944
  if (userCmakeArgs && userCmakeArgs.length > 0) {
921
945
  if (!buildArgs.includes('--'))
@@ -941,36 +965,39 @@ export const Toolchain = {
941
965
  const stdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
942
966
  const stderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
943
967
  const output = stdout + stderr + (pristineRetry
944
- ? '\n[cuttlefish] dependency cycle detected in the cached build dir — retried with a pristine build'
968
+ ? '\n[typecad-hal] dependency cycle detected in the cached build dir — retried with a pristine build'
945
969
  : '');
946
970
  // Prefix the build log with how west was resolved, for transparency.
947
971
  const header = `Using west via ${inv.install.source}` +
948
972
  (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) {
973
+ // After a successful build on a gdb-capable board, keep the VS Code debug
974
+ // artifacts current: always under --debug, or on a plain build when they
975
+ // need it (still in create-time starter shape, an outDir rename moved the
976
+ // app root, or a pre-west self-managed-server entry). Non-fatal on
977
+ // failure — a missing artifact doesn't block the build.
978
+ if (result.status === 0 && debugMode === 'gdb') {
955
979
  try {
956
980
  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
- });
981
+ if (isGdbDebug
982
+ || debugArtifactsNeedRewrite(workspaceRoot, appRel, readRunnersFacts(buildDir)?.gdb?.replace(/\\/g, '/'))) {
983
+ writeDebugConfig({
984
+ projectRoot,
985
+ workspaceRoot,
986
+ appRel,
987
+ target: board,
988
+ buildDir,
989
+ sourceMapPath: join(dirname(o.sourcePath), `${basename(o.sourcePath)}.thcppmap.json`),
990
+ });
991
+ }
965
992
  }
966
993
  catch (e) {
967
- console.warn(`[cuttlefish] gdb debug config generation failed: ${e.message}`);
994
+ console.warn(`[typecad-hal] gdb debug config generation failed: ${e.message}`);
968
995
  }
969
996
  }
970
997
  // As-built snapshot: after a successful build, the resolved devicetree
971
998
  // at <buildDir>/zephyr/zephyr.dts carries the board's pinctrl labels —
972
999
  // the STABLE name grammar, immune to vendor macro churn. Harvest its
973
- // routes into .cuttlefish/as-built.json; the next build's board-module
1000
+ // routes into .typecad-hal/as-built.json; the next build's board-module
974
1001
  // generation merges them per-pin over the catalog harvest (build wins,
975
1002
  // silently when they agree). One-build freshness lag on first setup,
976
1003
  // self-maintaining after. Best-effort — a missing/unparseable artifact
@@ -982,14 +1009,14 @@ export const Toolchain = {
982
1009
  const facts = parseZephyrDts(dtsText);
983
1010
  const total = facts.adc.length + facts.pwm.length + facts.dac.length;
984
1011
  if (total > 0) {
985
- // Write beside the project's board module — the .cuttlefish dir the
1012
+ // Write beside the project's board module — the .typecad-hal dir the
986
1013
  // config loader reads from, discovered by walking up to the
987
1014
  // generated board.json (the scaffold root and the config root are
988
1015
  // different dirs in the standard layout: src/out vs project root).
989
- let cfDir = join(projectRoot, '.cuttlefish');
1016
+ let cfDir = join(projectRoot, '.typecad-hal');
990
1017
  for (let dir = projectRoot;; dir = dirname(dir)) {
991
- if (existsSync(join(dir, '.cuttlefish', 'board.json'))) {
992
- cfDir = join(dir, '.cuttlefish');
1018
+ if (existsSync(join(dir, '.typecad-hal', 'board.json'))) {
1019
+ cfDir = join(dir, '.typecad-hal');
993
1020
  break;
994
1021
  }
995
1022
  const parent = dirname(dir);
@@ -1018,6 +1045,22 @@ export const Toolchain = {
1018
1045
  if (!probe.ok) {
1019
1046
  return { success: false, output: `-- west flash: ${probe.error}` };
1020
1047
  }
1048
+ // Flashing over a probe needs it EXCLUSIVE: a debug server still bound to
1049
+ // the gdb port (live session or an orphan whose wrapper died — a VS Code
1050
+ // window reload kills task terminals without killing their children on
1051
+ // Windows) makes openocd fail with LIBUSB_ERROR_ACCESS before any retry
1052
+ // logic can help. Reclaim it up front — it is ours by convention.
1053
+ {
1054
+ const holder = portOwnerPid(DEBUG_SERVER_PORT);
1055
+ if (holder !== undefined && holder !== process.pid) {
1056
+ console.log(`-- west flash: stopping debug server (pid ${holder}) — flashing needs exclusive probe access`);
1057
+ killPidTree(holder);
1058
+ try {
1059
+ rmSync(join(projectRoot, '.typecad-hal', 'debug-server.pid'), { force: true });
1060
+ }
1061
+ catch { /* already gone */ }
1062
+ }
1063
+ }
1021
1064
  // BOSSA bootloader boards with touch-reset data: open the app's console
1022
1065
  // port at 1200 baud (the firmware's USB shim reboots into the
1023
1066
  // bootloader), wait for the bootloader identity, and flash THAT port.
@@ -1035,7 +1078,7 @@ export const Toolchain = {
1035
1078
  if (uploadRequiresPort(flashRunner) && !flashPort) {
1036
1079
  return {
1037
1080
  success: false,
1038
- output: `-- upload requires a port for ${flashRunner} flashing. Set --port <port> on the command line (or the CUTTLEFISH_PORT env var).`,
1081
+ output: `-- upload requires a port for ${flashRunner} flashing. Set --port <port> on the command line (or the TYPECAD_HAL_PORT env var).`,
1039
1082
  };
1040
1083
  }
1041
1084
  if (flashRunner === 'bossac' && flashPort && chip.usb?.touchReset) {
@@ -1172,6 +1215,14 @@ export const Toolchain = {
1172
1215
  // python we spawn resolves pyserial from the same venv). Falls back to the
1173
1216
  // process env when no install is discovered.
1174
1217
  const env = install ? buildEnv(install) : process.env;
1218
+ // Post-flash re-enumeration: the chip resets when `--upload` finishes, the
1219
+ // OS tears the serial device object down and re-creates it, and miniterm's
1220
+ // single open can land inside that window (a FileNotFoundError even though
1221
+ // the board never unplugged). Wait for the port to (re)appear first.
1222
+ if (!waitForSerialPort(py, env, projectRootFromOptions(o), o.port)) {
1223
+ process.exitCode = 1;
1224
+ return;
1225
+ }
1175
1226
  spawnSync(py, ['-m', 'serial.tools.miniterm', o.port, String(baud)], {
1176
1227
  cwd: projectRootFromOptions(o),
1177
1228
  env,
@@ -1206,4 +1257,301 @@ export const Toolchain = {
1206
1257
  });
1207
1258
  spawnSync(inv.command, inv.args, inv.options);
1208
1259
  },
1260
+ debugServer(o, action) {
1261
+ const projectRoot = projectRootFromOptions(o);
1262
+ const buildDir = join(projectRoot, 'build');
1263
+ const pidFile = join(projectRoot, '.typecad-hal', 'debug-server.pid');
1264
+ if (action === 'stop') {
1265
+ stopDebugServer(pidFile);
1266
+ return;
1267
+ }
1268
+ // start: wrap `west debugserver` as a long-running foreground process (the
1269
+ // VS Code background task owns this process; postDebugTask runs `stop`).
1270
+ if (!existsSync(join(buildDir, 'zephyr', 'runners.yaml'))) {
1271
+ console.error(`! No build at ${buildDir} — run 'npm run compile' (or F5's preLaunchTask) first.`);
1272
+ process.exitCode = 1;
1273
+ return;
1274
+ }
1275
+ const stale = readStaleServerPid(pidFile);
1276
+ if (stale !== undefined) {
1277
+ // Already running (pid alive): just re-emit the ready marker so the
1278
+ // task's problem matcher completes immediately.
1279
+ console.log(`[typecad-hal] west debugserver already running (pid ${stale})`);
1280
+ console.log(`TYPECAD_HAL: debug server ready on ${DEBUG_SERVER_PORT}`);
1281
+ return;
1282
+ }
1283
+ try {
1284
+ rmSync(pidFile, { force: true });
1285
+ }
1286
+ catch { /* already gone */ }
1287
+ // A wrapper/west that died without cleanup can leave openocd bound to
1288
+ // the gdb port with a stale pidfile — reclaim it or the new server
1289
+ // cannot bind (and gdb would attach to the orphan).
1290
+ const orphan = portOwnerPid(DEBUG_SERVER_PORT);
1291
+ if (orphan !== undefined && orphan !== process.pid) {
1292
+ console.log(`[typecad-hal] reclaiming orphaned debug server on :${DEBUG_SERVER_PORT} (pid ${orphan})`);
1293
+ killPidTree(orphan);
1294
+ }
1295
+ // Runner + quirk parity with flash (resolveProbeMethod, same as `west
1296
+ // debug`): an explicit zephyr.probe selects the runner; either way, an
1297
+ // srst-based openocd cfg behind an unwired NRST (the probeRunnerQuirks
1298
+ // condition, read from the board's own method data) makes `reset init`
1299
+ // time out — the IDE's post-attach reset would hang the session. Apply
1300
+ // the core-reset override server-side unless the config's runnerArgs
1301
+ // already carry it (the create flow bakes it in).
1302
+ const board = targetFromOptions(o);
1303
+ const chip = chipForBuild(projectRoot, board);
1304
+ const zc = o.zephyrConfig;
1305
+ const probe = resolveProbeMethod(zc, chip, 'debug');
1306
+ // The runner west will actually drive: the explicit choice, else the
1307
+ // board's declared debug-runner default from the build's runners.yaml.
1308
+ const runner = ((probe.ok && probe.runner) || undefined)
1309
+ ?? readRunnersFacts(buildDir)?.debugRunner;
1310
+ const isOcd = runner === undefined || runner === 'openocd' || runner.startsWith('openocd');
1311
+ if (!isOcd) {
1312
+ console.warn(`! debug-server: the IDE wiring (ready marker, quirk args) targets the ` +
1313
+ `openocd runner; this build's debug runner is '${runner}'. Starting it plain — ` +
1314
+ `the F5 session may not connect.`);
1315
+ }
1316
+ const serverArgs = ['debugserver', '-d', buildDir];
1317
+ if (probe.ok) {
1318
+ if (probe.runner)
1319
+ serverArgs.push('--runner', probe.runner);
1320
+ serverArgs.push(...probe.args);
1321
+ }
1322
+ if (isOcd) {
1323
+ serverArgs.push('--gdb-port', String(DEBUG_SERVER_PORT),
1324
+ // Pinned so the readiness poll below has a deterministic port.
1325
+ '--tcl-port', String(DEBUG_SERVER_TCL_PORT),
1326
+ // The board's own openocd.cfg may declare gdb-attach/gdb-detach
1327
+ // events (reset-on-attach for standalone sessions). Under an
1328
+ // IDE-managed session a stop event mid-initialization aborts
1329
+ // debugger setup, so neutralize them: west's --cmd-pre-init lands
1330
+ // AFTER the cfg files in the openocd command line, so these win.
1331
+ '--cmd-pre-init', '$_TARGETNAME configure -event gdb-attach {}', '--cmd-pre-init', '$_TARGETNAME configure -event gdb-detach {}');
1332
+ const method = zc?.probe
1333
+ ? chip.probeMethods?.find((m) => m.id === zc?.probe)
1334
+ : chip.probeMethods?.find((m) => m.debug !== false);
1335
+ const cfg = method?.debugCfg ?? [];
1336
+ const srst = cfg.some((l) => /reset_config\s+srst/.test(l));
1337
+ const connectAssert = cfg.some((l) => /connect_assert_srst/.test(l));
1338
+ if (method?.runner === 'openocd' && srst && !connectAssert
1339
+ && !(probe.ok && probe.args.includes('--cmd-pre-init=reset_config none'))) {
1340
+ serverArgs.push('--cmd-pre-init', 'reset_config none');
1341
+ }
1342
+ }
1343
+ const inv = westSpawn(serverArgs, { cwd: projectRoot });
1344
+ const child = spawn(inv.command, inv.args, {
1345
+ ...inv.options,
1346
+ stdio: ['ignore', 'pipe', 'pipe'],
1347
+ // Own process group on POSIX so `stop` can signal the whole tree.
1348
+ ...(process.platform !== 'win32' ? { detached: true } : {}),
1349
+ });
1350
+ // @types/node 26 types ChildProcess's on/once through the internal
1351
+ // InternalEventEmitter base. When `tsc -b` rechecks this package in the
1352
+ // same solution pass that rebuilds its project references, that
1353
+ // inheritance can fail to surface and the spawned child's type loses
1354
+ // .on/.once (TS2339) — while stream types, which COPY the event
1355
+ // signatures, keep working. Subscribe through a structural copy of the
1356
+ // one signature this file needs: the copy-don't-inherit prescription
1357
+ // @types/node itself applies to multi-level emitter classes. The `unknown`
1358
+ // hop keeps the cast legal in both resolution states.
1359
+ const childExit = child;
1360
+ mkdirSync(join(projectRoot, '.typecad-hal'), { recursive: true });
1361
+ writeFileSync(pidFile, String(child.pid), 'utf-8');
1362
+ console.log(`[typecad-hal] starting west debugserver (gdb on localhost:${DEBUG_SERVER_PORT})`);
1363
+ // A reader that goes away (closed task terminal, piped head) must not
1364
+ // take the server down with an EPIPE.
1365
+ process.stdout?.on?.('error', () => { });
1366
+ process.stderr?.on?.('error', () => { });
1367
+ child.stdout?.on('data', (d) => process.stdout.write(d));
1368
+ child.stderr?.on('data', (d) => process.stderr.write(d));
1369
+ // Ready = the TCL port accepts connections. NOT the gdb port: openocd's
1370
+ // gdb server takes ONE client, so a TCP probe there both logs
1371
+ // "attempted 'gdb' connection rejected" and can race the real gdb
1372
+ // connection for the slot. The tcl listener opens at the END of openocd
1373
+ // init (after the gdb listener and the startup halt) — a truer signal —
1374
+ // and probes there are inert.
1375
+ const deadline = Date.now() + DEBUG_SERVER_START_TIMEOUT_MS;
1376
+ const poll = () => {
1377
+ const sock = netConnect(DEBUG_SERVER_TCL_PORT, '127.0.0.1');
1378
+ sock.once('connect', () => {
1379
+ sock.destroy();
1380
+ console.log(`TYPECAD_HAL: debug server ready on ${DEBUG_SERVER_PORT}`);
1381
+ });
1382
+ sock.once('error', () => {
1383
+ sock.destroy();
1384
+ if (child.exitCode !== null)
1385
+ return; // server died — exit handler reports
1386
+ if (Date.now() > deadline) {
1387
+ console.error(`! west debugserver did not open :${DEBUG_SERVER_TCL_PORT} within `
1388
+ + `${DEBUG_SERVER_START_TIMEOUT_MS / 1000}s — see its output above.`);
1389
+ stopDebugServer(pidFile);
1390
+ process.exitCode = 1;
1391
+ return;
1392
+ }
1393
+ setTimeout(poll, 250);
1394
+ });
1395
+ };
1396
+ poll();
1397
+ childExit.on('exit', (code) => {
1398
+ try {
1399
+ rmSync(pidFile, { force: true });
1400
+ }
1401
+ catch { /* already gone */ }
1402
+ // Exit before ready: surface as a task failure (the debugger never
1403
+ // connects and VS Code reports the background task's non-zero exit).
1404
+ if (code !== null && code !== 0)
1405
+ process.exitCode = code;
1406
+ });
1407
+ const forwardSignal = () => {
1408
+ stopDebugServer(pidFile);
1409
+ childExit.once('exit', () => process.exit(0));
1410
+ setTimeout(() => process.exit(0), 1500).unref();
1411
+ };
1412
+ process.on('SIGINT', forwardSignal);
1413
+ process.on('SIGTERM', forwardSignal);
1414
+ },
1209
1415
  };
1416
+ /** How long `debug-server start` waits for the gdb port before failing. */
1417
+ const DEBUG_SERVER_START_TIMEOUT_MS = 45_000;
1418
+ /**
1419
+ * Read the pidfile and return the pid when that process is still alive,
1420
+ * undefined otherwise (no file, dead pid, or unparseable). Best-effort.
1421
+ */
1422
+ function readStaleServerPid(pidFile) {
1423
+ try {
1424
+ const pid = Number.parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
1425
+ if (!Number.isInteger(pid))
1426
+ return undefined;
1427
+ process.kill(pid, 0); // throws ESRCH when dead
1428
+ return pid;
1429
+ }
1430
+ catch {
1431
+ return undefined;
1432
+ }
1433
+ }
1434
+ /**
1435
+ * The pid of whatever process is LISTENING on the gdb port — the recovery
1436
+ * path for orphaned servers (the wrapper and west can die while openocd
1437
+ * survives, e.g. a killed task terminal; the pidfile is then stale but the
1438
+ * port stays bound and the next session would attach to the orphan).
1439
+ * Best-effort: netstat on Windows, lsof on POSIX; undefined when the port is
1440
+ * free or the platform tool is unavailable.
1441
+ */
1442
+ function portOwnerPid(port) {
1443
+ try {
1444
+ if (process.platform === 'win32') {
1445
+ const out = spawnSync('netstat', ['-ano', '-p', 'tcp'], { encoding: 'utf-8' });
1446
+ if (out.status !== 0)
1447
+ return undefined;
1448
+ for (const line of (out.stdout ?? '').split(/\r?\n/)) {
1449
+ const cols = line.trim().split(/\s+/);
1450
+ if (cols.length >= 5 && cols[3] === 'LISTENING'
1451
+ && cols[1].endsWith(`:${port}`)) {
1452
+ const pid = Number.parseInt(cols[4], 10);
1453
+ if (Number.isInteger(pid))
1454
+ return pid;
1455
+ }
1456
+ }
1457
+ return undefined;
1458
+ }
1459
+ const out = spawnSync('lsof', ['-ti', `tcp:${port}`], { encoding: 'utf-8' });
1460
+ if (out.status !== 0 || !out.stdout?.trim())
1461
+ return undefined;
1462
+ const pid = Number.parseInt(out.stdout.trim().split(/\s+/)[0], 10);
1463
+ return Number.isInteger(pid) ? pid : undefined;
1464
+ }
1465
+ catch {
1466
+ return undefined;
1467
+ }
1468
+ }
1469
+ /** Kill a pid tree (Windows: taskkill /T; POSIX: the process group). */
1470
+ function killPidTree(pid) {
1471
+ if (process.platform === 'win32') {
1472
+ // /T: the whole tree (the pid may be the micromamba/west wrapper; openocd
1473
+ // is its grandchild). /F: force — the server has no stdin to close.
1474
+ spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { encoding: 'utf-8' });
1475
+ }
1476
+ else {
1477
+ try {
1478
+ process.kill(-pid, 'SIGTERM'); // the detached process group
1479
+ }
1480
+ catch {
1481
+ try {
1482
+ process.kill(pid, 'SIGTERM');
1483
+ }
1484
+ catch { /* already gone */ }
1485
+ }
1486
+ }
1487
+ }
1488
+ /** Kill the debug server tree (micromamba/west → openocd) and drop the pidfile.
1489
+ * Falls back to the gdb-port owner when the recorded pid is already dead —
1490
+ * that orphan would otherwise serve stale sessions forever. */
1491
+ function stopDebugServer(pidFile) {
1492
+ const pid = readStaleServerPid(pidFile) ?? portOwnerPid(DEBUG_SERVER_PORT);
1493
+ if (pid === undefined) {
1494
+ try {
1495
+ rmSync(pidFile, { force: true });
1496
+ }
1497
+ catch { /* already gone */ }
1498
+ console.log('[typecad-hal] debug server not running');
1499
+ return;
1500
+ }
1501
+ killPidTree(pid);
1502
+ try {
1503
+ rmSync(pidFile, { force: true });
1504
+ }
1505
+ catch { /* already gone */ }
1506
+ console.log(`[typecad-hal] debug server stopped (pid ${pid})`);
1507
+ }
1508
+ // -- serial monitor: post-flash re-enumeration wait --------------------------
1509
+ /** How long `--monitor` waits for the port to (re)appear after a flash reset. */
1510
+ export const MONITOR_PORT_WAIT_MS = 8000;
1511
+ /**
1512
+ * The pyserial port wait run before miniterm (argv built by
1513
+ * serialPortWaitArgs). Polls list_ports — never OPENS the port, because
1514
+ * toggling DTR on an open can itself reset some boards — until the device
1515
+ * name matches case-insensitively or the timeout elapses. Exit codes:
1516
+ * 0 port present, 1 timeout (message printed to stderr), 2 pyserial
1517
+ * unavailable (the caller lets miniterm surface the real error instead of
1518
+ * reporting a bogus timeout).
1519
+ */
1520
+ const SERIAL_PORT_WAIT_PY = [
1521
+ 'import sys, time',
1522
+ 'try:',
1523
+ ' from serial.tools import list_ports',
1524
+ 'except Exception:',
1525
+ ' sys.exit(2)',
1526
+ 'port, timeout = sys.argv[1], float(sys.argv[2])',
1527
+ 'def present():',
1528
+ ' return any(d.device.lower() == port.lower() for d in list_ports.comports())',
1529
+ 'if not present():',
1530
+ " print('waiting for %s to re-enumerate (the board resets after flashing)...' % port, flush=True)",
1531
+ ' end = time.time() + timeout',
1532
+ ' while not present():',
1533
+ ' if time.time() >= end:',
1534
+ " 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)",
1535
+ ' sys.exit(1)',
1536
+ ' time.sleep(0.25)',
1537
+ ].join('\n');
1538
+ /** argv for the pyserial port wait — exported for the toolchain unit tests. */
1539
+ export function serialPortWaitArgs(port, timeoutMs) {
1540
+ return ['-c', SERIAL_PORT_WAIT_PY, port, String(timeoutMs / 1000)];
1541
+ }
1542
+ /**
1543
+ * Wait for `port` to be listed by the venv's pyserial before miniterm opens
1544
+ * it once. True = proceed to miniterm (port present, or the wait itself was
1545
+ * best-effort-skipped — a missing python/pyserial lets miniterm show the
1546
+ * underlying failure); false = the port never came back (message printed).
1547
+ */
1548
+ function waitForSerialPort(py, env, cwd, port) {
1549
+ const res = spawnSync(py, serialPortWaitArgs(port, MONITOR_PORT_WAIT_MS), {
1550
+ cwd,
1551
+ env,
1552
+ stdio: 'inherit',
1553
+ });
1554
+ if (res.error || res.status === null || res.status === 2)
1555
+ return true;
1556
+ return res.status === 0;
1557
+ }
@@ -0,0 +1,16 @@
1
+ export interface RunnersFacts {
2
+ /** The board's declared default debug runner (e.g. 'openocd', 'jlink'). */
3
+ readonly debugRunner?: string;
4
+ /** Absolute path to the arch gdb binary (config.gdb). */
5
+ readonly gdb?: string;
6
+ /** All runners the board configured. */
7
+ readonly runners: readonly string[];
8
+ }
9
+ /**
10
+ * Parse the subset of runners.yaml cuttlefish consumes. Exported for tests.
11
+ * Returns undefined on shapes it does not understand (callers treat as
12
+ * "no facts" — never crash a build over diagnostics data).
13
+ */
14
+ export declare function parseRunnersYaml(text: string): RunnersFacts | undefined;
15
+ /** Read the resolved runner facts for a build dir. undefined when absent/unparseable. */
16
+ export declare function readRunnersFacts(buildDir: string): RunnersFacts | undefined;