@typecad/cuttlefish 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 (343) hide show
  1. package/README.md +82 -79
  2. package/assets/editor-extensions/typecad-debug/README.md +141 -0
  3. package/assets/editor-extensions/typecad-debug/out/extension.js +347 -0
  4. package/assets/editor-extensions/typecad-debug/out/extension.js.map +1 -0
  5. package/assets/editor-extensions/typecad-debug/package.json +76 -0
  6. package/assets/editor-extensions/typecad-ui/LICENSE +27 -0
  7. package/assets/editor-extensions/typecad-ui/README.md +52 -0
  8. package/assets/editor-extensions/typecad-ui/file-icon-dark.png +0 -0
  9. package/assets/editor-extensions/typecad-ui/file-icon-light.png +0 -0
  10. package/assets/editor-extensions/typecad-ui/icon.png +0 -0
  11. package/assets/editor-extensions/typecad-ui/language-configuration.json +43 -0
  12. package/assets/editor-extensions/typecad-ui/package.json +54 -0
  13. package/assets/editor-extensions/typecad-ui/snippets/typecad-ui.json +80 -0
  14. package/assets/editor-extensions/typecad-ui/syntaxes/markdown-ui.json +45 -0
  15. package/assets/editor-extensions/typecad-ui/syntaxes/typecad-ui.tmLanguage.json +1269 -0
  16. package/dist/api/config.d.ts +30 -27
  17. package/dist/api/schema/types.d.ts +20 -3
  18. package/dist/api/shared/async-runtime-static.d.ts +1 -1
  19. package/dist/api/shared/async-runtime-static.js +5 -5
  20. package/dist/api/shared/async-types.d.ts +3 -2
  21. package/dist/api/shared/board-catalog.d.ts +2 -0
  22. package/dist/api/shared/board-catalog.js +6 -0
  23. package/dist/api/shared/board-resolver.d.ts +4 -7
  24. package/dist/api/shared/board-resolver.js +2 -1
  25. package/dist/api/shared/coop-scheduler.d.ts +1 -1
  26. package/dist/api/shared/coop-scheduler.js +1 -1
  27. package/dist/api/shared/cpp-type-ir.d.ts +1 -1
  28. package/dist/api/shared/cpp-type-ir.js +1 -1
  29. package/dist/api/shared/display-profile.d.ts +1 -1
  30. package/dist/api/shared/display-profile.js +3 -5
  31. package/dist/api/shared/framework-manifest-registry.d.ts +1 -1
  32. package/dist/api/shared/framework-manifest-registry.js +0 -1
  33. package/dist/api/shared/framework-manifest.d.ts +65 -507
  34. package/dist/api/shared/framework-manifest.js +6 -11
  35. package/dist/api/shared/hal-op-ir.d.ts +294 -685
  36. package/dist/api/shared/hal-op-ir.js +31 -65
  37. package/dist/api/shared/index.d.ts +0 -4
  38. package/dist/api/shared/index.js +1 -7
  39. package/dist/api/shared/ir-core.d.ts +6 -0
  40. package/dist/api/shared/ir-declarations.d.ts +4 -0
  41. package/dist/api/shared/ir.d.ts +1 -3
  42. package/dist/api/shared/platform-strategy.d.ts +14 -37
  43. package/dist/api/shared/polyfill-helper-registry.js +0 -5
  44. package/dist/api/shared/polyfill-types.js +0 -27
  45. package/dist/api/shared/promise-runtime.js +3 -3
  46. package/dist/api/shared/spdx-licenses.d.ts +12 -10
  47. package/dist/api/shared/spdx-licenses.js +1 -1
  48. package/dist/api/shared/toolchain-types.d.ts +1 -1
  49. package/dist/api/shared/toolchain-types.js +3 -3
  50. package/dist/api/shared/types.d.ts +0 -3
  51. package/dist/api/shared/validate-framework-manifest.js +13 -33
  52. package/dist/board-catalog/dts-reader.d.ts +220 -0
  53. package/dist/board-catalog/dts-reader.js +1052 -0
  54. package/dist/board-catalog/index.d.ts +11 -0
  55. package/dist/board-catalog/index.js +12 -0
  56. package/dist/board-catalog/pinconfig.d.ts +48 -0
  57. package/dist/board-catalog/pinconfig.js +263 -0
  58. package/dist/board-catalog/sdk.d.ts +82 -0
  59. package/dist/board-catalog/sdk.js +180 -0
  60. package/dist/board-catalog/store.d.ts +196 -0
  61. package/dist/board-catalog/store.js +363 -0
  62. package/dist/board-catalog/types.d.ts +233 -0
  63. package/dist/board-catalog/types.js +7 -0
  64. package/dist/board-catalog/walker.d.ts +112 -0
  65. package/dist/board-catalog/walker.js +1101 -0
  66. package/dist/cli-utils.d.ts +2 -2
  67. package/dist/cli-utils.js +5 -5
  68. package/dist/cli.js +227 -244
  69. package/dist/config-loader.d.ts +14 -19
  70. package/dist/config-loader.js +242 -78
  71. package/dist/config-schema.d.ts +19 -227
  72. package/dist/config-schema.js +9 -8
  73. package/dist/contract/board-generator.d.ts +4 -4
  74. package/dist/contract/board-generator.js +16 -5
  75. package/dist/contract/contract-parser.d.ts +36 -127
  76. package/dist/contract/contract-parser.js +76 -0
  77. package/dist/contract/index.d.ts +1 -1
  78. package/dist/contract/index.js +70 -21
  79. package/dist/create/active-board-catalog.d.ts +9 -0
  80. package/dist/create/active-board-catalog.js +94 -0
  81. package/dist/create/board-catalog.generated.d.ts +96 -0
  82. package/dist/create/board-catalog.generated.js +128737 -0
  83. package/dist/create/board-search.d.ts +23 -0
  84. package/dist/create/board-search.js +88 -0
  85. package/dist/create/editor-integration.d.ts +32 -0
  86. package/dist/create/editor-integration.js +257 -0
  87. package/dist/create/eslint-rules-template.d.ts +2 -2
  88. package/dist/create/framework-catalog.d.ts +48 -8
  89. package/dist/create/framework-catalog.js +58 -44
  90. package/dist/create/index.d.ts +8 -13
  91. package/dist/create/index.js +5 -9
  92. package/dist/create/mcu-target.d.ts +39 -0
  93. package/dist/create/mcu-target.js +80 -0
  94. package/dist/create/pack-targets.d.ts +19 -0
  95. package/dist/create/pack-targets.js +56 -0
  96. package/dist/create/{init-scaffold.d.ts → scaffold.d.ts} +8 -11
  97. package/dist/create/{init-scaffold.js → scaffold.js} +28 -87
  98. package/dist/create/templates.d.ts +51 -0
  99. package/dist/create/{init-templates.js → templates.js} +135 -116
  100. package/dist/create/wizard.d.ts +19 -0
  101. package/dist/create/wizard.js +314 -0
  102. package/dist/debug/preprocessor.d.ts +5 -0
  103. package/dist/debug/preprocessor.js +31 -7
  104. package/dist/diagnostics/diagnostics-report.d.ts +1 -1
  105. package/dist/diagnostics/diagnostics-report.js +3 -11
  106. package/dist/diagnostics/json-schema.d.ts +1 -1
  107. package/dist/diagnostics/md-writer.js +2 -2
  108. package/dist/diagnostics/mermaid-builder.d.ts +1 -1
  109. package/dist/diagnostics/mermaid-builder.js +1 -1
  110. package/dist/emit/cpp-emitter.js +4 -1
  111. package/dist/emit/emitters/class-emitter.js +32 -4
  112. package/dist/emit/emitters/emitter-context.d.ts +11 -0
  113. package/dist/emit/emitters/entrypoint-synthesizer.js +1 -1
  114. package/dist/emit/emitters/function-emitter-impl.js +33 -17
  115. package/dist/emit/emitters/output-finalizer.d.ts +7 -0
  116. package/dist/emit/emitters/output-finalizer.js +31 -23
  117. package/dist/emit/emitters/setup.js +147 -63
  118. package/dist/emit/emitters/top-level-prep.js +1 -1
  119. package/dist/emit/emitters/type-decl-emitter.js +13 -8
  120. package/dist/emit/emitters/ui-emitter.js +1 -1
  121. package/dist/emit/expression-renderer.d.ts +3 -3
  122. package/dist/emit/expression-renderer.js +17 -7
  123. package/dist/emit/route-hal-op.js +17 -48
  124. package/dist/emit/snprintf-helpers.d.ts +1 -3
  125. package/dist/emit/snprintf-helpers.js +1 -39
  126. package/dist/emit/statement-renderer.d.ts +0 -4
  127. package/dist/emit/statement-renderer.js +54 -45
  128. package/dist/emit/utils/async-state-machine.d.ts +16 -9
  129. package/dist/emit/utils/async-state-machine.js +56 -54
  130. package/dist/emit/utils/comment-helpers.d.ts +0 -8
  131. package/dist/emit/utils/comment-helpers.js +1 -1
  132. package/dist/emit/utils/cpp-helpers.d.ts +2 -2
  133. package/dist/emit/utils/cpp-helpers.js +2 -2
  134. package/dist/emit/utils/hal-op-cpp-type.js +4 -6
  135. package/dist/emit/utils/index.d.ts +3 -3
  136. package/dist/emit/utils/index.js +3 -3
  137. package/dist/emit/utils/type-inference.d.ts +1 -29
  138. package/dist/emit/utils/type-inference.js +0 -75
  139. package/dist/framework-package.d.ts +3 -0
  140. package/dist/framework-package.js +23 -6
  141. package/dist/framework-registry.d.ts +5 -5
  142. package/dist/frameworks/native/graphics/terminal-preview.d.ts +5 -0
  143. package/dist/frameworks/native/graphics/terminal-preview.js +33 -0
  144. package/dist/frameworks/native/index.d.ts +3 -0
  145. package/dist/frameworks/native/index.js +15 -0
  146. package/dist/frameworks/native/native-compile.d.ts +17 -0
  147. package/dist/frameworks/native/native-compile.js +170 -0
  148. package/dist/frameworks/native/native-config.d.ts +41 -0
  149. package/dist/frameworks/native/native-config.js +7 -0
  150. package/dist/frameworks/native/strategy.d.ts +97 -0
  151. package/dist/frameworks/native/strategy.js +664 -0
  152. package/dist/ir/board-resolver.d.ts +18 -23
  153. package/dist/ir/board-resolver.js +43 -401
  154. package/dist/ir/build-ir-state.d.ts +3 -0
  155. package/dist/ir/build-ir-state.js +42 -0
  156. package/dist/ir/build-ir.d.ts +1 -1
  157. package/dist/ir/build-ir.js +64 -45
  158. package/dist/ir/declaration-builders.js +1 -0
  159. package/dist/ir/expression-to-ir.js +17 -10
  160. package/dist/ir/hal/hal-emitter.d.ts +0 -2
  161. package/dist/ir/hal/hal-emitter.js +92 -41
  162. package/dist/ir/hal/hal-parser.d.ts +5 -5
  163. package/dist/ir/hal/hal-parser.js +128 -36
  164. package/dist/ir/hal/hal-plugins.d.ts +0 -1
  165. package/dist/ir/hal/hal-plugins.js +425 -716
  166. package/dist/ir/heap-analysis.js +2 -41
  167. package/dist/ir/identifier-collector.js +32 -20
  168. package/dist/ir/network-validation.js +7 -11
  169. package/dist/ir/ownership-analysis.js +75 -13
  170. package/dist/ir/peripheral-usage.d.ts +8 -2
  171. package/dist/ir/peripheral-usage.js +78 -149
  172. package/dist/ir/peripheral-validation.js +23 -3
  173. package/dist/ir/pin-capability-validation.d.ts +12 -0
  174. package/dist/ir/pin-capability-validation.js +61 -97
  175. package/dist/ir/pin-mode-validation.js +5 -5
  176. package/dist/ir/program-analysis.d.ts +8 -15
  177. package/dist/ir/program-analysis.js +140 -323
  178. package/dist/ir/pulldown-validation.js +11 -4
  179. package/dist/ir/resource-analysis.js +15 -3
  180. package/dist/ir/statement-to-ir.js +0 -6
  181. package/dist/ir/timing-validation.js +1 -1
  182. package/dist/ir/transformers/array-methods.js +11 -5
  183. package/dist/ir/transformers/call-statement.js +20 -87
  184. package/dist/ir/transformers/callback-context-registry.d.ts +1 -1
  185. package/dist/ir/transformers/callback-context-registry.js +8 -16
  186. package/dist/ir/transformers/control-flow.js +0 -13
  187. package/dist/ir/transformers/expressions.js +10 -9
  188. package/dist/ir/transformers/hal-call-resolver.d.ts +2 -2
  189. package/dist/ir/transformers/hal-call-resolver.js +24 -10
  190. package/dist/ir/transformers/hal-emit-helpers.d.ts +2 -2
  191. package/dist/ir/transformers/hal-emit-helpers.js +2 -2
  192. package/dist/ir/transformers/namespace-methods.js +1 -1
  193. package/dist/ir/transformers/ui-callback-lowering.d.ts +0 -2
  194. package/dist/ir/transformers/ui-callback-lowering.js +0 -24
  195. package/dist/ir/transformers/variables.js +338 -42
  196. package/dist/ir/try-catch-validation.js +4 -3
  197. package/dist/ir/type-resolution.js +15 -4
  198. package/dist/ir/validation-orchestrator.js +0 -9
  199. package/dist/libdef/cpp-to-decl.d.ts +0 -9
  200. package/dist/libdef/cpp-to-decl.js +0 -72
  201. package/dist/libdef/registry.d.ts +0 -1
  202. package/dist/libdef/registry.js +1 -24
  203. package/dist/library/catalog.d.ts +35 -0
  204. package/dist/library/catalog.js +67 -0
  205. package/dist/library/cli.d.ts +2 -0
  206. package/dist/library/cli.js +168 -0
  207. package/dist/library/init.d.ts +25 -0
  208. package/dist/library/init.js +397 -0
  209. package/dist/library/install.d.ts +9 -0
  210. package/dist/library/install.js +80 -0
  211. package/dist/library/registry-search.d.ts +34 -0
  212. package/dist/library/registry-search.js +50 -0
  213. package/dist/library/validate.d.ts +17 -0
  214. package/dist/library/validate.js +175 -0
  215. package/dist/library-packages.d.ts +95 -0
  216. package/dist/library-packages.js +275 -0
  217. package/dist/mapping/peripheral-names.js +9 -4
  218. package/dist/mapping/source-map.d.ts +3 -4
  219. package/dist/mapping/source-map.js +12 -18
  220. package/dist/orchestrator/graph-builder.d.ts +3 -3
  221. package/dist/orchestrator/graph-builder.js +29 -12
  222. package/dist/orchestrator/type-checker.d.ts +3 -3
  223. package/dist/orchestrator/type-checker.js +3 -3
  224. package/dist/platform/async-runtime.js +1 -1
  225. package/dist/platform/coop-scheduler-runtime.js +2 -2
  226. package/dist/platform/generic-debug-codegen.d.ts +36 -0
  227. package/dist/platform/generic-debug-codegen.js +185 -0
  228. package/dist/platform/generic-strategy.d.ts +3 -4
  229. package/dist/platform/generic-strategy.js +5 -80
  230. package/dist/platform/index.js +1 -1
  231. package/dist/platform/registry.d.ts +2 -2
  232. package/dist/platform/registry.js +2 -2
  233. package/dist/preview/client.js +17 -3
  234. package/dist/safety/asil-decorators.d.ts +32 -0
  235. package/dist/safety/asil-decorators.js +32 -0
  236. package/dist/safety/authoring.d.ts +85 -0
  237. package/dist/safety/authoring.js +61 -0
  238. package/dist/safety/engine.d.ts +3 -0
  239. package/dist/safety/engine.js +83 -0
  240. package/dist/safety/hal/ops.d.ts +48 -0
  241. package/dist/safety/hal/ops.js +46 -0
  242. package/dist/safety/iso26262/analyze.d.ts +13 -0
  243. package/dist/safety/iso26262/analyze.js +29 -0
  244. package/dist/safety/iso26262/asil.d.ts +10 -0
  245. package/dist/safety/iso26262/asil.js +22 -0
  246. package/dist/safety/iso26262/collect.d.ts +14 -0
  247. package/dist/safety/iso26262/collect.js +137 -0
  248. package/dist/safety/iso26262/goto-checker.d.ts +10 -0
  249. package/dist/safety/iso26262/goto-checker.js +72 -0
  250. package/dist/safety/iso26262/heap-checker.d.ts +7 -0
  251. package/dist/safety/iso26262/heap-checker.js +57 -0
  252. package/dist/safety/iso26262/init-checker.d.ts +13 -0
  253. package/dist/safety/iso26262/init-checker.js +127 -0
  254. package/dist/safety/iso26262/loop-checker.d.ts +7 -0
  255. package/dist/safety/iso26262/loop-checker.js +86 -0
  256. package/dist/safety/iso26262/recursion-checker.d.ts +9 -0
  257. package/dist/safety/iso26262/recursion-checker.js +134 -0
  258. package/dist/safety/iso26262/sidecar-writer.d.ts +25 -0
  259. package/dist/safety/iso26262/sidecar-writer.js +13 -0
  260. package/dist/safety/passes/pinMode-intercept.d.ts +3 -0
  261. package/dist/safety/passes/pinMode-intercept.js +60 -0
  262. package/dist/safety/runtime/mode-table.d.ts +5 -0
  263. package/dist/safety/runtime/mode-table.js +55 -0
  264. package/dist/safety/runtime/polyfills.d.ts +6 -0
  265. package/dist/safety/runtime/polyfills.js +12 -0
  266. package/dist/safety/runtime/result-struct.d.ts +11 -0
  267. package/dist/safety/runtime/result-struct.js +98 -0
  268. package/dist/safety/runtime/safe-int.d.ts +33 -0
  269. package/dist/safety/runtime/safe-int.js +170 -0
  270. package/dist/safety/runtime/safe-traits.d.ts +4 -0
  271. package/dist/safety/runtime/safe-traits.js +95 -0
  272. package/dist/safety/runtime/safe-variable.d.ts +39 -0
  273. package/dist/safety/runtime/safe-variable.js +226 -0
  274. package/dist/safety/runtime/vote.d.ts +8 -0
  275. package/dist/safety/runtime/vote.js +92 -0
  276. package/dist/safety/runtime/write-verify.d.ts +8 -0
  277. package/dist/safety/runtime/write-verify.js +71 -0
  278. package/dist/safety/safe-int-types.d.ts +45 -0
  279. package/dist/safety/safe-int-types.js +25 -0
  280. package/dist/safety/safe-variable-types.d.ts +29 -0
  281. package/dist/safety/safe-variable-types.js +19 -0
  282. package/dist/safety/safety-bridge.d.ts +5 -6
  283. package/dist/safety/safety-bridge.js +15 -28
  284. package/dist/safety/sidecar-bridge.d.ts +1 -3
  285. package/dist/safety/sidecar-bridge.js +3 -20
  286. package/dist/safety/specifiers.d.ts +6 -0
  287. package/dist/safety/specifiers.js +14 -0
  288. package/dist/safety-hook.d.ts +8 -7
  289. package/dist/safety-hook.js +12 -13
  290. package/dist/testing.d.ts +5 -6
  291. package/dist/testing.js +5 -5
  292. package/dist/transpile/resolution.d.ts +1 -1
  293. package/dist/transpile/resolution.js +18 -9
  294. package/dist/transpile/test-pins.d.ts +18 -0
  295. package/dist/transpile/test-pins.js +142 -0
  296. package/dist/transpile.d.ts +1 -2
  297. package/dist/transpile.js +90 -29
  298. package/dist/types.d.ts +45 -26
  299. package/dist/ui-hook.d.ts +1 -1
  300. package/dist/utils/cli.d.ts +2 -2
  301. package/dist/utils/cli.js +160 -157
  302. package/dist/utils/fs.d.ts +3 -4
  303. package/dist/utils/fs.js +4 -5
  304. package/dist/utils/strings.d.ts +0 -1
  305. package/dist/utils/strings.js +0 -8
  306. package/dist/utils/ui.d.ts +5 -2
  307. package/dist/utils/ui.js +13 -3
  308. package/package.json +158 -149
  309. package/dist/add-preset.d.ts +0 -4
  310. package/dist/add-preset.js +0 -74
  311. package/dist/api/shared/async-symbol-detector.d.ts +0 -11
  312. package/dist/api/shared/async-symbol-detector.js +0 -140
  313. package/dist/api/shared/native-display-op-resolver.d.ts +0 -10
  314. package/dist/api/shared/native-display-op-resolver.js +0 -64
  315. package/dist/create/board-checklist.d.ts +0 -2
  316. package/dist/create/board-checklist.js +0 -52
  317. package/dist/create/board-codegen.d.ts +0 -9
  318. package/dist/create/board-codegen.js +0 -238
  319. package/dist/create/board-generators.d.ts +0 -12
  320. package/dist/create/board-generators.js +0 -651
  321. package/dist/create/board-spec.d.ts +0 -637
  322. package/dist/create/board-spec.js +0 -214
  323. package/dist/create/init-templates.d.ts +0 -28
  324. package/dist/create/init-wizard.d.ts +0 -8
  325. package/dist/create/init-wizard.js +0 -182
  326. package/dist/install/framework-catalog.d.ts +0 -53
  327. package/dist/install/framework-catalog.js +0 -107
  328. package/dist/install/handle-install.d.ts +0 -35
  329. package/dist/install/handle-install.js +0 -177
  330. package/dist/install/index.d.ts +0 -4
  331. package/dist/install/index.js +0 -3
  332. package/dist/ir/pin-state-tracking.d.ts +0 -58
  333. package/dist/ir/pin-state-tracking.js +0 -182
  334. package/dist/ir/pwm-timer-sharing.d.ts +0 -4
  335. package/dist/ir/pwm-timer-sharing.js +0 -94
  336. package/dist/ir/timer0-pwm-timing-conflict.d.ts +0 -4
  337. package/dist/ir/timer0-pwm-timing-conflict.js +0 -72
  338. package/dist/libdef/component-decls.d.ts +0 -2
  339. package/dist/libdef/component-decls.js +0 -6
  340. package/dist/libdef/component-discovery.d.ts +0 -43
  341. package/dist/libdef/component-discovery.js +0 -83
  342. package/dist/theme-tokens.d.ts +0 -22
  343. package/dist/theme-tokens.js +0 -172
@@ -0,0 +1,1052 @@
1
+ // ---------------------------------------------------------------------------
2
+ // dts-reader.ts — tolerant Zephyr board-devicetree reader
3
+ //
4
+ // Extracts the BOARD-LEVEL facts the generated board module needs from a
5
+ // board's own DTS files: aliases, the chosen console, gpio-leds / gpio-keys
6
+ // children (LED/BUTTON devicetree specs), connector nexus gpio-maps
7
+ // (arduino-header-r3, seeed,xiao-gpio), buses, USB wiring, watchdog,
8
+ // flash/storage and counters. Silicon pinctrl routes (STM32 tim/adc/dac
9
+ // *-pinctrl.dtsi) are harvested here in RAW form — the manifest generator
10
+ // maps them to global pins and carries the family conventions for SoCs whose
11
+ // silicon data is not in devicetree (ESP32 C headers, nRF SAADC AIN tables).
12
+ //
13
+ // DELIBERATELY NOT a devicetree compiler. The reader follows only the
14
+ // board's LOCAL include chain (relative *.dtsi and in-tree <...> includes it
15
+ // can resolve), and treats C macros it cannot expand as opaque flag tokens
16
+ // (`GPIO_ACTIVE_LOW` stays the literal text). It does not resolve phandles
17
+ // beyond &label capture, does not evaluate expressions, and ignores every
18
+ // property it has no consumer for. Boards whose interesting nodes live in
19
+ // deeper SoC includes simply yield fewer facts — the generator's tiering
20
+ // handles that honestly.
21
+ //
22
+ // Used by the catalog walker: `cuttlefish board sync` at user-sync time
23
+ // against the installed Zephyr tree (and by the fixture-tree walker tests).
24
+ // ----------------------------------------------------------------------------
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ function resolveInclude(spec, fromFile, roots) {
28
+ // Strip the wrapper: <...> or "..."
29
+ const inner = spec.slice(1, -1);
30
+ // Quoted includes are relative to the including file.
31
+ if (!spec.startsWith('<')) {
32
+ const p = path.resolve(path.dirname(fromFile), inner);
33
+ return fs.existsSync(p) ? p : undefined;
34
+ }
35
+ // Board files use ../boards/common/... relative includes and bare filenames
36
+ // resolved against the boards tree; try the file's own dir first, then the
37
+ // configured angle roots.
38
+ const local = path.resolve(path.dirname(fromFile), inner);
39
+ if (fs.existsSync(local))
40
+ return local;
41
+ for (const root of roots.angle) {
42
+ const p = path.resolve(root, inner);
43
+ if (fs.existsSync(p))
44
+ return p;
45
+ }
46
+ return undefined;
47
+ }
48
+ /** Strip block/line comments, keeping one attachment channel for gpio-map labels. */
49
+ function stripCommentsKeepMarkers(src) {
50
+ // Replace comment bodies but KEEP a marker: /* D0 */ → ⟨D0⟩ inline, so the
51
+ // gpio-map parser can pick silkscreen labels out of the cell stream.
52
+ return src
53
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => '⟨' + m.slice(2, -2).trim() + '⟩')
54
+ .replace(/\/\/[^\n]*/g, '');
55
+ }
56
+ function emptyNode(name, parent) {
57
+ return { labels: [], name, props: new Map(), children: new Map(), parent };
58
+ }
59
+ /** Statement scanner: emits node headers (at every '{'), property
60
+ * statements ('a = <...>' at their owning depth), and node closes ('}')
61
+ * in source order, building the node tree with &label override merges.
62
+ * `refOverrides` records every label an `&label { }` block targeted —
63
+ * with SoC dtsis absorbed, an override merges into the real labeled node
64
+ * instead of creating a synthetic '&label' one, so this set is the only
65
+ * way to tell "the board overrode this node" from "the SoC defines it". */
66
+ function parseStatements(src) {
67
+ const root = emptyNode('/');
68
+ const byLabel = new Map();
69
+ const refOverrides = new Set();
70
+ const stack = [root];
71
+ let depth = 0;
72
+ let start = 0;
73
+ const emit = (kind, text) => {
74
+ if (kind === 'end') {
75
+ if (stack.length > 1)
76
+ stack.pop();
77
+ return;
78
+ }
79
+ const t = text.trim().replace(/^[\r\n\t ]+/, '');
80
+ if (!t)
81
+ return;
82
+ const top = stack[stack.length - 1];
83
+ if (kind === 'hdr') {
84
+ const refHeader = t.match(/^&([\w-]+)\s*\{?$/);
85
+ if (refHeader) {
86
+ refOverrides.add(refHeader[1]);
87
+ let n = byLabel.get(refHeader[1]);
88
+ if (!n) {
89
+ n = emptyNode('&' + refHeader[1], top);
90
+ byLabel.set(refHeader[1], n);
91
+ // Attach to the synthetic tree so subtree walkers (connectors,
92
+ // gpio nodes, led strips, bus pinctrl) see &ref blocks — the SoC
93
+ // dtsi that parents them is not part of the local include chain.
94
+ top.children.set(n.name, n);
95
+ }
96
+ stack.push(n);
97
+ return;
98
+ }
99
+ // Header text may carry trailing debris from prior statements (the
100
+ // scanner slices from after the previous ';'/'}'), so isolate the LAST
101
+ // node-header line: [label:] name[@unit]
102
+ const headerLine = t.split(/[\r\n]+/).map((l) => l.trim()).filter(Boolean).pop() ?? t;
103
+ const parts = headerLine.split(':').map((s) => s.trim()).filter(Boolean);
104
+ const name = parts.length > 0 ? parts[parts.length - 1] : 'anon';
105
+ const labels = parts.slice(0, -1);
106
+ let node = top.children.get(name);
107
+ if (!node) {
108
+ node = emptyNode(name, top);
109
+ top.children.set(name, node);
110
+ }
111
+ for (const l of labels) {
112
+ if (!node.labels.includes(l))
113
+ node.labels.push(l);
114
+ byLabel.set(l, node);
115
+ }
116
+ stack.push(node);
117
+ return;
118
+ }
119
+ // property
120
+ // Leading comment markers (a comment line above a property, inside the
121
+ // same node — 96b_nitrogen's `/* gpio flags need validation */` before
122
+ // button0's gpios) must not hide the property: the anchored match would
123
+ // fail and the node would silently lose its facts. Strip them first;
124
+ // markers INSIDE a value stay (gpio-map silkscreen labels).
125
+ const stripped = t.replace(/^(?:⟨[^⟩]*⟩\s*)+/, '');
126
+ const prop = stripped.match(/^([\w,#@-]+)\s*=\s*([\s\S]*?)\s*;?$/);
127
+ if (prop && top !== root) {
128
+ top.props.set(prop[1], prop[2].replace(/;$/, ''));
129
+ return;
130
+ }
131
+ const bare = stripped.match(/^([\w,#@-]+)\s*;?$/);
132
+ if (bare && top !== root)
133
+ top.props.set(bare[1], '');
134
+ };
135
+ let i = 0;
136
+ while (i < src.length) {
137
+ const ch = src[i];
138
+ if (ch === '{') {
139
+ const stmt = src.slice(start, i);
140
+ emit('hdr', stmt);
141
+ depth++;
142
+ start = i + 1;
143
+ }
144
+ else if (ch === '}') {
145
+ const inner = src.slice(start, i).trim();
146
+ if (inner)
147
+ emit('prop', inner);
148
+ emit('end', '');
149
+ if (depth > 0)
150
+ depth--;
151
+ start = i + 1;
152
+ }
153
+ else if (ch === ';') {
154
+ const stmt = src.slice(start, i);
155
+ if (depth > 0)
156
+ emit('prop', stmt);
157
+ start = i + 1;
158
+ }
159
+ i++;
160
+ }
161
+ return { root, byLabel, refOverrides };
162
+ }
163
+ // ── cell parsing ───────────────────────────────────────────────────────────
164
+ /** Parse a property value like `<&gpioa 5 GPIO_ACTIVE_LOW>` into tokens. */
165
+ function parseCells(value) {
166
+ return value
167
+ // Angle-bracket cell wrappers are punctuation, not content.
168
+ .replace(/[<>]/g, ' ')
169
+ // Comment markers may carry multi-word labels ('D14 / A0', 'Pin 1,
170
+ // LEDK') — shield their internal whitespace AND commas from the split,
171
+ // restoring each to its own character afterwards (collapsing a comma
172
+ // to a space would hide it from the label splitter, and an unshielded
173
+ // comma tore the marker apart mid-token — '⟨Pin 1' + 'LEDK⟩' — whose
174
+ // truncated form minted garbage labels like 'Pin').
175
+ .replace(/⟨[^⟩]*⟩/g, (m) => ' ' + m.replace(/,/g, '\x1f').replace(/\s+/g, '\x1e') + ' ')
176
+ .split(/[\s,]+/)
177
+ .map((s) => s.trim().replace(/\x1f/g, ',').replace(/\x1e/g, ' '))
178
+ .filter(Boolean);
179
+ }
180
+ function parseGpioMap(value, parentCells) {
181
+ const toks = parseCells(value);
182
+ const entries = [];
183
+ let i = 0;
184
+ let pendingLabel;
185
+ const splitNames = (raw) =>
186
+ // '/' separates co-names of one pad ('D14 / A0'); ',' separates a pin
187
+ // number from its net name ('Pin 1, LEDK' → 'LEDK'). Whitespace is NOT
188
+ // a separator — 'Pin 1' must not collapse to 'Pin'.
189
+ raw.split(/[\/,]/).map((s) => s.trim()).filter((s) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s));
190
+ while (i < toks.length) {
191
+ // Sweep up any ⟨label⟩ markers preceding the numeric entry.
192
+ while (i < toks.length && toks[i].startsWith('⟨')) {
193
+ const inner = toks[i].slice(1, -1).trim();
194
+ if (inner)
195
+ pendingLabel = inner;
196
+ i++;
197
+ }
198
+ const spec = toks[i];
199
+ if (spec === undefined)
200
+ break;
201
+ let childSpec = Number(spec);
202
+ // Macro child specifiers (the nano header's ARDUINO_NANO_HEADER_D14):
203
+ // the numeric suffix is the specifier, the alphabetic suffix carries the
204
+ // silkscreen name when no comment label exists.
205
+ let macroLabel;
206
+ if (!Number.isFinite(childSpec)) {
207
+ // Multi-word macros (ARDUINO_NANO_HEADER_D14): the LAST underscore
208
+ // splits the prefix from the <letters><digits> silkscreen suffix.
209
+ const m = spec.match(/^([A-Za-z][A-Za-z0-9_]*)_([A-Za-z]+)(\d+)$/);
210
+ if (m) {
211
+ childSpec = Number(m[3]);
212
+ macroLabel = `${m[2]}${m[3]}`;
213
+ }
214
+ else {
215
+ i++;
216
+ continue;
217
+ }
218
+ }
219
+ // child-spec, pad, then &ctrl
220
+ const ctrl = toks[i + 2];
221
+ if (ctrl === undefined || !ctrl.startsWith('&')) {
222
+ i++;
223
+ continue;
224
+ }
225
+ const pin = Number(toks[i + 3]);
226
+ // Macro/expression pins (RTIO routing etc.) cannot be placed on the HAL
227
+ // pin map — skip the entry, keep scanning.
228
+ if (!Number.isFinite(pin)) {
229
+ i++;
230
+ continue;
231
+ }
232
+ const flags = [];
233
+ for (let f = i + 4; f < i + 3 + parentCells + 1 && f < toks.length; f++) {
234
+ const t = toks[f];
235
+ if (t.startsWith('&') || Number.isFinite(Number(t)))
236
+ break;
237
+ if (!t.startsWith('⟨'))
238
+ flags.push(t);
239
+ }
240
+ // A comment can name one pad twice ('D14 / A0' — the digital and analog
241
+ // silkscreen of the same pad): first name is the primary label, the rest
242
+ // ride as altLabels.
243
+ const names = pendingLabel ? splitNames(pendingLabel) : [];
244
+ const label = names[0] ?? macroLabel;
245
+ const altLabels = names.slice(1);
246
+ entries.push({
247
+ childSpecifier: childSpec,
248
+ ...(label ? { label } : {}),
249
+ ...(altLabels.length > 0 ? { altLabels } : {}),
250
+ ref: { controller: ctrl.slice(1), pin, flags },
251
+ });
252
+ pendingLabel = undefined;
253
+ // advance: spec + pad + &ctrl + parentCells numbers
254
+ i = i + 3 + parentCells;
255
+ // sweep trailing markers before the next entry
256
+ while (i < toks.length && toks[i].startsWith('⟨')) {
257
+ const inner = toks[i].slice(1, -1).trim();
258
+ if (inner && entries.length > 0) {
259
+ const trailing = splitNames(inner);
260
+ const last = entries[entries.length - 1];
261
+ if (trailing.length > 0 && !last.label)
262
+ last.label = trailing[0];
263
+ if (trailing.length > 1)
264
+ last.altLabels = [...(last.altLabels ?? []), ...trailing.slice(1)];
265
+ }
266
+ i++;
267
+ }
268
+ }
269
+ return entries;
270
+ }
271
+ // ── board facts extraction ─────────────────────────────────────────────────
272
+ function gpioRefFromProp(value) {
273
+ const toks = parseCells(value);
274
+ // Expect: &ctrl pin [flags...]
275
+ if (toks.length < 2 || !toks[0].startsWith('&'))
276
+ return undefined;
277
+ const pin = Number(toks[1]);
278
+ if (!Number.isFinite(pin))
279
+ return undefined;
280
+ // Normalize flag tokens: strip grouping parens, drop '|' separators.
281
+ const flags = toks
282
+ .slice(2)
283
+ .filter((t) => !t.startsWith('⟨') && t !== '|')
284
+ .map((t) => t.replace(/[()]/g, ''))
285
+ .filter(Boolean);
286
+ return { controller: toks[0].slice(1), pin, flags };
287
+ }
288
+ // ── silicon pinctrl harvest ────────────────────────────────────────────────
289
+ // Per-family route grammars live in the PINCTRL_DIALECTS table below (each
290
+ // name/macro token encodes the complete route — source/channel/port/bit).
291
+ // The name↔value cross-check in lintNameValueAgreement covers each family's
292
+ // VALUE grammar (the pinmux macro's port/bit tuple).
293
+ // Labeled PWM controller device nodes (nRF `pwm0: pwm@4001c000` — any GPIO
294
+ // pad can carry any of a peripheral's four psel-routed channels, the matrix
295
+ // the manifest generator synthesizes from). ESP's ledc0 keeps its own
296
+ // LEDC_NODE_RE; STM32's pwm children are label-less and never match.
297
+ const PWM_NODE_RE = /(pwm\d+):\s*pwm@[0-9a-f]+\s*\{/g;
298
+ // ── NXP i.MX RT pinctrl dtsi (node-name shape, STM32's form) ───────────────
299
+ // `iomuxc_<pad>_adc1_in1: …` — the pad-to-GPIO join is in-band: every pad
300
+ // also declares `iomuxc_<pad>_gpio<port>_io<pin>`. FlexPWM routes the two
301
+ // subchannel outputs (`pwma`/`pwmb`, channel 0/1 of the DT child
302
+ // `flexpwm<N>_pwm<K>`); the complementary `pwmx` is skipped. This family is
303
+ // NOT in PINCTRL_DIALECTS — its routes resolve through a two-phase join.
304
+ //
305
+ // Two FlexPWM node-name conventions exist across the RT parts (both
306
+ // harvested): the classic rt10xx `…_flexpwm2_pwma3`, and the newer
307
+ // rt11xx/rt116x/rt118x `…_flexpwm1_pwm0_a` (pwm<K>_<a|b>, A→0/B→1; the `_x`
308
+ // complementary stays out, mirroring `pwmx`). The newer parts' ADC is the
309
+ // LPADC (`lpadc<N>`) — a different driver with a differential `ch<N>a|b`
310
+ // input-pair channel model — and stays OUT like the LPC55 lpadc, pending a
311
+ // dedicated channel-model gate.
312
+ const IMX_GPIO_JOIN_RE = /iomuxc_([a-z0-9_]+)_gpio(\d+)_io(\d+):/g;
313
+ const IMX_ADC_RE = /(iomuxc_[a-z0-9_]+_adc(\d+)_in(\d+)):\s*\w+\s*\{[^}]*?pinmux/g;
314
+ const IMX_PWM_RE = /(iomuxc_[a-z0-9_]+_flexpwm(\d+)_pwm([ab])(\d+)):\s*\w+\s*\{[^}]*?pinmux/g;
315
+ const IMX_PWM2_RE = /(iomuxc_[a-z0-9_]+_flexpwm(\d+)_pwm(\d+)_([ab])):\s*\w+\s*\{[^}]*?pinmux/g;
316
+ const PWM = 'pwm';
317
+ const ADC = 'adc';
318
+ const DAC = 'dac';
319
+ /** The self-contained route grammars, one per family×function. */
320
+ const PINCTRL_DIALECTS = [
321
+ // STM32 vendor-HAL pinctrl dtsi node shape (all SoC families):
322
+ // /omit-if-no-ref/ tim4_ch1_pb6: tim4_ch1_pb6 { pinmux = <STM32_PINMUX('B', 6, AF2)>; };
323
+ // The node NAME is captured verbatim (the overlay reference); the pinmux
324
+ // macro carries the authoritative port/bit for the cross-check. The F1
325
+ // family (AFIO model) spells the macro STM32F1_PINMUX — covered by the
326
+ // optional F<unit> in the value grammar.
327
+ { kind: PWM, re: /\/omit-if-no-ref\/?\s+(tim(\d+)_ch(\d+)_p[a-z]\d+):\s*\w+\s*\{[^}]*?pinmux\s*=\s*<STM32(?:F\d+)?_PINMUX\('([A-Z])',\s*(\d+)/g, token: (m) => m[1], source: (m) => `tim${m[2]}`, channel: (m) => Number(m[3]), port: (m) => m[4], bit: (m) => Number(m[5]) },
328
+ // STM32F1 (AFIO) PWM output — `tim1_ch1_pwm_out_pa8`. The `_pwm_in_`
329
+ // (input capture), `_remapN_` (AFIO remap) and `_n` (complementary)
330
+ // spellings are excluded by the token grammar, mirroring the pwmx/inn rules.
331
+ { kind: PWM, re: /\/omit-if-no-ref\/?\s+(tim(\d+)_ch(\d+)_pwm_out_p[a-z]\d+):\s*\w+\s*\{[^}]*?pinmux\s*=\s*<STM32F?\d*_PINMUX\('([A-Z])',\s*(\d+)/g, token: (m) => m[1], source: (m) => `tim${m[2]}`, channel: (m) => Number(m[3]), port: (m) => m[4], bit: (m) => Number(m[5]) },
332
+ // NXP Kinetis per-part header macros: FTM0_CH5_PTA0 (FTM = the pwm DT label).
333
+ { kind: PWM, re: /#define FTM(\d+)_CH(\d+)_PT([A-Z])(\d+)\b/g, token: (m) => `FTM${m[1]}_CH${m[2]}_PT${m[3]}${m[4]}`, source: (m) => `ftm${m[1]}`, channel: (m) => Number(m[2]), port: (m) => m[3], bit: (m) => Number(m[4]) },
334
+ // NXP LPC55 CTIMER match outputs (`ctimerN` = the DT label; MATCHn = ch n).
335
+ { kind: PWM, re: /#define CTIMER(\d+)_MATCH(\d+)_PIO(\d+)_(\d+)\b/g, token: (m) => `CTIMER${m[1]}_MATCH${m[2]}_PIO${m[3]}_${m[4]}`, source: (m) => `ctimer${m[1]}`, channel: (m) => Number(m[2]), port: (m) => m[3], bit: (m) => Number(m[4]) },
336
+ // GigaDevice GD32 TIMER macros (TIMER N is the DT timer label verbatim).
337
+ { kind: PWM, re: /#define TIMER(\d+)_CH(\d+)_P([A-Z])(\d+)\b/g, token: (m) => `TIMER${m[1]}_CH${m[2]}_P${m[3]}${m[4]}`, source: (m) => `timer${m[1]}`, channel: (m) => Number(m[2]), port: (m) => m[3], bit: (m) => Number(m[4]) },
338
+ // GigaDevice GD32 ADC — the header macros are package-specific (the board's
339
+ // include chain reaches its own part's header), so the header is the
340
+ // authoritative source, not the pinconfig YAML (whose package key is absent
341
+ // from the GD32 soc segment). All unit spellings: ADC_IN (digitless),
342
+ // ADC0/1/2_IN (single unit), ADC01_IN / ADC012_IN (shared — primary adc0).
343
+ { kind: ADC, re: /#define ADC(\d*)_IN(\d+)_P([A-Z])(\d+)\b/g, token: (m) => `ADC${m[1]}_IN${m[2]}_P${m[3]}${m[4]}`, source: (m) => `adc${m[1] ? m[1][0] : ''}`, channel: (m) => Number(m[2]), port: (m) => m[3], bit: (m) => Number(m[4]) },
344
+ // STM32 ADC — three vendor spellings: adc1_in1_pa1 (multi-unit),
345
+ // adc_in0_pa0 (single-unit, no unit digit), adc1_inp16_pa0 (H7 positive).
346
+ { kind: ADC, re: /\/omit-if-no-ref\/?\s+(adc(\d*)_(?:in|inp)(\d+)_p[a-z]\d+):\s*\w+\s*\{[^}]*?pinmux\s*=\s*<STM32(?:F\d+)?_PINMUX\('([A-Z])',\s*(\d+)/g, token: (m) => m[1], source: (m) => `adc${m[2]}`, channel: (m) => Number(m[3]), port: (m) => m[4], bit: (m) => Number(m[5]) },
347
+ // NXP Kinetis ADC16 single-ended (source `adcN` = the DT label).
348
+ { kind: ADC, re: /#define ADC(\d+)_SE(\d+)_PT([A-Z])(\d+)\b/g, token: (m) => `ADC${m[1]}_SE${m[2]}_PT${m[3]}${m[4]}`, source: (m) => `adc${m[1]}`, channel: (m) => Number(m[2]), port: (m) => m[3], bit: (m) => Number(m[4]) },
349
+ // STM32 DAC — dac1_out1_pa4 (the pinctrl source IS the DT nodelabel).
350
+ { kind: DAC, re: /\/omit-if-no-ref\/?\s+(dac(\d+)_out(\d+)_p[a-z]\d+):\s*\w+\s*\{[^}]*?pinmux\s*=\s*<STM32(?:F\d+)?_PINMUX\('([A-Z])',\s*(\d+)/g, token: (m) => m[1], source: (m) => `dac${m[2]}`, channel: (m) => Number(m[3]), port: (m) => m[4], bit: (m) => Number(m[5]) },
351
+ // STM32F1 (AFIO) DAC — `dac_out1_pa4` (digitless unit in the name, like the
352
+ // F1 ADC; the DT nodelabel is the conventional dac1).
353
+ { kind: DAC, re: /\/omit-if-no-ref\/?\s+(dac_out(\d+)_p[a-z]\d+):\s*\w+\s*\{[^}]*?pinmux\s*=\s*<STM32F?\d*_PINMUX\('([A-Z])',\s*(\d+)/g, token: (m) => m[1], source: (m) => 'dac1', channel: (m) => Number(m[2]), port: (m) => m[3], bit: (m) => Number(m[4]) },
354
+ ];
355
+ /** Harvest silicon routes from one pinctrl source text. Families by source
356
+ * shape: STM32/GD32/Kinetis/LPC C-header macros + i.MX RT dtsi node names;
357
+ * other families contribute nothing yet — see the family-convention notes
358
+ * in the manifest generator. Deduplicates by pinctrl node name / macro. */
359
+ function harvestPinctrlPins(src, pwm, adc, dac) {
360
+ const target = { pwm, adc, dac };
361
+ for (const d of PINCTRL_DIALECTS) {
362
+ d.re.lastIndex = 0;
363
+ let m;
364
+ while ((m = d.re.exec(src))) {
365
+ const token = d.token(m);
366
+ const map = target[d.kind];
367
+ if (map.has(token))
368
+ continue;
369
+ map.set(token, {
370
+ source: d.source(m),
371
+ channel: d.channel(m),
372
+ port: d.port(m),
373
+ bit: d.bit(m),
374
+ pinctrl: token,
375
+ });
376
+ }
377
+ }
378
+ // ── NXP i.MX RT pinctrl dtsi (node-name shape) ──────────────────────────
379
+ // `iomuxc_<pad>_adc1_in1: …` — the pad-to-GPIO join is in-band: every pad
380
+ // also declares `iomuxc_<pad>_gpio<port>_io<pin>`. FlexPWM routes the two
381
+ // subchannel outputs (`pwma`/`pwmb`, channel 0/1 of the DT child
382
+ // `flexpwm<N>_pwm<K>`); the complementary `pwmx` is skipped. The join map
383
+ // is built first, then the routes resolve against it.
384
+ const imxPadToGpio = new Map();
385
+ IMX_GPIO_JOIN_RE.lastIndex = 0;
386
+ let jm;
387
+ while ((jm = IMX_GPIO_JOIN_RE.exec(src))) {
388
+ if (!imxPadToGpio.has(jm[1]))
389
+ imxPadToGpio.set(jm[1], { port: jm[2], bit: Number(jm[3]) });
390
+ }
391
+ IMX_PWM_RE.lastIndex = 0;
392
+ while ((jm = IMX_PWM_RE.exec(src))) {
393
+ const pad = jm[1].replace(/^iomuxc_/, '').replace(/_flexpwm\d+_pwm[ab]\d+$/, '');
394
+ const gpio = imxPadToGpio.get(pad);
395
+ if (!gpio || pwm.has(jm[1]))
396
+ continue;
397
+ pwm.set(jm[1], {
398
+ source: `flexpwm${jm[2]}_pwm${jm[4]}`,
399
+ channel: jm[3] === 'b' ? 1 : 0,
400
+ port: gpio.port,
401
+ bit: gpio.bit,
402
+ pinctrl: jm[1],
403
+ });
404
+ }
405
+ // NEW FlexPWM spelling: `…_flexpwm1_pwm0_a` (pwm<K>_<a|b>, channel a=0/b=1).
406
+ IMX_PWM2_RE.lastIndex = 0;
407
+ while ((jm = IMX_PWM2_RE.exec(src))) {
408
+ const pad = jm[1].replace(/^iomuxc_/, '').replace(/_flexpwm\d+_pwm\d+_[ab]$/, '');
409
+ const gpio = imxPadToGpio.get(pad);
410
+ if (!gpio || pwm.has(jm[1]))
411
+ continue;
412
+ pwm.set(jm[1], {
413
+ source: `flexpwm${jm[2]}_pwm${jm[3]}`,
414
+ channel: jm[4] === 'b' ? 1 : 0,
415
+ port: gpio.port,
416
+ bit: gpio.bit,
417
+ pinctrl: jm[1],
418
+ });
419
+ }
420
+ IMX_ADC_RE.lastIndex = 0;
421
+ while ((jm = IMX_ADC_RE.exec(src))) {
422
+ const pad = jm[1].replace(/^iomuxc_/, '').replace(/_adc\d+_in\d+$/, '');
423
+ const gpio = imxPadToGpio.get(pad);
424
+ if (!gpio || adc.has(jm[1]))
425
+ continue;
426
+ adc.set(jm[1], {
427
+ source: `adc${jm[2]}`,
428
+ channel: Number(jm[3]),
429
+ port: gpio.port,
430
+ bit: gpio.bit,
431
+ pinctrl: jm[1],
432
+ });
433
+ }
434
+ }
435
+ /**
436
+ * Cross-validate every harvested route's NAME against its pinmux VALUE —
437
+ * both encode port/bit, independently (the name grammar and the macro's
438
+ * argument tuple). A disagreement means either a harvest regex bug or an
439
+ * upstream typo, and the route IS dropped: a silently-wrong fact is worse
440
+ * than a missing one. Per-family value extractors; a route whose value form
441
+ * is unknown is left unchecked (tolerant — the harvest stands).
442
+ */
443
+ function lintNameValueAgreement(src, pwm, adc, dac) {
444
+ const warnings = [];
445
+ // The port/bit each family's NAME grammar encodes as its trailing pad
446
+ // (STM32 `…_pa1`; Kinetis `…_PTB0`; LPC `…_PIO0_10`; GD32 `…_PA0`).
447
+ const namePad = (name) => {
448
+ let m = name.match(/_p([a-z])(\d+)$/);
449
+ if (m)
450
+ return { port: m[1].toUpperCase(), bit: Number(m[2]) };
451
+ m = name.match(/_PT([A-Z])(\d+)$/);
452
+ if (m)
453
+ return { port: m[1], bit: Number(m[2]) };
454
+ m = name.match(/_PIO(\d+)_(\d+)$/);
455
+ if (m)
456
+ return { port: m[1], bit: Number(m[2]) };
457
+ m = name.match(/_P([A-Z])(\d+)$/);
458
+ if (m)
459
+ return { port: m[1], bit: Number(m[2]) };
460
+ return undefined;
461
+ };
462
+ // The port/bit each family's VALUE macro carries. The value text sits
463
+ // within a small window after the name (same line for STM32/Kinetis/LPC,
464
+ // a backslash continuation for GD32).
465
+ const valuePad = (after) => {
466
+ let m = after.match(/STM32(?:F\d+)?_PINMUX\('([A-Z])',\s*(\d+)/);
467
+ if (m)
468
+ return { port: m[1], bit: Number(m[2]) };
469
+ m = after.match(/KINETIS_MUX\('([A-Z])',\s*(\d+)/);
470
+ if (m)
471
+ return { port: m[1], bit: Number(m[2]) };
472
+ m = after.match(/GD32_PINMUX_AFIO\('([A-Z])',\s*(\d+)/);
473
+ if (m)
474
+ return { port: m[1], bit: Number(m[2]) };
475
+ // IOCON_MUX(pin, type, func) — the GLOBAL pin index across LPC's 32-wide
476
+ // ports (PIO1_0 = 32), decoded back to port/bit.
477
+ m = after.match(/IOCON_MUX\((\d+),/);
478
+ if (m) {
479
+ const g = Number(m[1]);
480
+ return { port: String(Math.floor(g / 32)), bit: g % 32 };
481
+ }
482
+ return undefined;
483
+ };
484
+ const check = (name, route) => {
485
+ const byName = namePad(name);
486
+ if (!byName)
487
+ return false;
488
+ // Word-boundary occurrence: a plain indexOf would prefix-match a longer
489
+ // sibling (tim1_ch1_pb1 inside tim1_ch1_pb15) and cross-check the wrong
490
+ // node's macro. Dtsi labels carry `name:`; header macros `name `.
491
+ const re = new RegExp(`${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[:\\s]`);
492
+ const hit = re.exec(src);
493
+ if (!hit)
494
+ return false;
495
+ const idx = hit.index;
496
+ // The value window must not cross into the NEXT definition (header
497
+ // `#define` or dtsi `/omit-if-no-ref/` node) — a foreign macro later in
498
+ // the window would cross-check against the wrong route. GD32's value
499
+ // sits on a backslash continuation line, inside the same boundary.
500
+ const raw = src.slice(idx, idx + 240);
501
+ let end = raw.length;
502
+ const nextDefine = raw.indexOf('\n#define', 1);
503
+ if (nextDefine > 0)
504
+ end = Math.min(end, nextDefine);
505
+ const nextNode = raw.indexOf('/omit-if-no-ref', 8);
506
+ if (nextNode > 0)
507
+ end = Math.min(end, nextNode);
508
+ const byValue = valuePad(raw.slice(0, end));
509
+ if (!byValue)
510
+ return false;
511
+ const portOk = byValue.port === byName.port;
512
+ if (!portOk || byValue.bit !== byName.bit) {
513
+ warnings.push(`pinctrl '${name}': name says ${byName.port}${byName.bit} but its pinmux value says ` +
514
+ `${byValue.port}${byValue.bit} — route dropped as untrustworthy (harvest bug or upstream typo).`);
515
+ return true;
516
+ }
517
+ return false;
518
+ };
519
+ for (const [name, r] of [...pwm])
520
+ if (check(name, r))
521
+ pwm.delete(name);
522
+ for (const [name, r] of [...adc])
523
+ if (check(name, r))
524
+ adc.delete(name);
525
+ for (const [name, r] of [...dac])
526
+ if (check(name, r))
527
+ dac.delete(name);
528
+ return warnings;
529
+ }
530
+ // A watchdog device node anywhere in the include chain (SoC dtsi shape:
531
+ // `iwdg: watchdog@40003000 { compatible = "st,stm32-watchdog"; }` — nRF
532
+ // `wdt0: watchdog@40010200 { compatible = "nordic,nrf-watchdog"; }`). The
533
+ // nodelabel is the fact; compatible confirms it is a watchdog.
534
+ const WDT_NODE_RE = /(\w+):\s*watchdog@[0-9a-f]+\s*\{[^}]*?compatible\s*=\s*"([-\w,]*watchdog[-\w,]*)"/g;
535
+ // ESP32 LEDC matrix controller node (esp32s3_common.dtsi:
536
+ // ledc0: ledc@60019000 { compatible = "espressif,esp32-ledc"; }).
537
+ const LEDC_NODE_RE = /(ledc\d+):\s*ledc@[0-9a-f]+\s*\{/g;
538
+ // Flash node size (flash0: flash@8000000 { reg = <0x08000000 DT_SIZE_K(512)>; }).
539
+ const FLASH_KB_RE = /flash\d*:\s*flash@[0-9a-f]+\s*\{[^}]*?DT_SIZE_([KM])\((\d+)\)/g;
540
+ // Variant-module &-override form (Espressif): `&flash0 { reg = <0x0 DT_SIZE_M(8)>; }`
541
+ const FLASH_OVERRIDE_KB_RE = /&flash\d*\s*\{[^}]*?reg\s*=\s*<[^>]*?DT_SIZE_([KM])\((\d+)\)>/g;
542
+ // Counter-capable devices. The Zephyr counter driver binds either a labeled
543
+ // node whose compatible IS a counter (nRF `rtc1: rtc@… nordic,nrf-rtc`;
544
+ // STM32 `rtc: rtc@… st,stm32-rtc` — the RTC bindings carry counter drivers)
545
+ // or an unlabeled `counter { compatible = "…-counter" }` child under a
546
+ // labeled timer parent (ESP32 timer0-2, Ambiq, STM32 timer counters).
547
+ // COUNTER_COMPATS mirrors dts/bindings/counter/* plus the two rtc compatibles
548
+ // with counter drivers (they live under bindings/rtc).
549
+ const COUNTER_COMPATS = new Set([
550
+ 'nordic,nrf-rtc', 'st,stm32-rtc',
551
+ ]);
552
+ // Child form: (parent label): …timer-ish node… { … counter { compatible = "X" … } }
553
+ const COUNTER_CHILD_RE = /(\w+):\s*(?:counter|timer|rtc)@[0-9a-f]+\s*\{(?:[^{}]|\{[^{}]*\})*?\bcounter\s*\{(?:[^{}]|\{[^{}]*\})*?compatible\s*=\s*"([-\w,]+)"/g;
554
+ // Self form: (label): node@… { … compatible = "X" … } where X ∈ COUNTER_COMPATS
555
+ const COUNTER_SELF_RE = /(\w+):\s*(?:rtc|counter|timer)@[0-9a-f]+\s*\{[^}]*?compatible\s*=\s*"([-\w,]+)"/g;
556
+ /** Harvest counter nodes from the merged include-chain text. */
557
+ function harvestCounterNodes(text) {
558
+ const out = [];
559
+ const seen = new Set();
560
+ let m;
561
+ COUNTER_CHILD_RE.lastIndex = 0;
562
+ while ((m = COUNTER_CHILD_RE.exec(text))) {
563
+ const key = `child:${m[1]}`;
564
+ if (!seen.has(key)) {
565
+ seen.add(key);
566
+ out.push({ compatible: m[2], parentLabel: m[1] });
567
+ }
568
+ }
569
+ COUNTER_SELF_RE.lastIndex = 0;
570
+ while ((m = COUNTER_SELF_RE.exec(text))) {
571
+ if (!COUNTER_COMPATS.has(m[2]))
572
+ continue;
573
+ const key = `self:${m[1]}`;
574
+ if (!seen.has(key)) {
575
+ seen.add(key);
576
+ out.push({ compatible: m[2], nodeLabel: m[1] });
577
+ }
578
+ }
579
+ return out;
580
+ }
581
+ /**
582
+ * Read one board DTS file (following its local include chain) and extract
583
+ * the board-level facts. `zephyrBoardsRoot` (e.g. <zephyr>/boards) resolves
584
+ * `<../boards/common/...>` style includes; `moduleDtsRoots` resolve the
585
+ * vendor HAL modules' pinctrl dtsi (`<st/f4/...-pinctrl.dtsi>` lives under
586
+ * modules/hal/stm32/dts — Zephyr's MODULE_DTS_ROOTS, mirrored here).
587
+ */
588
+ export function readBoardDts(dtsPath, opts = {}) {
589
+ const roots = {
590
+ angle: [...(opts.moduleDtsRoots ?? []), ...(opts.zephyrBoardsRoot ? [opts.zephyrBoardsRoot] : [])],
591
+ };
592
+ const seen = new Set();
593
+ const pwmPins = new Map();
594
+ const adcPins = new Map();
595
+ const dacPins = new Map();
596
+ const pinctrlWarnings = [];
597
+ let wdtSocNode;
598
+ let ledcNode;
599
+ let flashKb;
600
+ let hasStoragePartition;
601
+ let storageReg;
602
+ let text = '';
603
+ const absorb = (file) => {
604
+ const real = path.resolve(file);
605
+ if (seen.has(real))
606
+ return;
607
+ seen.add(real);
608
+ const src = fs.readFileSync(real, 'utf8');
609
+ // Silicon routes ride the pinctrl dtsi files pulled in by the board's
610
+ // include chain — harvested from every absorbed file (harmless no-op on
611
+ // files whose grammars don't match).
612
+ harvestPinctrlPins(src, pwmPins, adcPins, dacPins);
613
+ pinctrlWarnings.push(...lintNameValueAgreement(src, pwmPins, adcPins, dacPins));
614
+ // Expand includes textualy (depth bounded by `seen`).
615
+ const includeRe = /#include\s+([<"][^>"]+[>"])/g;
616
+ let last = 0;
617
+ let m;
618
+ let out = '';
619
+ while ((m = includeRe.exec(src))) {
620
+ out += src.slice(last, m.index);
621
+ const resolved = resolveInclude(m[1], real, roots);
622
+ if (resolved)
623
+ absorb(resolved);
624
+ last = includeRe.lastIndex;
625
+ }
626
+ out += src.slice(last);
627
+ text += out + '\n';
628
+ };
629
+ absorb(dtsPath);
630
+ // SoC-level watchdog node: first watchdog-compatible node in the include
631
+ // chain (a board-level watchdog0 alias, checked by the walker, wins).
632
+ WDT_NODE_RE.lastIndex = 0;
633
+ const wdtMatch = WDT_NODE_RE.exec(text);
634
+ if (wdtMatch)
635
+ wdtSocNode = wdtMatch[1];
636
+ // SoC-level ADC/DAC device nodelabels (adc1:/dac1: device nodes). The
637
+ // pinctrl files sometimes carry routes for peripherals the SoC dtsi never
638
+ // declares (L4S5: dac1_out1_pa4 exists, no dac1 device) — routes for an
639
+ // undeclared device are dropped by the manifest generator.
640
+ const analogDevices = new Set();
641
+ // Digitless labels included: samd2x names its single controller `adc:`
642
+ // (the pinconfigs YAML's source spelling) — without it in analogDevices,
643
+ // the non-empty cross-check dropped every Atmel route. The label — not the
644
+ // node name — is the semantic signal, so non-standard node names
645
+ // (`gau_adc0`, `eadc`, `adc_etc`) are accepted; the label grammar covers
646
+ // every ADC/DAC device naming in the tree: adc\d* / dac\d* (classic),
647
+ // lpadc\d* (NXP LPADC), eadc\d* (Nuvoton), sadc\d* (Renesas/various), and
648
+ // the underscore forms adc_\d+ / dac_\d+ (NXP MCX).
649
+ const DEV_RE = /\b((?:lpadc|eadc|sadc|adc|dac)\d*|(?:adc|dac)_\d+):\s*\w+@[0-9a-f]+\s*\{/g;
650
+ let dm;
651
+ while ((dm = DEV_RE.exec(text)))
652
+ analogDevices.add(dm[1]);
653
+ // ESP32 LEDC matrix controller node (any SoC dtsi in the chain declaring
654
+ // one — esp32s3_common.dtsi's ledc0).
655
+ LEDC_NODE_RE.lastIndex = 0;
656
+ const ledcMatch = LEDC_NODE_RE.exec(text);
657
+ if (ledcMatch)
658
+ ledcNode = ledcMatch[1];
659
+ // Labeled PWM controller nodes (nRF's psel-routed matrix peripherals).
660
+ const pwmNodes = new Set();
661
+ let pm;
662
+ PWM_NODE_RE.lastIndex = 0;
663
+ while ((pm = PWM_NODE_RE.exec(text)))
664
+ pwmNodes.add(pm[1]);
665
+ // Flash size (largest reg across the chain — the package variant dtsi
666
+ // overrides the SoC base with the real size). Two spellings: the SoC form
667
+ // `flash0: flash@... { reg = <... DT_SIZE_K(n)>; }` and the variant
668
+ // &-override `&flash0 { reg = <0x0 DT_SIZE_M(n)>; }` (Espressif WROOM
669
+ // modules). DT_SIZE_M carries megabytes.
670
+ for (const re of [FLASH_KB_RE, FLASH_OVERRIDE_KB_RE]) {
671
+ re.lastIndex = 0;
672
+ let fm;
673
+ while ((fm = re.exec(text))) {
674
+ // Group 1 is the DT_SIZE_ unit, group 2 the magnitude.
675
+ const kb = fm[1] === 'M' ? Number(fm[2]) * 1024 : Number(fm[2]);
676
+ if (!flashKb || kb > flashKb)
677
+ flashKb = kb;
678
+ }
679
+ }
680
+ hasStoragePartition = /storage_partitions*:/.test(text);
681
+ // The board's own storage_partition reg (offset + size), when shipped —
682
+ // ESP32's AMP partition layout defines one; those boards carry their real
683
+ // facts instead of a synthesized region.
684
+ const sm = /storage_partitions*:\s*partition@[0-9a-f]+\s*\{[^}]*?reg\s*=\s*<0x([0-9a-f]+)\s+DT_SIZE_([KM])\((\d+)\)>/s.exec(text);
685
+ if (sm) {
686
+ const sizeKb = sm[2] === 'M' ? Number(sm[3]) * 1024 : Number(sm[3]);
687
+ storageReg = { offsetBytes: parseInt(sm[1], 16), sizeBytes: sizeKb * 1024 };
688
+ }
689
+ const counterNodes = harvestCounterNodes(text);
690
+ const { root: syntheticRoot, byLabel, refOverrides } = parseStatements(stripCommentsKeepMarkers(text));
691
+ // The DTS root ('/ { ... }') is a child of the synthetic outer node —
692
+ // resolve it so board-level nodes (aliases/chosen/leds/gpio_keys/&) sit at
693
+ // the depth the extractors expect.
694
+ const root = syntheticRoot.children.get('/') ?? syntheticRoot;
695
+ // aliases {} and chosen {} — phandle-ref properties
696
+ const aliases = {};
697
+ const chosen = {};
698
+ for (const name of ['aliases', 'chosen']) {
699
+ const node = root.children.get(name);
700
+ if (!node)
701
+ continue;
702
+ for (const [k, v] of node.props) {
703
+ const ref = v.match(/^&([\w,-]+)$/);
704
+ if (ref) {
705
+ if (name === 'aliases')
706
+ aliases[k] = ref[1];
707
+ else
708
+ chosen[k] = ref[1];
709
+ }
710
+ }
711
+ }
712
+ // GPIO controller inventory: every labeled node with the universal
713
+ // `gpio-controller` property. The SoC dtsi (absorbed) declares the FULL
714
+ // port list; the board's facts only name the ports it wires. `ngpios`
715
+ // carries the width when the dtsi states one.
716
+ const gpioControllers = [];
717
+ for (const [label, node] of byLabel) {
718
+ if (!node.props.has('gpio-controller'))
719
+ continue;
720
+ const ngpiosRaw = node.props.get('ngpios');
721
+ const ngpios = ngpiosRaw !== undefined ? Number(ngpiosRaw.replace(/[<>]/g, '')) : undefined;
722
+ gpioControllers.push({
723
+ nodelabel: node.name.startsWith('&') ? node.name.slice(1) : label,
724
+ ...(ngpios !== undefined && Number.isFinite(ngpios) ? { ngpios } : {}),
725
+ });
726
+ }
727
+ gpioControllers.sort((a, b) => a.nodelabel.localeCompare(b.nodelabel));
728
+ // gpio-leds / gpio-keys children (match on compatible OR node name)
729
+ const leds = [];
730
+ const buttons = [];
731
+ const collectGpioNodes = (node) => {
732
+ const compat = node.props.get('compatible') ?? '';
733
+ const isLeds = compat.includes('gpio-leds');
734
+ const isKeys = compat.includes('gpio-keys');
735
+ if (isLeds || isKeys) {
736
+ // A label can be aliased several times (led0 + mcuboot-led0); collect
737
+ // ALL aliases per label, then prefer the canonical led<N>/sw<N> form.
738
+ const aliasesByLabel = new Map();
739
+ for (const [alias, label] of Object.entries(aliases)) {
740
+ const list = aliasesByLabel.get(label) ?? [];
741
+ list.push(alias);
742
+ aliasesByLabel.set(label, list);
743
+ }
744
+ for (const child of node.children.values()) {
745
+ const ref = gpioRefFromProp(child.props.get('gpios') ?? '');
746
+ if (!ref)
747
+ continue;
748
+ const nodelabel = child.labels[0] ?? child.name;
749
+ const aliasCandidates = child.labels.flatMap((l) => aliasesByLabel.get(l) ?? []);
750
+ const alias = aliasCandidates.find((a) => /^(led|sw|button)[0-9]*$/.test(a)) ?? aliasCandidates[0];
751
+ const entry = {
752
+ ...ref,
753
+ nodelabel,
754
+ alias,
755
+ };
756
+ (isLeds ? leds : buttons).push(entry);
757
+ }
758
+ return; // do not recurse into a leds/keys container
759
+ }
760
+ for (const child of node.children.values())
761
+ collectGpioNodes(child);
762
+ };
763
+ collectGpioNodes(root);
764
+ // Connector nexus nodes: any node with a gpio-map property
765
+ const connectors = [];
766
+ const findConnectors = (node) => {
767
+ const map = node.props.get('gpio-map');
768
+ if (map) {
769
+ const cells = Number(node.props.get('#gpio-cells')?.replace(/[<>]/g, '') ?? '2');
770
+ const parentCells = Number.isFinite(cells) && cells > 0 ? cells : 2;
771
+ const entries = parseGpioMap(map, parentCells);
772
+ // The LAST entry's trailing /* label */ comment can land after the
773
+ // property's terminating ';' — its marker never enters the value. When
774
+ // labeled siblings share a prefix+number pattern (D0…D9), extend it to
775
+ // the unlabeled tail entry (D10) instead of falling back to p<N>.
776
+ const labeled = entries.filter((e) => e.label);
777
+ const pattern = labeled.length >= 2
778
+ ? (() => {
779
+ const m = labeled[0].label.match(/^([A-Za-z]+)([0-9]+)$/);
780
+ if (!m)
781
+ return undefined;
782
+ const prefix = m[1];
783
+ const consistent = labeled.every((e) => e.label.startsWith(prefix) && Number(e.label.slice(prefix.length)) === e.childSpecifier);
784
+ return consistent ? prefix : undefined;
785
+ })()
786
+ : undefined;
787
+ const pins = {};
788
+ for (const e of entries) {
789
+ const label = e.label ?? (pattern ? `${pattern}${e.childSpecifier}` : `p${e.childSpecifier}`);
790
+ pins[label] = e.ref;
791
+ for (const alt of e.altLabels ?? [])
792
+ pins[alt] = e.ref;
793
+ }
794
+ // Complete the analog run when the FINAL entry's comment lands after
795
+ // the property's ';' (its marker never enters the value): the header's
796
+ // D/A pairing increments in lockstep (D14/A0 … D21/A7), so a consistent
797
+ // D−A offset observed on the captured entries extends to the tail.
798
+ const observed = entries.filter((e) => {
799
+ const d = e.label?.match(/^D(\d+)$/);
800
+ return d && e.altLabels?.some((a) => /^A\d+$/.test(a));
801
+ });
802
+ if (observed.length >= 2) {
803
+ const offsetOf = (e) => Number(e.label.slice(1)) - Number(e.altLabels.find((a) => /^A\d+$/.test(a)).slice(1));
804
+ const offset = offsetOf(observed[0]);
805
+ if (observed.every((e) => offsetOf(e) === offset)) {
806
+ for (const e of entries) {
807
+ const d = e.label?.match(/^D(\d+)$/);
808
+ if (!d || e.altLabels?.some((a) => /^A\d+$/.test(a)))
809
+ continue;
810
+ // Only positive A indices within the observed run — D0…D13 sit
811
+ // below the analog block (offset would go negative).
812
+ const aIdx = Number(d[1]) - offset;
813
+ if (aIdx < 0)
814
+ continue;
815
+ if (!pins[`A${aIdx}`])
816
+ pins[`A${aIdx}`] = e.ref;
817
+ }
818
+ }
819
+ }
820
+ connectors.push({
821
+ nodelabel: node.labels[0] ?? node.name,
822
+ compatible: (node.props.get('compatible') ?? '').replace(/"/g, '') || undefined,
823
+ pins,
824
+ });
825
+ }
826
+ for (const child of node.children.values())
827
+ findConnectors(child);
828
+ };
829
+ findConnectors(root);
830
+ // Connector io-channel-maps (the DKs' arduino,uno-adc nodes): entries
831
+ // `<idx &adc ch>` name the channel a CONNECTOR pin is wired to; the
832
+ // trailing /* A0 … */ comment names the pin. Joined against the gpio-map
833
+ // pins of the board's connectors by that label — board-authored wiring
834
+ // truth for SoCs whose silicon map has no other in-tree source.
835
+ const connectorAdc = [];
836
+ const ioEntries = [];
837
+ const findIoChannels = (node) => {
838
+ const map = node.props.get('io-channel-map');
839
+ if (map) {
840
+ // Labels ride the ⟨…⟩ comment markers (stripCommentsKeepMarkers) —
841
+ // the same channel the gpio-map parser reads silkscreen from. A `,`
842
+ // (mid-list) or `;` (last entry) may sit between the cell and its
843
+ // marker.
844
+ for (const m of map.matchAll(/<(\d+)\s+&([\w,-]+)\s+(\d+)\s*>[\s;,]*(?:⟨([A-Za-z_][\w-]*)?[^⟩]*⟩)?/g)) {
845
+ const channel = Number(m[3]);
846
+ if (!Number.isFinite(channel))
847
+ continue;
848
+ ioEntries.push({ ...(m[4] ? { label: m[4] } : {}), source: m[2], channel });
849
+ }
850
+ }
851
+ for (const child of node.children.values())
852
+ findIoChannels(child);
853
+ };
854
+ findIoChannels(root);
855
+ for (const io of ioEntries) {
856
+ if (!io.label)
857
+ continue; // unlabeled entries can't be joined to a pad
858
+ for (const c of connectors) {
859
+ const ref = c.pins[io.label];
860
+ if (ref) {
861
+ connectorAdc.push({ source: io.source, channel: io.channel, controller: ref.controller, pin: ref.pin });
862
+ break;
863
+ }
864
+ }
865
+ }
866
+ // Addressable user LED (worldsemi,ws2812-*). Three wiring forms in the
867
+ // tree: the RP2040 PIO variant nests a plain `gpios = <&ctrl pin …>` node;
868
+ // the SPI/I2S variants sit on a bus whose pinctrl names the data pad in
869
+ // the macro itself (SPIM3_MOSI_GPIO33, I2S0_O_SD_GPIO48 — the OUTPUT SD
870
+ // is the data line; I_SD is capture, wrong direction). Walks the
871
+ // SYNTHETIC root: the strips ride &ref blocks (siblings of the board's
872
+ // `/ {}` node), which the resolved root's walkers never see.
873
+ const stripLed = findStripLed(syntheticRoot, byLabel);
874
+ // PWM-driven LEDs (pwm-leds children): `pwms = <&pwmN channel [period]
875
+ // [flags]>`. Only aliased children are addressable by the PWM lowering
876
+ // (DT_ALIAS(pwm_led0)); carry the alias through.
877
+ const reverseAlias = new Map();
878
+ for (const [name, label] of Object.entries(aliases))
879
+ reverseAlias.set(label, name);
880
+ const pwmLeds = [];
881
+ for (const ledsNode of root.children.values()) {
882
+ if ((ledsNode.props.get('compatible') ?? '').replace(/"/g, '') !== 'pwm-leds')
883
+ continue;
884
+ for (const child of ledsNode.children.values()) {
885
+ const pwms = child.props.get('pwms');
886
+ if (!pwms)
887
+ continue;
888
+ const toks = parseCells(pwms);
889
+ if (toks.length < 2 || !toks[0].startsWith('&'))
890
+ continue;
891
+ const channel = Number(toks[1]);
892
+ if (!Number.isFinite(channel))
893
+ continue;
894
+ const period = Number(toks[2]);
895
+ const flags = toks.slice(Number.isFinite(period) ? 3 : 2)
896
+ .filter((t) => !t.startsWith('⟨') && !Number.isFinite(Number(t)) && !/[()]/.test(t));
897
+ const led = {
898
+ controller: toks[0].slice(1),
899
+ channel,
900
+ ...(Number.isFinite(period) ? { periodNs: period } : {}),
901
+ ...(flags.length > 0 ? { flags } : {}),
902
+ ...(child.labels[0] && reverseAlias.has(child.labels[0]) ? { alias: reverseAlias.get(child.labels[0]) } : {}),
903
+ };
904
+ pwmLeds.push(led);
905
+ }
906
+ }
907
+ // USB device wiring: the board's &usbd / zephyr_udc0 node status. The
908
+ // label form (`zephyr_udc0: &usbd { … }`) attaches under both names.
909
+ let usbDevice;
910
+ let usbController;
911
+ for (const key of ['zephyr_udc0', 'usbd']) {
912
+ const n = byLabel.get(key);
913
+ if (!n)
914
+ continue;
915
+ const status = n.props.get('status');
916
+ if (status === '"okay"') {
917
+ usbDevice = 'enabled';
918
+ usbController = key;
919
+ break;
920
+ }
921
+ if (status === '"disabled"' && usbDevice === undefined)
922
+ usbDevice = 'disabled';
923
+ }
924
+ // Bus controller nodelabels the board wires up: &ref blocks whose status
925
+ // is okay (or that carry no status override but configure the bus — the
926
+ // app overlay decides enablement; cuttlefish's own overlay writes it).
927
+ // Classified by nodelabel — bus node names are vendor-stable (i2c0,
928
+ // spi2, usart0, eusart1, lpuart1, …).
929
+ const buses = { i2c: [], spi: [], uart: [] };
930
+ // Multi-function bus blocks whose nodelabel carries no function (Atmel SAM
931
+ // sercomN: the SAME controller can be I2C, SPI, or UART) are classified by
932
+ // their compatible (atmel,sam0-i2c / atmel,sam0-spi / atmel,sam0-uart).
933
+ const classifyBus = (label, compatible) => {
934
+ if (/^i2c|^twi/.test(label))
935
+ return 'i2c';
936
+ if (/^spi|^ssp/.test(label))
937
+ return 'spi';
938
+ if (/uart|usart/.test(label))
939
+ return 'uart';
940
+ const compat = compatible.replace(/^"|"$/g, '');
941
+ if (/-i2c$|,twi$/.test(compat))
942
+ return 'i2c';
943
+ if (/-spi$/.test(compat))
944
+ return 'spi';
945
+ if (/-uart$|-usart$/.test(compat))
946
+ return 'uart';
947
+ return undefined;
948
+ };
949
+ // Only labels the DTS OVERRODE with an &ref block count (`xiao_i2c: &i2c1
950
+ // { … }` → nodelabel 'i2c1'). With SoC dtsis absorbed, an override merges
951
+ // into the SoC's real labeled node (no synthetic '&i2c1' name), so the
952
+ // refOverrides set — not the node's name — carries the signal. Pinctrl
953
+ // groups ('i2c0_default') never appear in it; _default/_sleep is belt and
954
+ // braces.
955
+ const seenBuses = new Set();
956
+ for (const [label, node] of byLabel) {
957
+ if (!refOverrides.has(label))
958
+ continue;
959
+ if (label === 'usbd' || /_(default|sleep)$/.test(label))
960
+ continue;
961
+ const nodelabel = node.name.startsWith('&') ? node.name.slice(1) : label;
962
+ const kind = classifyBus(nodelabel, node.props.get('compatible') ?? '');
963
+ if (!kind || seenBuses.has(nodelabel))
964
+ continue;
965
+ const status = node.props.get('status');
966
+ if (status === '"disabled"')
967
+ continue;
968
+ seenBuses.add(nodelabel);
969
+ buses[kind].push(nodelabel);
970
+ }
971
+ for (const kind of ['i2c', 'spi', 'uart'])
972
+ buses[kind].sort();
973
+ void byLabel; // (labels already captured during parse)
974
+ return {
975
+ aliases, chosen, leds, buttons, connectors, connectorAdc, pwmLeds, buses,
976
+ ...(pinctrlWarnings.length > 0 ? { pinctrlWarnings } : {}),
977
+ pwmPins: [...pwmPins.values()],
978
+ adcPins: [...adcPins.values()],
979
+ dacPins: [...dacPins.values()],
980
+ analogDevices: [...analogDevices],
981
+ ...(wdtSocNode ? { wdtSocNode } : {}),
982
+ ...(ledcNode ? { ledcNode } : {}),
983
+ ...(pwmNodes.size > 0 ? { pwmNodes: [...pwmNodes].sort() } : {}),
984
+ ...(flashKb ? { flashKb } : {}),
985
+ ...(hasStoragePartition ? { hasStoragePartition } : {}),
986
+ ...(storageReg ? { storageReg } : {}),
987
+ counterNodes,
988
+ gpioControllers,
989
+ ...(stripLed ? { stripLed } : {}),
990
+ ...(usbDevice ? { usbDevice } : {}),
991
+ ...(usbController ? { usbController } : {}),
992
+ };
993
+ }
994
+ /** First `gpios` property in a subtree (the PIO strip nests it one level down). */
995
+ function gpiosInSubtree(node) {
996
+ const own = node.props.get('gpios');
997
+ if (own) {
998
+ const ref = gpioRefFromProp(own);
999
+ if (ref)
1000
+ return ref;
1001
+ }
1002
+ for (const child of node.children.values()) {
1003
+ const ref = gpiosInSubtree(child);
1004
+ if (ref)
1005
+ return ref;
1006
+ }
1007
+ return undefined;
1008
+ }
1009
+ /** All pinmux property text in a pinctrl group subtree. */
1010
+ function pinmuxTextIn(node, out = []) {
1011
+ const pm = node.props.get('pinmux');
1012
+ if (pm)
1013
+ out.push(pm);
1014
+ for (const child of node.children.values())
1015
+ pinmuxTextIn(child, out);
1016
+ return out;
1017
+ }
1018
+ function findStripLed(root, byLabel) {
1019
+ let found;
1020
+ const walk = (node) => {
1021
+ const compat = node.props.get('compatible') ?? '';
1022
+ if (compat.includes('worldsemi,ws2812') && !found) {
1023
+ found = stripRefOf(node, byLabel);
1024
+ }
1025
+ for (const child of node.children.values())
1026
+ walk(child);
1027
+ };
1028
+ walk(root);
1029
+ return found;
1030
+ }
1031
+ function stripRefOf(node, byLabel) {
1032
+ // PIO form: a descendant carries the pad as a plain gpio ref.
1033
+ const direct = gpiosInSubtree(node);
1034
+ if (direct)
1035
+ return direct;
1036
+ // SPI/I2S forms: the enclosing bus's default pinctrl group names the
1037
+ // data pad in its pinmux macro. Every ws2812-spi/i2s board in the tree is
1038
+ // ESP32-family, whose controllers are the 32-pin gpio0/gpio1 pair.
1039
+ const bus = node.parent;
1040
+ const pcLabel = bus?.props.get('pinctrl-0')?.match(/&([\w-]+)/)?.[1];
1041
+ const pinctrl = pcLabel ? byLabel.get(pcLabel) : undefined;
1042
+ if (!pinctrl)
1043
+ return undefined;
1044
+ const text = pinmuxTextIn(pinctrl).join(' ');
1045
+ const m = text.match(/MOSI_GPIO(\d+)|I2S\d*_O_SD_GPIO(\d+)/);
1046
+ const global = m ? Number(m[1] ?? m[2]) : undefined;
1047
+ if (global == null)
1048
+ return undefined;
1049
+ return global < 32
1050
+ ? { controller: 'gpio0', pin: global, flags: [] }
1051
+ : { controller: 'gpio1', pin: global - 32, flags: [] };
1052
+ }