@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
package/src/doctor.ts CHANGED
@@ -1,77 +1,110 @@
1
- // ---------------------------------------------------------------------------
2
- // @typecad/framework-zephyr — Zephyr environment doctor
3
- //
4
- // `cuttlefish doctor` (Zephyr framework) — verify west (the Zephyr build tool)
5
- // is installed + responsive, the Zephyr RTOS is inside the framework's declared
6
- // compat range, and the configured board target exists in the checkout. Exits 0
7
- // if the environment is OK, non-zero otherwise. Mirrors framework-arduino's
8
- // doctor shape (dispatched via the framework's `doctor` export) and reuses
9
- // checkZephyrEnv so the detection logic can be shared with the build/test gates.
10
- // ---------------------------------------------------------------------------
11
-
12
- import * as ui from '@typecad/cuttlefish/utils/ui';
13
- import { loadCuttlefishConfig } from '@typecad/cuttlefish/config-loader';
14
- import { checkZephyrEnv } from './toolchain/env-check.js';
15
-
16
- /**
17
- * Verify west is installed + responsive, the Zephyr RTOS is inside the supported
18
- * range, and the configured board target exists in the checkout. Sets
19
- * process.exitCode = 1 on failure. Thin presenter over checkZephyrEnv.
20
- */
21
- export function runDoctor(): void {
22
- ui.printHeader();
23
- ui.printStep('Checking Zephyr environment...');
24
-
25
- const config = loadCuttlefishConfig(process.cwd());
26
- const buildTarget = config?.buildTarget;
27
-
28
- const result = checkZephyrEnv(buildTarget);
29
- const c = result.check;
30
-
31
- // west (the Zephyr build tool) — the analog of arduino-cli presence.
32
- if (c.westFound) {
33
- const ver = c.westVersion ?? 'found';
34
- const src = c.westSource ? ` (${c.westSource})` : '';
35
- ui.printInfo(`west ............. ${ver} ✓${src}`);
36
- } else {
37
- ui.printError('west ............. NOT FOUND');
38
- }
39
-
40
- // Zephyr RTOS version + declared compat range.
41
- ui.printInfo(`ZEPHYR_BASE ...... ${c.zephyrBase ?? '(not set)'}`);
42
- ui.printInfo(`Zephyr version .. ${c.zephyrVersion ?? 'unknown (could not read ZEPHYR_BASE/VERSION)'}`);
43
- ui.printInfo(`Supported range . ${c.compatRange ?? '(none declared)'}`);
44
-
45
- if (c.compatStatus === 'out-of-range') {
46
- ui.printError(`Zephyr ${c.zephyrVersion} is OUTSIDE the supported range (${c.compatRange}).`);
47
- } else if (c.compatStatus === 'undetectable') {
48
- ui.printWarning('Could not detect the Zephyr version (is ZEPHYR_BASE set?) compat check skipped.');
49
- } else {
50
- ui.printInfo('Zephyr compat ... OK');
51
- }
52
-
53
- // Board target the analog of the Arduino core presence check.
54
- if (buildTarget) {
55
- const resolved = c.resolvedBoardTarget ?? buildTarget;
56
- const arrow = resolved === buildTarget ? '' : ` → ${resolved}`;
57
- if (c.boardTargetSupported === false) {
58
- ui.printError(`Board target .... ${buildTarget}${arrow} NOT found in this Zephyr checkout`);
59
- ui.printInfo(' → check the board id, or run: west boards');
60
- } else if (c.boardTargetSupported === undefined) {
61
- ui.printInfo(`Board target .... ${buildTarget}${arrow}`);
62
- ui.printInfo('(could not verify board presence — no ZEPHYR_BASE boards/ tree found)');
63
- } else {
64
- ui.printInfo(`Board target .... ${buildTarget}${arrow}`);
65
- }
66
- } else {
67
- ui.printInfo('(no buildTarget in cuttlefish.config.ts skipping board check)');
68
- }
69
-
70
- // Exit code — mirrors framework-arduino's doctor.
71
- if (result.ok) {
72
- ui.printSuccess('Environment OK');
73
- return; // exitCode stays unset => 0
74
- }
75
- for (const line of result.messages) ui.printInfo(line);
76
- process.exitCode = 1;
77
- }
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/framework-zephyr — Zephyr environment doctor
3
+ //
4
+ // `cuttlefish doctor` (Zephyr framework) — verify west (the Zephyr build tool)
5
+ // is installed + responsive, the Zephyr RTOS is inside the framework's declared
6
+ // compat range, and the configured board target exists in the checkout. Exits 0
7
+ // if the environment is OK, non-zero otherwise. Follows the same contract
8
+ // doctor shape (dispatched via the framework's `doctor` export) and reuses
9
+ // checkZephyrEnv so the detection logic can be shared with the build/test gates.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import * as ui from '@typecad/cuttlefish/utils/ui';
15
+ import { loadCuttlefishConfig } from '@typecad/cuttlefish/config-loader';
16
+ import { checkZephyrEnv } from './toolchain/env-check.js';
17
+ import { PINNED_ZEPHYR_MANIFEST_REV, PINNED_ZEPHYR_SDK_VERSION, sdkFingerprint } from '@typecad/cuttlefish/board-catalog';
18
+ import { resolveChipFromBoard } from './chips/resolve.js';
19
+
20
+ /**
21
+ * Verify west is installed + responsive, the Zephyr RTOS is inside the supported
22
+ * range, and the configured board target exists in the checkout. Sets
23
+ * process.exitCode = 1 on failure. Thin presenter over checkZephyrEnv.
24
+ */
25
+ export function runDoctor(): void {
26
+ ui.printHeader();
27
+ ui.printStep('Checking Zephyr environment...');
28
+
29
+ const config = loadCuttlefishConfig(process.cwd());
30
+ const buildTarget = config?.buildTarget;
31
+
32
+ const result = checkZephyrEnv(buildTarget);
33
+ const c = result.check;
34
+
35
+ // west (the Zephyr build tool) presence.
36
+ if (c.westFound) {
37
+ const ver = c.westVersion ?? 'found';
38
+ const src = c.westSource ? ` (${c.westSource})` : '';
39
+ ui.printInfo(`west ............. ${ver} ✓${src}`);
40
+ } else {
41
+ ui.printError('west ............. NOT FOUND');
42
+ }
43
+
44
+ // Zephyr RTOS version + declared compat range.
45
+ ui.printInfo(`ZEPHYR_BASE ...... ${c.zephyrBase ?? '(not set)'}`);
46
+ ui.printInfo(`Zephyr version .. ${c.zephyrVersion ?? 'unknown (could not read ZEPHYR_BASE/VERSION)'}`);
47
+ ui.printInfo(`Workspace pin ... ${PINNED_ZEPHYR_MANIFEST_REV} (SDK ${PINNED_ZEPHYR_SDK_VERSION})`);
48
+ const fp = c.zephyrBase ? sdkFingerprint(c.zephyrBase) : undefined;
49
+ ui.printInfo(`SDK fingerprint . ${fp ?? '(no tree — run ' + 'npx --package @typecad/framework-zephyr zephyr-installer)'}`);
50
+ ui.printInfo(`Supported range . ${c.compatRange ?? '(none declared)'}`);
51
+
52
+ if (c.compatStatus === 'out-of-range') {
53
+ ui.printError(`Zephyr ${c.zephyrVersion} is OUTSIDE the supported range (${c.compatRange}).`);
54
+ } else if (c.compatStatus === 'undetectable') {
55
+ ui.printWarning('Could not detect the Zephyr version (is ZEPHYR_BASE set?) — compat check skipped.');
56
+ } else {
57
+ ui.printInfo('Zephyr compat ... OK');
58
+ }
59
+
60
+ // Board target availability.
61
+ if (buildTarget) {
62
+ const resolved = c.resolvedBoardTarget ?? buildTarget;
63
+ const arrow = resolved === buildTarget ? '' : ` → ${resolved}`;
64
+ if (c.boardTargetSupported === false) {
65
+ ui.printError(`Board target .... ${buildTarget}${arrow} NOT found in this Zephyr checkout`);
66
+ ui.printInfo(' check the board id, or run: west boards');
67
+ } else if (c.boardTargetSupported === undefined) {
68
+ ui.printInfo(`Board target .... ${buildTarget}${arrow}`);
69
+ ui.printInfo('(could not verify board presence — no ZEPHYR_BASE boards/ tree found)');
70
+ } else {
71
+ ui.printInfo(`Board target .... ${buildTarget}${arrow}`);
72
+ }
73
+ } else {
74
+ ui.printInfo('(no buildTarget in cuttlefish.config.ts — skipping board check)');
75
+ }
76
+
77
+ // Probe methods — from the board package's table, via the board constants
78
+ // the transpile persists (candidates cover the standard out-dir layouts).
79
+ // The debug line lists the debug-capable subset (bootloaders can't debug).
80
+ if (buildTarget) {
81
+ const bcPath = [
82
+ join(process.cwd(), 'out', 'src', 'board-constants.json'),
83
+ join(process.cwd(), 'out', 'board-constants.json'),
84
+ join(process.cwd(), 'src', 'out', 'src', 'board-constants.json'),
85
+ join(process.cwd(), 'src', 'out', 'board-constants.json'),
86
+ ].find((p) => existsSync(p));
87
+ if (bcPath) {
88
+ try {
89
+ const raw = JSON.parse(readFileSync(bcPath, 'utf8')) as Record<string, string | number | boolean>;
90
+ const chip = resolveChipFromBoard(new Map(Object.entries(raw)));
91
+ const methods = chip?.probeMethods ?? [];
92
+ if (methods.length > 0) {
93
+ ui.printInfo(`Probe methods .... ${methods.map((m) => m.id).join(', ')} (zephyr.probe / --probe)`);
94
+ const debuggable = methods.filter((m) => m.debug !== false).map((m) => m.id);
95
+ ui.printInfo(`Debug methods .... ${debuggable.join(', ') || '(none — an external probe is required)'}`);
96
+ }
97
+ } catch { /* best-effort listing */ }
98
+ } else {
99
+ ui.printInfo('Probe methods .... (build once to list them)');
100
+ }
101
+ }
102
+
103
+ // Exit code — 0 ok, non-zero otherwise.
104
+ if (result.ok) {
105
+ ui.printSuccess('Environment OK');
106
+ return; // exitCode stays unset => 0
107
+ }
108
+ for (const line of result.messages) ui.printInfo(line);
109
+ process.exitCode = 1;
110
+ }
@@ -0,0 +1,325 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Custom board generation — an out-of-tree Zephyr board for an MCU-only target
3
+ //
4
+ // When a project targets an MCU package with no board package (`mcu:` set,
5
+ // `board:` absent) there may be no Zephyr board for the user's hardware. The
6
+ // chip descriptor's `customBoard` block (silicon facts from the MCU package:
7
+ // SoC name, devicetree includes, default console mux, default clock plan) is
8
+ // all a minimal HWMv2 board needs, so we generate one into the project:
9
+ //
10
+ // boards/typecad/<name>/board.yml — board + SoC declaration
11
+ // boards/typecad/<name>/<name>.dts — includes, chosen, console, clocks
12
+ // boards/typecad/<name>/Kconfig.<name> — BOARD_<NAME> symbol selecting the SoC
13
+ // boards/typecad/<name>/<name>_defconfig— baseline (GPIO + console) every
14
+ // in-tree board carries
15
+ //
16
+ // The app's CMakeLists.txt adds the project root to BOARD_ROOT (see
17
+ // toolchain/scaffold.ts), so Zephyr discovers the board without touching the
18
+ // checkout. Everything else (buses, ADC, PWM, USB) stays usage-driven: the
19
+ // overlay generator keeps layering `boards/<name>.overlay` on top exactly as
20
+ // it does for upstream boards.
21
+ //
22
+ // Verified against Zephyr 4.3/4.4 (HWMv2): a minimal board is exactly these
23
+ // four files — Kconfig.<name> + CONFIG_GPIO=y are load-bearing (a board
24
+ // without them fails with "ARCH not defined" / gpio link errors).
25
+ // ---------------------------------------------------------------------------
26
+
27
+ import { mkdirSync } from 'node:fs';
28
+ import { join } from 'node:path';
29
+
30
+ import type { ZephyrChipDescriptor } from '../chips/types.js';
31
+ import { writeIfChanged } from '../toolchain/scaffold.js';
32
+
33
+ /** The vendor segment of the generated board path + board.yml. */
34
+ const CUSTOM_BOARD_VENDOR = 'typecad';
35
+
36
+ /**
37
+ * Sanitize a raw name into a legal Zephyr board name: lowercase, `[a-z0-9_]`
38
+ * (dashes/dots/spaces collapse to underscores, leading/trailing underscores
39
+ * trimmed). Board names become C identifiers in Kconfig symbols, so anything
40
+ * stricter would break `config BOARD_<NAME>`.
41
+ */
42
+ export function sanitizeBoardName(raw: string): string {
43
+ const cleaned = raw
44
+ .toLowerCase()
45
+ .replace(/[^a-z0-9_]+/g, '_')
46
+ .replace(/^_+|_+$/g, '');
47
+ return cleaned.length > 0 ? cleaned : 'custom_board';
48
+ }
49
+
50
+ /** Kconfig symbol fragment: `cb_f411` → `CB_F411`. */
51
+ function kconfigFragment(name: string): string {
52
+ return name.toUpperCase();
53
+ }
54
+
55
+ /** Generated-board file set, all under `<projectRoot>/boards/typecad/<name>/`. */
56
+ export interface GeneratedCustomBoard {
57
+ /** Absolute directory of the generated board. */
58
+ dir: string;
59
+ /** The sanitized board name (the `west build -b` argument). */
60
+ name: string;
61
+ /** Whether any file changed (idempotent regeneration signal). */
62
+ changed: boolean;
63
+ }
64
+
65
+ /**
66
+ * Emit the custom board for a chip whose descriptor carries `customBoard`
67
+ * silicon data. No-op (returns null) when the chip has no generator inputs —
68
+ * board-resolved chips don't need one.
69
+ */
70
+ export function generateCustomBoard(
71
+ projectRoot: string,
72
+ chip: ZephyrChipDescriptor,
73
+ rawName: string,
74
+ ): GeneratedCustomBoard | null {
75
+ const cb = chip.customBoard;
76
+ if (!cb) return null;
77
+
78
+ const name = sanitizeBoardName(rawName);
79
+ const soc = cb.socs[0];
80
+ if (!soc) return null;
81
+
82
+ const dir = join(projectRoot, 'boards', CUSTOM_BOARD_VENDOR, name);
83
+ mkdirSync(dir, { recursive: true });
84
+
85
+ let changed = false;
86
+
87
+ // ── board.yml ────────────────────────────────────────────────────────────
88
+ changed =
89
+ writeIfChanged(
90
+ join(dir, 'board.yml'),
91
+ [
92
+ '# Auto-generated by @typecad/framework-zephyr from the MCU package',
93
+ '# silicon definition. Regenerated on every compile — edit the MCU',
94
+ '# package (or add a board package) instead of this file.',
95
+ 'board:',
96
+ ` name: ${name}`,
97
+ ` full_name: Cuttlefish custom board (${chip.soc})`,
98
+ ` vendor: ${CUSTOM_BOARD_VENDOR}`,
99
+ ' socs:',
100
+ ` - name: ${soc}`,
101
+ '',
102
+ ].join('\n'),
103
+ ) || changed;
104
+
105
+ // ── Kconfig.<name> — the board symbol selecting the SoC ─────────────────
106
+ // Load-bearing: without the BOARD_<NAME> symbol selecting SOC_<SOC>, Kconfig
107
+ // completes with no SoC and the build dies with "ARCH not defined".
108
+ changed =
109
+ writeIfChanged(
110
+ join(dir, `Kconfig.${name}`),
111
+ [
112
+ '# Auto-generated by @typecad/framework-zephyr. Do not edit.',
113
+ '',
114
+ `config BOARD_${kconfigFragment(name)}`,
115
+ `\tselect SOC_${kconfigFragment(soc)}`,
116
+ '',
117
+ ].join('\n'),
118
+ ) || changed;
119
+
120
+ // ── <name>.dts — includes, chosen, console, clock plan ───────────────────
121
+ const lines: string[] = [
122
+ '// Auto-generated by @typecad/framework-zephyr from the MCU package',
123
+ '// silicon definition. Regenerated on every compile — edit the MCU',
124
+ '// package (or add a board package) instead of this file.',
125
+ '/dts-v1/;',
126
+ ...cb.dtsIncludes.map((inc) => `#include <${inc}>`),
127
+ '',
128
+ '/ {',
129
+ ` model = "Cuttlefish custom board (${chip.soc})";`,
130
+ ` compatible = "${CUSTOM_BOARD_VENDOR},${name.replace(/_/g, '-')}";`,
131
+ '',
132
+ ' chosen {',
133
+ ` zephyr,console = &${cb.console.nodeLabel};`,
134
+ ' zephyr,sram = &sram0;',
135
+ ' zephyr,flash = &flash0;',
136
+ ' };',
137
+ '};',
138
+ '',
139
+ ];
140
+
141
+ // The chip descriptor's DT-aliased GPIOs (sw0/led0 from the reference
142
+ // board data): the generated firmware addresses them via DT_ALIAS, so the
143
+ // custom board must define the nodes. Pins map through gpioControllers
144
+ // (port-block numbering: pin = minPin + port bit).
145
+ const controllerFor = (pin: number): { label: string; bit: number } | undefined => {
146
+ const c = chip.gpioControllers?.find((c) => pin >= c.minPin && pin <= c.maxPin);
147
+ if (c) return { label: c.nodelabel, bit: pin - c.minPin };
148
+ return undefined;
149
+ };
150
+ const keys = chip.gpio.dtSpecs.filter((s) => s.dtSpec.startsWith('sw') || s.dtSpec.startsWith('button'));
151
+ const leds = chip.gpio.dtSpecs.filter((s) => s.dtSpec.startsWith('led'));
152
+ if (keys.length > 0 || leds.length > 0) {
153
+ lines.push('/ {');
154
+ if (leds.length > 0) {
155
+ lines.push(' leds {', ' compatible = "gpio-leds";');
156
+ for (const l of leds) {
157
+ const c = controllerFor(l.pin);
158
+ if (!c) continue;
159
+ lines.push(
160
+ ` ${l.dtSpec}_node: ${l.dtSpec.replace(/_/g, '-')} {`,
161
+ ` gpios = <&${c.label} ${c.bit} GPIO_ACTIVE_LOW>;`,
162
+ ' };',
163
+ );
164
+ }
165
+ lines.push(' };');
166
+ }
167
+ if (keys.length > 0) {
168
+ lines.push(' gpio_keys {', ' compatible = "gpio-keys";');
169
+ for (const k of keys) {
170
+ const c = controllerFor(k.pin);
171
+ if (!c) continue;
172
+ lines.push(
173
+ ` ${k.dtSpec}_node: ${k.dtSpec.replace(/_/g, '-')} {`,
174
+ ` gpios = <&${c.label} ${c.bit} (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;`,
175
+ ' };',
176
+ );
177
+ }
178
+ lines.push(' };');
179
+ lines.push(' aliases {');
180
+ for (const k of keys) lines.push(` ${k.dtSpec} = &${k.dtSpec}_node;`);
181
+ for (const l of leds) lines.push(` ${l.dtSpec} = &${l.dtSpec}_node;`);
182
+ lines.push(' };');
183
+ }
184
+ lines.push('};', '');
185
+ }
186
+
187
+ lines.push(
188
+ // Console UART — the one controller the board enables unconditionally;
189
+ // every other bus is enabled by the usage-driven overlay.
190
+ `&${cb.console.nodeLabel} {`,
191
+ ` pinctrl-0 = <&${cb.console.tx} &${cb.console.rx}>;`,
192
+ ' pinctrl-names = "default";',
193
+ ' status = "okay";',
194
+ ` current-speed = <${cb.console.speed}>;`,
195
+ '};',
196
+ '',
197
+ );
198
+
199
+ // USB: alias the silicon node under the Zephyr convention the descriptor's
200
+ // usb.controller names (e.g. zephyr_udc0: &usbotg_fs), pre-enabled with its
201
+ // pinctrl — the st,stm32-otgfs binding requires pinctrl-0 on an enabled
202
+ // node, and the overlay's CDC composition only flips status/enables the
203
+ // stack (Kconfig-gated), never pins. Mirrors the blackpill board shape.
204
+ if (cb.usbNode && chip.usb) {
205
+ lines.push(
206
+ `${chip.usb.controller}: &${cb.usbNode} {`,
207
+ ...(cb.usbPinctrl && cb.usbPinctrl.length > 0
208
+ ? [
209
+ ` pinctrl-0 = <${cb.usbPinctrl.map((p) => `&${p}`).join(' ')}>;`,
210
+ ' pinctrl-names = "default";',
211
+ ]
212
+ : []),
213
+ ' status = "okay";',
214
+ '};',
215
+ '',
216
+ );
217
+ }
218
+
219
+ // PWM: the overlay enables `&pwm<N>` and synthesizes pwm-leds consumers
220
+ // against it — but the pwm<N> LABEL only exists when a board DTS declares
221
+ // it (the SoC dtsi ships the timers' pwm child unlabeled), and the
222
+ // st,stm32-pwm binding requires pinctrl-0 on an enabled node. Emit the
223
+ // blackpill-shape block per controller whose specs all carry pinctrl
224
+ // tokens; controllers without full token coverage stay to board packages.
225
+ if (chip.pwm) {
226
+ const byController = new Map<string, { n: string; tokens: string[]; complete: boolean }>();
227
+ for (const s of chip.pwm.specs) {
228
+ if (!s.controller) continue;
229
+ const timersMatch = s.controller.match(/^pwm(\d+)$/);
230
+ if (!timersMatch) continue;
231
+ const entry = byController.get(s.controller) ?? { n: timersMatch[1], tokens: [], complete: true };
232
+ if (s.pinctrl) {
233
+ entry.tokens.push(s.pinctrl);
234
+ } else {
235
+ entry.complete = false;
236
+ }
237
+ byController.set(s.controller, entry);
238
+ }
239
+ for (const [controller, entry] of byController) {
240
+ if (!entry.complete || entry.tokens.length === 0) continue;
241
+ lines.push(
242
+ `&timers${entry.n} {`,
243
+ ' status = "okay";',
244
+ '',
245
+ ` ${controller}: pwm {`,
246
+ ` pinctrl-0 = <${entry.tokens.map((t) => `&${t}`).join(' ')}>;`,
247
+ ' pinctrl-names = "default";',
248
+ ' status = "okay";',
249
+ ' };',
250
+ '};',
251
+ '',
252
+ );
253
+ }
254
+ }
255
+
256
+ // ADC: some SoC drivers require board-DTS properties on an enabled adc node
257
+ // (STM32 F4: st,adc-clock-source + st,adc-prescaler, or the binding rejects
258
+ // the node when the usage-driven overlay enables it). Pre-enable with the
259
+ // silicon-required props + a baseline pinctrl; the overlay rewrites
260
+ // pinctrl-0 to the channels the program actually reads.
261
+ if (cb.adcNode) {
262
+ const a = cb.adcNode;
263
+ lines.push(
264
+ `&${a.nodeLabel} {`,
265
+ ` pinctrl-0 = <&${a.pinctrl}>;`,
266
+ ' pinctrl-names = "default";',
267
+ ` st,adc-clock-source = "${a.clockSource}";`,
268
+ ` st,adc-prescaler = <${a.prescaler}>;`,
269
+ ' status = "okay";',
270
+ '};',
271
+ '',
272
+ );
273
+ }
274
+
275
+ // Clock plan — STM32 F4 shape today (the only family carrying a clocks
276
+ // block so far): HSE crystal, PLL, RCC prescalers. SoCs whose dtsi clocks
277
+ // work out of the box ship no clocks block and emit nothing here.
278
+ const clk = cb.clocks;
279
+ lines.push(
280
+ `&clk_hse {`,
281
+ ` clock-frequency = <DT_FREQ_M(${clk.hseMHz})>;`,
282
+ ' status = "okay";',
283
+ '};',
284
+ '',
285
+ '&pll {',
286
+ ` div-m = <${clk.pll.divM}>;`,
287
+ ` mul-n = <${clk.pll.mulN}>;`,
288
+ ` div-p = <${clk.pll.divP}>;`,
289
+ ` div-q = <${clk.pll.divQ}>;`,
290
+ ' clocks = <&clk_hse>;',
291
+ ' status = "okay";',
292
+ '};',
293
+ '',
294
+ '&rcc {',
295
+ ' clocks = <&pll>;',
296
+ ` clock-frequency = <DT_FREQ_M(${clk.sysMHz})>;`,
297
+ ` ahb-prescaler = <${clk.ahbPrescaler}>;`,
298
+ ` apb1-prescaler = <${clk.apb1Prescaler}>;`,
299
+ ` apb2-prescaler = <${clk.apb2Prescaler}>;`,
300
+ '};',
301
+ '',
302
+ );
303
+
304
+ changed = writeIfChanged(join(dir, `${name}.dts`), lines.join('\n')) || changed;
305
+
306
+ // ── <name>_defconfig — the baseline every in-tree board carries ──────────
307
+ // Load-bearing: without CONFIG_GPIO the STM32 pinctrl driver's gpio_ports
308
+ // table references devices that are never built (undefined __device_dts_ord
309
+ // at link). Console/serial match what the DTS enables.
310
+ changed =
311
+ writeIfChanged(
312
+ join(dir, `${name}_defconfig`),
313
+ [
314
+ '# Auto-generated by @typecad/framework-zephyr. Do not edit.',
315
+ '',
316
+ 'CONFIG_GPIO=y',
317
+ 'CONFIG_SERIAL=y',
318
+ 'CONFIG_CONSOLE=y',
319
+ 'CONFIG_UART_CONSOLE=y',
320
+ '',
321
+ ].join('\n'),
322
+ ) || changed;
323
+
324
+ return { dir, name, changed };
325
+ }