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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +22 -35
  2. package/dist/as-built.d.ts +33 -0
  3. package/dist/as-built.js +188 -0
  4. package/dist/async/timer-polyfill.js +1 -1
  5. package/dist/boardgen.d.ts +128 -0
  6. package/dist/boardgen.js +1366 -0
  7. package/dist/chips/board-overrides.d.ts +7 -0
  8. package/dist/chips/board-overrides.js +11 -0
  9. package/dist/chips/controllers.d.ts +28 -8
  10. package/dist/chips/controllers.js +49 -12
  11. package/dist/chips/esp32s3.js +49 -0
  12. package/dist/chips/index.d.ts +6 -12
  13. package/dist/chips/index.js +18 -34
  14. package/dist/chips/resolve.d.ts +16 -2
  15. package/dist/chips/resolve.js +298 -15
  16. package/dist/chips/soc/.d.ts +2 -0
  17. package/dist/chips/soc/.js +129 -0
  18. package/dist/chips/soc/esp32.d.ts +2 -0
  19. package/dist/chips/soc/esp32.js +120 -0
  20. package/dist/chips/soc/esp32c3.d.ts +2 -0
  21. package/dist/chips/soc/esp32c3.js +90 -0
  22. package/dist/chips/soc/esp32c6.d.ts +2 -0
  23. package/dist/chips/soc/esp32c6.js +109 -0
  24. package/dist/chips/soc/esp32s3.d.ts +2 -0
  25. package/dist/chips/soc/esp32s3.js +189 -0
  26. package/dist/chips/soc/index.d.ts +2 -0
  27. package/dist/chips/soc/index.js +23 -0
  28. package/dist/chips/soc/nrf52840.d.ts +2 -0
  29. package/dist/chips/soc/nrf52840.js +130 -0
  30. package/dist/chips/soc/rp2040.d.ts +2 -0
  31. package/dist/chips/soc/rp2040.js +141 -0
  32. package/dist/chips/soc/rp2350a.d.ts +2 -0
  33. package/dist/chips/soc/rp2350a.js +145 -0
  34. package/dist/chips/soc/samd21g18a.d.ts +2 -0
  35. package/dist/chips/soc/samd21g18a.js +143 -0
  36. package/dist/chips/soc/stm32f411xe.d.ts +2 -0
  37. package/dist/chips/soc/stm32f411xe.js +251 -0
  38. package/dist/chips/types.d.ts +382 -12
  39. package/dist/chips/xiao-ble.js +33 -0
  40. package/dist/display/profiles.js +1 -1
  41. package/dist/doctor.js +38 -4
  42. package/dist/dt-config/custom-board.d.ts +23 -0
  43. package/dist/dt-config/custom-board.js +227 -0
  44. package/dist/dt-config/kconfig.d.ts +83 -1
  45. package/dist/dt-config/kconfig.js +116 -27
  46. package/dist/dt-config/overlay.js +623 -18
  47. package/dist/framework.manifest.d.ts +20 -30
  48. package/dist/framework.manifest.js +124 -178
  49. package/dist/index.d.ts +4 -1
  50. package/dist/index.js +8 -2
  51. package/dist/lowering/adc.d.ts +26 -5
  52. package/dist/lowering/adc.js +137 -31
  53. package/dist/lowering/ble.js +31 -28
  54. package/dist/lowering/dac.js +9 -9
  55. package/dist/lowering/fs.js +18 -10
  56. package/dist/lowering/gpio.d.ts +2 -0
  57. package/dist/lowering/gpio.js +90 -10
  58. package/dist/lowering/http.js +39 -14
  59. package/dist/lowering/hwtimer.d.ts +4 -0
  60. package/dist/lowering/hwtimer.js +35 -10
  61. package/dist/lowering/i2c.js +46 -54
  62. package/dist/lowering/index.d.ts +5 -5
  63. package/dist/lowering/index.js +15 -16
  64. package/dist/lowering/interrupts.d.ts +22 -5
  65. package/dist/lowering/interrupts.js +104 -22
  66. package/dist/lowering/mqtt.js +12 -6
  67. package/dist/lowering/power.js +0 -4
  68. package/dist/lowering/preferences.js +49 -77
  69. package/dist/lowering/pulse.js +5 -36
  70. package/dist/lowering/pwm.d.ts +32 -3
  71. package/dist/lowering/pwm.js +105 -17
  72. package/dist/lowering/random.js +3 -3
  73. package/dist/lowering/sensor.d.ts +37 -0
  74. package/dist/lowering/sensor.js +119 -0
  75. package/dist/lowering/spi.d.ts +12 -0
  76. package/dist/lowering/spi.js +65 -40
  77. package/dist/lowering/thread.d.ts +14 -0
  78. package/dist/lowering/thread.js +55 -0
  79. package/dist/lowering/timing.js +20 -46
  80. package/dist/lowering/tone.js +18 -5
  81. package/dist/lowering/uart.d.ts +13 -0
  82. package/dist/lowering/uart.js +72 -51
  83. package/dist/lowering/usb.d.ts +34 -0
  84. package/dist/lowering/usb.js +206 -0
  85. package/dist/lowering/wdt.d.ts +1 -1
  86. package/dist/lowering/wdt.js +17 -7
  87. package/dist/lowering/wifi.d.ts +3 -2
  88. package/dist/lowering/wifi.js +120 -111
  89. package/dist/sdk/board-catalog-sync.d.ts +16 -0
  90. package/dist/sdk/board-catalog-sync.js +41 -0
  91. package/dist/sdk/board-data.generated.d.ts +2 -0
  92. package/dist/sdk/board-data.generated.js +4 -0
  93. package/dist/sdk/catalog-walker.d.ts +90 -0
  94. package/dist/sdk/catalog-walker.js +682 -0
  95. package/dist/sdk/dts-reader.d.ts +83 -0
  96. package/dist/sdk/dts-reader.js +596 -0
  97. package/dist/strategy.d.ts +136 -17
  98. package/dist/strategy.js +881 -212
  99. package/dist/toolchain/bossac-touch.d.ts +18 -0
  100. package/dist/toolchain/bossac-touch.js +88 -0
  101. package/dist/toolchain/compat.d.ts +2 -2
  102. package/dist/toolchain/compat.js +8 -3
  103. package/dist/toolchain/debug-config.d.ts +21 -26
  104. package/dist/toolchain/debug-config.js +212 -72
  105. package/dist/toolchain/env-check.d.ts +4 -4
  106. package/dist/toolchain/env-check.js +5 -5
  107. package/dist/toolchain/index.d.ts +66 -1
  108. package/dist/toolchain/index.js +713 -58
  109. package/dist/toolchain/scaffold.d.ts +9 -0
  110. package/dist/toolchain/scaffold.js +84 -7
  111. package/dist/toolchain/west-discover.d.ts +8 -1
  112. package/dist/toolchain/west-discover.js +19 -6
  113. package/dist/toolchain/west-spawn.js +17 -5
  114. package/installer/CHANGELOG.md +473 -0
  115. package/installer/README.md +328 -0
  116. package/installer/environment.yml +46 -0
  117. package/installer/etc/conda/activate.d/zephyr.bat +7 -0
  118. package/installer/etc/conda/activate.d/zephyr.ps1 +12 -0
  119. package/installer/etc/conda/activate.d/zephyr.sh +20 -0
  120. package/installer/etc/conda/deactivate.d/zephyr.bat +6 -0
  121. package/installer/etc/conda/deactivate.d/zephyr.ps1 +6 -0
  122. package/installer/etc/conda/deactivate.d/zephyr.sh +4 -0
  123. package/installer/install.mjs +580 -0
  124. package/installer/install.ps1 +619 -0
  125. package/installer/install.sh +310 -0
  126. package/installer/lib/detect-platform.sh +62 -0
  127. package/installer/lib/fetch-sdk.sh +304 -0
  128. package/installer/lib/init-workspace.sh +153 -0
  129. package/installer/lib/write-activation.sh +76 -0
  130. package/installer/patches/tfm-c-include-separators.patch +19 -0
  131. package/installer/patches/zephyr-tfm-nordic-zephyr-include.patch +24 -0
  132. package/installer/templates/project/.typecad/activate-zephyr.ps1 +31 -0
  133. package/installer/templates/project/.typecad/activate-zephyr.sh +31 -0
  134. package/installer/templates/project/.vscode/settings.json +30 -0
  135. package/installer/templates/project/README.md +58 -0
  136. package/installer/versions.env +110 -0
  137. package/package.json +16 -16
  138. package/src/as-built.ts +206 -0
  139. package/src/boardgen.ts +1428 -0
  140. package/src/chips/controllers.ts +61 -12
  141. package/src/chips/index.ts +35 -54
  142. package/src/chips/resolve.ts +316 -14
  143. package/src/chips/types.ts +388 -13
  144. package/src/display/profiles.ts +1 -1
  145. package/src/doctor.ts +110 -77
  146. package/src/dt-config/custom-board.ts +325 -0
  147. package/src/dt-config/kconfig.ts +177 -19
  148. package/src/dt-config/overlay.ts +1058 -450
  149. package/src/framework.manifest.ts +125 -178
  150. package/src/index.ts +57 -41
  151. package/src/lowering/adc.ts +222 -102
  152. package/src/lowering/ble.ts +32 -28
  153. package/src/lowering/dac.ts +82 -82
  154. package/src/lowering/fs.ts +18 -10
  155. package/src/lowering/gpio.ts +242 -152
  156. package/src/lowering/http.ts +512 -488
  157. package/src/lowering/hwtimer.ts +139 -101
  158. package/src/lowering/i2c.ts +116 -126
  159. package/src/lowering/index.ts +91 -93
  160. package/src/lowering/interrupts.ts +113 -21
  161. package/src/lowering/mqtt.ts +12 -6
  162. package/src/lowering/preferences.ts +327 -354
  163. package/src/lowering/pwm.ts +167 -74
  164. package/src/lowering/random.ts +3 -3
  165. package/src/lowering/sensor.ts +155 -0
  166. package/src/lowering/spi.ts +164 -124
  167. package/src/lowering/thread.ts +63 -0
  168. package/src/lowering/timing.ts +46 -72
  169. package/src/lowering/uart.ts +73 -51
  170. package/src/lowering/usb.ts +221 -0
  171. package/src/lowering/wdt.ts +95 -84
  172. package/src/lowering/wifi.ts +120 -111
  173. package/src/sdk/board-catalog-sync.ts +62 -0
  174. package/src/strategy.ts +888 -229
  175. package/src/toolchain/bossac-touch.ts +112 -0
  176. package/src/toolchain/compat.ts +8 -3
  177. package/src/toolchain/debug-config.ts +666 -522
  178. package/src/toolchain/env-check.ts +9 -9
  179. package/src/toolchain/index.ts +765 -44
  180. package/src/toolchain/scaffold.ts +81 -7
  181. package/src/toolchain/west-discover.ts +334 -316
  182. package/src/toolchain/west-spawn.ts +17 -5
  183. package/src/async/timer-polyfill.ts +0 -107
  184. package/src/chips/esp32.ts +0 -74
  185. package/src/chips/esp32s3.ts +0 -57
  186. package/src/chips/xiao-ble.ts +0 -70
  187. package/src/lowering/power.ts +0 -70
  188. package/src/lowering/pulse.ts +0 -93
  189. package/src/lowering/tone.ts +0 -60
  190. package/src/lowering/worker-backing.ts +0 -94
  191. package/src/lowering/worker.ts +0 -17
@@ -0,0 +1,83 @@
1
+ /** A GPIO reference inside the devicetree: controller nodelabel + pin + flags. */
2
+ export interface DtsGpioRef {
3
+ /** Controller nodelabel, e.g. 'gpioa', 'gpio0'. */
4
+ readonly controller: string;
5
+ /** Pin number within the controller. */
6
+ readonly pin: number;
7
+ /** Raw flag tokens as written (macros stay opaque): ['GPIO_ACTIVE_LOW', 'GPIO_PULL_UP']. */
8
+ readonly flags: readonly string[];
9
+ }
10
+ /** A gpio-leds / gpio-keys child with its devicetree alias when aliased. */
11
+ export interface DtsGpioNode extends DtsGpioRef {
12
+ /** Node's own nodelabel, e.g. 'user_led', 'button0'. */
13
+ readonly nodelabel: string;
14
+ /** Devicetree alias pointing here, e.g. 'led0', 'sw0' (from the aliases node). */
15
+ readonly alias?: string;
16
+ }
17
+ /** A connector nexus (e.g. the XIAO's D0–D10 gpio-map). */
18
+ export interface DtsConnector {
19
+ /** Connector nodelabel, e.g. 'xiao_d', 'arduino_header_d'. */
20
+ readonly nodelabel: string;
21
+ /** Compatible string, e.g. 'seeed,xiao-gpio', 'arduino-header-r3'. */
22
+ readonly compatible?: string;
23
+ /** Silkscreen-ish label (from the map's trailing comments) → ref. */
24
+ readonly pins: Readonly<Record<string, DtsGpioRef>>;
25
+ }
26
+ /** Everything the reader could extract from one board DTS (post local includes). */
27
+ export interface DtsBoardFacts {
28
+ /** alias name → nodelabel ('led0' → 'user_led', 'sw0' → 'button0'). */
29
+ readonly aliases: Readonly<Record<string, string>>;
30
+ /** chosen property → nodelabel ('zephyr,console' → 'uart0'). */
31
+ readonly chosen: Readonly<Record<string, string>>;
32
+ /** gpio-leds children. */
33
+ readonly leds: readonly DtsGpioNode[];
34
+ /** gpio-keys children. */
35
+ readonly buttons: readonly DtsGpioNode[];
36
+ /** Connector nexus maps. */
37
+ readonly connectors: readonly DtsConnector[];
38
+ /** Addressable user LED (worldsemi,ws2812-*): the pad driving the pixel.
39
+ * NOT a gpio-leds node — consumers must treat it as a plain GPIO LED
40
+ * (no led0 devicetree spec), same as a curated board override. */
41
+ readonly stripLed?: DtsGpioRef;
42
+ /** PWM-driven LEDs (pwm-leds children), in board order. `alias` is the
43
+ * devicetree alias ('pwm-led0') — the addressable form; non-aliased
44
+ * channels carry their controller+channel only. */
45
+ readonly pwmLeds: readonly DtsPwmLed[];
46
+ /** The board's USB device wiring: 'enabled' when the DTS turns the device
47
+ * controller on (zephyr_udc0/&usbd status okay), 'disabled' when it
48
+ * explicitly turns it off, undefined when the DTS is silent (the app
49
+ * overlay may still compose a CDC device). */
50
+ readonly usbDevice?: 'enabled' | 'disabled';
51
+ /** The USB device controller nodelabel backing usbDevice ('zephyr_udc0',
52
+ * or the board's own label for &usbd). */
53
+ readonly usbController?: string;
54
+ /** Bus controller nodelabels the board DTS wires up (&i2c0/&spi2/&usart1
55
+ * status-okay overrides; &ref blocks). Board-level facts only — a bus
56
+ * enabled solely in the SoC dtsi is invisible here, by design. */
57
+ readonly buses: {
58
+ readonly i2c: readonly string[];
59
+ readonly spi: readonly string[];
60
+ readonly uart: readonly string[];
61
+ };
62
+ }
63
+ /** A pwm-leds child: one PWM-driven LED channel. */
64
+ export interface DtsPwmLed {
65
+ /** Devicetree alias ('pwm-led0') when aliased — the DT_ALIAS addressing form. */
66
+ readonly alias?: string;
67
+ /** PWM controller nodelabel ('pwm1'). */
68
+ readonly controller: string;
69
+ readonly channel: number;
70
+ /** Period cell from the pwms property, when present. */
71
+ readonly periodNs?: number;
72
+ /** Flag tokens (PWM_POLARITY_INVERTED, …). Macro expressions like
73
+ * PWM_MSEC(20) are period shorthand — never flags. */
74
+ readonly flags?: readonly string[];
75
+ }
76
+ /**
77
+ * Read one board DTS file (following its local include chain) and extract
78
+ * the board-level facts. `zephyrBoardsRoot` (e.g. <zephyr>/boards) resolves
79
+ * `<../boards/common/...>` style includes.
80
+ */
81
+ export declare function readBoardDts(dtsPath: string, opts?: {
82
+ zephyrBoardsRoot?: string;
83
+ }): DtsBoardFacts;
@@ -0,0 +1,596 @@
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), and connector nexus gpio-maps
7
+ // (arduino-header-r3, seeed,xiao-gpio). Silicon facts (ADC channel maps, PWM
8
+ // channel counts) are NOT here — they never live in devicetree; they are the
9
+ // curated soc descriptors in ../chips/soc/.
10
+ //
11
+ // DELIBERATELY NOT a devicetree compiler. The reader follows only the
12
+ // board's LOCAL include chain (relative *.dtsi and in-tree <...> includes it
13
+ // can resolve), and treats C macros it cannot expand as opaque flag tokens
14
+ // (`GPIO_ACTIVE_LOW` stays the literal text). It does not resolve phandles
15
+ // beyond &label capture, does not evaluate expressions, and ignores every
16
+ // property it has no consumer for. Boards whose interesting nodes live in
17
+ // deeper SoC includes simply yield fewer facts — the generator's tiering
18
+ // handles that honestly.
19
+ //
20
+ // Used by scripts/gen-zephyr-board-data.mjs at repo-build time against the
21
+ // pinned Zephyr tree; never runs in user builds.
22
+ // ----------------------------------------------------------------------------
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ function resolveInclude(spec, fromFile, roots) {
26
+ // Strip the wrapper: <...> or "..."
27
+ const inner = spec.slice(1, -1);
28
+ // Quoted includes are relative to the including file.
29
+ if (!spec.startsWith('<')) {
30
+ const p = path.resolve(path.dirname(fromFile), inner);
31
+ return fs.existsSync(p) ? p : undefined;
32
+ }
33
+ // Board files use ../boards/common/... relative includes and bare filenames
34
+ // resolved against the boards tree; try the file's own dir first, then the
35
+ // configured angle roots.
36
+ const local = path.resolve(path.dirname(fromFile), inner);
37
+ if (fs.existsSync(local))
38
+ return local;
39
+ for (const root of roots.angle) {
40
+ const p = path.resolve(root, inner);
41
+ if (fs.existsSync(p))
42
+ return p;
43
+ }
44
+ return undefined;
45
+ }
46
+ /** Strip block/line comments, keeping one attachment channel for gpio-map labels. */
47
+ function stripCommentsKeepMarkers(src) {
48
+ // Replace comment bodies but KEEP a marker: /* D0 */ → ⟨D0⟩ inline, so the
49
+ // gpio-map parser can pick silkscreen labels out of the cell stream.
50
+ return src
51
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => '⟨' + m.slice(2, -2).trim() + '⟩')
52
+ .replace(/\/\/[^\n]*/g, '');
53
+ }
54
+ function emptyNode(name, parent) {
55
+ return { labels: [], name, props: new Map(), children: new Map(), parent };
56
+ }
57
+ /** Statement scanner: emits node headers (at every '{'), property
58
+ * statements ('a = <...>' at their owning depth), and node closes ('}')
59
+ * in source order, building the node tree with &label override merges. */
60
+ function parseStatements(src) {
61
+ const root = emptyNode('/');
62
+ const byLabel = new Map();
63
+ const stack = [root];
64
+ let depth = 0;
65
+ let start = 0;
66
+ const emit = (kind, text) => {
67
+ if (kind === 'end') {
68
+ if (stack.length > 1)
69
+ stack.pop();
70
+ return;
71
+ }
72
+ const t = text.trim().replace(/^[\r\n\t ]+/, '');
73
+ if (!t)
74
+ return;
75
+ const top = stack[stack.length - 1];
76
+ if (kind === 'hdr') {
77
+ const refHeader = t.match(/^&([\w-]+)\s*\{?$/);
78
+ if (refHeader) {
79
+ let n = byLabel.get(refHeader[1]);
80
+ if (!n) {
81
+ n = emptyNode('&' + refHeader[1], top);
82
+ byLabel.set(refHeader[1], n);
83
+ // Attach to the synthetic tree so subtree walkers (connectors,
84
+ // gpio nodes, led strips, bus pinctrl) see &ref blocks — the SoC
85
+ // dtsi that parents them is not part of the local include chain.
86
+ top.children.set(n.name, n);
87
+ }
88
+ stack.push(n);
89
+ return;
90
+ }
91
+ // Header text may carry trailing debris from prior statements (the
92
+ // scanner slices from after the previous ';'/'}'), so isolate the LAST
93
+ // node-header line: [label:] name[@unit]
94
+ const headerLine = t.split(/[\r\n]+/).map((l) => l.trim()).filter(Boolean).pop() ?? t;
95
+ const parts = headerLine.split(':').map((s) => s.trim()).filter(Boolean);
96
+ const name = parts.length > 0 ? parts[parts.length - 1] : 'anon';
97
+ const labels = parts.slice(0, -1);
98
+ let node = top.children.get(name);
99
+ if (!node) {
100
+ node = emptyNode(name, top);
101
+ top.children.set(name, node);
102
+ }
103
+ for (const l of labels) {
104
+ if (!node.labels.includes(l))
105
+ node.labels.push(l);
106
+ byLabel.set(l, node);
107
+ }
108
+ stack.push(node);
109
+ return;
110
+ }
111
+ // property
112
+ // Leading comment markers (a comment line above a property, inside the
113
+ // same node — 96b_nitrogen's `/* gpio flags need validation */` before
114
+ // button0's gpios) must not hide the property: the anchored match would
115
+ // fail and the node would silently lose its facts. Strip them first;
116
+ // markers INSIDE a value stay (gpio-map silkscreen labels).
117
+ const stripped = t.replace(/^(?:⟨[^⟩]*⟩\s*)+/, '');
118
+ const prop = stripped.match(/^([\w,#@-]+)\s*=\s*([\s\S]*?)\s*;?$/);
119
+ if (prop && top !== root) {
120
+ top.props.set(prop[1], prop[2].replace(/;$/, ''));
121
+ return;
122
+ }
123
+ const bare = stripped.match(/^([\w,#@-]+)\s*;?$/);
124
+ if (bare && top !== root)
125
+ top.props.set(bare[1], '');
126
+ };
127
+ let i = 0;
128
+ while (i < src.length) {
129
+ const ch = src[i];
130
+ if (ch === '{') {
131
+ const stmt = src.slice(start, i);
132
+ emit('hdr', stmt);
133
+ depth++;
134
+ start = i + 1;
135
+ }
136
+ else if (ch === '}') {
137
+ const inner = src.slice(start, i).trim();
138
+ if (inner)
139
+ emit('prop', inner);
140
+ emit('end', '');
141
+ if (depth > 0)
142
+ depth--;
143
+ start = i + 1;
144
+ }
145
+ else if (ch === ';') {
146
+ const stmt = src.slice(start, i);
147
+ if (depth > 0)
148
+ emit('prop', stmt);
149
+ start = i + 1;
150
+ }
151
+ i++;
152
+ }
153
+ return { root, byLabel };
154
+ }
155
+ // ── cell parsing ───────────────────────────────────────────────────────────
156
+ /** Parse a property value like `<&gpioa 5 GPIO_ACTIVE_LOW>` into tokens. */
157
+ function parseCells(value) {
158
+ return value
159
+ // Angle-bracket cell wrappers are punctuation, not content.
160
+ .replace(/[<>]/g, ' ')
161
+ // Comment markers may carry multi-word labels ('D14 / A0', 'Pin 1,
162
+ // LEDK') — shield their internal whitespace AND commas from the split,
163
+ // restoring each to its own character afterwards (collapsing a comma
164
+ // to a space would hide it from the label splitter, and an unshielded
165
+ // comma tore the marker apart mid-token — '⟨Pin 1' + 'LEDK⟩' — whose
166
+ // truncated form minted garbage labels like 'Pin').
167
+ .replace(/⟨[^⟩]*⟩/g, (m) => ' ' + m.replace(/,/g, '\x1f').replace(/\s+/g, '\x1e') + ' ')
168
+ .split(/[\s,]+/)
169
+ .map((s) => s.trim().replace(/\x1f/g, ',').replace(/\x1e/g, ' '))
170
+ .filter(Boolean);
171
+ }
172
+ function parseGpioMap(value, parentCells) {
173
+ const toks = parseCells(value);
174
+ const entries = [];
175
+ let i = 0;
176
+ let pendingLabel;
177
+ const splitNames = (raw) =>
178
+ // '/' separates co-names of one pad ('D14 / A0'); ',' separates a pin
179
+ // number from its net name ('Pin 1, LEDK' → 'LEDK'). Whitespace is NOT
180
+ // a separator — 'Pin 1' must not collapse to 'Pin'.
181
+ raw.split(/[\/,]/).map((s) => s.trim()).filter((s) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s));
182
+ while (i < toks.length) {
183
+ // Sweep up any ⟨label⟩ markers preceding the numeric entry.
184
+ while (i < toks.length && toks[i].startsWith('⟨')) {
185
+ const inner = toks[i].slice(1, -1).trim();
186
+ if (inner)
187
+ pendingLabel = inner;
188
+ i++;
189
+ }
190
+ const spec = toks[i];
191
+ if (spec === undefined)
192
+ break;
193
+ let childSpec = Number(spec);
194
+ // Macro child specifiers (the nano header's ARDUINO_NANO_HEADER_D14):
195
+ // the numeric suffix is the specifier, the alphabetic suffix carries the
196
+ // silkscreen name when no comment label exists.
197
+ let macroLabel;
198
+ if (!Number.isFinite(childSpec)) {
199
+ // Multi-word macros (ARDUINO_NANO_HEADER_D14): the LAST underscore
200
+ // splits the prefix from the <letters><digits> silkscreen suffix.
201
+ const m = spec.match(/^([A-Za-z][A-Za-z0-9_]*)_([A-Za-z]+)(\d+)$/);
202
+ if (m) {
203
+ childSpec = Number(m[3]);
204
+ macroLabel = `${m[2]}${m[3]}`;
205
+ }
206
+ else {
207
+ i++;
208
+ continue;
209
+ }
210
+ }
211
+ // child-spec, pad, then &ctrl
212
+ const ctrl = toks[i + 2];
213
+ if (ctrl === undefined || !ctrl.startsWith('&')) {
214
+ i++;
215
+ continue;
216
+ }
217
+ const pin = Number(toks[i + 3]);
218
+ // Macro/expression pins (RTIO routing etc.) cannot be placed on the HAL
219
+ // pin map — skip the entry, keep scanning.
220
+ if (!Number.isFinite(pin)) {
221
+ i++;
222
+ continue;
223
+ }
224
+ const flags = [];
225
+ for (let f = i + 4; f < i + 3 + parentCells + 1 && f < toks.length; f++) {
226
+ const t = toks[f];
227
+ if (t.startsWith('&') || Number.isFinite(Number(t)))
228
+ break;
229
+ if (!t.startsWith('⟨'))
230
+ flags.push(t);
231
+ }
232
+ // A comment can name one pad twice ('D14 / A0' — the digital and analog
233
+ // silkscreen of the same pad): first name is the primary label, the rest
234
+ // ride as altLabels.
235
+ const names = pendingLabel ? splitNames(pendingLabel) : [];
236
+ const label = names[0] ?? macroLabel;
237
+ const altLabels = names.slice(1);
238
+ entries.push({
239
+ childSpecifier: childSpec,
240
+ ...(label ? { label } : {}),
241
+ ...(altLabels.length > 0 ? { altLabels } : {}),
242
+ ref: { controller: ctrl.slice(1), pin, flags },
243
+ });
244
+ pendingLabel = undefined;
245
+ // advance: spec + pad + &ctrl + parentCells numbers
246
+ i = i + 3 + parentCells;
247
+ // sweep trailing markers before the next entry
248
+ while (i < toks.length && toks[i].startsWith('⟨')) {
249
+ const inner = toks[i].slice(1, -1).trim();
250
+ if (inner && entries.length > 0) {
251
+ const trailing = splitNames(inner);
252
+ const last = entries[entries.length - 1];
253
+ if (trailing.length > 0 && !last.label)
254
+ last.label = trailing[0];
255
+ if (trailing.length > 1)
256
+ last.altLabels = [...(last.altLabels ?? []), ...trailing.slice(1)];
257
+ }
258
+ i++;
259
+ }
260
+ }
261
+ return entries;
262
+ }
263
+ // ── board facts extraction ─────────────────────────────────────────────────
264
+ function gpioRefFromProp(value) {
265
+ const toks = parseCells(value);
266
+ // Expect: &ctrl pin [flags...]
267
+ if (toks.length < 2 || !toks[0].startsWith('&'))
268
+ return undefined;
269
+ const pin = Number(toks[1]);
270
+ if (!Number.isFinite(pin))
271
+ return undefined;
272
+ // Normalize flag tokens: strip grouping parens, drop '|' separators.
273
+ const flags = toks
274
+ .slice(2)
275
+ .filter((t) => !t.startsWith('⟨') && t !== '|')
276
+ .map((t) => t.replace(/[()]/g, ''))
277
+ .filter(Boolean);
278
+ return { controller: toks[0].slice(1), pin, flags };
279
+ }
280
+ /**
281
+ * Read one board DTS file (following its local include chain) and extract
282
+ * the board-level facts. `zephyrBoardsRoot` (e.g. <zephyr>/boards) resolves
283
+ * `<../boards/common/...>` style includes.
284
+ */
285
+ export function readBoardDts(dtsPath, opts = {}) {
286
+ const roots = { angle: opts.zephyrBoardsRoot ? [opts.zephyrBoardsRoot] : [] };
287
+ const seen = new Set();
288
+ let text = '';
289
+ const absorb = (file) => {
290
+ const real = path.resolve(file);
291
+ if (seen.has(real))
292
+ return;
293
+ seen.add(real);
294
+ const src = fs.readFileSync(real, 'utf8');
295
+ // Expand includes textualy (depth bounded by `seen`).
296
+ const includeRe = /#include\s+([<"][^>"]+[>"])/g;
297
+ let last = 0;
298
+ let m;
299
+ let out = '';
300
+ while ((m = includeRe.exec(src))) {
301
+ out += src.slice(last, m.index);
302
+ const resolved = resolveInclude(m[1], real, roots);
303
+ if (resolved)
304
+ absorb(resolved);
305
+ last = includeRe.lastIndex;
306
+ }
307
+ out += src.slice(last);
308
+ text += out + '\n';
309
+ };
310
+ absorb(dtsPath);
311
+ const { root: syntheticRoot, byLabel } = parseStatements(stripCommentsKeepMarkers(text));
312
+ // The DTS root ('/ { ... }') is a child of the synthetic outer node —
313
+ // resolve it so board-level nodes (aliases/chosen/leds/gpio_keys/&) sit at
314
+ // the depth the extractors expect.
315
+ const root = syntheticRoot.children.get('/') ?? syntheticRoot;
316
+ // aliases {} and chosen {} — phandle-ref properties
317
+ const aliases = {};
318
+ const chosen = {};
319
+ for (const name of ['aliases', 'chosen']) {
320
+ const node = root.children.get(name);
321
+ if (!node)
322
+ continue;
323
+ for (const [k, v] of node.props) {
324
+ const ref = v.match(/^&([\w,-]+)$/);
325
+ if (ref) {
326
+ if (name === 'aliases')
327
+ aliases[k] = ref[1];
328
+ else
329
+ chosen[k] = ref[1];
330
+ }
331
+ }
332
+ }
333
+ // gpio-leds / gpio-keys children (match on compatible OR node name)
334
+ const leds = [];
335
+ const buttons = [];
336
+ const collectGpioNodes = (node) => {
337
+ const compat = node.props.get('compatible') ?? '';
338
+ const isLeds = compat.includes('gpio-leds');
339
+ const isKeys = compat.includes('gpio-keys');
340
+ if (isLeds || isKeys) {
341
+ // A label can be aliased several times (led0 + mcuboot-led0); collect
342
+ // ALL aliases per label, then prefer the canonical led<N>/sw<N> form.
343
+ const aliasesByLabel = new Map();
344
+ for (const [alias, label] of Object.entries(aliases)) {
345
+ const list = aliasesByLabel.get(label) ?? [];
346
+ list.push(alias);
347
+ aliasesByLabel.set(label, list);
348
+ }
349
+ for (const child of node.children.values()) {
350
+ const ref = gpioRefFromProp(child.props.get('gpios') ?? '');
351
+ if (!ref)
352
+ continue;
353
+ const nodelabel = child.labels[0] ?? child.name;
354
+ const aliasCandidates = child.labels.flatMap((l) => aliasesByLabel.get(l) ?? []);
355
+ const alias = aliasCandidates.find((a) => /^(led|sw|button)[0-9]*$/.test(a)) ?? aliasCandidates[0];
356
+ const entry = {
357
+ ...ref,
358
+ nodelabel,
359
+ alias,
360
+ };
361
+ (isLeds ? leds : buttons).push(entry);
362
+ }
363
+ return; // do not recurse into a leds/keys container
364
+ }
365
+ for (const child of node.children.values())
366
+ collectGpioNodes(child);
367
+ };
368
+ collectGpioNodes(root);
369
+ // Connector nexus nodes: any node with a gpio-map property
370
+ const connectors = [];
371
+ const findConnectors = (node) => {
372
+ const map = node.props.get('gpio-map');
373
+ if (map) {
374
+ const cells = Number(node.props.get('#gpio-cells')?.replace(/[<>]/g, '') ?? '2');
375
+ const parentCells = Number.isFinite(cells) && cells > 0 ? cells : 2;
376
+ const entries = parseGpioMap(map, parentCells);
377
+ // The LAST entry's trailing /* label */ comment can land after the
378
+ // property's terminating ';' — its marker never enters the value. When
379
+ // labeled siblings share a prefix+number pattern (D0…D9), extend it to
380
+ // the unlabeled tail entry (D10) instead of falling back to p<N>.
381
+ const labeled = entries.filter((e) => e.label);
382
+ const pattern = labeled.length >= 2
383
+ ? (() => {
384
+ const m = labeled[0].label.match(/^([A-Za-z]+)([0-9]+)$/);
385
+ if (!m)
386
+ return undefined;
387
+ const prefix = m[1];
388
+ const consistent = labeled.every((e) => e.label.startsWith(prefix) && Number(e.label.slice(prefix.length)) === e.childSpecifier);
389
+ return consistent ? prefix : undefined;
390
+ })()
391
+ : undefined;
392
+ const pins = {};
393
+ for (const e of entries) {
394
+ const label = e.label ?? (pattern ? `${pattern}${e.childSpecifier}` : `p${e.childSpecifier}`);
395
+ pins[label] = e.ref;
396
+ for (const alt of e.altLabels ?? [])
397
+ pins[alt] = e.ref;
398
+ }
399
+ // Complete the analog run when the FINAL entry's comment lands after
400
+ // the property's ';' (its marker never enters the value): the header's
401
+ // D/A pairing increments in lockstep (D14/A0 … D21/A7), so a consistent
402
+ // D−A offset observed on the captured entries extends to the tail.
403
+ const observed = entries.filter((e) => {
404
+ const d = e.label?.match(/^D(\d+)$/);
405
+ return d && e.altLabels?.some((a) => /^A\d+$/.test(a));
406
+ });
407
+ if (observed.length >= 2) {
408
+ const offsetOf = (e) => Number(e.label.slice(1)) - Number(e.altLabels.find((a) => /^A\d+$/.test(a)).slice(1));
409
+ const offset = offsetOf(observed[0]);
410
+ if (observed.every((e) => offsetOf(e) === offset)) {
411
+ for (const e of entries) {
412
+ const d = e.label?.match(/^D(\d+)$/);
413
+ if (!d || e.altLabels?.some((a) => /^A\d+$/.test(a)))
414
+ continue;
415
+ // Only positive A indices within the observed run — D0…D13 sit
416
+ // below the analog block (offset would go negative).
417
+ const aIdx = Number(d[1]) - offset;
418
+ if (aIdx < 0)
419
+ continue;
420
+ if (!pins[`A${aIdx}`])
421
+ pins[`A${aIdx}`] = e.ref;
422
+ }
423
+ }
424
+ }
425
+ connectors.push({
426
+ nodelabel: node.labels[0] ?? node.name,
427
+ compatible: (node.props.get('compatible') ?? '').replace(/"/g, '') || undefined,
428
+ pins,
429
+ });
430
+ }
431
+ for (const child of node.children.values())
432
+ findConnectors(child);
433
+ };
434
+ findConnectors(root);
435
+ // Addressable user LED (worldsemi,ws2812-*). Three wiring forms in the
436
+ // tree: the RP2040 PIO variant nests a plain `gpios = <&ctrl pin …>` node;
437
+ // the SPI/I2S variants sit on a bus whose pinctrl names the data pad in
438
+ // the macro itself (SPIM3_MOSI_GPIO33, I2S0_O_SD_GPIO48 — the OUTPUT SD
439
+ // is the data line; I_SD is capture, wrong direction). Walks the
440
+ // SYNTHETIC root: the strips ride &ref blocks (siblings of the board's
441
+ // `/ {}` node), which the resolved root's walkers never see.
442
+ const stripLed = findStripLed(syntheticRoot, byLabel);
443
+ // PWM-driven LEDs (pwm-leds children): `pwms = <&pwmN channel [period]
444
+ // [flags]>`. Only aliased children are addressable by the PWM lowering
445
+ // (DT_ALIAS(pwm_led0)); carry the alias through.
446
+ const reverseAlias = new Map();
447
+ for (const [name, label] of Object.entries(aliases))
448
+ reverseAlias.set(label, name);
449
+ const pwmLeds = [];
450
+ for (const ledsNode of root.children.values()) {
451
+ if ((ledsNode.props.get('compatible') ?? '').replace(/"/g, '') !== 'pwm-leds')
452
+ continue;
453
+ for (const child of ledsNode.children.values()) {
454
+ const pwms = child.props.get('pwms');
455
+ if (!pwms)
456
+ continue;
457
+ const toks = parseCells(pwms);
458
+ if (toks.length < 2 || !toks[0].startsWith('&'))
459
+ continue;
460
+ const channel = Number(toks[1]);
461
+ if (!Number.isFinite(channel))
462
+ continue;
463
+ const period = Number(toks[2]);
464
+ const flags = toks.slice(Number.isFinite(period) ? 3 : 2)
465
+ .filter((t) => !t.startsWith('⟨') && !Number.isFinite(Number(t)) && !/[()]/.test(t));
466
+ const led = {
467
+ controller: toks[0].slice(1),
468
+ channel,
469
+ ...(Number.isFinite(period) ? { periodNs: period } : {}),
470
+ ...(flags.length > 0 ? { flags } : {}),
471
+ ...(child.labels[0] && reverseAlias.has(child.labels[0]) ? { alias: reverseAlias.get(child.labels[0]) } : {}),
472
+ };
473
+ pwmLeds.push(led);
474
+ }
475
+ }
476
+ // USB device wiring: the board's &usbd / zephyr_udc0 node status. The
477
+ // label form (`zephyr_udc0: &usbd { … }`) attaches under both names.
478
+ let usbDevice;
479
+ let usbController;
480
+ for (const key of ['zephyr_udc0', 'usbd']) {
481
+ const n = byLabel.get(key);
482
+ if (!n)
483
+ continue;
484
+ const status = n.props.get('status');
485
+ if (status === '"okay"') {
486
+ usbDevice = 'enabled';
487
+ usbController = key;
488
+ break;
489
+ }
490
+ if (status === '"disabled"' && usbDevice === undefined)
491
+ usbDevice = 'disabled';
492
+ }
493
+ // Bus controller nodelabels the board wires up: &ref blocks whose status
494
+ // is okay (or that carry no status override but configure the bus — the
495
+ // app overlay decides enablement; cuttlefish's own overlay writes it).
496
+ // Classified by nodelabel — bus node names are vendor-stable (i2c0,
497
+ // spi2, usart0, eusart1, lpuart1, …).
498
+ const buses = { i2c: [], spi: [], uart: [] };
499
+ const classifyBus = (label) => {
500
+ if (/^i2c|^twi/.test(label))
501
+ return 'i2c';
502
+ if (/^spi|^ssp/.test(label))
503
+ return 'spi';
504
+ if (/uart|usart/.test(label))
505
+ return 'uart';
506
+ return undefined;
507
+ };
508
+ // Only &ref override blocks count (`xiao_i2c: &i2c1 { … }` → nodelabel
509
+ // 'i2c1' via the node's ref NAME) — label-captured pinctrl groups
510
+ // ('i2c0_default') are not buses. Pinctrl-group names also end in
511
+ // _default/_sleep; belt and braces.
512
+ const seenBuses = new Set();
513
+ for (const node of byLabel.values()) {
514
+ if (!node.name.startsWith('&'))
515
+ continue;
516
+ const nodelabel = node.name.slice(1);
517
+ if (nodelabel === 'usbd' || /_(default|sleep)$/.test(nodelabel))
518
+ continue;
519
+ const kind = classifyBus(nodelabel);
520
+ if (!kind || seenBuses.has(nodelabel))
521
+ continue;
522
+ const status = node.props.get('status');
523
+ if (status === '"disabled"')
524
+ continue;
525
+ seenBuses.add(nodelabel);
526
+ buses[kind].push(nodelabel);
527
+ }
528
+ for (const kind of ['i2c', 'spi', 'uart'])
529
+ buses[kind].sort();
530
+ void byLabel; // (labels already captured during parse)
531
+ return {
532
+ aliases, chosen, leds, buttons, connectors, pwmLeds, buses,
533
+ ...(stripLed ? { stripLed } : {}),
534
+ ...(usbDevice ? { usbDevice } : {}),
535
+ ...(usbController ? { usbController } : {}),
536
+ };
537
+ }
538
+ /** First `gpios` property in a subtree (the PIO strip nests it one level down). */
539
+ function gpiosInSubtree(node) {
540
+ const own = node.props.get('gpios');
541
+ if (own) {
542
+ const ref = gpioRefFromProp(own);
543
+ if (ref)
544
+ return ref;
545
+ }
546
+ for (const child of node.children.values()) {
547
+ const ref = gpiosInSubtree(child);
548
+ if (ref)
549
+ return ref;
550
+ }
551
+ return undefined;
552
+ }
553
+ /** All pinmux property text in a pinctrl group subtree. */
554
+ function pinmuxTextIn(node, out = []) {
555
+ const pm = node.props.get('pinmux');
556
+ if (pm)
557
+ out.push(pm);
558
+ for (const child of node.children.values())
559
+ pinmuxTextIn(child, out);
560
+ return out;
561
+ }
562
+ function findStripLed(root, byLabel) {
563
+ let found;
564
+ const walk = (node) => {
565
+ const compat = node.props.get('compatible') ?? '';
566
+ if (compat.includes('worldsemi,ws2812') && !found) {
567
+ found = stripRefOf(node, byLabel);
568
+ }
569
+ for (const child of node.children.values())
570
+ walk(child);
571
+ };
572
+ walk(root);
573
+ return found;
574
+ }
575
+ function stripRefOf(node, byLabel) {
576
+ // PIO form: a descendant carries the pad as a plain gpio ref.
577
+ const direct = gpiosInSubtree(node);
578
+ if (direct)
579
+ return direct;
580
+ // SPI/I2S forms: the enclosing bus's default pinctrl group names the
581
+ // data pad in its pinmux macro. Every ws2812-spi/i2s board in the tree is
582
+ // ESP32-family, whose controllers are the 32-pin gpio0/gpio1 pair.
583
+ const bus = node.parent;
584
+ const pcLabel = bus?.props.get('pinctrl-0')?.match(/&([\w-]+)/)?.[1];
585
+ const pinctrl = pcLabel ? byLabel.get(pcLabel) : undefined;
586
+ if (!pinctrl)
587
+ return undefined;
588
+ const text = pinmuxTextIn(pinctrl).join(' ');
589
+ const m = text.match(/MOSI_GPIO(\d+)|I2S\d*_O_SD_GPIO(\d+)/);
590
+ const global = m ? Number(m[1] ?? m[2]) : undefined;
591
+ if (global == null)
592
+ return undefined;
593
+ return global < 32
594
+ ? { controller: 'gpio0', pin: global, flags: [] }
595
+ : { controller: 'gpio1', pin: global - 32, flags: [] };
596
+ }