@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
@@ -1,296 +1,296 @@
1
- // ---------------------------------------------------------------------------
2
- // Zephyr project scaffolding
3
- //
4
- // Writes the CMakeLists.txt + prj.conf around the cuttlefish-emitted src/main.cpp
5
- // so `west build` has a valid Zephyr application. Idempotent: overwrites the
6
- // generated files only when their content changes (avoids invalidating the
7
- // Ninja incremental build's mtime-based dependency tracking).
8
- // ---------------------------------------------------------------------------
9
-
10
- import { writeFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
11
- import { join } from 'node:path';
12
- import { resolveKconfigFragments, type KconfigUsage } from '../dt-config/kconfig.js';
13
- import { scanSensorParts } from './index.js';
14
- import { readCuttlefishLibrarySidecar } from '@typecad/cuttlefish/library-packages';
15
- import { SENSOR_PART_INFO } from '@typecad/hal';
16
-
17
- /** Write a file only if the content differs from the existing file.
18
- * Returns true when the file was written (content changed or file was new). */
19
- export function writeIfChanged(filePath: string, content: string): boolean {
20
- if (existsSync(filePath)) {
21
- try {
22
- if (readFileSync(filePath, 'utf8') === content) return false;
23
- } catch {
24
- // Read failed — fall through to write.
25
- }
26
- }
27
- writeFileSync(filePath, content);
28
- return true;
29
- }
30
-
31
- /**
32
- * Names of the cuttlefish-emitted C/C++ sources under src/ (top level only,
33
- * matching the old `src/*.cpp src/*.c` glob; sorted so the generated
34
- * CMakeLists.txt is stable across readdir orderings). Empty when no sources
35
- * exist yet (first prepare call).
36
- */
37
- function listEmittedSources(srcDir: string): string[] {
38
- if (!existsSync(srcDir)) return [];
39
- const names = readdirSync(srcDir).filter((name) => name.endsWith('.cpp') || name.endsWith('.c'));
40
- names.sort();
41
- return names;
42
- }
43
-
44
- /**
45
- * Concatenate all emitted source under src/ so the scaffold can detect which
46
- * peripherals the program actually uses. The cuttlefish lowering emits
47
- * well-known driver API tokens (adc_read, spi_transceive, bt_*, …), so scanning
48
- * the post-transpile source is an authoritative usage signal — and it keeps
49
- * the scaffold self-contained (no need to thread analysis through the toolchain
50
- * contract). Returns '' when no sources exist yet (first prepare call).
51
- */
52
- function readEmittedSources(srcDir: string): string {
53
- let out = '';
54
- for (const name of listEmittedSources(srcDir)) {
55
- try {
56
- out += readFileSync(join(srcDir, name), 'utf8');
57
- } catch {
58
- // ignore unreadable files
59
- }
60
- }
61
- return out;
62
- }
63
-
64
- /**
65
- * Append cuttlefish library packages' devicetree overlay fragments to the
66
- * generated overlay. Library entries come from the transpiler's libraries.json
67
- * sidecar (next to the emitted sources) and are already gated on the
68
- * library's include token appearing in the emitted sources — no re-detection.
69
- * Fragments merge after the framework overlay so library nodes (e.g.
70
- * @typecad/zephyr-esp32s3-rgb's WS2812 node on I2S0) layer over it.
71
- */
72
- export function appendLibraryOverlayFragments(overlay: string, projectRoot: string): string {
73
- let out = overlay;
74
- for (const entry of readCuttlefishLibrarySidecar(join(projectRoot, 'src'))) {
75
- if (!entry.overlay) continue;
76
- try {
77
- const fragment = readFileSync(entry.overlay, 'utf8').trim();
78
- if (fragment.length > 0) {
79
- out += (out.endsWith('\n') ? '' : '\n') + '\n' + fragment + '\n';
80
- }
81
- } catch {
82
- // best-effort; a missing fragment surfaces as a DT error
83
- }
84
- }
85
- return out;
86
- }
87
-
88
- /**
89
- * Emit the Zephyr application skeleton around the generated src/main.cpp.
90
- *
91
- * Layout written:
92
- * <projectRoot>/
93
- * CMakeLists.txt (find_package(Zephyr), target_sources app ← src/*.cpp)
94
- * prj.conf (CONFIG_* for GPIO + C++ + libc)
95
- * src/main.cpp (owned by cuttlefish's emit pipeline — NOT touched here)
96
- *
97
- * Idempotent. Mirrors scaffoldEspIdfProject's writeIfChanged discipline.
98
- */
99
- export function scaffoldZephyrProject(projectRoot: string, debug = false, userKconfig?: Record<string, string>, psram?: 'opi' | 'quad'): boolean {
100
- const srcDir = join(projectRoot, 'src');
101
- if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
102
-
103
- // Detect which peripherals the program actually uses by scanning the emitted
104
- // source. The cuttlefish lowering emits well-known driver API tokens, so this
105
- // is authoritative. Usage-gating the Kconfig symbols keeps a GPIO-only
106
- // program from pulling in (and linking) stacks it doesn't need — notably
107
- // NimBLE (CONFIG_BT), whose Espressif prebuilt blobs are not always present
108
- // in a workspace. The symbol set itself lives in resolveKconfigFragments
109
- // (dt-config/kconfig.ts), unit-tested separately.
110
- const src = readEmittedSources(srcDir);
111
- // Boundary-anchored token scan: a bare `src.includes('power_')` would match
112
- // `tx_power_dbm` (emitted by the WiFi shim) and flip CONFIG_PM on for a
113
- // WiFi-only program — on the ESP32-S3 that spins the PM soft-off retry loop
114
- // forever and starves the app. Anchor each `<prefix>_` token at a leading
115
- // word boundary so it matches the intended driver/HAL symbol, not a suffix.
116
- const uses = (token: string): boolean => {
117
- if (token.endsWith('_')) {
118
- return new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`).test(src);
119
- }
120
- return src.includes(token);
121
- };
122
- const usage: KconfigUsage = {
123
- usesAdc: uses('adc_'),
124
- usesPwm: uses('pwm_'),
125
- usesDac: uses('dac_') || uses('__tc_dac'),
126
- usesFS: uses('__tc_fs'),
127
- usesHwtimer: uses('counter_') || uses('__tc_hw'),
128
- // The __tc_<bus> alternative matches the shim's state block — a program
129
- // that only calls begin() emits `(void)__tc_i2c1_dev;` (no driver API
130
- // call yet), but the shim still declared DEVICE_DT_GET(DT_NODELABEL(...))
131
- // so the overlay must enable the node or the device symbol is missing.
132
- usesI2c: uses('i2c_') || uses('__tc_i2c'),
133
- // DT-bound sensor parts: the __tc_sensor_* state references and the
134
- // sensor_sample_fetch/sensor_channel_get calls both carry the token.
135
- usesSensor: uses('sensor_') || uses('__tc_sensor'),
136
- usesFloatFormat: /%[-0-9.]*[eEfFgG]/.test(src),
137
- usesSpi: uses('spi_') || uses('__tc_spi'),
138
- usesUart: uses('uart_') || uses('__tc_uart'),
139
- // USB CDC serial: every usb.* lowering calls into the __tc_usb<N>_* shim
140
- // (device + init helper emitted under usesUsb).
141
- usesUsb: uses('__tc_usb'),
142
- // STM32F4 DBGMCU keep-SWD-alive init present (emitted for stm32f4 socs).
143
- usesStm32DebugSleep: uses('__tc_stm32_dbgmcu'),
144
- usesWdt: uses('wdt_'),
145
- usesBle: uses('bt_') || uses('bt_gatt') || uses('bt_le_'),
146
- usesDisplay: uses('display_write') || uses('display_init') || uses('display_fill_rect') || uses('__tc_display_dev') || uses('CuttlefishDisplayTarget'),
147
- usesTouch: uses('ft6336u') || uses('touch_'),
148
- // (The power HAL is removed; its former pm_/k_sleep token scan is gone —
149
- // nothing here must match tx_power_dbm's `power` substring.)
150
- usesWifi: uses('wifi_') || uses('net_mgmt') || uses('conn_mgr'),
151
- // HTTP: the __tc_http_* shim + http_client_req + getaddrinfo. The '_'-anchored
152
- // 'http_' token matches __tc_http_* and http_client_req (the core HTTP lib
153
- // symbol), mirroring how wifi_ detects the wifi shim. An http-only program
154
- // still pulls the networking stack even without usesWifi.
155
- usesHttp: uses('http_') || uses('__tc_http') || uses('http_client_req'),
156
- // MQTT: the __tc_mqtt_* shim + mqtt_connect/mqtt_publish/mqtt_subscribe.
157
- usesMqtt: uses('mqtt_') || uses('__tc_mqtt') || uses('mqtt_connect'),
158
- // Preferences: the __tc_prefs_* shim + the settings_* API the shim calls.
159
- // settings_load/save_one/delete + SETTINGS_STATIC_HANDLER_DEFINE all emit
160
- // `settings_` symbols; __tc_prefs catches the typed accessors (put_int etc.
161
- // template into __tc_prefs_put<...>, which keeps the __tc_prefs token).
162
- usesPreferences: uses('settings_') || uses('__tc_prefs'),
163
- // Random: the __tc_rand_* shim + the sys_rand_get entropy tap it seeds from.
164
- usesRandom: uses('__tc_rand') || uses('sys_rand_get'),
165
- psram,
166
- };
167
-
168
- let changed = false;
169
-
170
- // ── Root CMakeLists.txt ─────────────────────────────────────────────────
171
- // The canonical Zephyr CMake application. The emitted source list is
172
- // explicit (no file(GLOB CONFIGURE_DEPENDS ...)): CONFIGURE_DEPENDS puts a
173
- // cmake.verify_globs step in the ninja graph that spawns CMake to re-check
174
- // the glob on every build, and the scaffold already rewrites this file via
175
- // writeIfChanged whenever the emitted file set changes — which flips
176
- // configChanged and reconfigures with the new list baked in.
177
- const sourceFiles = listEmittedSources(srcDir);
178
- const cmakeLists = [
179
- '# Auto-generated by @typecad/framework-zephyr from cuttlefish.config.ts.',
180
- 'cmake_minimum_required(VERSION 3.20.0)',
181
- '',
182
- '# The app is its own board root: MCU-only targets generate an out-of-tree',
183
- '# custom board under boards/typecad/<name>/ that Zephyr must discover.',
184
- '# Harmless when boards/ holds only .overlay files (BOARD_ROOT just needs',
185
- '# the directory to exist).',
186
- 'list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})',
187
- '',
188
- 'find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})',
189
- '',
190
- 'project(zephyr_app)',
191
- '',
192
- ...(sourceFiles.length > 0
193
- ? [
194
- '# Cuttlefish-emitted sources. This list is regenerated whenever the',
195
- '# emitted file set changes (the scaffold rewrites CMakeLists.txt).',
196
- 'target_sources(app PRIVATE',
197
- ...sourceFiles.map((name) => ` src/${name}`),
198
- ')',
199
- ]
200
- : [
201
- '# No emitted sources yet — the scaffold regenerates this list on the',
202
- '# next compile once src/ contains .cpp/.c files.',
203
- ]),
204
- // When PSRAM is configured, define BOARD_HAS_PSRAM so the UI runtime's
205
- // PSRAM canvas allocator (ui_create_canvas_best) is compiled in.
206
- ...(psram ? ['', '# PSRAM enabled: activate the runtime PSRAM canvas paths.', 'target_compile_definitions(app PRIVATE BOARD_HAS_PSRAM)', ''] : ['']),
207
- ].join('\n');
208
- if (writeIfChanged(join(projectRoot, 'CMakeLists.txt'), cmakeLists)) changed = true;
209
-
210
- // ── prj.conf ────────────────────────────────────────────────────────────
211
- // Driver Kconfig symbols are usage-gated on the emitted source: only a
212
- // peripheral whose driver API the program actually references is enabled.
213
- // Zephyr's Kconfig treats an enabled-but-unused driver as harmless, BUT
214
- // several driver stacks pull in large/specific dependencies — notably
215
- // NimBLE (CONFIG_BT) needs Espressif prebuilt blobs that are not always
216
- // fetched (`west blobs fetch hal_espressif`). Usage-gating keeps a GPIO-only
217
- // blink from requiring those, and shrinks the link for every program. The
218
- // core GPIO driver and C++ support stay unconditional. Symbol selection is
219
- // delegated to resolveKconfigFragments (unit-tested in dt-config/kconfig).
220
- const symbols = resolveKconfigFragments(usage, debug);
221
-
222
- const prjConf: string[] = [
223
- '# Auto-generated by @typecad/framework-zephyr from cuttlefish.config.ts.',
224
- '# Edit in cuttlefish.config.ts (frameworkData), not here.',
225
- '# Driver symbols are usage-gated on the emitted source — only peripherals',
226
- '# the program references are enabled.',
227
- '',
228
- '# Route printf/stdout to the console UART (needed by the @typecad/expect',
229
- '# test runner protocol, which uses printf via __tc_print/__tc_println).',
230
- 'CONFIG_STDOUT_CONSOLE=y',
231
- 'CONFIG_PRINTK=y',
232
- '',
233
- ];
234
- // Emit a section header before the BT block when present.
235
- let btHeaderEmitted = false;
236
- let wifiHeaderEmitted = false;
237
- for (const [sym, val] of symbols) {
238
- if (sym === 'CONFIG_BT' && !btHeaderEmitted) {
239
- prjConf.push('', '# Bluetooth (NimBLE peripheral).');
240
- btHeaderEmitted = true;
241
- }
242
- if (sym === 'CONFIG_WIFI' && !wifiHeaderEmitted) {
243
- prjConf.push('', '# WiFi / networking (conn_mgr + esp32 wifi driver).');
244
- wifiHeaderEmitted = true;
245
- }
246
- // Skip auto-detected symbols that the user explicitly overrides
247
- // in cuttlefish.config.ts zephyr.kconfig — the user value is
248
- // emitted in the User Kconfig section below and takes precedence.
249
- if (userKconfig && userKconfig.hasOwnProperty(sym)) continue;
250
- prjConf.push(`${sym}=${val}`);
251
- }
252
- // ── Cuttlefish library packages ─────────────────────────────────────────
253
- // Libraries the program imports (recorded in the transpiler's libraries.json
254
- // sidecar, next to the emitted sources) contribute their manifest's kconfig
255
- // lines — e.g. @typecad/zephyr-esp32s3-rgb contributes CONFIG_LED_STRIP.
256
- // Sidecar entries are already gated on the library's include token
257
- // appearing in the emitted sources, so no re-detection here. User
258
- // zephyr.kconfig overrides still win.
259
- const libraryEntries = readCuttlefishLibrarySidecar(srcDir);
260
- if (libraryEntries.length > 0) {
261
- prjConf.push('', '# Library packages (cuttlefish.library.json contributions).');
262
- for (const entry of libraryEntries) {
263
- for (const line of entry.kconfig) {
264
- const sym = line.split('=')[0];
265
- if (userKconfig && sym !== undefined && userKconfig.hasOwnProperty(sym)) continue;
266
- prjConf.push(line);
267
- }
268
- }
269
- }
270
- // Per-part Kconfig exceptions: catalog parts whose driver is NOT default-y
271
- // on its DT node carry extra lines here (empty today — every in-tree sensor
272
- // driver lights up from the node; the path exists so the first exception
273
- // found by the generator has somewhere to go).
274
- const sensorParts = scanSensorParts(src);
275
- if (sensorParts.length > 0) {
276
- const extra = [...new Set(sensorParts.flatMap((sp) => SENSOR_PART_INFO[sp.part]?.kconfig ?? []))];
277
- if (extra.length > 0) {
278
- prjConf.push('', '# Sensor part Kconfig (catalog exceptions).');
279
- for (const line of extra) prjConf.push(line);
280
- }
281
- }
282
- // Emit user-specified Kconfig from cuttlefish.config.ts zephyr.kconfig.
283
- // These override any matching auto-detected symbol (skipped above).
284
- if (userKconfig) {
285
- prjConf.push('', '# User Kconfig (cuttlefish.config.ts → zephyr.kconfig).');
286
- for (const [sym, val] of Object.entries(userKconfig)) {
287
- prjConf.push(`${sym}=${val}`);
288
- }
289
- }
290
- // CONFIG_BT_DEVICE_NAME is a string value not produced by the resolver — add
291
- // it after the BT block when BLE is used (parity with the previous inline form).
292
- if (usage.usesBle) prjConf.push('CONFIG_BT_DEVICE_NAME="TypeCAD"');
293
- prjConf.push('');
294
- if (writeIfChanged(join(projectRoot, 'prj.conf'), prjConf.join('\n'))) changed = true;
295
- return changed;
296
- }
1
+ // ---------------------------------------------------------------------------
2
+ // Zephyr project scaffolding
3
+ //
4
+ // Writes the CMakeLists.txt + prj.conf around the typecad-hal-emitted src/main.cpp
5
+ // so `west build` has a valid Zephyr application. Idempotent: overwrites the
6
+ // generated files only when their content changes (avoids invalidating the
7
+ // Ninja incremental build's mtime-based dependency tracking).
8
+ // ---------------------------------------------------------------------------
9
+
10
+ import { writeFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { resolveKconfigFragments, type KconfigUsage } from '../dt-config/kconfig.js';
13
+ import { scanSensorParts } from './index.js';
14
+ import { readCuttlefishLibrarySidecar } from '@typecad/cuttlefish/library-packages';
15
+ import { SENSOR_PART_INFO } from '@typecad/hal';
16
+
17
+ /** Write a file only if the content differs from the existing file.
18
+ * Returns true when the file was written (content changed or file was new). */
19
+ export function writeIfChanged(filePath: string, content: string): boolean {
20
+ if (existsSync(filePath)) {
21
+ try {
22
+ if (readFileSync(filePath, 'utf8') === content) return false;
23
+ } catch {
24
+ // Read failed — fall through to write.
25
+ }
26
+ }
27
+ writeFileSync(filePath, content);
28
+ return true;
29
+ }
30
+
31
+ /**
32
+ * Names of the typecad-hal-emitted C/C++ sources under src/ (top level only,
33
+ * matching the old `src/*.cpp src/*.c` glob; sorted so the generated
34
+ * CMakeLists.txt is stable across readdir orderings). Empty when no sources
35
+ * exist yet (first prepare call).
36
+ */
37
+ function listEmittedSources(srcDir: string): string[] {
38
+ if (!existsSync(srcDir)) return [];
39
+ const names = readdirSync(srcDir).filter((name) => name.endsWith('.cpp') || name.endsWith('.c'));
40
+ names.sort();
41
+ return names;
42
+ }
43
+
44
+ /**
45
+ * Concatenate all emitted source under src/ so the scaffold can detect which
46
+ * peripherals the program actually uses. The cuttlefish lowering emits
47
+ * well-known driver API tokens (adc_read, spi_transceive, bt_*, …), so scanning
48
+ * the post-transpile source is an authoritative usage signal — and it keeps
49
+ * the scaffold self-contained (no need to thread analysis through the toolchain
50
+ * contract). Returns '' when no sources exist yet (first prepare call).
51
+ */
52
+ function readEmittedSources(srcDir: string): string {
53
+ let out = '';
54
+ for (const name of listEmittedSources(srcDir)) {
55
+ try {
56
+ out += readFileSync(join(srcDir, name), 'utf8');
57
+ } catch {
58
+ // ignore unreadable files
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+
64
+ /**
65
+ * Append typecad-hal library packages' devicetree overlay fragments to the
66
+ * generated overlay. Library entries come from the transpiler's libraries.json
67
+ * sidecar (next to the emitted sources) and are already gated on the
68
+ * library's include token appearing in the emitted sources — no re-detection.
69
+ * Fragments merge after the framework overlay so library nodes (e.g.
70
+ * @typecad/zephyr-esp32s3-rgb's WS2812 node on I2S0) layer over it.
71
+ */
72
+ export function appendLibraryOverlayFragments(overlay: string, projectRoot: string): string {
73
+ let out = overlay;
74
+ for (const entry of readCuttlefishLibrarySidecar(join(projectRoot, 'src'))) {
75
+ if (!entry.overlay) continue;
76
+ try {
77
+ const fragment = readFileSync(entry.overlay, 'utf8').trim();
78
+ if (fragment.length > 0) {
79
+ out += (out.endsWith('\n') ? '' : '\n') + '\n' + fragment + '\n';
80
+ }
81
+ } catch {
82
+ // best-effort; a missing fragment surfaces as a DT error
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+
88
+ /**
89
+ * Emit the Zephyr application skeleton around the generated src/main.cpp.
90
+ *
91
+ * Layout written:
92
+ * <projectRoot>/
93
+ * CMakeLists.txt (find_package(Zephyr), target_sources app ← src/*.cpp)
94
+ * prj.conf (CONFIG_* for GPIO + C++ + libc)
95
+ * src/main.cpp (owned by cuttlefish's emit pipeline — NOT touched here)
96
+ *
97
+ * Idempotent. Mirrors scaffoldEspIdfProject's writeIfChanged discipline.
98
+ */
99
+ export function scaffoldZephyrProject(projectRoot: string, debug = false, userKconfig?: Record<string, string>, psram?: 'opi' | 'quad'): boolean {
100
+ const srcDir = join(projectRoot, 'src');
101
+ if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
102
+
103
+ // Detect which peripherals the program actually uses by scanning the emitted
104
+ // source. The cuttlefish lowering emits well-known driver API tokens, so this
105
+ // is authoritative. Usage-gating the Kconfig symbols keeps a GPIO-only
106
+ // program from pulling in (and linking) stacks it doesn't need — notably
107
+ // NimBLE (CONFIG_BT), whose Espressif prebuilt blobs are not always present
108
+ // in a workspace. The symbol set itself lives in resolveKconfigFragments
109
+ // (dt-config/kconfig.ts), unit-tested separately.
110
+ const src = readEmittedSources(srcDir);
111
+ // Boundary-anchored token scan: a bare `src.includes('power_')` would match
112
+ // `tx_power_dbm` (emitted by the WiFi shim) and flip CONFIG_PM on for a
113
+ // WiFi-only program — on the ESP32-S3 that spins the PM soft-off retry loop
114
+ // forever and starves the app. Anchor each `<prefix>_` token at a leading
115
+ // word boundary so it matches the intended driver/HAL symbol, not a suffix.
116
+ const uses = (token: string): boolean => {
117
+ if (token.endsWith('_')) {
118
+ return new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`).test(src);
119
+ }
120
+ return src.includes(token);
121
+ };
122
+ const usage: KconfigUsage = {
123
+ usesAdc: uses('adc_'),
124
+ usesPwm: uses('pwm_'),
125
+ usesDac: uses('dac_') || uses('__tc_dac'),
126
+ usesFS: uses('__tc_fs'),
127
+ usesHwtimer: uses('counter_') || uses('__tc_hw'),
128
+ // The __tc_<bus> alternative matches the shim's state block — a program
129
+ // that only calls begin() emits `(void)__tc_i2c1_dev;` (no driver API
130
+ // call yet), but the shim still declared DEVICE_DT_GET(DT_NODELABEL(...))
131
+ // so the overlay must enable the node or the device symbol is missing.
132
+ usesI2c: uses('i2c_') || uses('__tc_i2c'),
133
+ // DT-bound sensor parts: the __tc_sensor_* state references and the
134
+ // sensor_sample_fetch/sensor_channel_get calls both carry the token.
135
+ usesSensor: uses('sensor_') || uses('__tc_sensor'),
136
+ usesFloatFormat: /%[-0-9.]*[eEfFgG]/.test(src),
137
+ usesSpi: uses('spi_') || uses('__tc_spi'),
138
+ usesUart: uses('uart_') || uses('__tc_uart'),
139
+ // USB CDC serial: every usb.* lowering calls into the __tc_usb<N>_* shim
140
+ // (device + init helper emitted under usesUsb).
141
+ usesUsb: uses('__tc_usb'),
142
+ // STM32F4 DBGMCU keep-SWD-alive init present (emitted for stm32f4 socs).
143
+ usesStm32DebugSleep: uses('__tc_stm32_dbgmcu'),
144
+ usesWdt: uses('wdt_'),
145
+ usesBle: uses('bt_') || uses('bt_gatt') || uses('bt_le_'),
146
+ usesDisplay: uses('display_write') || uses('display_init') || uses('display_fill_rect') || uses('__tc_display_dev') || uses('CuttlefishDisplayTarget'),
147
+ usesTouch: uses('ft6336u') || uses('touch_'),
148
+ // (The power HAL is removed; its former pm_/k_sleep token scan is gone —
149
+ // nothing here must match tx_power_dbm's `power` substring.)
150
+ usesWifi: uses('wifi_') || uses('net_mgmt') || uses('conn_mgr'),
151
+ // HTTP: the __tc_http_* shim + http_client_req + getaddrinfo. The '_'-anchored
152
+ // 'http_' token matches __tc_http_* and http_client_req (the core HTTP lib
153
+ // symbol), mirroring how wifi_ detects the wifi shim. An http-only program
154
+ // still pulls the networking stack even without usesWifi.
155
+ usesHttp: uses('http_') || uses('__tc_http') || uses('http_client_req'),
156
+ // MQTT: the __tc_mqtt_* shim + mqtt_connect/mqtt_publish/mqtt_subscribe.
157
+ usesMqtt: uses('mqtt_') || uses('__tc_mqtt') || uses('mqtt_connect'),
158
+ // Preferences: the __tc_prefs_* shim + the settings_* API the shim calls.
159
+ // settings_load/save_one/delete + SETTINGS_STATIC_HANDLER_DEFINE all emit
160
+ // `settings_` symbols; __tc_prefs catches the typed accessors (put_int etc.
161
+ // template into __tc_prefs_put<...>, which keeps the __tc_prefs token).
162
+ usesPreferences: uses('settings_') || uses('__tc_prefs'),
163
+ // Random: the __tc_rand_* shim + the sys_rand_get entropy tap it seeds from.
164
+ usesRandom: uses('__tc_rand') || uses('sys_rand_get'),
165
+ psram,
166
+ };
167
+
168
+ let changed = false;
169
+
170
+ // ── Root CMakeLists.txt ─────────────────────────────────────────────────
171
+ // The canonical Zephyr CMake application. The emitted source list is
172
+ // explicit (no file(GLOB CONFIGURE_DEPENDS ...)): CONFIGURE_DEPENDS puts a
173
+ // cmake.verify_globs step in the ninja graph that spawns CMake to re-check
174
+ // the glob on every build, and the scaffold already rewrites this file via
175
+ // writeIfChanged whenever the emitted file set changes — which flips
176
+ // configChanged and reconfigures with the new list baked in.
177
+ const sourceFiles = listEmittedSources(srcDir);
178
+ const cmakeLists = [
179
+ '# Auto-generated by @typecad/framework-zephyr from typecad-hal.config.ts.',
180
+ 'cmake_minimum_required(VERSION 3.20.0)',
181
+ '',
182
+ '# The app is its own board root: MCU-only targets generate an out-of-tree',
183
+ '# custom board under boards/typecad/<name>/ that Zephyr must discover.',
184
+ '# Harmless when boards/ holds only .overlay files (BOARD_ROOT just needs',
185
+ '# the directory to exist).',
186
+ 'list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})',
187
+ '',
188
+ 'find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})',
189
+ '',
190
+ 'project(zephyr_app)',
191
+ '',
192
+ ...(sourceFiles.length > 0
193
+ ? [
194
+ '# TypeCAD-emitted sources. This list is regenerated whenever the',
195
+ '# emitted file set changes (the scaffold rewrites CMakeLists.txt).',
196
+ 'target_sources(app PRIVATE',
197
+ ...sourceFiles.map((name) => ` src/${name}`),
198
+ ')',
199
+ ]
200
+ : [
201
+ '# No emitted sources yet — the scaffold regenerates this list on the',
202
+ '# next compile once src/ contains .cpp/.c files.',
203
+ ]),
204
+ // When PSRAM is configured, define BOARD_HAS_PSRAM so the UI runtime's
205
+ // PSRAM canvas allocator (ui_create_canvas_best) is compiled in.
206
+ ...(psram ? ['', '# PSRAM enabled: activate the runtime PSRAM canvas paths.', 'target_compile_definitions(app PRIVATE BOARD_HAS_PSRAM)', ''] : ['']),
207
+ ].join('\n');
208
+ if (writeIfChanged(join(projectRoot, 'CMakeLists.txt'), cmakeLists)) changed = true;
209
+
210
+ // ── prj.conf ────────────────────────────────────────────────────────────
211
+ // Driver Kconfig symbols are usage-gated on the emitted source: only a
212
+ // peripheral whose driver API the program actually references is enabled.
213
+ // Zephyr's Kconfig treats an enabled-but-unused driver as harmless, BUT
214
+ // several driver stacks pull in large/specific dependencies — notably
215
+ // NimBLE (CONFIG_BT) needs Espressif prebuilt blobs that are not always
216
+ // fetched (`west blobs fetch hal_espressif`). Usage-gating keeps a GPIO-only
217
+ // blink from requiring those, and shrinks the link for every program. The
218
+ // core GPIO driver and C++ support stay unconditional. Symbol selection is
219
+ // delegated to resolveKconfigFragments (unit-tested in dt-config/kconfig).
220
+ const symbols = resolveKconfigFragments(usage, debug);
221
+
222
+ const prjConf: string[] = [
223
+ '# Auto-generated by @typecad/framework-zephyr from typecad-hal.config.ts.',
224
+ '# Edit in typecad-hal.config.ts (frameworkData), not here.',
225
+ '# Driver symbols are usage-gated on the emitted source — only peripherals',
226
+ '# the program references are enabled.',
227
+ '',
228
+ '# Route printf/stdout to the console UART (needed by the typecad-hal',
229
+ '# test runner protocol, which uses printf via __tc_print/__tc_println).',
230
+ 'CONFIG_STDOUT_CONSOLE=y',
231
+ 'CONFIG_PRINTK=y',
232
+ '',
233
+ ];
234
+ // Emit a section header before the BT block when present.
235
+ let btHeaderEmitted = false;
236
+ let wifiHeaderEmitted = false;
237
+ for (const [sym, val] of symbols) {
238
+ if (sym === 'CONFIG_BT' && !btHeaderEmitted) {
239
+ prjConf.push('', '# Bluetooth (NimBLE peripheral).');
240
+ btHeaderEmitted = true;
241
+ }
242
+ if (sym === 'CONFIG_WIFI' && !wifiHeaderEmitted) {
243
+ prjConf.push('', '# WiFi / networking (conn_mgr + esp32 wifi driver).');
244
+ wifiHeaderEmitted = true;
245
+ }
246
+ // Skip auto-detected symbols that the user explicitly overrides
247
+ // in typecad-hal.config.ts zephyr.kconfig — the user value is
248
+ // emitted in the User Kconfig section below and takes precedence.
249
+ if (userKconfig && userKconfig.hasOwnProperty(sym)) continue;
250
+ prjConf.push(`${sym}=${val}`);
251
+ }
252
+ // ── Cuttlefish library packages ─────────────────────────────────────────
253
+ // Libraries the program imports (recorded in the transpiler's libraries.json
254
+ // sidecar, next to the emitted sources) contribute their manifest's kconfig
255
+ // lines — e.g. @typecad/zephyr-esp32s3-rgb contributes CONFIG_LED_STRIP.
256
+ // Sidecar entries are already gated on the library's include token
257
+ // appearing in the emitted sources, so no re-detection here. User
258
+ // zephyr.kconfig overrides still win.
259
+ const libraryEntries = readCuttlefishLibrarySidecar(srcDir);
260
+ if (libraryEntries.length > 0) {
261
+ prjConf.push('', '# Library packages (typecad-hal.library.json contributions).');
262
+ for (const entry of libraryEntries) {
263
+ for (const line of entry.kconfig) {
264
+ const sym = line.split('=')[0];
265
+ if (userKconfig && sym !== undefined && userKconfig.hasOwnProperty(sym)) continue;
266
+ prjConf.push(line);
267
+ }
268
+ }
269
+ }
270
+ // Per-part Kconfig exceptions: catalog parts whose driver is NOT default-y
271
+ // on its DT node carry extra lines here (empty today — every in-tree sensor
272
+ // driver lights up from the node; the path exists so the first exception
273
+ // found by the generator has somewhere to go).
274
+ const sensorParts = scanSensorParts(src);
275
+ if (sensorParts.length > 0) {
276
+ const extra = [...new Set(sensorParts.flatMap((sp) => SENSOR_PART_INFO[sp.part]?.kconfig ?? []))];
277
+ if (extra.length > 0) {
278
+ prjConf.push('', '# Sensor part Kconfig (catalog exceptions).');
279
+ for (const line of extra) prjConf.push(line);
280
+ }
281
+ }
282
+ // Emit user-specified Kconfig from typecad-hal.config.ts zephyr.kconfig.
283
+ // These override any matching auto-detected symbol (skipped above).
284
+ if (userKconfig) {
285
+ prjConf.push('', '# User Kconfig (typecad-hal.config.ts → zephyr.kconfig).');
286
+ for (const [sym, val] of Object.entries(userKconfig)) {
287
+ prjConf.push(`${sym}=${val}`);
288
+ }
289
+ }
290
+ // CONFIG_BT_DEVICE_NAME is a string value not produced by the resolver — add
291
+ // it after the BT block when BLE is used (parity with the previous inline form).
292
+ if (usage.usesBle) prjConf.push('CONFIG_BT_DEVICE_NAME="TypeCAD"');
293
+ prjConf.push('');
294
+ if (writeIfChanged(join(projectRoot, 'prj.conf'), prjConf.join('\n'))) changed = true;
295
+ return changed;
296
+ }