@typecad/framework-zephyr 1.0.0-alpha.13 → 1.0.0-alpha.15

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 (191) hide show
  1. package/README.md +22 -35
  2. package/dist/as-built.d.ts +33 -0
  3. package/dist/as-built.js +188 -0
  4. package/dist/async/timer-polyfill.js +1 -1
  5. package/dist/boardgen.d.ts +128 -0
  6. package/dist/boardgen.js +1366 -0
  7. package/dist/chips/board-overrides.d.ts +7 -0
  8. package/dist/chips/board-overrides.js +11 -0
  9. package/dist/chips/controllers.d.ts +28 -8
  10. package/dist/chips/controllers.js +49 -12
  11. package/dist/chips/esp32s3.js +49 -0
  12. package/dist/chips/index.d.ts +6 -12
  13. package/dist/chips/index.js +18 -34
  14. package/dist/chips/resolve.d.ts +16 -2
  15. package/dist/chips/resolve.js +298 -15
  16. package/dist/chips/soc/.d.ts +2 -0
  17. package/dist/chips/soc/.js +129 -0
  18. package/dist/chips/soc/esp32.d.ts +2 -0
  19. package/dist/chips/soc/esp32.js +120 -0
  20. package/dist/chips/soc/esp32c3.d.ts +2 -0
  21. package/dist/chips/soc/esp32c3.js +90 -0
  22. package/dist/chips/soc/esp32c6.d.ts +2 -0
  23. package/dist/chips/soc/esp32c6.js +109 -0
  24. package/dist/chips/soc/esp32s3.d.ts +2 -0
  25. package/dist/chips/soc/esp32s3.js +189 -0
  26. package/dist/chips/soc/index.d.ts +2 -0
  27. package/dist/chips/soc/index.js +23 -0
  28. package/dist/chips/soc/nrf52840.d.ts +2 -0
  29. package/dist/chips/soc/nrf52840.js +130 -0
  30. package/dist/chips/soc/rp2040.d.ts +2 -0
  31. package/dist/chips/soc/rp2040.js +141 -0
  32. package/dist/chips/soc/rp2350a.d.ts +2 -0
  33. package/dist/chips/soc/rp2350a.js +145 -0
  34. package/dist/chips/soc/samd21g18a.d.ts +2 -0
  35. package/dist/chips/soc/samd21g18a.js +143 -0
  36. package/dist/chips/soc/stm32f411xe.d.ts +2 -0
  37. package/dist/chips/soc/stm32f411xe.js +251 -0
  38. package/dist/chips/types.d.ts +382 -12
  39. package/dist/chips/xiao-ble.js +33 -0
  40. package/dist/display/profiles.js +1 -1
  41. package/dist/doctor.js +38 -4
  42. package/dist/dt-config/custom-board.d.ts +23 -0
  43. package/dist/dt-config/custom-board.js +227 -0
  44. package/dist/dt-config/kconfig.d.ts +83 -1
  45. package/dist/dt-config/kconfig.js +116 -27
  46. package/dist/dt-config/overlay.js +623 -18
  47. package/dist/framework.manifest.d.ts +20 -30
  48. package/dist/framework.manifest.js +124 -178
  49. package/dist/index.d.ts +4 -1
  50. package/dist/index.js +8 -2
  51. package/dist/lowering/adc.d.ts +26 -5
  52. package/dist/lowering/adc.js +137 -31
  53. package/dist/lowering/ble.js +31 -28
  54. package/dist/lowering/dac.js +9 -9
  55. package/dist/lowering/fs.js +18 -10
  56. package/dist/lowering/gpio.d.ts +2 -0
  57. package/dist/lowering/gpio.js +90 -10
  58. package/dist/lowering/http.js +39 -14
  59. package/dist/lowering/hwtimer.d.ts +4 -0
  60. package/dist/lowering/hwtimer.js +35 -10
  61. package/dist/lowering/i2c.js +46 -54
  62. package/dist/lowering/index.d.ts +5 -5
  63. package/dist/lowering/index.js +15 -16
  64. package/dist/lowering/interrupts.d.ts +22 -5
  65. package/dist/lowering/interrupts.js +104 -22
  66. package/dist/lowering/mqtt.js +12 -6
  67. package/dist/lowering/power.js +0 -4
  68. package/dist/lowering/preferences.js +49 -77
  69. package/dist/lowering/pulse.js +5 -36
  70. package/dist/lowering/pwm.d.ts +32 -3
  71. package/dist/lowering/pwm.js +105 -17
  72. package/dist/lowering/random.js +3 -3
  73. package/dist/lowering/sensor.d.ts +37 -0
  74. package/dist/lowering/sensor.js +119 -0
  75. package/dist/lowering/spi.d.ts +12 -0
  76. package/dist/lowering/spi.js +65 -40
  77. package/dist/lowering/thread.d.ts +14 -0
  78. package/dist/lowering/thread.js +55 -0
  79. package/dist/lowering/timing.js +20 -46
  80. package/dist/lowering/tone.js +18 -5
  81. package/dist/lowering/uart.d.ts +13 -0
  82. package/dist/lowering/uart.js +72 -51
  83. package/dist/lowering/usb.d.ts +34 -0
  84. package/dist/lowering/usb.js +206 -0
  85. package/dist/lowering/wdt.d.ts +1 -1
  86. package/dist/lowering/wdt.js +17 -7
  87. package/dist/lowering/wifi.d.ts +3 -2
  88. package/dist/lowering/wifi.js +120 -111
  89. package/dist/sdk/board-catalog-sync.d.ts +16 -0
  90. package/dist/sdk/board-catalog-sync.js +41 -0
  91. package/dist/sdk/board-data.generated.d.ts +2 -0
  92. package/dist/sdk/board-data.generated.js +4 -0
  93. package/dist/sdk/catalog-walker.d.ts +90 -0
  94. package/dist/sdk/catalog-walker.js +682 -0
  95. package/dist/sdk/dts-reader.d.ts +83 -0
  96. package/dist/sdk/dts-reader.js +596 -0
  97. package/dist/strategy.d.ts +136 -17
  98. package/dist/strategy.js +881 -212
  99. package/dist/toolchain/bossac-touch.d.ts +18 -0
  100. package/dist/toolchain/bossac-touch.js +88 -0
  101. package/dist/toolchain/compat.d.ts +2 -2
  102. package/dist/toolchain/compat.js +8 -3
  103. package/dist/toolchain/debug-config.d.ts +21 -26
  104. package/dist/toolchain/debug-config.js +212 -72
  105. package/dist/toolchain/env-check.d.ts +4 -4
  106. package/dist/toolchain/env-check.js +5 -5
  107. package/dist/toolchain/index.d.ts +66 -1
  108. package/dist/toolchain/index.js +713 -58
  109. package/dist/toolchain/scaffold.d.ts +9 -0
  110. package/dist/toolchain/scaffold.js +84 -7
  111. package/dist/toolchain/west-discover.d.ts +8 -1
  112. package/dist/toolchain/west-discover.js +19 -6
  113. package/dist/toolchain/west-spawn.js +17 -5
  114. package/installer/CHANGELOG.md +473 -0
  115. package/installer/README.md +328 -0
  116. package/installer/environment.yml +46 -0
  117. package/installer/etc/conda/activate.d/zephyr.bat +7 -0
  118. package/installer/etc/conda/activate.d/zephyr.ps1 +12 -0
  119. package/installer/etc/conda/activate.d/zephyr.sh +20 -0
  120. package/installer/etc/conda/deactivate.d/zephyr.bat +6 -0
  121. package/installer/etc/conda/deactivate.d/zephyr.ps1 +6 -0
  122. package/installer/etc/conda/deactivate.d/zephyr.sh +4 -0
  123. package/installer/install.mjs +580 -0
  124. package/installer/install.ps1 +619 -0
  125. package/installer/install.sh +310 -0
  126. package/installer/lib/detect-platform.sh +62 -0
  127. package/installer/lib/fetch-sdk.sh +304 -0
  128. package/installer/lib/init-workspace.sh +153 -0
  129. package/installer/lib/write-activation.sh +76 -0
  130. package/installer/patches/tfm-c-include-separators.patch +19 -0
  131. package/installer/patches/zephyr-tfm-nordic-zephyr-include.patch +24 -0
  132. package/installer/templates/project/.typecad/activate-zephyr.ps1 +31 -0
  133. package/installer/templates/project/.typecad/activate-zephyr.sh +31 -0
  134. package/installer/templates/project/.vscode/settings.json +30 -0
  135. package/installer/templates/project/README.md +58 -0
  136. package/installer/versions.env +110 -0
  137. package/package.json +16 -16
  138. package/src/as-built.ts +206 -0
  139. package/src/boardgen.ts +1428 -0
  140. package/src/chips/controllers.ts +61 -12
  141. package/src/chips/index.ts +35 -54
  142. package/src/chips/resolve.ts +316 -14
  143. package/src/chips/types.ts +388 -13
  144. package/src/display/profiles.ts +1 -1
  145. package/src/doctor.ts +110 -77
  146. package/src/dt-config/custom-board.ts +325 -0
  147. package/src/dt-config/kconfig.ts +177 -19
  148. package/src/dt-config/overlay.ts +1058 -450
  149. package/src/framework.manifest.ts +125 -178
  150. package/src/index.ts +57 -41
  151. package/src/lowering/adc.ts +222 -102
  152. package/src/lowering/ble.ts +32 -28
  153. package/src/lowering/dac.ts +82 -82
  154. package/src/lowering/fs.ts +18 -10
  155. package/src/lowering/gpio.ts +242 -152
  156. package/src/lowering/http.ts +512 -488
  157. package/src/lowering/hwtimer.ts +139 -101
  158. package/src/lowering/i2c.ts +116 -126
  159. package/src/lowering/index.ts +91 -93
  160. package/src/lowering/interrupts.ts +113 -21
  161. package/src/lowering/mqtt.ts +12 -6
  162. package/src/lowering/preferences.ts +327 -354
  163. package/src/lowering/pwm.ts +167 -74
  164. package/src/lowering/random.ts +3 -3
  165. package/src/lowering/sensor.ts +155 -0
  166. package/src/lowering/spi.ts +164 -124
  167. package/src/lowering/thread.ts +63 -0
  168. package/src/lowering/timing.ts +46 -72
  169. package/src/lowering/uart.ts +73 -51
  170. package/src/lowering/usb.ts +221 -0
  171. package/src/lowering/wdt.ts +95 -84
  172. package/src/lowering/wifi.ts +120 -111
  173. package/src/sdk/board-catalog-sync.ts +62 -0
  174. package/src/strategy.ts +888 -229
  175. package/src/toolchain/bossac-touch.ts +112 -0
  176. package/src/toolchain/compat.ts +8 -3
  177. package/src/toolchain/debug-config.ts +666 -522
  178. package/src/toolchain/env-check.ts +9 -9
  179. package/src/toolchain/index.ts +765 -44
  180. package/src/toolchain/scaffold.ts +81 -7
  181. package/src/toolchain/west-discover.ts +334 -316
  182. package/src/toolchain/west-spawn.ts +17 -5
  183. package/src/async/timer-polyfill.ts +0 -107
  184. package/src/chips/esp32.ts +0 -74
  185. package/src/chips/esp32s3.ts +0 -57
  186. package/src/chips/xiao-ble.ts +0 -70
  187. package/src/lowering/power.ts +0 -70
  188. package/src/lowering/pulse.ts +0 -93
  189. package/src/lowering/tone.ts +0 -60
  190. package/src/lowering/worker-backing.ts +0 -94
  191. package/src/lowering/worker.ts +0 -17
@@ -0,0 +1,682 @@
1
+ // ---------------------------------------------------------------------------
2
+ // catalog-walker.ts — walk a Zephyr tree's boards/ and build the board data
3
+ // catalog (records + provenance).
4
+ //
5
+ // For every board variant yaml that carries an `identifier:` (the qualified
6
+ // west build target) and a same-basename .dts, extract the board-level facts
7
+ // with the tolerant dts-reader. One record per board VARIANT (identifier),
8
+ // keyed by the identifier.
9
+ //
10
+ // This is the ONE walker shared by both catalog producers:
11
+ // - repo build: scripts/gen-zephyr-board-data.mjs emits the compiled-in
12
+ // pack (packages/cuttlefish/src/create/board-catalog.generated.ts);
13
+ // - user runtime: `cuttlefish board sync` (sdk/board-catalog-sync.ts)
14
+ // regenerates a local overlay from the user's own Zephyr tree, so board
15
+ // add/change/remove tracks `west update` instead of cuttlefish releases.
16
+ //
17
+ // Silicon facts (ADC channels, PWM matrices) are NOT extracted — they never
18
+ // live in devicetree. They are the curated chips/soc/ descriptors.
19
+ // ----------------------------------------------------------------------------
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { readBoardDts } from './dts-reader.js';
23
+ /** Cut an inline `# comment` — safe inside quoted values (variant yamls
24
+ * carry them, e.g. ai_m61_32s_kit's revision note). */
25
+ function stripYamlComment(value) {
26
+ const q = value[0];
27
+ if (q === '"' || q === "'") {
28
+ const end = value.indexOf(q, 1);
29
+ return end > 0 ? value.slice(0, end + 1) : value;
30
+ }
31
+ const hash = value.indexOf(' #');
32
+ return hash >= 0 ? value.slice(0, hash) : value;
33
+ }
34
+ /** Minimal line yaml reads (identifier:, name:) — full yaml not needed. */
35
+ function yamlField(file, field) {
36
+ const m = fs.readFileSync(file, 'utf8').match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
37
+ return m ? stripYamlComment(m[1]).trim() : undefined;
38
+ }
39
+ /** Read board.yml's board→soc index (see BoardYmlSocIndex). */
40
+ export function boardYmlSocIndex(dir) {
41
+ const yml = path.join(dir, 'board.yml');
42
+ if (!fs.existsSync(yml))
43
+ return { byName: new Map(), uniqueSocs: [], boardNames: [] };
44
+ const byName = new Map();
45
+ const uniqueSocs = [];
46
+ const boardNames = [];
47
+ let inBoardsList = false; // top-level `boards:` present (multi-board format)
48
+ let currentBoard;
49
+ let currentSocs = []; // socs collected for currentBoard
50
+ // A key line with its indent, innermost last. A `- name:` list entry is
51
+ // owned by the topmost key at an indent ≤ the entry's — the tree's
52
+ // board.ymls are not indent-consistent (xiao_ble puts soc entries at the
53
+ // same indent as the socs: key, lyra nests them two deeper), so the
54
+ // enclosing KEY, not the indentation depth, decides ownership.
55
+ const keyStack = [];
56
+ const flushEntry = () => {
57
+ // Only single-soc entries map board name → soc: multi-soc entries need
58
+ // qualified identifiers in their variant yamls.
59
+ if (currentBoard && currentSocs.length === 1)
60
+ byName.set(currentBoard, currentSocs[0]);
61
+ };
62
+ for (const rawLine of fs.readFileSync(yml, 'utf8').split('\n')) {
63
+ const line = rawLine.replace(/\r$/, '');
64
+ const trimmed = line.trim();
65
+ if (!trimmed || trimmed.startsWith('#'))
66
+ continue;
67
+ const indent = line.length - line.trimStart().length;
68
+ const entry = trimmed.match(/^- name:\s*(.+)$/);
69
+ const key = trimmed.match(/^([\w.-]+):(?:\s|$)/);
70
+ if (key) {
71
+ while (keyStack.length > 0 && keyStack[keyStack.length - 1].indent > indent)
72
+ keyStack.pop();
73
+ keyStack.push({ indent, key: key[1] });
74
+ if (indent === 0 && key[1] === 'boards')
75
+ inBoardsList = true;
76
+ continue;
77
+ }
78
+ // Legacy single-board header: `board: xiao_ble` (value on the key line).
79
+ if (indent === 0) {
80
+ const legacyBoard = trimmed.match(/^board:\s*(\S.*)$/);
81
+ if (legacyBoard)
82
+ boardNames.push(stripYamlComment(legacyBoard[1]).trim());
83
+ }
84
+ if (entry) {
85
+ const name = stripYamlComment(entry[1]).trim();
86
+ if (!name)
87
+ continue;
88
+ // Innermost enclosing key that owns a list at this entry's indent.
89
+ let owner;
90
+ for (let i = keyStack.length - 1; i >= 0; i--) {
91
+ if (keyStack[i].indent <= indent) {
92
+ owner = keyStack[i];
93
+ break;
94
+ }
95
+ }
96
+ if (owner?.key === 'boards') {
97
+ // A board entry under boards: — its socs follow under its own
98
+ // socs: key.
99
+ flushEntry();
100
+ currentBoard = name;
101
+ currentSocs = [];
102
+ boardNames.push(name);
103
+ }
104
+ else if (owner?.key === 'socs') {
105
+ if (inBoardsList)
106
+ currentSocs.push(name);
107
+ else
108
+ uniqueSocs.push(name);
109
+ }
110
+ // Every other list (variants:, …) is not ours.
111
+ }
112
+ }
113
+ flushEntry();
114
+ return { byName, uniqueSocs, boardNames };
115
+ }
116
+ /**
117
+ * west runner → probe-method metadata. Ids match the curated soc tables
118
+ * ('stlink', 'dfu', 'jlink', 'bossac', …) so `zephyr.probe` values stay
119
+ * stable across tiers; runners with no curated counterpart use their west
120
+ * runner name as the id (stm32cubeprogrammer, uf2, …). Flash-only tools
121
+ * carry debug: false (a bootloader is not a debugger — the debug path
122
+ * rejects them, resolveProbeMethod enforces it). Simulation-only runners
123
+ * (renode, simics) and debug-server helpers (stlink_gdbserver, trace32)
124
+ * stay unmapped — cuttlefish flashes real hardware.
125
+ */
126
+ const RUNNER_METHODS = {
127
+ 'openocd': { id: 'openocd', description: 'Any SWD/JTAG probe openocd supports', debug: true },
128
+ // Vendor-flavored openocd runner includes — the same openocd runner with
129
+ // vendor defaults (ST: onboard ST-Link; nRF5: nRF tap config). Without
130
+ // these, every ST board (118 in the tree) lost its primary probe method.
131
+ 'openocd-stm32': { id: 'openocd', description: 'ST-Link onboard or any SWD probe openocd supports', debug: true },
132
+ 'openocd-nrf5': { id: 'openocd', description: 'Any SWD/JTAG probe openocd supports (nRF5 defaults)', debug: true },
133
+ 'jlink': { id: 'jlink', description: 'J-Link probe (SWD)', debug: true },
134
+ 'dfu-util': { id: 'dfu', description: 'Built-in USB DFU bootloader: hold BOOT0, tap reset (no debug)', debug: false },
135
+ 'pyocd': { id: 'pyocd', description: 'Any CMSIS-DAP probe via pyOCD (no debug)', debug: false },
136
+ 'blackmagicprobe': { id: 'blackmagicprobe', description: 'Black Magic Probe (no debug)', debug: false },
137
+ 'stm32flash': { id: 'stm32flash', description: 'Built-in UART bootloader (no debug)', debug: false },
138
+ 'bossac': { id: 'bossac', description: 'Built-in USB bootloader: double-tap reset (no debug)', debug: false },
139
+ 'nrfjprog': { id: 'nrfjprog', description: 'Segger nRF command-line flasher (no debug)', debug: false },
140
+ 'esptool': { id: 'esptool', description: 'Espressif ROM bootloader over USB-serial (no debug)', debug: false },
141
+ // The ESP32 family's include name for the esptool runner.
142
+ 'esp32': { id: 'esptool', description: 'Espressif ROM bootloader over USB-serial (no debug)', debug: false },
143
+ 'linkserver': { id: 'linkserver', description: 'NXP LinkServer (no debug)', debug: false },
144
+ 'ezflashcli': { id: 'ezflashcli', description: 'Renesas EZ flash CLI (no debug)', debug: false },
145
+ 'stm32cubeprogrammer': { id: 'stm32cubeprogrammer', description: 'STM32CubeProgrammer CLI via ST-Link (no debug)', debug: false },
146
+ 'nrfutil': { id: 'nrfutil', description: 'nrfutil device — Nordic USB DFU / serial bootloader (no debug)', debug: false },
147
+ 'uf2': { id: 'uf2', description: 'UF2 bootloader: copy the firmware file (no debug)', debug: false },
148
+ 'silabs_commander': { id: 'silabs_commander', description: 'Simplicity Commander CLI (no debug)', debug: false },
149
+ 'probe-rs': { id: 'probe-rs', description: 'probe-rs flasher (no debug)', debug: false },
150
+ 'rfp': { id: 'rfp', description: 'Renesas Flash Programmer CLI (no debug)', debug: false },
151
+ 'bflb_mcu_tool': { id: 'bflb_mcu_tool', description: 'BouffaloLab bflb_mcu_tool (no debug)', debug: false },
152
+ 'xsdb': { id: 'xsdb', description: 'Xilinx xsdb flasher (no debug)', debug: false },
153
+ 'wchisp': { id: 'wchisp', description: 'WCH ISP bootloader (no debug)', debug: false },
154
+ 'spsdk': { id: 'spsdk', description: 'NXP SPSDK blhost (no debug)', debug: false },
155
+ 'minichlink': { id: 'minichlink', description: 'minichlink (WCH CH32, no debug)', debug: false },
156
+ 'mdb-hw': { id: 'mdb-hw', description: 'Synopsys MetaWare Debugger, hardware target (no debug)', debug: false },
157
+ 'wlink': { id: 'wlink', description: 'WCH-Link probe (no debug)', debug: false },
158
+ 'gd32isp': { id: 'gd32isp', description: 'GD32 UART ISP bootloader (no debug)', debug: false },
159
+ 'sftool': { id: 'sftool', description: 'SiFli sftool (no debug)', debug: false },
160
+ 'teensy': { id: 'teensy', description: 'Teensy loader (no debug)', debug: false },
161
+ 'nulink': { id: 'nulink', description: 'Nuvoton NuLink (no debug)', debug: false },
162
+ };
163
+ /**
164
+ * Scan board.cmake line-by-line tracking if/elseif/else/endif, recording
165
+ * every board_runner_args occurrence and every runner include with its
166
+ * condition stack. Variant selection happens later (pickArgs /
167
+ * activeRunners) — a dir shared by several boards guards each variant's
168
+ * runner args behind its own CONFIG_BOARD_<ID>, and boards guard whole
169
+ * RUNNERS behind core-specific configs (variscite's jlink sits inside
170
+ * if(CONFIG_SOC_MIMX8ML8_M7): the A53 target has no jlink at all).
171
+ * Args containing CMake variable references (${CONFIG_SOC}, …) only
172
+ * resolve inside the build — carried verbatim they would poison a real
173
+ * `west flash` invocation, so they are dropped at capture.
174
+ */
175
+ function scanBoardCmake(text) {
176
+ const runnerArgs = new Map();
177
+ const includes = [];
178
+ const conds = [];
179
+ for (const line of text.split('\n')) {
180
+ const branch = line.match(/^\s*(if|elseif|else|endif)\b\s*\(?\s*(.*?)\s*\)?\s*$/);
181
+ if (branch) {
182
+ const kw = branch[1];
183
+ const cond = kw === 'else' ? '' : branch[2];
184
+ if (kw === 'if')
185
+ conds.push(cond);
186
+ else if (kw === 'elseif')
187
+ conds[conds.length - 1] = cond;
188
+ else if (kw === 'else')
189
+ conds[conds.length - 1] = '';
190
+ else if (kw === 'endif')
191
+ conds.pop();
192
+ continue;
193
+ }
194
+ const argsM = line.match(/^\s*board_runner_args\(([\w.-]+)\s+(.*)\)\s*$/);
195
+ if (argsM) {
196
+ const args = [...argsM[2].matchAll(/"([^"]*)"/g)].map((q) => q[1]).filter((q) => !q.includes('${'));
197
+ const list = runnerArgs.get(argsM[1]) ?? [];
198
+ list.push({ conds: [...conds], args });
199
+ runnerArgs.set(argsM[1], list);
200
+ continue;
201
+ }
202
+ const incM = line.match(/^\s*include\(\$\{ZEPHYR_BASE\}\/boards\/common\/([\w.-]+)\.board\.cmake\)/);
203
+ if (incM)
204
+ includes.push({ name: incM[1], conds: [...conds] });
205
+ }
206
+ return { runnerArgs, includes };
207
+ }
208
+ /**
209
+ * Does one condition line apply to THIS target? Empty = always. A
210
+ * CONFIG_BOARD stem must match on a token boundary (a prefix must not
211
+ * match a longer board name); a CONFIG_SOC_<soc> guard matches any
212
+ * variant of that soc, and CONFIG_SOC_<soc>_<X> matches variants whose
213
+ * qualifiers include X.
214
+ */
215
+ function conditionMatchesTarget(cond, boardConfigs, soc, quals) {
216
+ if (!cond)
217
+ return true;
218
+ const cu = cond.toUpperCase();
219
+ for (const cfg of boardConfigs) {
220
+ if (new RegExp(`CONFIG_BOARD_${cfg}(?![A-Za-z0-9_])`).test(cu))
221
+ return true;
222
+ }
223
+ if (soc) {
224
+ const socUpper = soc.toUpperCase();
225
+ if (new RegExp(`CONFIG_SOC_${socUpper}(?![A-Za-z0-9_])`).test(cu))
226
+ return true;
227
+ if (quals.some((q) => cu.includes(`_${q.toUpperCase()}`)) && cu.includes(`CONFIG_SOC_${socUpper}`))
228
+ return true;
229
+ }
230
+ return false;
231
+ }
232
+ /**
233
+ * Pick the occurrence for one variant. Priority:
234
+ * 1. a condition naming one of this target's CONFIG_BOARD_<name> tokens
235
+ * (both Zephyr naming shapes: the board.yml entry name — duo_board_b —
236
+ * and the full underscored target — raytac's …_NRF5340_CPUNET), matched
237
+ * with a token boundary so CONFIG_BOARD_IMX8MM_EVK does not match
238
+ * …_EVK_MIMX8MM6_M4;
239
+ * 2. a CONFIG_SOC_<soc>_… condition mentioning one of the target's
240
+ * qualifier tokens (imx8mm guards its A53 args that way);
241
+ * 3. an unconditional occurrence;
242
+ * 4. the first (documented fallback).
243
+ */
244
+ function pickArgs(list, boardConfigs, soc, quals) {
245
+ for (const cfg of boardConfigs) {
246
+ const boardGuarded = list.find((o) => o.conds.some((c) => conditionMatchesTarget(c, [cfg], undefined, [])));
247
+ if (boardGuarded)
248
+ return boardGuarded.args;
249
+ }
250
+ if (soc && quals.length > 0) {
251
+ const socGuarded = list.find((o) => o.conds.some((c) => c && conditionMatchesTarget(c, [], soc, quals) && /CONFIG_SOC_/i.test(c)));
252
+ if (socGuarded)
253
+ return socGuarded.args;
254
+ }
255
+ const plain = list.find((o) => o.conds.length === 0 || o.conds.every((c) => c === ''));
256
+ if (plain)
257
+ return plain.args;
258
+ return list[0]?.args ?? [];
259
+ }
260
+ export function boardProbeMethods(dir, variant) {
261
+ const cmake = path.join(dir, 'board.cmake');
262
+ if (!fs.existsSync(cmake))
263
+ return undefined;
264
+ const text = fs.readFileSync(cmake, 'utf8');
265
+ const scan = scanBoardCmake(text);
266
+ const boardConfigs = variant?.boardConfigs ?? [];
267
+ const soc = variant?.soc;
268
+ const quals = variant?.quals ?? [];
269
+ // Active runners for THIS variant: unconditional includes always apply;
270
+ // guarded includes apply only when their condition names this target
271
+ // (variscite's jlink is inside if(CONFIG_SOC_…_M7) — the A53 target has
272
+ // no jlink at all, and offering it would flash the wrong core). Only
273
+ // ~24 files in the tree guard every include; a target matching none of
274
+ // a file's guards honestly gets no probe methods.
275
+ let includeNames = scan.includes.map((i) => i.name);
276
+ const anyGuarded = scan.includes.some((i) => i.conds.length > 0 && i.conds.some((c) => c !== ''));
277
+ if (anyGuarded) {
278
+ const active = new Set();
279
+ for (const inc of scan.includes) {
280
+ if (inc.conds.length === 0 || inc.conds.every((c) => c === ''))
281
+ active.add(inc.name);
282
+ else if (inc.conds.some((c) => conditionMatchesTarget(c, boardConfigs, soc, quals)))
283
+ active.add(inc.name);
284
+ }
285
+ includeNames = [...active];
286
+ }
287
+ // First include = west's default runner (preserve scan order).
288
+ const runners = [...new Set(includeNames)];
289
+ if (runners.length === 0)
290
+ return undefined;
291
+ let cfgLines = null;
292
+ const cfgPath = path.join(dir, 'support', 'openocd.cfg');
293
+ if (fs.existsSync(cfgPath)) {
294
+ cfgLines = fs.readFileSync(cfgPath, 'utf8').split('\n').map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith('#'));
295
+ }
296
+ const methods = [];
297
+ for (const runner of runners) {
298
+ // The vendor-flavored openocd includes get the same cfg treatment as
299
+ // plain openocd (stlink interface detection + debugCfg attach).
300
+ if (runner === 'openocd' || runner === 'openocd-stm32' || runner === 'openocd-nrf5') {
301
+ // An ST-Link interface cfg means the method is really 'stlink' — the
302
+ // curated-table id users know. debugCfg carries the whole cfg file,
303
+ // original line order (source/transport/events), comma-free join is
304
+ // not assumed by the consumer.
305
+ const isStlink = cfgLines?.some((l) => l.includes('interface/stlink')) ?? false;
306
+ methods.push({
307
+ id: isStlink ? 'stlink' : 'openocd',
308
+ description: isStlink
309
+ ? 'ST-Link or any SWD probe openocd supports (no BOOT0 needed) — also debugs'
310
+ : 'Any SWD/JTAG probe openocd supports — also debugs',
311
+ runner,
312
+ debug: true,
313
+ ...(cfgLines ? { debugCfg: cfgLines } : {}),
314
+ });
315
+ continue;
316
+ }
317
+ const meta = RUNNER_METHODS[runner];
318
+ if (!meta)
319
+ continue;
320
+ // Per-variant args: the occurrence whose CONFIG_BOARD / CONFIG_SOC
321
+ // guard names THIS variant wins (pickArgs).
322
+ const args = pickArgs(scan.runnerArgs.get(runner) ?? [], boardConfigs, soc, quals);
323
+ if (runner === 'jlink') {
324
+ const device = args.find((a) => a.startsWith('--device='));
325
+ methods.push({
326
+ id: 'jlink',
327
+ description: 'J-Link probe (SWD) — also debugs',
328
+ runner,
329
+ debug: true,
330
+ ...(device ? { debugDevice: device.slice('--device='.length) } : {}),
331
+ });
332
+ continue;
333
+ }
334
+ methods.push({ ...meta, runner, ...(args.length > 0 ? { args: [...args] } : {}) });
335
+ }
336
+ // Dedupe by method id: vendor-flavored includes can coexist with the
337
+ // plain one (openocd + openocd-stm32) — one method per id, first wins.
338
+ const seen = new Set();
339
+ const unique = methods.filter((m) => (seen.has(m.id) ? false : (seen.add(m.id), true)));
340
+ return unique.length > 0 ? unique : undefined;
341
+ }
342
+ /** VERSION file → 'MAJOR.MINOR.PATCH' ('' when unreadable). */
343
+ export function zephyrVersionOf(zephyrBase) {
344
+ try {
345
+ const text = fs.readFileSync(path.join(zephyrBase, 'VERSION'), 'utf8');
346
+ const m = text.match(/VERSION_MAJOR\s*=\s*(\d+)[\s\S]*?VERSION_MINOR\s*=\s*(\d+)/);
347
+ if (!m)
348
+ return '';
349
+ const patch = text.match(/PATCHLEVEL\s*=\s*(\d+)/);
350
+ return patch ? `${m[1]}.${m[2]}.${patch[1]}` : `${m[1]}.${m[2]}`;
351
+ }
352
+ catch {
353
+ return '';
354
+ }
355
+ }
356
+ /**
357
+ * Git HEAD of the tree, read straight off the filesystem (no git spawn —
358
+ * sync runs where git may be absent). Handles the plain clone (.git dir,
359
+ * HEAD → ref → loose ref or packed-refs) and the worktree/submodule form
360
+ * (.git file pointing at a gitdir). Best-effort: undefined when unreadable.
361
+ */
362
+ export function gitHeadOf(zephyrBase) {
363
+ try {
364
+ const dotGit = path.join(zephyrBase, '.git');
365
+ let gitDir = dotGit;
366
+ const st = fs.statSync(dotGit);
367
+ if (st.isFile()) {
368
+ const text = fs.readFileSync(dotGit, 'utf8');
369
+ const m = text.match(/gitdir:\s*(.+)/);
370
+ if (!m)
371
+ return undefined;
372
+ const dir = m[1].trim();
373
+ gitDir = path.isAbsolute(dir) ? dir : path.resolve(zephyrBase, dir);
374
+ }
375
+ const head = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
376
+ if (!head.startsWith('ref: '))
377
+ return head || undefined;
378
+ const ref = head.slice(5).trim();
379
+ const refFile = path.join(gitDir, ref);
380
+ if (fs.existsSync(refFile))
381
+ return fs.readFileSync(refFile, 'utf8').trim() || undefined;
382
+ const packed = fs.readFileSync(path.join(gitDir, 'packed-refs'), 'utf8');
383
+ const m = packed.match(new RegExp(`^([0-9a-f]{40,}) ${ref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm'));
384
+ return m ? m[1] : undefined;
385
+ }
386
+ catch {
387
+ return undefined;
388
+ }
389
+ }
390
+ /**
391
+ * Parse board.yml into its declared build targets, both shapes:
392
+ * multi-board (`boards:` list) and legacy (`board:` block), with socs and
393
+ * (nested) variants. Variant chains accumulate through nesting levels
394
+ * (board/soc/v1/v2).
395
+ */
396
+ export function boardYmlTargets(dir) {
397
+ const yml = path.join(dir, 'board.yml');
398
+ if (!fs.existsSync(yml))
399
+ return [];
400
+ const targets = [];
401
+ const boards = [];
402
+ let board;
403
+ let soc;
404
+ const keyStack = [];
405
+ const chainBases = [];
406
+ let chain = [];
407
+ const flushSoc = () => {
408
+ if (board && soc) {
409
+ board.socs.set(soc.name, soc.chains);
410
+ soc = undefined;
411
+ chain = [];
412
+ chainBases.length = 0;
413
+ }
414
+ };
415
+ const flushBoard = () => {
416
+ flushSoc();
417
+ board = undefined;
418
+ };
419
+ for (const rawLine of fs.readFileSync(yml, 'utf8').split('\n')) {
420
+ const line = rawLine.replace(/\r$/, '');
421
+ const trimmed = line.trim();
422
+ if (!trimmed || trimmed.startsWith('#'))
423
+ continue;
424
+ const indent = line.length - line.trimStart().length;
425
+ const entry = trimmed.match(/^- name:\s*(.+)$/);
426
+ const key = trimmed.match(/^([\w.-]+):(?:\s+(.*))?$/);
427
+ if (key && !trimmed.startsWith('-')) {
428
+ while (keyStack.length > 0 && keyStack[keyStack.length - 1].indent > indent)
429
+ keyStack.pop();
430
+ const k = key[1];
431
+ const v = (key[2] ?? '').trim();
432
+ if (indent === 0 && k === 'boards') {
433
+ flushBoard();
434
+ }
435
+ else if (indent === 0 && k === 'board' && v) {
436
+ // Legacy inline form: board: xiao_ble
437
+ flushBoard();
438
+ board = { name: stripYamlComment(v).trim(), socs: new Map() };
439
+ boards.push(board);
440
+ }
441
+ else if (k === 'variants') {
442
+ // Chains under this level accumulate from the enclosing chain.
443
+ chainBases.push([...chain]);
444
+ }
445
+ else if ((k === 'name' || k === 'full_name' || k === 'vendor') && v) {
446
+ const val = stripYamlComment(v).trim();
447
+ // A name: under a legacy board: block names the board.
448
+ if (k === 'name' && !board && keyStack.some((s) => s.key === 'board')) {
449
+ board = { name: val, socs: new Map() };
450
+ boards.push(board);
451
+ }
452
+ else if (k !== 'name' && board && !soc) {
453
+ if (k === 'full_name')
454
+ board.fullName = val;
455
+ if (k === 'vendor')
456
+ board.vendor = val;
457
+ }
458
+ }
459
+ keyStack.push({ indent, key: k });
460
+ continue;
461
+ }
462
+ if (entry) {
463
+ const name = stripYamlComment(entry[1]).trim().replace(/^['"]|['"]$/g, '');
464
+ if (!name)
465
+ continue;
466
+ let owner;
467
+ for (let i = keyStack.length - 1; i >= 0; i--) {
468
+ if (keyStack[i].indent <= indent) {
469
+ owner = keyStack[i].key;
470
+ break;
471
+ }
472
+ }
473
+ if (owner === 'boards') {
474
+ flushBoard();
475
+ board = { name, socs: new Map() };
476
+ boards.push(board);
477
+ }
478
+ else if (owner === 'socs') {
479
+ flushSoc();
480
+ soc = { name, chains: [] };
481
+ }
482
+ else if (owner === 'variants') {
483
+ const base = chainBases[chainBases.length - 1] ?? [];
484
+ chain = [...base, name];
485
+ soc?.chains.push([...chain]);
486
+ }
487
+ }
488
+ }
489
+ flushBoard();
490
+ for (const b of boards) {
491
+ for (const [socName, chains] of b.socs) {
492
+ targets.push({ identifier: `${b.name}/${socName}`, boardName: b.name, soc: socName, chain: [], ...(b.fullName ? { fullName: b.fullName } : {}), ...(b.vendor ? { vendor: b.vendor } : {}) });
493
+ for (const c of chains) {
494
+ targets.push({ identifier: [b.name, socName, ...c].join('/'), boardName: b.name, soc: socName, chain: c, ...(b.fullName ? { fullName: b.fullName } : {}), ...(b.vendor ? { vendor: b.vendor } : {}) });
495
+ }
496
+ }
497
+ }
498
+ return targets;
499
+ }
500
+ /** Compose the catalog record from one DTS's extracted facts. The canonical
501
+ * LED/button picks follow the DEVICETREE numbering, not child order: led0
502
+ * when aliased (mm_swiftio declares led0 = green_led as the SECOND child —
503
+ * picking the first aliased child made the manifest's LED0 contradict the
504
+ * devicetree's led0). Same for sw0. Extras then carry the remaining
505
+ * aliased nodes and the LED<N>/BUTTON<N> indices line up with dtSpec
506
+ * numbering. */
507
+ function composeRecord(base, facts) {
508
+ const led = facts.leds.find((l) => l.alias === 'led0') ?? facts.leds.find((l) => l.alias);
509
+ const button = facts.buttons.find((b) => b.alias === 'sw0') ?? facts.buttons.find((b) => b.alias);
510
+ const rec = {
511
+ ...base,
512
+ console: facts.chosen['zephyr,console'],
513
+ // Addressable user LED (ws2812) only when no gpio-leds LED exists —
514
+ // a board with both keeps its devicetree-chosen led0.
515
+ ...(facts.stripLed && !led ? { stripLed: { controller: facts.stripLed.controller, pin: facts.stripLed.pin } } : {}),
516
+ ...(facts.usbDevice ? { usbDevice: facts.usbDevice } : {}),
517
+ ...(facts.usbController ? { usbController: facts.usbController } : {}),
518
+ ...(facts.aliases['watchdog0'] ? { wdtNodeLabel: facts.aliases['watchdog0'] } : {}),
519
+ ...(facts.buses.i2c.length > 0 || facts.buses.spi.length > 0 || facts.buses.uart.length > 0
520
+ ? { buses: facts.buses }
521
+ : {}),
522
+ ...(facts.pwmLeds.length > 0
523
+ ? { pwmLeds: facts.pwmLeds.map((p) => ({
524
+ ...(p.alias ? { alias: p.alias } : {}),
525
+ controller: p.controller, channel: p.channel,
526
+ ...(p.periodNs != null ? { periodNs: p.periodNs } : {}),
527
+ ...(p.flags && p.flags.length > 0 ? { flags: [...p.flags] } : {}),
528
+ })) }
529
+ : {}),
530
+ ...(led ? { led: { dtSpec: led.alias, controller: led.controller, pin: led.pin, flags: led.flags } } : {}),
531
+ ...(button ? { button: { dtSpec: button.alias, controller: button.controller, pin: button.pin, flags: button.flags } } : {}),
532
+ ...(facts.leds.length > 1 || facts.buttons.length > 1
533
+ ? {
534
+ extraLeds: facts.leds.filter((l) => l !== led && l.alias).map((l) => ({ dtSpec: l.alias, controller: l.controller, pin: l.pin, flags: l.flags })),
535
+ extraButtons: facts.buttons.filter((b) => b !== button && b.alias).map((b) => ({ dtSpec: b.alias, controller: b.controller, pin: b.pin, flags: b.flags })),
536
+ }
537
+ : {}),
538
+ ...(facts.connectors.filter((c) => Object.keys(c.pins).length > 0).length > 0
539
+ ? { connectors: facts.connectors.filter((c) => Object.keys(c.pins).length > 0).map((c) => ({ nodelabel: c.nodelabel, compatible: c.compatible, pins: c.pins })) }
540
+ : {}),
541
+ };
542
+ // prune empty arrays
543
+ for (const k of ['extraLeds', 'extraButtons'])
544
+ if (rec[k]?.length === 0)
545
+ delete rec[k];
546
+ const hasFacts = Boolean(rec.console || rec.led || rec.button || rec.connectors);
547
+ return { record: rec, hasFacts };
548
+ }
549
+ /** Probe-method pin for one identifier (CONFIG_BOARD stems + soc quals). */
550
+ function variantPinFor(identifier, socIndex) {
551
+ const parts = identifier.split('/');
552
+ const underscored = identifier.replace(/\//g, '_');
553
+ const entry = socIndex.boardNames.find((n) => identifier === n || identifier.startsWith(n + '/'));
554
+ return {
555
+ boardConfigs: [...new Set([...(entry ? [entry] : []), underscored])]
556
+ .map((n) => n.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()),
557
+ ...(parts[1] ? { soc: parts[1] } : {}),
558
+ ...(parts.length > 2 ? { quals: parts.slice(2) } : {}),
559
+ };
560
+ }
561
+ /**
562
+ * Walk `<zephyrBase>/boards` and extract one record per board variant.
563
+ * Reader failures degrade to the base record (identifier/name/vendor/dts +
564
+ * probe methods) — a board whose DTS defeats the reader still resolves, it
565
+ * just carries no pin facts.
566
+ */
567
+ export function walkBoardCatalog(zephyrBase) {
568
+ const boardsRoot = path.join(zephyrBase, 'boards');
569
+ if (!fs.existsSync(boardsRoot)) {
570
+ throw new Error(`no boards/ directory at ${boardsRoot} — not a Zephyr tree?`);
571
+ }
572
+ const records = [];
573
+ let variants = 0, withFacts = 0, failures = 0;
574
+ const vendors = fs.readdirSync(boardsRoot, { withFileTypes: true })
575
+ .filter((d) => d.isDirectory());
576
+ for (const vendorDir of vendors) {
577
+ const boardDirs = fs.readdirSync(path.join(boardsRoot, vendorDir.name), { withFileTypes: true })
578
+ .filter((d) => d.isDirectory());
579
+ for (const boardDir of boardDirs) {
580
+ const dir = path.join(boardsRoot, vendorDir.name, boardDir.name);
581
+ const files = fs.readdirSync(dir);
582
+ const socIndex = boardYmlSocIndex(dir);
583
+ const seenIds = new Set();
584
+ for (const f of files) {
585
+ if (!f.endsWith('.yaml') || f === 'board.yml')
586
+ continue;
587
+ const raw = yamlField(path.join(dir, f), 'identifier');
588
+ if (!raw)
589
+ continue;
590
+ // Qualify: bare identifiers take the soc segment from board.yml —
591
+ // their own entry's soc in multi-board dirs, the sole soc in
592
+ // single-board dirs.
593
+ const identifier = raw.includes('/')
594
+ ? raw
595
+ : (() => {
596
+ const soc = socIndex.byName.get(raw)
597
+ ?? (socIndex.uniqueSocs.length === 1 ? socIndex.uniqueSocs[0] : null);
598
+ return soc ? `${raw}/${soc}` : null;
599
+ })();
600
+ if (!identifier || seenIds.has(identifier))
601
+ continue;
602
+ variants++;
603
+ seenIds.add(identifier);
604
+ const dtsBase = f.replace(/\.yaml$/, '');
605
+ const dtsFile = path.join(dir, dtsBase + '.dts');
606
+ if (!fs.existsSync(dtsFile))
607
+ continue;
608
+ // Per-variant: runner args are selected by the variant's own
609
+ // CONFIG_BOARD guard, so this must run per yaml, not per dir.
610
+ const probeMethods = boardProbeMethods(dir, variantPinFor(identifier, socIndex));
611
+ const base = {
612
+ identifier,
613
+ name: yamlField(path.join(dir, f), 'name') ?? identifier,
614
+ vendor: vendorDir.name,
615
+ dts: dtsBase + '.dts',
616
+ ...(probeMethods ? { probeMethods } : {}),
617
+ };
618
+ try {
619
+ const facts = readBoardDts(dtsFile, { zephyrBoardsRoot: boardsRoot });
620
+ const { record, hasFacts } = composeRecord(base, facts);
621
+ if (hasFacts)
622
+ withFacts++;
623
+ records.push(record);
624
+ }
625
+ catch (err) {
626
+ failures++;
627
+ if (failures <= 5)
628
+ console.error(`reader failed on ${identifier}: ${err.message}`);
629
+ records.push(base);
630
+ }
631
+ }
632
+ // Synthesis pass: board.yml-driven dirs ship NO per-variant yamls —
633
+ // nucleo_n657x0_q's `sb` target exists only as a board.yml entry.
634
+ // Emit every declared target the yaml flow did not already cover.
635
+ for (const t of boardYmlTargets(dir)) {
636
+ if (seenIds.has(t.identifier))
637
+ continue;
638
+ seenIds.add(t.identifier);
639
+ const dtsFile = [
640
+ path.join(dir, t.identifier.replace(/\//g, '_') + '.dts'),
641
+ path.join(dir, t.boardName + '.dts'),
642
+ ].find((p) => fs.existsSync(p));
643
+ if (!dtsFile)
644
+ continue;
645
+ variants++;
646
+ const probeMethods = boardProbeMethods(dir, variantPinFor(t.identifier, socIndex));
647
+ const base = {
648
+ identifier: t.identifier,
649
+ name: t.fullName ?? t.identifier,
650
+ vendor: t.vendor ?? vendorDir.name,
651
+ dts: path.basename(dtsFile),
652
+ ...(probeMethods ? { probeMethods } : {}),
653
+ };
654
+ try {
655
+ const facts = readBoardDts(dtsFile, { zephyrBoardsRoot: boardsRoot });
656
+ const { record, hasFacts } = composeRecord(base, facts);
657
+ if (hasFacts)
658
+ withFacts++;
659
+ records.push(record);
660
+ }
661
+ catch (err) {
662
+ failures++;
663
+ if (failures <= 5)
664
+ console.error(`reader failed on ${t.identifier}: ${err.message}`);
665
+ records.push(base);
666
+ }
667
+ }
668
+ }
669
+ }
670
+ return {
671
+ boards: Object.fromEntries(records.map((r) => [r.identifier, r])),
672
+ provenance: {
673
+ zephyrBase: path.resolve(zephyrBase),
674
+ version: zephyrVersionOf(zephyrBase),
675
+ ...(gitHeadOf(zephyrBase) ? { gitHead: gitHeadOf(zephyrBase) } : {}),
676
+ boardsMtimeMs: fs.statSync(boardsRoot).mtimeMs,
677
+ generatedAt: new Date().toISOString(),
678
+ variants: records.length,
679
+ },
680
+ stats: { variants, withFacts, failures },
681
+ };
682
+ }