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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/LICENSE +202 -21
  2. package/README.md +49 -87
  3. package/dist/as-built.d.ts +2 -2
  4. package/dist/as-built.js +2 -2
  5. package/dist/boardgen.d.ts +1 -9
  6. package/dist/boardgen.js +228 -45
  7. package/dist/chips/types.d.ts +1 -1
  8. package/dist/debug-codegen.js +1 -1
  9. package/dist/display/touch-adapter.js +1 -1
  10. package/dist/display/ui-adapter.js +549 -549
  11. package/dist/doctor.js +4 -4
  12. package/dist/dt-config/custom-board.js +2 -2
  13. package/dist/dt-config/kconfig.js +18 -12
  14. package/dist/dt-config/overlay.d.ts +2 -2
  15. package/dist/dt-config/overlay.js +2 -2
  16. package/dist/framework.manifest.d.ts +3 -3
  17. package/dist/framework.manifest.js +10 -7
  18. package/dist/index.js +5 -5
  19. package/dist/licenses.d.ts +2 -2
  20. package/dist/licenses.js +8 -8
  21. package/dist/lowering/fs.js +1 -1
  22. package/dist/lowering/gpio.js +0 -32
  23. package/dist/lowering/http.js +264 -32
  24. package/dist/lowering/i2c.js +0 -8
  25. package/dist/lowering/interrupts.js +6 -0
  26. package/dist/lowering/mqtt.js +110 -8
  27. package/dist/lowering/usb.js +11 -11
  28. package/dist/lowering/wdt.js +2 -29
  29. package/dist/sdk/board-catalog-sync.d.ts +1 -3
  30. package/dist/sdk/board-catalog-sync.js +4 -10
  31. package/dist/strategy.d.ts +22 -44
  32. package/dist/strategy.js +152 -155
  33. package/dist/tmp-probe.d.ts +2 -0
  34. package/dist/tmp-probe.js +9 -0
  35. package/dist/toolchain/debug-config.d.ts +50 -90
  36. package/dist/toolchain/debug-config.js +241 -502
  37. package/dist/toolchain/env-check.d.ts +1 -3
  38. package/dist/toolchain/env-check.js +2 -7
  39. package/dist/toolchain/index.d.ts +65 -2
  40. package/dist/toolchain/index.js +432 -55
  41. package/dist/toolchain/runners.d.ts +16 -0
  42. package/dist/toolchain/runners.js +75 -0
  43. package/dist/toolchain/scaffold.d.ts +1 -1
  44. package/dist/toolchain/scaffold.js +12 -12
  45. package/dist/toolchain/west-discover.d.ts +6 -0
  46. package/dist/toolchain/west-discover.js +36 -13
  47. package/dist/toolchain/west-spawn.js +8 -2
  48. package/installer/README.md +328 -328
  49. package/installer/install.sh +2 -2
  50. package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
  51. package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
  52. package/installer/templates/project/.vscode/settings.json +1 -1
  53. package/installer/templates/project/README.md +2 -2
  54. package/package.json +5 -5
  55. package/src/as-built.ts +206 -206
  56. package/src/boardgen.ts +214 -48
  57. package/src/chips/types.ts +567 -567
  58. package/src/display/touch-adapter.ts +204 -204
  59. package/src/display/ui-adapter.ts +781 -781
  60. package/src/doctor.ts +4 -4
  61. package/src/dt-config/custom-board.ts +2 -2
  62. package/src/dt-config/kconfig.ts +18 -12
  63. package/src/dt-config/overlay.ts +1058 -1058
  64. package/src/framework.manifest.ts +538 -535
  65. package/src/index.ts +5 -5
  66. package/src/licenses.ts +425 -425
  67. package/src/lowering/fs.ts +135 -135
  68. package/src/lowering/gpio.ts +0 -33
  69. package/src/lowering/http.ts +264 -32
  70. package/src/lowering/i2c.ts +0 -9
  71. package/src/lowering/interrupts.ts +6 -0
  72. package/src/lowering/mqtt.ts +109 -8
  73. package/src/lowering/usb.ts +221 -221
  74. package/src/lowering/wdt.ts +2 -25
  75. package/src/sdk/board-catalog-sync.ts +4 -25
  76. package/src/strategy.ts +2265 -2309
  77. package/src/toolchain/debug-config.ts +264 -514
  78. package/src/toolchain/env-check.ts +279 -285
  79. package/src/toolchain/index.ts +1703 -1286
  80. package/src/toolchain/runners.ts +80 -0
  81. package/src/toolchain/scaffold.ts +296 -296
  82. package/src/toolchain/west-discover.ts +35 -13
  83. package/src/toolchain/west-spawn.ts +174 -168
  84. package/dist/async/timer-polyfill.d.ts +0 -10
  85. package/dist/async/timer-polyfill.js +0 -95
  86. package/dist/chips/board-overrides.d.ts +0 -7
  87. package/dist/chips/board-overrides.js +0 -11
  88. package/dist/chips/esp32.d.ts +0 -2
  89. package/dist/chips/esp32.js +0 -71
  90. package/dist/chips/esp32s3.d.ts +0 -2
  91. package/dist/chips/esp32s3.js +0 -103
  92. package/dist/chips/soc/.d.ts +0 -2
  93. package/dist/chips/soc/.js +0 -129
  94. package/dist/chips/soc/esp32.d.ts +0 -2
  95. package/dist/chips/soc/esp32.js +0 -120
  96. package/dist/chips/soc/esp32c3.d.ts +0 -2
  97. package/dist/chips/soc/esp32c3.js +0 -90
  98. package/dist/chips/soc/esp32c6.d.ts +0 -2
  99. package/dist/chips/soc/esp32c6.js +0 -109
  100. package/dist/chips/soc/esp32s3.d.ts +0 -2
  101. package/dist/chips/soc/esp32s3.js +0 -189
  102. package/dist/chips/soc/index.d.ts +0 -2
  103. package/dist/chips/soc/index.js +0 -23
  104. package/dist/chips/soc/nrf52840.d.ts +0 -2
  105. package/dist/chips/soc/nrf52840.js +0 -130
  106. package/dist/chips/soc/rp2040.d.ts +0 -2
  107. package/dist/chips/soc/rp2040.js +0 -141
  108. package/dist/chips/soc/rp2350a.d.ts +0 -2
  109. package/dist/chips/soc/rp2350a.js +0 -145
  110. package/dist/chips/soc/samd21g18a.d.ts +0 -2
  111. package/dist/chips/soc/samd21g18a.js +0 -143
  112. package/dist/chips/soc/stm32f411xe.d.ts +0 -2
  113. package/dist/chips/soc/stm32f411xe.js +0 -251
  114. package/dist/chips/xiao-ble.d.ts +0 -2
  115. package/dist/chips/xiao-ble.js +0 -100
  116. package/dist/lowering/power.d.ts +0 -9
  117. package/dist/lowering/power.js +0 -60
  118. package/dist/lowering/pulse.d.ts +0 -7
  119. package/dist/lowering/pulse.js +0 -51
  120. package/dist/lowering/tone.d.ts +0 -10
  121. package/dist/lowering/tone.js +0 -63
  122. package/dist/lowering/worker-backing.d.ts +0 -14
  123. package/dist/lowering/worker-backing.js +0 -79
  124. package/dist/lowering/worker.d.ts +0 -6
  125. package/dist/lowering/worker.js +0 -14
  126. package/dist/sdk/board-data.generated.d.ts +0 -2
  127. package/dist/sdk/board-data.generated.js +0 -4
  128. package/dist/sdk/catalog-walker.d.ts +0 -90
  129. package/dist/sdk/catalog-walker.js +0 -682
  130. package/dist/sdk/dts-reader.d.ts +0 -83
  131. package/dist/sdk/dts-reader.js +0 -596
  132. package/src/debug-codegen.ts +0 -207
@@ -304,7 +304,7 @@ For the CURRENT session without a new shell:
304
304
  eval "\$("$MAMBA" shell hook --shell bash)"
305
305
  micromamba activate $ENV_NAME
306
306
 
307
- Then a typeCAD/cuttlefish Zephyr build should discover west automatically:
308
- cd <project> && npx cuttlefish build
307
+ Then a typeCAD/typecad-hal Zephyr build should discover west automatically:
308
+ cd <project> && npx typecad-hal build
309
309
 
310
310
  EOF
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # Activates the micromamba env created by the typeCAD Zephyr installer. After
8
8
  # activation, `west` is on PATH and ZEPHYR_BASE / ZEPHYR_SDK_INSTALL_DIR point
9
- # at the new install, so `npx cuttlefish build` uses it automatically.
9
+ # at the new install, so `npx typecad-hal build` uses it automatically.
10
10
  #
11
11
  # Override the env name with $TYPECAD_ZEPHYR_ENV (default: "zephyr").
12
12
  # ---------------------------------------------------------------------------
@@ -7,7 +7,7 @@
7
7
  #
8
8
  # Activates the micromamba env created by the typeCAD Zephyr installer. After
9
9
  # activation, `west` is on PATH and ZEPHYR_BASE / ZEPHYR_SDK_INSTALL_DIR point
10
- # at the new install, so `npx cuttlefish build` uses it automatically.
10
+ # at the new install, so `npx typecad-hal build` uses it automatically.
11
11
  #
12
12
  # Override the env name with $TYPECAD_ZEPHYR_ENV (default: "zephyr").
13
13
  # ---------------------------------------------------------------------------
@@ -5,7 +5,7 @@
5
5
  // .typecad/activate-zephyr.ps1 (-NoExit keeps the shell open afterward). This
6
6
  // makes `micromamba activate zephyr` happen automatically, so `west` is on
7
7
  // PATH and ZEPHYR_BASE / ZEPHYR_SDK_INSTALL_DIR are set without per-session
8
- // typing - and `npx cuttlefish build` uses the new install.
8
+ // typing - and `npx typecad-hal build` uses the new install.
9
9
  //
10
10
  // If this project already has a .vscode/settings.json, MERGE these keys into
11
11
  // it by hand rather than overwriting the whole file.
@@ -1,9 +1,9 @@
1
1
  # typeCAD Zephyr — per-project activation template
2
2
 
3
- Copy these files into a cuttlefish Zephyr project so that opening a terminal in
3
+ Copy these files into a typecad-hal Zephyr project so that opening a terminal in
4
4
  it auto-activates the micromamba env created by the typeCAD Zephyr installer.
5
5
  Once active, `west` is on PATH and `ZEPHYR_BASE` / `ZEPHYR_SDK_INSTALL_DIR` are
6
- set, so `npx cuttlefish build` uses the new install automatically — no
6
+ set, so `npx typecad-hal build` uses the new install automatically — no
7
7
  per-session `micromamba activate` typing.
8
8
 
9
9
  ## Files
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@typecad/framework-zephyr",
3
- "version": "1.0.0-alpha.15",
3
+ "version": "1.0.0-alpha.18",
4
4
  "description": "TypeCAD framework package for the Zephyr RTOS \u2014 west/CMake build, devicetree-driven GPIO",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "exports": {
9
+ "./package.json": "./package.json",
9
10
  ".": {
10
11
  "types": "./dist/index.d.ts",
11
12
  "default": "./dist/index.js"
@@ -38,14 +39,13 @@
38
39
  "dry-run": "node installer/install.mjs --dry-run"
39
40
  },
40
41
  "dependencies": {
41
- "@typecad/cuttlefish": "1.0.0-alpha.15",
42
- "@typecad/hal": "1.0.0-alpha.15"
42
+ "@typecad/cuttlefish": "1.0.0-alpha.18"
43
43
  },
44
44
  "devDependencies": {
45
- "@typecad/expect": "1.0.0-alpha.15",
45
+ "@typecad/hal": "1.0.0-alpha.18",
46
46
  "typescript": "^5.7.3"
47
47
  },
48
- "license": "MIT",
48
+ "license": "Apache-2.0",
49
49
  "publishConfig": {
50
50
  "access": "public"
51
51
  },
package/src/as-built.ts CHANGED
@@ -1,206 +1,206 @@
1
- // ---------------------------------------------------------------------------
2
- // as-built.ts — harvest pin routes from a build's resolved zephyr.dts
3
- //
4
- // The third fact source: after any successful `west build`, the fully
5
- // merged devicetree at <build>/zephyr/zephyr.dts contains the board's
6
- // pinctrl nodes with LABELS verbatim — the stable name grammar
7
- // (`adc1_in1_pa1`, `tim4_ch1_pb6`, `iomuxc_<pad>_adc1_in1`) — with the
8
- // vendor macro values already expanded away. Parsing names only needs ONE
9
- // grammar per shape, immune to vendor macro churn, and corrects a broken
10
- // catalog harvest per-pin before the next build (the walker's macro-value
11
- // grammars are the fragile half; this is the robust half).
12
- //
13
- // Scope is honest: only node-name families appear (STM32-style dtsi nodes
14
- // and the i.MX RT label grammar with its in-band pad join). Header-matrix
15
- // families (Kinetis/LPC/GD32 macro routing, ESP32/RP2 matrices) never
16
- // exist as DT nodes — the walker + family tables remain their source.
17
- // ----------------------------------------------------------------------------
18
-
19
- export interface AsBuiltRoute {
20
- source: string;
21
- channel: number;
22
- port: string;
23
- bit: number;
24
- pinctrl: string;
25
- }
26
-
27
- export interface AsBuiltFacts {
28
- adc: AsBuiltRoute[];
29
- pwm: AsBuiltRoute[];
30
- dac: AsBuiltRoute[];
31
- }
32
-
33
- /** The .cuttlefish/as-built.json shape (machine-authored facts file). */
34
- export interface AsBuiltFile {
35
- version: 1;
36
- board: string;
37
- generatedAt: string;
38
- routes: {
39
- adcPins: AsBuiltRoute[];
40
- pwmPins: AsBuiltRoute[];
41
- dacPins: AsBuiltRoute[];
42
- };
43
- }
44
-
45
- // STM32-style pinctrl dtsi labels (all spellings the walker knows):
46
- // adc1_in1_pa1 / adc_in0_pa0 / adc1_inp16_pa0, tim4_ch1_pb6,
47
- // dac1_out1_pa4. Labels appear verbatim in zephyr.dts
48
- // (`adc1_in0_pa0: adc1_in0_pa0 {`). The `inn` negative inputs stay out —
49
- // the thin HAL is single-ended. The F1 (AFIO) family uses `tim1_ch1_pwm_out_pa8`
50
- // and `dac_out1_pa4` — separate grammars.
51
- const ST_ADC = /^([a-z]*adc\d*_(?:in|inp)\d+_p([a-z])(\d+))$/;
52
- const ST_PWM = /^(tim(\d+)_ch(\d+)_p([a-z])(\d+))$/;
53
- const ST_PWM_F1 = /^(tim(\d+)_ch(\d+)_pwm_out_p([a-z])(\d+))$/;
54
- const ST_DAC = /^(dac(\d+)_out(\d+)_p([a-z])(\d+))$/;
55
- const ST_DAC_F1 = /^(dac_out(\d+)_p([a-z])(\d+))$/;
56
- // i.MX RT labels: iomuxc_<pad>_adc1_in1 / iomuxc_<pad>_flexpwm2_pwma3,
57
- // with the pad→GPIO join as sibling iomuxc_<pad>_gpio1_io12 labels. The NEW
58
- // RT parts (rt11xx/rt116x/rt118x) use `…_flexpwm1_pwm0_a` (pwm<K>_<a|b> →
59
- // A=0/B=1). Their ADC is the LPADC (`lpadc<N>`) and stays out — see
60
- // dts-reader.ts.
61
- const IMX_GPIO = /^iomuxc_([a-z0-9_]+)_gpio(\d+)_io(\d+)$/;
62
- const IMX_ADC = /^iomuxc_([a-z0-9_]+)_adc(\d+)_in(\d+)$/;
63
- const IMX_PWM = /^iomuxc_([a-z0-9_]+)_flexpwm(\d+)_pwm([ab])(\d+)$/;
64
- const IMX_PWM2 = /^iomuxc_([a-z0-9_]+)_flexpwm(\d+)_pwm(\d+)_([ab])$/;
65
-
66
- /**
67
- * Parse a resolved zephyr.dts into raw pin routes. Labels are matched as
68
- * whole tokens (`label:` or `label {`) — in dtc output every node carries
69
- * its labels inline before the node name.
70
- */
71
- export function parseZephyrDts(text: string): AsBuiltFacts {
72
- const facts: AsBuiltFacts = { adc: [], pwm: [], dac: [] };
73
- const imxPadToGpio = new Map<string, { port: string; bit: number }>();
74
- const seen = new Set<string>();
75
- // Every label occurrence: `label:` (label before node) — zephyr.dts
76
- // renders `label: nodename {`, INDENTED with tabs. Whole-token labels.
77
- for (const m of text.matchAll(/^\s*([a-z][a-z0-9_]*):\s/gm)) {
78
- const label = m[1]!;
79
- if (seen.has(label)) continue;
80
- let g: RegExpMatchArray | null;
81
- if ((g = label.match(ST_ADC))) {
82
- seen.add(label);
83
- // Source: the unit prefix from the name — `adc1_in1…` → adc1, the
84
- // single-unit spelling `adc_in0…` → adc.
85
- const unit = label.match(/^([a-z]+?\d*)_in/);
86
- facts.adc.push({
87
- source: unit ? unit[1]! : 'adc',
88
- channel: Number(label.match(/_(?:in|inp)(\d+)_/)![1]),
89
- port: g[2]!.toUpperCase(),
90
- bit: Number(g[3]),
91
- pinctrl: label,
92
- });
93
- } else if ((g = label.match(ST_PWM))) {
94
- seen.add(label);
95
- facts.pwm.push({
96
- source: `tim${g[2]}`,
97
- channel: Number(g[3]),
98
- port: g[4]!.toUpperCase(),
99
- bit: Number(g[5]),
100
- pinctrl: label,
101
- });
102
- } else if ((g = label.match(ST_PWM_F1))) {
103
- seen.add(label);
104
- facts.pwm.push({
105
- source: `tim${g[2]}`,
106
- channel: Number(g[3]),
107
- port: g[4]!.toUpperCase(),
108
- bit: Number(g[5]),
109
- pinctrl: label,
110
- });
111
- } else if ((g = label.match(ST_DAC))) {
112
- seen.add(label);
113
- facts.dac.push({
114
- source: `dac${g[2]}`,
115
- channel: Number(g[3]),
116
- port: g[4]!.toUpperCase(),
117
- bit: Number(g[5]),
118
- pinctrl: label,
119
- });
120
- } else if ((g = label.match(ST_DAC_F1))) {
121
- seen.add(label);
122
- facts.dac.push({
123
- source: 'dac1',
124
- channel: Number(g[2]),
125
- port: g[3]!.toUpperCase(),
126
- bit: Number(g[4]),
127
- pinctrl: label,
128
- });
129
- } else if ((g = label.match(IMX_GPIO))) {
130
- seen.add(label);
131
- if (!imxPadToGpio.has(g[1]!)) imxPadToGpio.set(g[1]!, { port: g[2]!, bit: Number(g[3]) });
132
- }
133
- }
134
- // i.MX routes resolve after the join map is complete.
135
- for (const m of text.matchAll(/^\s*([a-z][a-z0-9_]*):\s/gm)) {
136
- const label = m[1]!;
137
- let g: RegExpMatchArray | null;
138
- if ((g = label.match(IMX_ADC))) {
139
- const gpio = imxPadToGpio.get(g[1]!);
140
- if (!gpio || seen.has(label)) continue;
141
- seen.add(label);
142
- facts.adc.push({
143
- source: `adc${g[2]}`,
144
- channel: Number(g[3]),
145
- port: gpio.port,
146
- bit: gpio.bit,
147
- pinctrl: label,
148
- });
149
- } else if ((g = label.match(IMX_PWM))) {
150
- const gpio = imxPadToGpio.get(g[1]!);
151
- if (!gpio || seen.has(label)) continue;
152
- seen.add(label);
153
- facts.pwm.push({
154
- source: `flexpwm${g[2]}_pwm${g[4]}`,
155
- channel: g[3] === 'b' ? 1 : 0,
156
- port: gpio.port,
157
- bit: gpio.bit,
158
- pinctrl: label,
159
- });
160
- } else if ((g = label.match(IMX_PWM2))) {
161
- const gpio = imxPadToGpio.get(g[1]!);
162
- if (!gpio || seen.has(label)) continue;
163
- seen.add(label);
164
- facts.pwm.push({
165
- source: `flexpwm${g[2]}_pwm${g[3]}`,
166
- channel: g[4] === 'b' ? 1 : 0,
167
- port: gpio.port,
168
- bit: gpio.bit,
169
- pinctrl: label,
170
- });
171
- }
172
- }
173
- return facts;
174
- }
175
-
176
- /** Parse + shape-validate .cuttlefish/as-built.json; errors name the file. */
177
- export function parseAsBuiltJson(text: string, source = '.cuttlefish/as-built.json'): AsBuiltFile {
178
- let parsed: unknown;
179
- try {
180
- parsed = JSON.parse(text);
181
- } catch (err) {
182
- throw new Error(`${source} is not valid JSON: ${(err as Error).message}`);
183
- }
184
- const file = parsed as AsBuiltFile;
185
- if (!file || typeof file !== 'object' || file.version !== 1 || typeof file.board !== 'string'
186
- || !file.routes || !Array.isArray(file.routes.adcPins) || !Array.isArray(file.routes.pwmPins)) {
187
- throw new Error(`${source} must carry version 1, a board target, and routes.{adcPins,pwmPins}.`);
188
- }
189
- return file;
190
- }
191
-
192
- /** Serialize the as-built snapshot (deterministic field order). */
193
- export function asBuiltJson(board: string, facts: AsBuiltFacts): string {
194
- const byPin = (a: AsBuiltRoute, b: AsBuiltRoute) =>
195
- a.port.localeCompare(b.port) || a.bit - b.bit || a.channel - b.channel;
196
- return JSON.stringify({
197
- version: 1,
198
- board,
199
- generatedAt: new Date().toISOString(),
200
- routes: {
201
- adcPins: [...facts.adc].sort(byPin),
202
- pwmPins: [...facts.pwm].sort(byPin),
203
- dacPins: [...facts.dac].sort(byPin),
204
- },
205
- }, null, 1);
206
- }
1
+ // ---------------------------------------------------------------------------
2
+ // as-built.ts — harvest pin routes from a build's resolved zephyr.dts
3
+ //
4
+ // The third fact source: after any successful `west build`, the fully
5
+ // merged devicetree at <build>/zephyr/zephyr.dts contains the board's
6
+ // pinctrl nodes with LABELS verbatim — the stable name grammar
7
+ // (`adc1_in1_pa1`, `tim4_ch1_pb6`, `iomuxc_<pad>_adc1_in1`) — with the
8
+ // vendor macro values already expanded away. Parsing names only needs ONE
9
+ // grammar per shape, immune to vendor macro churn, and corrects a broken
10
+ // catalog harvest per-pin before the next build (the walker's macro-value
11
+ // grammars are the fragile half; this is the robust half).
12
+ //
13
+ // Scope is honest: only node-name families appear (STM32-style dtsi nodes
14
+ // and the i.MX RT label grammar with its in-band pad join). Header-matrix
15
+ // families (Kinetis/LPC/GD32 macro routing, ESP32/RP2 matrices) never
16
+ // exist as DT nodes — the walker + family tables remain their source.
17
+ // ----------------------------------------------------------------------------
18
+
19
+ export interface AsBuiltRoute {
20
+ source: string;
21
+ channel: number;
22
+ port: string;
23
+ bit: number;
24
+ pinctrl: string;
25
+ }
26
+
27
+ export interface AsBuiltFacts {
28
+ adc: AsBuiltRoute[];
29
+ pwm: AsBuiltRoute[];
30
+ dac: AsBuiltRoute[];
31
+ }
32
+
33
+ /** The .typecad-hal/as-built.json shape (machine-authored facts file). */
34
+ export interface AsBuiltFile {
35
+ version: 1;
36
+ board: string;
37
+ generatedAt: string;
38
+ routes: {
39
+ adcPins: AsBuiltRoute[];
40
+ pwmPins: AsBuiltRoute[];
41
+ dacPins: AsBuiltRoute[];
42
+ };
43
+ }
44
+
45
+ // STM32-style pinctrl dtsi labels (all spellings the walker knows):
46
+ // adc1_in1_pa1 / adc_in0_pa0 / adc1_inp16_pa0, tim4_ch1_pb6,
47
+ // dac1_out1_pa4. Labels appear verbatim in zephyr.dts
48
+ // (`adc1_in0_pa0: adc1_in0_pa0 {`). The `inn` negative inputs stay out —
49
+ // the thin HAL is single-ended. The F1 (AFIO) family uses `tim1_ch1_pwm_out_pa8`
50
+ // and `dac_out1_pa4` — separate grammars.
51
+ const ST_ADC = /^([a-z]*adc\d*_(?:in|inp)\d+_p([a-z])(\d+))$/;
52
+ const ST_PWM = /^(tim(\d+)_ch(\d+)_p([a-z])(\d+))$/;
53
+ const ST_PWM_F1 = /^(tim(\d+)_ch(\d+)_pwm_out_p([a-z])(\d+))$/;
54
+ const ST_DAC = /^(dac(\d+)_out(\d+)_p([a-z])(\d+))$/;
55
+ const ST_DAC_F1 = /^(dac_out(\d+)_p([a-z])(\d+))$/;
56
+ // i.MX RT labels: iomuxc_<pad>_adc1_in1 / iomuxc_<pad>_flexpwm2_pwma3,
57
+ // with the pad→GPIO join as sibling iomuxc_<pad>_gpio1_io12 labels. The NEW
58
+ // RT parts (rt11xx/rt116x/rt118x) use `…_flexpwm1_pwm0_a` (pwm<K>_<a|b> →
59
+ // A=0/B=1). Their ADC is the LPADC (`lpadc<N>`) and stays out — see
60
+ // dts-reader.ts.
61
+ const IMX_GPIO = /^iomuxc_([a-z0-9_]+)_gpio(\d+)_io(\d+)$/;
62
+ const IMX_ADC = /^iomuxc_([a-z0-9_]+)_adc(\d+)_in(\d+)$/;
63
+ const IMX_PWM = /^iomuxc_([a-z0-9_]+)_flexpwm(\d+)_pwm([ab])(\d+)$/;
64
+ const IMX_PWM2 = /^iomuxc_([a-z0-9_]+)_flexpwm(\d+)_pwm(\d+)_([ab])$/;
65
+
66
+ /**
67
+ * Parse a resolved zephyr.dts into raw pin routes. Labels are matched as
68
+ * whole tokens (`label:` or `label {`) — in dtc output every node carries
69
+ * its labels inline before the node name.
70
+ */
71
+ export function parseZephyrDts(text: string): AsBuiltFacts {
72
+ const facts: AsBuiltFacts = { adc: [], pwm: [], dac: [] };
73
+ const imxPadToGpio = new Map<string, { port: string; bit: number }>();
74
+ const seen = new Set<string>();
75
+ // Every label occurrence: `label:` (label before node) — zephyr.dts
76
+ // renders `label: nodename {`, INDENTED with tabs. Whole-token labels.
77
+ for (const m of text.matchAll(/^\s*([a-z][a-z0-9_]*):\s/gm)) {
78
+ const label = m[1]!;
79
+ if (seen.has(label)) continue;
80
+ let g: RegExpMatchArray | null;
81
+ if ((g = label.match(ST_ADC))) {
82
+ seen.add(label);
83
+ // Source: the unit prefix from the name — `adc1_in1…` → adc1, the
84
+ // single-unit spelling `adc_in0…` → adc.
85
+ const unit = label.match(/^([a-z]+?\d*)_in/);
86
+ facts.adc.push({
87
+ source: unit ? unit[1]! : 'adc',
88
+ channel: Number(label.match(/_(?:in|inp)(\d+)_/)![1]),
89
+ port: g[2]!.toUpperCase(),
90
+ bit: Number(g[3]),
91
+ pinctrl: label,
92
+ });
93
+ } else if ((g = label.match(ST_PWM))) {
94
+ seen.add(label);
95
+ facts.pwm.push({
96
+ source: `tim${g[2]}`,
97
+ channel: Number(g[3]),
98
+ port: g[4]!.toUpperCase(),
99
+ bit: Number(g[5]),
100
+ pinctrl: label,
101
+ });
102
+ } else if ((g = label.match(ST_PWM_F1))) {
103
+ seen.add(label);
104
+ facts.pwm.push({
105
+ source: `tim${g[2]}`,
106
+ channel: Number(g[3]),
107
+ port: g[4]!.toUpperCase(),
108
+ bit: Number(g[5]),
109
+ pinctrl: label,
110
+ });
111
+ } else if ((g = label.match(ST_DAC))) {
112
+ seen.add(label);
113
+ facts.dac.push({
114
+ source: `dac${g[2]}`,
115
+ channel: Number(g[3]),
116
+ port: g[4]!.toUpperCase(),
117
+ bit: Number(g[5]),
118
+ pinctrl: label,
119
+ });
120
+ } else if ((g = label.match(ST_DAC_F1))) {
121
+ seen.add(label);
122
+ facts.dac.push({
123
+ source: 'dac1',
124
+ channel: Number(g[2]),
125
+ port: g[3]!.toUpperCase(),
126
+ bit: Number(g[4]),
127
+ pinctrl: label,
128
+ });
129
+ } else if ((g = label.match(IMX_GPIO))) {
130
+ seen.add(label);
131
+ if (!imxPadToGpio.has(g[1]!)) imxPadToGpio.set(g[1]!, { port: g[2]!, bit: Number(g[3]) });
132
+ }
133
+ }
134
+ // i.MX routes resolve after the join map is complete.
135
+ for (const m of text.matchAll(/^\s*([a-z][a-z0-9_]*):\s/gm)) {
136
+ const label = m[1]!;
137
+ let g: RegExpMatchArray | null;
138
+ if ((g = label.match(IMX_ADC))) {
139
+ const gpio = imxPadToGpio.get(g[1]!);
140
+ if (!gpio || seen.has(label)) continue;
141
+ seen.add(label);
142
+ facts.adc.push({
143
+ source: `adc${g[2]}`,
144
+ channel: Number(g[3]),
145
+ port: gpio.port,
146
+ bit: gpio.bit,
147
+ pinctrl: label,
148
+ });
149
+ } else if ((g = label.match(IMX_PWM))) {
150
+ const gpio = imxPadToGpio.get(g[1]!);
151
+ if (!gpio || seen.has(label)) continue;
152
+ seen.add(label);
153
+ facts.pwm.push({
154
+ source: `flexpwm${g[2]}_pwm${g[4]}`,
155
+ channel: g[3] === 'b' ? 1 : 0,
156
+ port: gpio.port,
157
+ bit: gpio.bit,
158
+ pinctrl: label,
159
+ });
160
+ } else if ((g = label.match(IMX_PWM2))) {
161
+ const gpio = imxPadToGpio.get(g[1]!);
162
+ if (!gpio || seen.has(label)) continue;
163
+ seen.add(label);
164
+ facts.pwm.push({
165
+ source: `flexpwm${g[2]}_pwm${g[3]}`,
166
+ channel: g[4] === 'b' ? 1 : 0,
167
+ port: gpio.port,
168
+ bit: gpio.bit,
169
+ pinctrl: label,
170
+ });
171
+ }
172
+ }
173
+ return facts;
174
+ }
175
+
176
+ /** Parse + shape-validate .typecad-hal/as-built.json; errors name the file. */
177
+ export function parseAsBuiltJson(text: string, source = '.typecad-hal/as-built.json'): AsBuiltFile {
178
+ let parsed: unknown;
179
+ try {
180
+ parsed = JSON.parse(text);
181
+ } catch (err) {
182
+ throw new Error(`${source} is not valid JSON: ${(err as Error).message}`);
183
+ }
184
+ const file = parsed as AsBuiltFile;
185
+ if (!file || typeof file !== 'object' || file.version !== 1 || typeof file.board !== 'string'
186
+ || !file.routes || !Array.isArray(file.routes.adcPins) || !Array.isArray(file.routes.pwmPins)) {
187
+ throw new Error(`${source} must carry version 1, a board target, and routes.{adcPins,pwmPins}.`);
188
+ }
189
+ return file;
190
+ }
191
+
192
+ /** Serialize the as-built snapshot (deterministic field order). */
193
+ export function asBuiltJson(board: string, facts: AsBuiltFacts): string {
194
+ const byPin = (a: AsBuiltRoute, b: AsBuiltRoute) =>
195
+ a.port.localeCompare(b.port) || a.bit - b.bit || a.channel - b.channel;
196
+ return JSON.stringify({
197
+ version: 1,
198
+ board,
199
+ generatedAt: new Date().toISOString(),
200
+ routes: {
201
+ adcPins: [...facts.adc].sort(byPin),
202
+ pwmPins: [...facts.pwm].sort(byPin),
203
+ dacPins: [...facts.dac].sort(byPin),
204
+ },
205
+ }, null, 1);
206
+ }