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

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 (185) hide show
  1. package/README.md +22 -47
  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/esp32s3.js +49 -0
  10. package/dist/chips/index.d.ts +6 -12
  11. package/dist/chips/index.js +18 -34
  12. package/dist/chips/resolve.d.ts +16 -2
  13. package/dist/chips/resolve.js +267 -10
  14. package/dist/chips/soc/.d.ts +2 -0
  15. package/dist/chips/soc/.js +129 -0
  16. package/dist/chips/soc/esp32.d.ts +2 -0
  17. package/dist/chips/soc/esp32.js +120 -0
  18. package/dist/chips/soc/esp32c3.d.ts +2 -0
  19. package/dist/chips/soc/esp32c3.js +90 -0
  20. package/dist/chips/soc/esp32c6.d.ts +2 -0
  21. package/dist/chips/soc/esp32c6.js +109 -0
  22. package/dist/chips/soc/esp32s3.d.ts +2 -0
  23. package/dist/chips/soc/esp32s3.js +189 -0
  24. package/dist/chips/soc/index.d.ts +2 -0
  25. package/dist/chips/soc/index.js +23 -0
  26. package/dist/chips/soc/nrf52840.d.ts +2 -0
  27. package/dist/chips/soc/nrf52840.js +130 -0
  28. package/dist/chips/soc/rp2040.d.ts +2 -0
  29. package/dist/chips/soc/rp2040.js +141 -0
  30. package/dist/chips/soc/rp2350a.d.ts +2 -0
  31. package/dist/chips/soc/rp2350a.js +145 -0
  32. package/dist/chips/soc/samd21g18a.d.ts +2 -0
  33. package/dist/chips/soc/samd21g18a.js +143 -0
  34. package/dist/chips/soc/stm32f411xe.d.ts +2 -0
  35. package/dist/chips/soc/stm32f411xe.js +251 -0
  36. package/dist/chips/types.d.ts +319 -0
  37. package/dist/chips/xiao-ble.js +21 -0
  38. package/dist/display/profiles.js +1 -1
  39. package/dist/doctor.js +38 -4
  40. package/dist/dt-config/custom-board.d.ts +23 -0
  41. package/dist/dt-config/custom-board.js +227 -0
  42. package/dist/dt-config/kconfig.d.ts +72 -1
  43. package/dist/dt-config/kconfig.js +115 -26
  44. package/dist/dt-config/overlay.js +548 -28
  45. package/dist/framework.manifest.d.ts +3 -3
  46. package/dist/framework.manifest.js +122 -185
  47. package/dist/index.d.ts +4 -1
  48. package/dist/index.js +8 -2
  49. package/dist/lowering/adc.d.ts +20 -2
  50. package/dist/lowering/adc.js +121 -29
  51. package/dist/lowering/ble.js +31 -28
  52. package/dist/lowering/dac.js +9 -9
  53. package/dist/lowering/fs.js +18 -10
  54. package/dist/lowering/gpio.d.ts +2 -0
  55. package/dist/lowering/gpio.js +82 -5
  56. package/dist/lowering/http.js +39 -14
  57. package/dist/lowering/hwtimer.d.ts +4 -0
  58. package/dist/lowering/hwtimer.js +35 -10
  59. package/dist/lowering/i2c.js +46 -54
  60. package/dist/lowering/index.d.ts +5 -5
  61. package/dist/lowering/index.js +15 -16
  62. package/dist/lowering/interrupts.d.ts +22 -5
  63. package/dist/lowering/interrupts.js +104 -22
  64. package/dist/lowering/mqtt.js +3 -5
  65. package/dist/lowering/power.js +0 -4
  66. package/dist/lowering/preferences.js +49 -77
  67. package/dist/lowering/pulse.js +0 -31
  68. package/dist/lowering/pwm.d.ts +12 -1
  69. package/dist/lowering/pwm.js +78 -14
  70. package/dist/lowering/random.js +3 -3
  71. package/dist/lowering/sensor.d.ts +37 -0
  72. package/dist/lowering/sensor.js +119 -0
  73. package/dist/lowering/spi.d.ts +12 -0
  74. package/dist/lowering/spi.js +65 -40
  75. package/dist/lowering/thread.d.ts +14 -0
  76. package/dist/lowering/thread.js +55 -0
  77. package/dist/lowering/timing.js +20 -46
  78. package/dist/lowering/tone.js +16 -4
  79. package/dist/lowering/uart.d.ts +13 -0
  80. package/dist/lowering/uart.js +72 -51
  81. package/dist/lowering/usb.d.ts +34 -0
  82. package/dist/lowering/usb.js +206 -0
  83. package/dist/lowering/wdt.d.ts +1 -1
  84. package/dist/lowering/wdt.js +17 -7
  85. package/dist/lowering/wifi.d.ts +3 -2
  86. package/dist/lowering/wifi.js +97 -111
  87. package/dist/sdk/board-catalog-sync.d.ts +16 -0
  88. package/dist/sdk/board-catalog-sync.js +41 -0
  89. package/dist/sdk/board-data.generated.d.ts +2 -0
  90. package/dist/sdk/board-data.generated.js +4 -0
  91. package/dist/sdk/catalog-walker.d.ts +90 -0
  92. package/dist/sdk/catalog-walker.js +682 -0
  93. package/dist/sdk/dts-reader.d.ts +83 -0
  94. package/dist/sdk/dts-reader.js +596 -0
  95. package/dist/strategy.d.ts +136 -17
  96. package/dist/strategy.js +839 -222
  97. package/dist/toolchain/bossac-touch.d.ts +18 -0
  98. package/dist/toolchain/bossac-touch.js +88 -0
  99. package/dist/toolchain/compat.d.ts +2 -2
  100. package/dist/toolchain/compat.js +8 -3
  101. package/dist/toolchain/debug-config.d.ts +21 -26
  102. package/dist/toolchain/debug-config.js +226 -76
  103. package/dist/toolchain/env-check.d.ts +4 -4
  104. package/dist/toolchain/env-check.js +5 -5
  105. package/dist/toolchain/index.d.ts +115 -1
  106. package/dist/toolchain/index.js +680 -71
  107. package/dist/toolchain/scaffold.js +39 -7
  108. package/dist/toolchain/west-discover.d.ts +4 -0
  109. package/dist/toolchain/west-discover.js +17 -6
  110. package/installer/CHANGELOG.md +473 -0
  111. package/installer/README.md +328 -0
  112. package/installer/environment.yml +46 -0
  113. package/installer/etc/conda/activate.d/zephyr.bat +7 -0
  114. package/installer/etc/conda/activate.d/zephyr.ps1 +12 -0
  115. package/installer/etc/conda/activate.d/zephyr.sh +20 -0
  116. package/installer/etc/conda/deactivate.d/zephyr.bat +6 -0
  117. package/installer/etc/conda/deactivate.d/zephyr.ps1 +6 -0
  118. package/installer/etc/conda/deactivate.d/zephyr.sh +4 -0
  119. package/installer/install.mjs +580 -0
  120. package/installer/install.ps1 +619 -0
  121. package/installer/install.sh +310 -0
  122. package/installer/lib/detect-platform.sh +62 -0
  123. package/installer/lib/fetch-sdk.sh +304 -0
  124. package/installer/lib/init-workspace.sh +153 -0
  125. package/installer/lib/write-activation.sh +76 -0
  126. package/installer/patches/tfm-c-include-separators.patch +19 -0
  127. package/installer/patches/zephyr-tfm-nordic-zephyr-include.patch +24 -0
  128. package/installer/templates/project/.typecad/activate-zephyr.ps1 +31 -0
  129. package/installer/templates/project/.typecad/activate-zephyr.sh +31 -0
  130. package/installer/templates/project/.vscode/settings.json +30 -0
  131. package/installer/templates/project/README.md +58 -0
  132. package/installer/versions.env +110 -0
  133. package/package.json +15 -15
  134. package/src/as-built.ts +206 -0
  135. package/src/boardgen.ts +1428 -0
  136. package/src/chips/index.ts +35 -54
  137. package/src/chips/resolve.ts +285 -10
  138. package/src/chips/types.ts +325 -1
  139. package/src/display/profiles.ts +1 -1
  140. package/src/doctor.ts +110 -77
  141. package/src/dt-config/custom-board.ts +325 -0
  142. package/src/dt-config/kconfig.ts +165 -18
  143. package/src/dt-config/overlay.ts +537 -25
  144. package/src/framework.manifest.ts +123 -185
  145. package/src/index.ts +57 -41
  146. package/src/lowering/adc.ts +222 -118
  147. package/src/lowering/ble.ts +32 -28
  148. package/src/lowering/dac.ts +82 -82
  149. package/src/lowering/fs.ts +18 -10
  150. package/src/lowering/gpio.ts +242 -155
  151. package/src/lowering/http.ts +512 -488
  152. package/src/lowering/hwtimer.ts +139 -101
  153. package/src/lowering/i2c.ts +116 -126
  154. package/src/lowering/index.ts +91 -93
  155. package/src/lowering/interrupts.ts +113 -21
  156. package/src/lowering/mqtt.ts +3 -5
  157. package/src/lowering/preferences.ts +327 -354
  158. package/src/lowering/pwm.ts +167 -99
  159. package/src/lowering/random.ts +3 -3
  160. package/src/lowering/sensor.ts +155 -0
  161. package/src/lowering/spi.ts +164 -124
  162. package/src/lowering/thread.ts +63 -0
  163. package/src/lowering/timing.ts +46 -72
  164. package/src/lowering/uart.ts +73 -51
  165. package/src/lowering/usb.ts +221 -0
  166. package/src/lowering/wdt.ts +95 -84
  167. package/src/lowering/wifi.ts +97 -112
  168. package/src/sdk/board-catalog-sync.ts +62 -0
  169. package/src/strategy.ts +848 -237
  170. package/src/toolchain/bossac-touch.ts +112 -0
  171. package/src/toolchain/compat.ts +8 -3
  172. package/src/toolchain/debug-config.ts +676 -522
  173. package/src/toolchain/env-check.ts +9 -9
  174. package/src/toolchain/index.ts +1359 -645
  175. package/src/toolchain/scaffold.ts +38 -7
  176. package/src/toolchain/west-discover.ts +19 -6
  177. package/src/async/timer-polyfill.ts +0 -107
  178. package/src/chips/esp32.ts +0 -74
  179. package/src/chips/esp32s3.ts +0 -57
  180. package/src/chips/xiao-ble.ts +0 -82
  181. package/src/lowering/power.ts +0 -70
  182. package/src/lowering/pulse.ts +0 -93
  183. package/src/lowering/tone.ts +0 -60
  184. package/src/lowering/worker-backing.ts +0 -94
  185. package/src/lowering/worker.ts +0 -17
@@ -18,30 +18,29 @@
18
18
  // target is known), GCC errors parsed via the shared parseCompileErrors helper.
19
19
  // ---------------------------------------------------------------------------
20
20
  import { spawnSync } from 'node:child_process';
21
- import { basename, dirname, join } from 'node:path';
22
- import { readdirSync, readFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
21
+ import { basename, delimiter, dirname, join } from 'node:path';
22
+ import { readdirSync, readFileSync, mkdirSync, rmSync, existsSync, writeFileSync } from 'node:fs';
23
23
  import { parseCompileErrors } from '@typecad/cuttlefish/api/shared';
24
24
  import { scaffoldZephyrProject, writeIfChanged, appendLibraryOverlayFragments } from './scaffold.js';
25
+ import { parseZephyrDts, asBuiltJson } from '../as-built.js';
25
26
  import { westSpawn, buildEnv } from './west-spawn.js';
26
27
  import { discoverWest } from './west-discover.js';
27
28
  import { writeDebugConfig, resolveDebugLocations } from './debug-config.js';
29
+ import { bossacTouchReset } from './bossac-touch.js';
28
30
  import { ZephyrStrategy } from '../strategy.js';
29
31
  import { generateOverlay } from '../dt-config/overlay.js';
30
- import { chipForTarget } from '../chips/index.js';
32
+ import { generateCustomBoard } from '../dt-config/custom-board.js';
33
+ import { NO_BOARD_CHIP } from '../chips/index.js';
31
34
  import { resolveChipFromBoard } from '../chips/resolve.js';
32
35
  import { pwmDtAliasToken } from '../lowering/pwm.js';
33
36
  import { detectZephyrVersion, checkZephyrCompat, resolveBoardTarget } from './compat.js';
34
37
  import { DEFAULT_ZEPHYR_DISPLAY_PROFILE } from '../display/profiles.js';
35
- /** Default board target — the framework's MVP canonical board. */
36
- const DEFAULT_BOARD = 'xiao_ble';
37
38
  /**
38
39
  * Resolve the chip for a build the same way the strategy does at emit time —
39
40
  * from the board constants the transpile persisted next to the emitted
40
- * source (`board-constants.json`), falling back to the hardcoded registry.
41
- * Board-package chips (rpi_pico, esp32c3/c6, blackpill) exist only in their
42
- * board packages; the registry fallback would silently resolve them to the
43
- * XIAO default and the overlay generator would emit wrong controller labels
44
- * (e.g. `&uart0` on an STM32, whose node is `usart1`).
41
+ * source (`board-constants.json`). There is no registry fallback: a build
42
+ * whose constants did not persist stays NO_BOARD_CHIP, exactly like the
43
+ * emit-time path.
45
44
  */
46
45
  function chipForBuild(projectRoot, board) {
47
46
  try {
@@ -52,13 +51,14 @@ function chipForBuild(projectRoot, board) {
52
51
  .find(p => existsSync(p));
53
52
  if (bcPath) {
54
53
  const raw = JSON.parse(readFileSync(bcPath, 'utf8'));
55
- const fromBoard = resolveChipFromBoard(new Map(Object.entries(raw)));
54
+ const bc = new Map(Object.entries(raw));
55
+ const fromBoard = resolveChipFromBoard(bc);
56
56
  if (fromBoard)
57
57
  return fromBoard;
58
58
  }
59
59
  }
60
- catch { /* fall back to the registry below */ }
61
- return chipForTarget(board);
60
+ catch { /* constants unreadable no board resolved */ }
61
+ return NO_BOARD_CHIP;
62
62
  }
63
63
  /**
64
64
  * HAL pins the emitted sources read via adc.* — scanned from the emitted
@@ -69,15 +69,42 @@ function chipForBuild(projectRoot, board) {
69
69
  */
70
70
  function scanAdcReadPins(src, chip) {
71
71
  const pins = [];
72
- // Match CALL SITES only (`__tc_adc<N>_setup()` with empty parens) — the
73
- // setup definitions emitted by adcInitLines have a `(void)` parameter list
74
- // and would otherwise mark every descriptor channel as used.
75
- for (const m of src.matchAll(/__tc_adc(\d+)_setup\(\)/g)) {
76
- const ch = Number(m[1]);
77
- const c = chip.adc?.channels.find((x) => x.channel === ch);
72
+ // Two used-signal forms:
73
+ // - `__tc_adc<N>_setup()` / `__tc_adc_<ctrl>_<N>_setup()` CALL SITES
74
+ // (empty parens the adcInitLines definitions have `(void)` and would
75
+ // otherwise mark every descriptor channel as used).
76
+ // - `__tc_adct<pin>_done` lazy-guard vars — the thin-ADC read lowering's
77
+ // inline setup (families without pinctrl groups, e.g. ESP32 SARADC,
78
+ // never emit the setup-function form at all).
79
+ // Channel indices are unique per CONTROLLER, so the labeled form resolves
80
+ // (controller, channel) before mapping back to the HAL pin.
81
+ for (const m of src.matchAll(/__tc_adc(?:(\w+?)_)?(\d+)_setup\(\)/g)) {
82
+ const ch = Number(m[2]);
83
+ const label = m[1];
84
+ const c = chip.adc?.channels.find((x) => x.channel === ch && (x.controller ?? chip.adc?.nodeLabel) === (label ?? chip.adc?.nodeLabel));
78
85
  if (c && !pins.includes(c.pin))
79
86
  pins.push(c.pin);
80
87
  }
88
+ for (const m of src.matchAll(/__tc_adct(\d+)_done/g)) {
89
+ const pin = Number(m[1]);
90
+ if (chip.adc?.channels.some((x) => x.pin === pin) && !pins.includes(pin))
91
+ pins.push(pin);
92
+ }
93
+ return pins;
94
+ }
95
+ /**
96
+ * HAL pins the emitted sources drive with dac.* — the DAC lowering's lazy
97
+ * per-pin setup guard is `__tc_dact<pin>_done` (lowering/dac.ts), so
98
+ * var-presence is the authoritative used-signal. Feeds the overlay's DAC
99
+ * pinctrl gating (same pattern as scanAdcReadPins).
100
+ */
101
+ function scanDacWritePins(src) {
102
+ const pins = [];
103
+ for (const m of src.matchAll(/__tc_dact(\d+)_done/g)) {
104
+ const pin = Number(m[1]);
105
+ if (!pins.includes(pin))
106
+ pins.push(pin);
107
+ }
81
108
  return pins;
82
109
  }
83
110
  /**
@@ -87,16 +114,150 @@ function scanAdcReadPins(src, chip) {
87
114
  * var-presence is the authoritative signal. Feeds the overlay's per-pin
88
115
  * pwm-leds gating (no dead DT channels).
89
116
  */
117
+ /**
118
+ * Inline-override markers (the escape hatch): the adc/pwm lowerings emit
119
+ * `/* cuttlefish-user-facts: <kind> pin=N [device=X] [pinctrl=P] channel=C *​/`
120
+ * comments when a construction carries routing overrides. Merged into the
121
+ * chip so the overlay synthesis + used-pin scans treat them as facts —
122
+ * the transpiler cannot create DT nodes, but this regen can.
123
+ */
124
+ function applyUserFactMarkers(chip, src) {
125
+ const adcAdds = [];
126
+ const pwmAdds = [];
127
+ for (const m of src.matchAll(/\/\* cuttlefish-user-facts: (adc|pwm) ([^*]*?) \*\//g)) {
128
+ const kind = m[1];
129
+ const fields = new Map();
130
+ for (const kv of m[2].split(/\s+/).filter(Boolean)) {
131
+ const eq = kv.indexOf('=');
132
+ if (eq > 0)
133
+ fields.set(kv.slice(0, eq), kv.slice(eq + 1));
134
+ }
135
+ const pin = Number(fields.get('pin'));
136
+ const channel = Number(fields.get('channel'));
137
+ if (!Number.isFinite(pin) || !Number.isFinite(channel))
138
+ continue;
139
+ if (kind === 'adc') {
140
+ adcAdds.push({
141
+ pin,
142
+ channel,
143
+ ...(fields.get('device') ? { controller: fields.get('device') } : {}),
144
+ ...(fields.get('pinctrl') ? { pinctrl: fields.get('pinctrl') } : {}),
145
+ });
146
+ }
147
+ else if (fields.get('controller')) {
148
+ pwmAdds.push({ pin, channel, controller: fields.get('controller') });
149
+ }
150
+ }
151
+ if (adcAdds.length === 0 && pwmAdds.length === 0)
152
+ return chip;
153
+ const adc = chip.adc
154
+ ? chip.adc
155
+ : { nodeLabel: adcAdds.find((a) => !a.controller)?.controller ?? 'adc', resolution: 12, vrefMv: 3000, channels: [] };
156
+ const adcChannels = [...adc.channels];
157
+ for (const a of adcAdds) {
158
+ const existing = adcChannels.findIndex((c) => c.pin === a.pin);
159
+ if (existing >= 0)
160
+ adcChannels.splice(existing, 1);
161
+ adcChannels.push({ pin: a.pin, channel: a.channel, ...(a.controller ? { controller: a.controller } : {}), ...(a.pinctrl ? { pinctrl: a.pinctrl } : {}) });
162
+ }
163
+ const pwmSpecs = [...(chip.pwm?.specs ?? [])];
164
+ for (const p of pwmAdds) {
165
+ const existing = pwmSpecs.findIndex((s) => s.pin === p.pin);
166
+ if (existing >= 0)
167
+ pwmSpecs.splice(existing, 1);
168
+ pwmSpecs.push({ pin: p.pin, controller: p.controller, channel: p.channel });
169
+ }
170
+ return {
171
+ ...chip,
172
+ adc: { ...adc, channels: adcChannels },
173
+ pwm: { ...(chip.pwm ?? { specs: [] }), specs: pwmSpecs },
174
+ };
175
+ }
90
176
  function scanPwmUsedPins(src, chip) {
91
- return (chip.pwm?.specs ?? [])
177
+ const pins = (chip.pwm?.specs ?? [])
92
178
  .filter((s) => src.includes(`__tc_pwm_${pwmDtAliasToken(s)}`))
93
179
  .map((s) => s.pin);
180
+ // Matrix pins (ESP32 LEDC) have no static specs — recover the driven pins
181
+ // from the same alias-var presence signal (`__tc_pwm_tc_pwm<N>`), keeping
182
+ // only pins the descriptor's matrix allows (the regex grabs the full
183
+ // number, so pin 4 never matches a reference to pin 45).
184
+ const matrix = chip.pwm?.matrix;
185
+ if (matrix) {
186
+ const present = new Set();
187
+ for (const match of src.matchAll(/__tc_pwm_tc_pwm(\d+)/g)) {
188
+ present.add(Number(match[1]));
189
+ }
190
+ for (const pin of matrix.pins) {
191
+ if (present.has(pin))
192
+ pins.push(pin);
193
+ }
194
+ }
195
+ return pins;
196
+ }
197
+ /**
198
+ * Bus controller indexes the emitted sources actually reference — the shim
199
+ * declares one `__tc_<bus><N>_dev` state block per used instance (gated by
200
+ * collectUsedBusIndices at transpile time), so var-presence is the
201
+ * authoritative signal. The overlay enables only these controllers: an
202
+ * enabled-but-unused one claims its default pins (i2c0's GP4/GP5 on the
203
+ * Pico) which a program driving the OTHER controller may want as GPIO.
204
+ * Empty list (no state blocks — e.g. display/touch composites that use the
205
+ * driver API directly) means "no signal"; the caller then passes undefined
206
+ * so the overlay enables every declared controller, preserving old behavior.
207
+ */
208
+ function scanUsedBusInstances(src, controllers, bus) {
209
+ if (!controllers)
210
+ return undefined;
211
+ const used = [];
212
+ for (let i = 0; i < controllers.length; i++) {
213
+ if (src.includes(`__tc_${bus}${i}_dev`))
214
+ used.push(i);
215
+ }
216
+ return used.length > 0 ? used : undefined;
217
+ }
218
+ /** Scan the emitted source for thin SPI targets (hal/spi-target.ts): the
219
+ * spi_dt_spec state block's tc-spit-cfg comment carries the construction
220
+ * facts, the same channel tc-sensor-cfg uses. */
221
+ export function scanSpiTargets(src) {
222
+ const out = new Map();
223
+ for (const m of src.matchAll(/tc-spit-cfg: tc_spit_spi(\d+)_cs(\d+) hz=(\d+) mode=(\d+)/g)) {
224
+ const ref = { busIndex: parseInt(m[1], 10), cs: parseInt(m[2], 10), hz: parseInt(m[3], 10), mode: parseInt(m[4], 10) };
225
+ out.set(`${ref.busIndex}|${ref.cs}`, ref);
226
+ }
227
+ return [...out.values()];
228
+ }
229
+ export function scanSensorParts(src) {
230
+ const out = new Map();
231
+ 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
+ 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 };
234
+ out.set(`${ref.part}|${ref.busKind}${ref.busIndex}|${ref.port}`, ref);
235
+ }
236
+ // Construction facts ride the state block's config comment — merge by
237
+ // nodelabel so the scanner stays the single source for the overlay.
238
+ 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
+ const port = m[4].startsWith('0x') ? parseInt(m[4], 16) : parseInt(m[4].slice(2), 10);
240
+ const key = `${m[1]}|${m[2]}${m[3]}|${port}`;
241
+ const existing = out.get(key);
242
+ if (existing) {
243
+ existing.spiHz = parseInt(m[5], 10);
244
+ existing.spiMode = parseInt(m[6], 10);
245
+ existing.alertPin = parseInt(m[7], 10);
246
+ }
247
+ }
248
+ return [...out.values()];
94
249
  }
95
250
  function targetFromOptions(o) {
96
- // The cuttlefish CLI populates ToolchainOptions.buildTarget from
97
- // config.frameworkData.buildTarget. Accept frameworkData.target as an alias.
251
+ // The cuttlefish CLI populates ToolchainOptions.buildTarget from the
252
+ // config's board: (frameworkData.buildTarget for board-less projects).
253
+ // Accept frameworkData.target as an alias. No silent default: building for
254
+ // a wrong hard-coded board is the split-brain trap.
98
255
  const fcTarget = o.frameworkConfig?.target;
99
- return o.buildTarget ?? fcTarget ?? DEFAULT_BOARD;
256
+ const board = o.buildTarget ?? fcTarget;
257
+ if (!board) {
258
+ throw new Error('No build target: set board: in cuttlefish.config.ts (or frameworkData.buildTarget for custom-board projects).');
259
+ }
260
+ return board;
100
261
  }
101
262
  /**
102
263
  * Derive the Zephyr project root from the cuttlefish-emitted source path.
@@ -119,32 +280,199 @@ export function projectRootFromOptions(o) {
119
280
  */
120
281
  const BUILD_TIMEOUT_MS = 600_000;
121
282
  const FLASH_TIMEOUT_MS = 120_000;
283
+ export function resolveProbeMethod(zc, chip, purpose = 'flash') {
284
+ const probe = zc?.probe;
285
+ const runner = zc?.runner;
286
+ const userArgs = zc?.runnerArgs ?? [];
287
+ if (probe && runner) {
288
+ return {
289
+ ok: false,
290
+ error: `cuttlefish.config.ts sets both zephyr.probe ('${probe}') and zephyr.runner ('${runner}'). ` +
291
+ `They are two ways to choose the probe method — remove one.`,
292
+ };
293
+ }
294
+ if (probe) {
295
+ const methods = chip.probeMethods ?? [];
296
+ const method = methods.find((m) => m.id === probe);
297
+ if (!method) {
298
+ const listAll = methods
299
+ .map((m) => `${m.id} (${m.runner}${m.description ? ` — ${m.description}` : ''})`)
300
+ .join('; ');
301
+ return {
302
+ ok: false,
303
+ error: methods.length > 0
304
+ ? `Unknown probe method '${probe}' for ${chip.id}. Supported: ${listAll}.`
305
+ : `This board (${chip.id}) ships no probe-method table, so 'zephyr.probe' cannot resolve '${probe}'. ` +
306
+ `Use the raw 'zephyr.runner' field instead (run 'west flash --context' in the build dir for options).`,
307
+ };
308
+ }
309
+ if (purpose === 'debug' && method.debug === false) {
310
+ const debuggable = methods.filter((m) => m.debug !== false).map((m) => m.id).join(', ');
311
+ return {
312
+ ok: false,
313
+ error: `The '${probe}' method cannot debug ${chip.id} — a bootloader is not a debugger. ` +
314
+ `Debug-capable methods: ${debuggable || '(none — this board needs an external probe)'}.`,
315
+ };
316
+ }
317
+ return {
318
+ ok: true,
319
+ runner: method.runner,
320
+ args: [...(method.args ?? []), ...userArgs],
321
+ };
322
+ }
323
+ return { ok: true, runner, args: userArgs };
324
+ }
325
+ export function resolveSessionOpenOcd(env = process.env, sdkInstallDir) {
326
+ const exeName = process.platform === 'win32' ? 'openocd.exe' : 'openocd';
327
+ if (env.OPENOCD && existsSync(env.OPENOCD)) {
328
+ return { exe: env.OPENOCD, searchDirs: [] };
329
+ }
330
+ const sdkRoot = env.ZEPHYR_SDK_INSTALL_DIR || sdkInstallDir;
331
+ if (sdkRoot) {
332
+ const exe = join(sdkRoot, 'hosttools', 'openocd', 'bin', exeName);
333
+ if (existsSync(exe)) {
334
+ const searchDirs = [
335
+ join(sdkRoot, 'hosttools', 'openocd', 'share', 'openocd', 'scripts'),
336
+ join(sdkRoot, 'hosttools', 'openocd', 'scripts'),
337
+ ].filter((d) => existsSync(d));
338
+ return { exe, searchDirs };
339
+ }
340
+ }
341
+ for (const dir of (env.PATH ?? '').split(delimiter)) {
342
+ if (!dir)
343
+ continue;
344
+ const candidate = join(dir, exeName);
345
+ if (existsSync(candidate))
346
+ return { exe: candidate, searchDirs: [] };
347
+ }
348
+ return undefined;
349
+ }
350
+ function openocdProbeSession(buildDir, zc, chip, commands) {
351
+ // Config resolution — two sources, in order:
352
+ // 1. The named probe method's verbatim debugCfg from the board catalog
353
+ // (written to a temp cfg), when zephyr.probe names a method that has
354
+ // one.
355
+ // 2. The board's own support/openocd.cfg in the Zephyr tree — the exact
356
+ // config `west flash` resolves for the openocd runner. This covers
357
+ // raw `zephyr.runner: 'openocd'` (no named probe) and probe methods
358
+ // that ship no debugCfg of their own.
359
+ const probeId = zc?.probe;
360
+ const method = chip.probeMethods?.find((m) => m.id === probeId);
361
+ const cfgLines = method?.debugCfg;
362
+ // Session reset policy: `reset_config none`. The session's resets are
363
+ // core-domain by design — vector-catch halt before the flash write,
364
+ // SYSRESETREQ to boot — so they must not depend on the SRST pin. Boards
365
+ // like the WeAct Black Pill don't break NRST out at all: under the board
366
+ // cfg's `srst_only`, every `reset` asserts a pin that reaches nothing
367
+ // (the target never resets, `reset halt` catches the core mid-app in
368
+ // dirty state and the flash algorithm times out) while the probe's
369
+ // floating SRST sense reports phantom "external reset detected" events
370
+ // that leave the session's halt state inconsistent. Method-declared west
371
+ // quirks (`--cmd-pre-init=…`) are appended after and override the
372
+ // default for boards whose facts carry one.
373
+ const preInit = [
374
+ 'reset_config none',
375
+ ...(method?.args ?? [])
376
+ .filter((a) => a.startsWith('--cmd-pre-init='))
377
+ .map((a) => a.slice('--cmd-pre-init='.length)),
378
+ ];
379
+ const install = discoverWest();
380
+ const sessionOpenOcd = resolveSessionOpenOcd(process.env, install?.sdkInstallDir);
381
+ if (!sessionOpenOcd)
382
+ return undefined;
383
+ const openocdExe = sessionOpenOcd.exe;
384
+ const searchArgs = sessionOpenOcd.searchDirs.flatMap((d) => ['-s', d]);
385
+ let cfgArgs;
386
+ let sessionCfg;
387
+ if (cfgLines && cfgLines.length > 0) {
388
+ sessionCfg = join(buildDir, 'cuttlefish-probe.cfg');
389
+ }
390
+ else {
391
+ // The board target's qualifier ('blackpill_f411ce/stm32f411xe' →
392
+ // 'blackpill_f411ce') identifies the board dir; the vendor segment is
393
+ // not part of the target, so probe the boards/ tree for it.
394
+ const zephyrBase = process.env.ZEPHYR_BASE || install?.zephyrBase;
395
+ const boardDir = (chip.id ?? '').split('/')[0];
396
+ if (!zephyrBase || !boardDir)
397
+ return undefined;
398
+ const boardsRoot = join(zephyrBase, 'boards');
399
+ let supportCfg;
400
+ try {
401
+ for (const vendor of readdirSync(boardsRoot)) {
402
+ const candidate = join(boardsRoot, vendor, boardDir, 'support', 'openocd.cfg');
403
+ if (existsSync(candidate)) {
404
+ supportCfg = candidate;
405
+ break;
406
+ }
407
+ }
408
+ }
409
+ catch {
410
+ return undefined;
411
+ }
412
+ if (!supportCfg)
413
+ return undefined;
414
+ cfgArgs = ['-s', dirname(supportCfg), '-f', supportCfg];
415
+ }
416
+ try {
417
+ mkdirSync(buildDir, { recursive: true });
418
+ if (sessionCfg) {
419
+ writeFileSync(sessionCfg, cfgLines.join('\n') + '\n', 'utf-8');
420
+ cfgArgs = ['-f', sessionCfg];
421
+ }
422
+ const res = spawnSync(openocdExe, [
423
+ ...searchArgs, ...cfgArgs,
424
+ // Pre-init TCL AFTER the cfg (overrides its reset_config) and BEFORE
425
+ // init — the same position west gives --cmd-pre-init.
426
+ ...preInit.map((c) => ['-c', c]).flat(),
427
+ '-c', 'init',
428
+ ...commands.map((c) => ['-c', c]).flat(),
429
+ '-c', 'shutdown',
430
+ ], {
431
+ cwd: buildDir,
432
+ encoding: 'utf-8',
433
+ timeout: 20_000,
434
+ });
435
+ return res.status === 0 ? `-- probe session ok: ${commands.join('; ')}` : undefined;
436
+ }
437
+ catch {
438
+ return undefined;
439
+ }
440
+ }
122
441
  /**
123
- * Build the `west flash` argument list for a board.
124
- *
125
- * Runner selection: each board's board.cmake declares a sensible default flash
126
- * runner for its hardware (xiao_ble nrfutil, esp32* → esptool), and `west
127
- * flash` resolves it automatically. The framework only intervenes where the
128
- * board default needs an argument it can't infer:
129
- * - An explicit `zephyr.runner` (from cuttlefish.config.ts) always wins.
130
- * - ESP32 boards forward the port via `--esp-device` (esptool reads the
131
- * device from it); board.cmake still picks the runner.
132
- * - Every other board trusts the board.cmake default. Previously this forced
133
- * `--runner nrfjprog` for every non-ESP32 target, which broke boards whose
134
- * default is not nrfjprog (xiao_ble defaults to nrfutil) and required
135
- * Nordic J-Link tools that a USB-bootloader board does not have.
442
+ * Whether a flash runner carries the upload over a serial port. Runner-gated,
443
+ * never board-name-gated: esptool and bossac are the only runners
444
+ * `buildFlashArgs` forwards `--port` to, so they are the only ones that
445
+ * cannot flash without one. Probe runners (openocd, jlink) and USB flows
446
+ * (dfu-util, uf2 mass storage) need no port a missing `--port` must not
447
+ * block them.
136
448
  *
137
- * Exported (pure) so the runner-selection contract is unit-testable without
449
+ * Exported (pure) so the port-requirement contract is unit-testable without
138
450
  * spawning west.
139
451
  */
140
- export function buildFlashArgs(buildDir, board, userRunner, port, runnerArgs) {
452
+ export function uploadRequiresPort(runner) {
453
+ return runner === 'esptool' || runner === 'bossac';
454
+ }
455
+ export function buildFlashArgs(buildDir, userRunner, port, flashRunner, runnerArgs) {
456
+ // flashRunner is the runner the flash will actually use — the explicit
457
+ // zephyr.runner when set, else the board's declared default from its probe
458
+ // table. It gates the esptool port forwarding below; only an EXPLICIT
459
+ // userRunner forces west's --runner (board defaults stay board.cmake's
460
+ // choice). Runner-gated, never board-name-gated: any board whose flash
461
+ // runs esptool gets the same forwarding.
141
462
  const args = ['flash', '-d', buildDir];
142
463
  if (userRunner) {
143
464
  args.push('--runner', userRunner);
144
465
  }
145
- if (port && board.startsWith('esp32')) {
466
+ if (port && flashRunner === 'esptool') {
146
467
  args.push('--esp-device', port);
147
468
  }
469
+ // The bossac runner defaults its port to /dev/ttyACM0 — on Windows that
470
+ // never matches, so the port MUST be forwarded or bossac fails with
471
+ // "No device found on /dev/ttyACM0" (same class of port-forwarding
472
+ // problem as the esptool --esp-device above).
473
+ if (port && flashRunner === 'bossac') {
474
+ args.push('--bossac-port', port);
475
+ }
148
476
  // Extra runner-specific flags, appended verbatim (west's runner parsers
149
477
  // accept them after the runner is selected).
150
478
  if (runnerArgs && runnerArgs.length > 0) {
@@ -186,6 +514,18 @@ function isUf2DriveVanishRace(output) {
186
514
  && /WinError 433/.test(output)
187
515
  && /copymode/.test(output);
188
516
  }
517
+ /**
518
+ * Whether a `west flash` (openocd) output carries one of the known
519
+ * target-ignored-SWD signatures — the DAP connect failing ("init mode
520
+ * failed (unable to connect to the target)", i.e. the DPIDR read never
521
+ * succeeded) or a reset/halt never landing ("timed out while waiting for
522
+ * target halted" / "TARGET: <name> - Not halted"). Both mean the board (or
523
+ * probe) needs a power-cycle or the SWD-free DFU path, not a retry of the
524
+ * same command. Centralized so the upload hint stays testable.
525
+ */
526
+ export function isTargetSwdFailure(output) {
527
+ return /unable to connect to the target|timed out while waiting for target halted|TARGET: \S+ - Not halted/.test(output);
528
+ }
189
529
  /**
190
530
  * Cleanse the `west flash` output shown to the user.
191
531
  *
@@ -246,8 +586,6 @@ export const Toolchain = {
246
586
  // logic in scaffold via the usage scan. Mirrors how Arduino's library
247
587
  // resolution is a pre-build artifact step.
248
588
  const projectRoot = basename(outputDir) === 'src' ? dirname(outputDir) : outputDir;
249
- const board = DEFAULT_BOARD;
250
- const chip = chipForBuild(projectRoot, board);
251
589
  // Scan the emitted source for usage tokens (same authoritative signal the
252
590
  // scaffold uses). entryPoint is the path to main.cpp; its dir is src/.
253
591
  const srcDir = dirname(entryPoint);
@@ -261,6 +599,12 @@ export const Toolchain = {
261
599
  }
262
600
  catch { /* src may not exist yet on first prepare */ }
263
601
  const uses = (t) => src.includes(t);
602
+ // Inline-override markers (the escape hatch): the lowerings emit
603
+ // `cuttlefish-user-facts` comments carrying routing the transpiler
604
+ // cannot synthesize (adc device/pinctrl, pwm controller/channel).
605
+ // Merged into the chip BEFORE the scans + overlay generation, so the DT
606
+ // nodes, pinctrl groups, and used-pin recovery treat them as facts.
607
+ const chip = applyUserFactMarkers(chipForBuild(projectRoot, ''), src);
264
608
  // Display usage tokens: the minimal GFX runtime (display_write/_fill_rect)
265
609
  // and the UI display adapter (display_init / __tc_display_dev /
266
610
  // DEVICE_DT_GET on the display nodelabel). Both paths need the DT overlay
@@ -277,21 +621,65 @@ export const Toolchain = {
277
621
  // references (FT6336U on I2C, XPT2046 on the display's SPI bus).
278
622
  const usesTouch = uses('ft6336u') || uses('touch_');
279
623
  const usesXpt = uses('xpt2046');
624
+ const sensorParts = scanSensorParts(src);
625
+ const spiTargetParts = scanSpiTargets(src);
280
626
  const overlay = generateOverlay(chip, {
281
- usesI2c: uses('i2c_'),
282
- usesSpi: uses('spi_'),
283
- usesUart: uses('uart_'),
627
+ // __tc_<bus> matches the shim state block — a begin()-only program
628
+ // emits no driver API call but still declares the DT device. A
629
+ // constructed sensor is also a bus user (its device handle is the
630
+ // only i2c reference a sensor-only program carries).
631
+ usesI2c: uses('i2c_') || uses('__tc_i2c') || sensorParts.length > 0,
632
+ usesSensor: uses('sensor_') || sensorParts.length > 0,
633
+ usesFloatFormat: /%[-0-9.]*[eEfFgG]/.test(src),
634
+ sensorParts,
635
+ spiTargets: spiTargetParts,
636
+ usesSpi: uses('spi_') || uses('__tc_spi') || spiTargetParts.length > 0,
637
+ usesUart: uses('__tc_uart'),
638
+ usesUsb: uses('__tc_usb'),
284
639
  usesPwm: uses('pwm_'),
285
640
  usesAdc: uses('adc_'),
286
641
  adcReadPins: scanAdcReadPins(src, chip),
642
+ dacWritePins: scanDacWritePins(src),
287
643
  pwmUsedPins: scanPwmUsedPins(src, chip),
644
+ i2cUsedInstances: scanUsedBusInstances(src, chip.i2c?.controllers, 'i2c'),
645
+ spiUsedInstances: scanUsedBusInstances(src, chip.spi?.controllers, 'spi'),
646
+ uartUsedInstances: scanUsedBusInstances(src, chip.uart?.controllers, 'uart'),
647
+ // Preferences/FS — same tokens scaffoldZephyrProject scans (the ZMS
648
+ // settings_* API + __tc_prefs shim, the __tc_fs mount shim); drive the
649
+ // storage-partition synthesis + /chosen settings pointer.
650
+ usesPreferences: uses('settings_') || uses('__tc_prefs'),
651
+ usesFS: uses('__tc_fs'),
652
+ usesWdt: uses('wdt_'),
653
+ usesHwtimer: uses('counter_') || uses('__tc_hw'),
288
654
  usesDisplay,
289
655
  usesTouch: usesTouch || usesXpt,
290
656
  touchController: usesXpt ? 'xpt2046' : 'ft6336u',
291
657
  }, displayProfile);
292
658
  const overlayDir = join(projectRoot, 'boards');
293
659
  mkdirSync(overlayDir, { recursive: true });
294
- writeIfChanged(join(overlayDir, `${board}.overlay`), overlay);
660
+ // prepare() runs before the real target is known — the placeholder name
661
+ // never matches `west build -b <board>` (compile rewrites the overlay
662
+ // under the actual board's name below).
663
+ writeIfChanged(join(overlayDir, 'board.overlay'), overlay);
664
+ // Thin SPI targets need an app-local binding: a compatible-less DT node
665
+ // generates NO property macros, so SPI_DT_SPEC_GET's spi-max-frequency
666
+ // lookup would not exist. The binding has no driver — it exists so
667
+ // gen_defines emits the spi properties for the target nodes.
668
+ if (spiTargetParts.length > 0) {
669
+ const bindingsDir = join(projectRoot, 'dts', 'bindings');
670
+ mkdirSync(bindingsDir, { recursive: true });
671
+ writeIfChanged(join(bindingsDir, 'cuttlefish,spi-target.yaml'), [
672
+ 'description: |',
673
+ ' Cuttlefish thin SPITarget (hal/spi-target.ts) — a raw spi_dt_spec',
674
+ ' peer. No driver binds this compatible; it exists so devicetree',
675
+ ' generation emits the spi properties (spi-max-frequency,',
676
+ ' spi-cpol/spi-cpha, reg = the cs-gpios index) that SPI_DT_SPEC_GET',
677
+ ' consumes from the generated C++.',
678
+ 'compatible: "cuttlefish,spi-target"',
679
+ 'include: spi-device.yaml',
680
+ '',
681
+ ].join('\n'));
682
+ }
295
683
  },
296
684
  compile(o) {
297
685
  const projectRoot = projectRootFromOptions(o);
@@ -323,6 +711,19 @@ export const Toolchain = {
323
711
  // Zephyr auto-detects boards/<board>.overlay under APPLICATION_CONFIG_DIR.
324
712
  try {
325
713
  const chip = chipForBuild(projectRoot, board);
714
+ // Custom-board generation: an MCU-only target (no board package) has no
715
+ // upstream Zephyr board — generate one under boards/typecad/<name>/ from
716
+ // 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.
718
+ // Idempotent — regenerated on every compile, before the overlay pass.
719
+ if (zc?.customBoard === true) {
720
+ const generated = generateCustomBoard(projectRoot, chip, board.split('/')[0]);
721
+ if (!generated) {
722
+ throw new Error(`zephyr.customBoard is set, but the resolved chip ('${chip.id}') carries no ` +
723
+ `silicon board data. Custom-board generation requires an MCU-only config ` +
724
+ `(mcu set, board absent) whose MCU package ships a zephyr block.`);
725
+ }
726
+ }
326
727
  const srcDir = join(projectRoot, 'src');
327
728
  let src = '';
328
729
  try {
@@ -408,14 +809,35 @@ export const Toolchain = {
408
809
  touchWiring = { controller: 'xpt2046', ...(touchWiring ?? {}) };
409
810
  }
410
811
  const overlayDiagnostics = [];
812
+ const sensorParts = scanSensorParts(src);
813
+ const spiTargetParts = scanSpiTargets(src);
411
814
  const overlay = generateOverlay(chip, {
412
- usesI2c: uses('i2c_'),
413
- usesSpi: uses('spi_'),
414
- usesUart: uses('uart_'),
815
+ // __tc_<bus> matches the shim state block — a begin()-only program
816
+ // emits no driver API call but still declares the DT device. A
817
+ // constructed sensor is also a bus user (see scanSensorParts).
818
+ usesI2c: uses('i2c_') || uses('__tc_i2c') || sensorParts.length > 0,
819
+ usesSensor: uses('sensor_') || sensorParts.length > 0,
820
+ usesFloatFormat: /%[-0-9.]*[eEfFgG]/.test(src),
821
+ sensorParts,
822
+ spiTargets: spiTargetParts,
823
+ usesSpi: uses('spi_') || uses('__tc_spi') || spiTargetParts.length > 0,
824
+ usesUart: uses('__tc_uart'),
825
+ usesUsb: uses('__tc_usb'),
415
826
  usesPwm: uses('pwm_'),
416
827
  usesAdc: uses('adc_'),
417
828
  adcReadPins: scanAdcReadPins(src, chip),
829
+ usesDac: uses('dac_') || uses('__tc_dac'),
830
+ dacWritePins: scanDacWritePins(src),
418
831
  pwmUsedPins: scanPwmUsedPins(src, chip),
832
+ i2cUsedInstances: scanUsedBusInstances(src, chip.i2c?.controllers, 'i2c'),
833
+ spiUsedInstances: scanUsedBusInstances(src, chip.spi?.controllers, 'spi'),
834
+ uartUsedInstances: scanUsedBusInstances(src, chip.uart?.controllers, 'uart'),
835
+ // Preferences/FS — same tokens scaffoldZephyrProject scans; drive the
836
+ // storage-partition synthesis + /chosen settings pointer.
837
+ usesPreferences: uses('settings_') || uses('__tc_prefs'),
838
+ usesWdt: uses('wdt_'),
839
+ usesHwtimer: uses('counter_') || uses('__tc_hw'),
840
+ usesFS: uses('__tc_fs'),
419
841
  usesDisplay,
420
842
  usesTouch: uses('ft6336u') || uses('touch_') || usesXpt,
421
843
  touchController: usesXpt ? 'xpt2046' : 'ft6336u',
@@ -433,6 +855,24 @@ export const Toolchain = {
433
855
  // packages' overlay fragments are appended by the scaffold helper.
434
856
  const boardId = board.split('/')[0];
435
857
  writeIfChanged(join(overlayDir, `${boardId}.overlay`), appendLibraryOverlayFragments(overlay, projectRoot));
858
+ // Thin SPI targets need the app-local binding (see the transpile-side
859
+ // write for the rationale): no compatible → no generated spi props →
860
+ // SPI_DT_SPEC_GET's spi-max-frequency lookup does not exist.
861
+ if (spiTargetParts.length > 0) {
862
+ const bindingsDir = join(projectRoot, 'dts', 'bindings');
863
+ mkdirSync(bindingsDir, { recursive: true });
864
+ writeIfChanged(join(bindingsDir, 'cuttlefish,spi-target.yaml'), [
865
+ 'description: |',
866
+ ' Cuttlefish thin SPITarget (hal/spi-target.ts) — a raw spi_dt_spec',
867
+ ' peer. No driver binds this compatible; it exists so devicetree',
868
+ ' generation emits the spi properties (spi-max-frequency,',
869
+ ' spi-cpol/spi-cpha, reg = the cs-gpios index) that SPI_DT_SPEC_GET',
870
+ ' consumes from the generated C++.',
871
+ 'compatible: "cuttlefish,spi-target"',
872
+ 'include: spi-device.yaml',
873
+ '',
874
+ ].join('\n'));
875
+ }
436
876
  }
437
877
  catch { /* best-effort overlay regen; the build surfaces DT errors */ }
438
878
  // Use a stable build dir so incremental builds reuse the Ninja graph.
@@ -513,11 +953,11 @@ export const Toolchain = {
513
953
  // toolchain compile() debug-config wiring.
514
954
  if (result.status === 0 && isGdbDebug) {
515
955
  try {
516
- const { workspaceRoot, sketchRel } = resolveDebugLocations(projectRoot);
956
+ const { workspaceRoot, appRel } = resolveDebugLocations(projectRoot);
517
957
  writeDebugConfig({
518
958
  projectRoot,
519
959
  workspaceRoot,
520
- sketchRel,
960
+ appRel,
521
961
  target: board,
522
962
  buildDir,
523
963
  sourceMapPath: join(dirname(o.sourcePath), `${basename(o.sourcePath)}.thcppmap.json`),
@@ -527,6 +967,41 @@ export const Toolchain = {
527
967
  console.warn(`[cuttlefish] gdb debug config generation failed: ${e.message}`);
528
968
  }
529
969
  }
970
+ // As-built snapshot: after a successful build, the resolved devicetree
971
+ // at <buildDir>/zephyr/zephyr.dts carries the board's pinctrl labels —
972
+ // the STABLE name grammar, immune to vendor macro churn. Harvest its
973
+ // routes into .cuttlefish/as-built.json; the next build's board-module
974
+ // generation merges them per-pin over the catalog harvest (build wins,
975
+ // silently when they agree). One-build freshness lag on first setup,
976
+ // self-maintaining after. Best-effort — a missing/unparseable artifact
977
+ // never fails the build.
978
+ if (result.status === 0) {
979
+ try {
980
+ const dtsPath = join(buildDir, 'zephyr', 'zephyr.dts');
981
+ const dtsText = readFileSync(dtsPath, 'utf8');
982
+ const facts = parseZephyrDts(dtsText);
983
+ const total = facts.adc.length + facts.pwm.length + facts.dac.length;
984
+ if (total > 0) {
985
+ // Write beside the project's board module — the .cuttlefish dir the
986
+ // config loader reads from, discovered by walking up to the
987
+ // generated board.json (the scaffold root and the config root are
988
+ // different dirs in the standard layout: src/out vs project root).
989
+ let cfDir = join(projectRoot, '.cuttlefish');
990
+ for (let dir = projectRoot;; dir = dirname(dir)) {
991
+ if (existsSync(join(dir, '.cuttlefish', 'board.json'))) {
992
+ cfDir = join(dir, '.cuttlefish');
993
+ break;
994
+ }
995
+ const parent = dirname(dir);
996
+ if (parent === dir)
997
+ break;
998
+ }
999
+ mkdirSync(cfDir, { recursive: true });
1000
+ writeIfChanged(join(cfDir, 'as-built.json'), asBuiltJson(board, facts));
1001
+ }
1002
+ }
1003
+ catch { /* best-effort snapshot — nothing to harvest or unreadable */ }
1004
+ }
530
1005
  return {
531
1006
  success: result.status === 0,
532
1007
  output: header + output,
@@ -538,21 +1013,143 @@ export const Toolchain = {
538
1013
  const buildDir = join(projectRoot, 'build');
539
1014
  const board = targetFromOptions(o);
540
1015
  const zc = o.zephyrConfig;
541
- const runner = zc?.runner;
542
- const runnerArgs = zc?.runnerArgs;
543
- const args = buildFlashArgs(buildDir, board, runner, o.port, runnerArgs);
544
- const inv = westSpawn(args, {
545
- cwd: projectRoot,
546
- encoding: 'utf-8',
547
- timeout: FLASH_TIMEOUT_MS,
548
- });
549
- const result = spawnSync(inv.command, inv.args, inv.options);
550
- const fstdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
551
- const fstderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
552
- const raw = fstdout + fstderr;
1016
+ const chip = chipForBuild(projectRoot, board);
1017
+ const probe = resolveProbeMethod(zc, chip, 'flash');
1018
+ if (!probe.ok) {
1019
+ return { success: false, output: `-- west flash: ${probe.error}` };
1020
+ }
1021
+ // BOSSA bootloader boards with touch-reset data: open the app's console
1022
+ // port at 1200 baud (the firmware's USB shim reboots into the
1023
+ // bootloader), wait for the bootloader identity, and flash THAT port.
1024
+ // Falls back to the configured port (manual double-tap) on any failure.
1025
+ let flashPort = o.port;
1026
+ const flashNotes = [];
1027
+ // The runner this flash will actually use: the explicit choice, else the
1028
+ // board's declared default (first probe-method entry). Gates port
1029
+ // forwarding and the bossac touch below — board.cmake still resolves
1030
+ // the default runner itself.
1031
+ const flashRunner = probe.runner ?? chip.probeMethods?.[0]?.runner;
1032
+ // Serial-port runners cannot flash without a port; every other runner
1033
+ // (probe or USB) proceeds — whether a port is required is the runner's
1034
+ // call, not the CLI's blanket gate.
1035
+ if (uploadRequiresPort(flashRunner) && !flashPort) {
1036
+ return {
1037
+ success: false,
1038
+ output: `-- upload requires a port for ${flashRunner} flashing. Set --port <port> on the command line (or the CUTTLEFISH_PORT env var).`,
1039
+ };
1040
+ }
1041
+ if (flashRunner === 'bossac' && flashPort && chip.usb?.touchReset) {
1042
+ const touch = bossacTouchReset(flashPort, chip.usb.touchReset);
1043
+ flashNotes.push(`-- ${touch.note}`);
1044
+ if (touch.port)
1045
+ flashPort = touch.port;
1046
+ }
1047
+ const args = buildFlashArgs(buildDir, probe.runner, flashPort, flashRunner, probe.args);
1048
+ // openocd flashes go through a dedicated session instead of `west flash`.
1049
+ // west's flow has three sequential races that each strand the board: the
1050
+ // connect happens against a running (often USB-active) application, the
1051
+ // erase precedes the write so the vector table is 0xFFFFFFFF while the
1052
+ // RAM algorithm runs (any exception → core LOCKUP at 0xFFFFFFFE, "timeout
1053
+ // waiting for algorithm"), and the trailing `reset run` frequently does
1054
+ // not reach the core. This session is deterministic end to end: halt at
1055
+ // the reset vector (static target for the DAP), mask interrupts for the
1056
+ // algorithm (exceptions cannot vector through erased flash), unmask
1057
+ // after, and boot the flashed app with a direct SYSRESETREQ. Falls back
1058
+ // to `west flash` when the image or session is unavailable.
1059
+ let westFallback = true;
1060
+ if (probe.runner === 'openocd') {
1061
+ const hex = join(buildDir, 'zephyr', 'zephyr.hex');
1062
+ if (existsSync(hex)) {
1063
+ // Forward slashes + TCL quoting so project paths with spaces work.
1064
+ const hexArg = `"${hex.replace(/\\/g, '/')}"`;
1065
+ // Target addressing: `cortex_m` is a PER-TARGET subcommand — a bare
1066
+ // `cortex_m maskisr on` is an unknown command (a silent no-op inside
1067
+ // catch). Resolve the session's target object once and address it.
1068
+ // Cortex-M-only, self-gating: on other cores the cortex_m method
1069
+ // errors and catch contains it (the plain flash path is safe there
1070
+ // without masking — the lockup class is Cortex-M vectoring).
1071
+ const flashed = openocdProbeSession(buildDir, zc, chip, [
1072
+ 'set _tgt [lindex [target names] 0]',
1073
+ 'reset halt',
1074
+ 'catch { $_tgt cortex_m maskisr on }',
1075
+ `flash write_image erase ${hexArg}`,
1076
+ 'catch { $_tgt cortex_m maskisr off }',
1077
+ // Boot the flashed app. Cortex-M: a direct SYSRESETREQ via AIRCR —
1078
+ // pin-independent, always reaches the core, and clears PRIMASK
1079
+ // (so the masked algorithm leaves nothing behind). Other cores:
1080
+ // openocd's generic reset run. A halted core STAYS halted across
1081
+ // a core-initiated reset (debug halt state survives — that is how
1082
+ // reset halt works), so resume it; on a running core resume errors
1083
+ // and the catch swallows it.
1084
+ 'if {[catch {$_tgt cortex_m maskisr on}] == 0} { $_tgt cortex_m maskisr off; mww 0xE000ED0C 0x05FA0004 } else { reset run }',
1085
+ 'sleep 100',
1086
+ 'catch { resume }',
1087
+ 'sleep 200',
1088
+ ]);
1089
+ if (flashed) {
1090
+ westFallback = false;
1091
+ flashNotes.push('-- probe flash ok: halt → masked write → SYSRESETREQ');
1092
+ }
1093
+ }
1094
+ }
1095
+ let result;
1096
+ let raw = '';
1097
+ let ok = false;
1098
+ if (westFallback) {
1099
+ const inv = westSpawn(args, {
1100
+ cwd: projectRoot,
1101
+ encoding: 'utf-8',
1102
+ timeout: FLASH_TIMEOUT_MS,
1103
+ });
1104
+ result = spawnSync(inv.command, inv.args, inv.options);
1105
+ const fstdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
1106
+ const fstderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
1107
+ raw = fstdout + fstderr;
1108
+ ok = classifyUploadResult(probe.runner, result.status, raw);
1109
+ // A SUCCESSFUL west openocd flash can still leave the core HALTED:
1110
+ // west's trailing `reset run` does not reach a core behind an unwired
1111
+ // SRST, and the user's only recourse is the NRST button. Boot it from
1112
+ // a probe session instead — one core reset (a halted core re-halts at
1113
+ // the reset vector; a running core restarts the just-flashed app)
1114
+ // plus a resume.
1115
+ if (ok && probe.runner === 'openocd') {
1116
+ const booted = openocdProbeSession(buildDir, zc, chip, [
1117
+ 'set _tgt [lindex [target names] 0]',
1118
+ 'if {[catch {$_tgt cortex_m maskisr on}] == 0} { $_tgt cortex_m maskisr off; mww 0xE000ED0C 0x05FA0004 } else { reset run }',
1119
+ 'sleep 100',
1120
+ 'catch { resume }',
1121
+ 'sleep 200',
1122
+ ]);
1123
+ if (booted)
1124
+ flashNotes.push('-- probe boot ok: SYSRESETREQ → resume');
1125
+ }
1126
+ // Fallback-path recovery: a failed west flash leaves the core in
1127
+ // lockup; a direct SYSRESETREQ clears it so the caller's retry (or a
1128
+ // later flash) starts from a clean chip.
1129
+ if (!ok && probe.runner === 'openocd') {
1130
+ const revived = openocdProbeSession(buildDir, zc, chip, [
1131
+ 'init',
1132
+ 'set _tgt [lindex [target names] 0]',
1133
+ 'if {[catch {$_tgt cortex_m maskisr on}] == 0} { $_tgt cortex_m maskisr off; mww 0xE000ED0C 0x05FA0004 } else { reset run }',
1134
+ 'sleep 300',
1135
+ ]);
1136
+ if (revived)
1137
+ flashNotes.push(revived);
1138
+ // Known SWD-failure signatures get a recovery pointer — a board that
1139
+ // ignores SWD until power-cycled (low-power state, lockup, a wedged
1140
+ // probe) otherwise reads as a toolchain bug.
1141
+ if (isTargetSwdFailure(raw)) {
1142
+ flashNotes.push('-- target ignored SWD — if a retry fails too: power-cycle the board, replug the probe, or skip SWD entirely (hold BOOT0, tap reset, re-run with --probe dfu)');
1143
+ }
1144
+ }
1145
+ }
1146
+ else {
1147
+ ok = true;
1148
+ }
553
1149
  return {
554
- success: classifyUploadResult(runner, result.status, raw),
555
- output: cleanseUploadOutput(runner, result.status, raw),
1150
+ success: ok,
1151
+ output: [...flashNotes, cleanseUploadOutput(probe.runner, westFallback ? result.status : 0, raw)]
1152
+ .filter(Boolean).join('\n'),
556
1153
  };
557
1154
  },
558
1155
  monitor(o) {
@@ -567,7 +1164,7 @@ export const Toolchain = {
567
1164
  }
568
1165
  // ESP32 USB-CDC console runs at 115200 (the Zephyr ESP32 board default).
569
1166
  // The CLI's generic default of 9600 is wrong for this target; honor an
570
- // explicit --baud / config.console.baudRate when given, else 115200.
1167
+ // explicit --baud when given, else 115200.
571
1168
  const baud = o.baud ?? 115200;
572
1169
  const install = discoverWest();
573
1170
  const py = install?.pythonExecutable ?? process.env.PYTHON ?? 'python';
@@ -582,15 +1179,27 @@ export const Toolchain = {
582
1179
  });
583
1180
  },
584
1181
  debug(o) {
585
- // Launch an interactive GDB session for the last build. `west debug`
586
- // auto-resolves the runner (openocd for esp32s3, nrfjprog/jlink for nRF)
587
- // and the GDB binary from the build dir's CMakeCache/board.cmake no
1182
+ // Launch an interactive GDB session for the last build. The probe method
1183
+ // resolves exactly like flashing (zephyr.probe / zephyr.runner the same
1184
+ // attach session, so the same quirks apply); debug-incapable methods
1185
+ // (bootloaders) are rejected with the debug-capable list. west debug
1186
+ // resolves the GDB binary from the build dir's CMakeCache — no
588
1187
  // hand-authored gdbinit needed. Inherits stdio so GDB runs interactively.
589
- // (Not invoked by the standard build/compile flow; powers an explicit
590
- // debug-attach entry point for terminal-driven debugging without VS Code.)
591
1188
  const projectRoot = projectRootFromOptions(o);
592
1189
  const buildDir = join(projectRoot, 'build');
593
- const inv = westSpawn(['debug', '-d', buildDir], {
1190
+ const board = targetFromOptions(o);
1191
+ const zc = o.zephyrConfig;
1192
+ const probe = resolveProbeMethod(zc, chipForBuild(projectRoot, board), 'debug');
1193
+ if (!probe.ok) {
1194
+ console.error(`-- west debug: ${probe.error}`);
1195
+ process.exitCode = 1;
1196
+ return;
1197
+ }
1198
+ const debugArgs = ['debug', '-d', buildDir];
1199
+ if (probe.runner)
1200
+ debugArgs.push('--runner', probe.runner);
1201
+ debugArgs.push(...probe.args);
1202
+ const inv = westSpawn(debugArgs, {
594
1203
  cwd: projectRoot,
595
1204
  encoding: 'utf-8',
596
1205
  stdio: 'inherit',