@typecad/framework-zephyr 1.0.0-alpha.10 → 1.0.0-alpha.12

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 (67) hide show
  1. package/dist/chips/esp32.js +12 -0
  2. package/dist/chips/types.d.ts +30 -0
  3. package/dist/chips/xiao-ble.js +6 -0
  4. package/dist/display/gfx.d.ts +12 -3
  5. package/dist/display/gfx.js +130 -17
  6. package/dist/display/profiles.d.ts +25 -0
  7. package/dist/display/profiles.js +30 -0
  8. package/dist/display/touch-adapter.d.ts +3 -4
  9. package/dist/display/touch-adapter.js +119 -16
  10. package/dist/display/ui-adapter.js +308 -139
  11. package/dist/doctor.d.ts +3 -3
  12. package/dist/doctor.js +56 -29
  13. package/dist/dt-config/kconfig.d.ts +8 -1
  14. package/dist/dt-config/kconfig.js +40 -5
  15. package/dist/dt-config/overlay.d.ts +18 -1
  16. package/dist/dt-config/overlay.js +124 -26
  17. package/dist/framework.manifest.d.ts +29 -28
  18. package/dist/framework.manifest.js +46 -17
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +5 -0
  21. package/dist/licenses.d.ts +59 -0
  22. package/dist/licenses.js +347 -0
  23. package/dist/lowering/ble.js +3 -1
  24. package/dist/lowering/dac.d.ts +15 -0
  25. package/dist/lowering/dac.js +69 -0
  26. package/dist/lowering/fs.d.ts +16 -0
  27. package/dist/lowering/fs.js +121 -0
  28. package/dist/lowering/hwtimer.d.ts +15 -0
  29. package/dist/lowering/hwtimer.js +84 -0
  30. package/dist/lowering/index.d.ts +4 -1
  31. package/dist/lowering/index.js +12 -3
  32. package/dist/strategy.d.ts +4 -0
  33. package/dist/strategy.js +275 -35
  34. package/dist/toolchain/compat.js +10 -1
  35. package/dist/toolchain/env-check.d.ts +93 -0
  36. package/dist/toolchain/env-check.js +190 -0
  37. package/dist/toolchain/index.js +39 -9
  38. package/dist/toolchain/scaffold.js +7 -2
  39. package/dist/toolchain/west-discover.d.ts +11 -3
  40. package/dist/toolchain/west-discover.js +84 -7
  41. package/dist/toolchain/west-spawn.js +15 -0
  42. package/package.json +4 -4
  43. package/src/chips/esp32.ts +12 -0
  44. package/src/chips/types.ts +29 -0
  45. package/src/chips/xiao-ble.ts +6 -0
  46. package/src/display/gfx.ts +135 -19
  47. package/src/display/profiles.ts +53 -0
  48. package/src/display/touch-adapter.ts +119 -15
  49. package/src/display/ui-adapter.ts +311 -139
  50. package/src/doctor.ts +77 -56
  51. package/src/dt-config/kconfig.ts +45 -6
  52. package/src/dt-config/overlay.ts +159 -29
  53. package/src/framework.manifest.ts +47 -17
  54. package/src/index.ts +6 -0
  55. package/src/licenses.ts +425 -0
  56. package/src/lowering/ble.ts +3 -1
  57. package/src/lowering/dac.ts +82 -0
  58. package/src/lowering/fs.ts +127 -0
  59. package/src/lowering/hwtimer.ts +101 -0
  60. package/src/lowering/index.ts +9 -2
  61. package/src/strategy.ts +271 -35
  62. package/src/toolchain/compat.ts +154 -145
  63. package/src/toolchain/env-check.ts +285 -0
  64. package/src/toolchain/index.ts +40 -9
  65. package/src/toolchain/scaffold.ts +7 -2
  66. package/src/toolchain/west-discover.ts +92 -9
  67. package/src/toolchain/west-spawn.ts +15 -0
@@ -0,0 +1,190 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Zephyr environment check — the shared detection behind `cuttlefish doctor`.
3
+ //
4
+ // Mirrors @typecad/arduino-cli's checkArduinoEnv(): gather the impure
5
+ // environment facts once (west presence + version, Zephyr version, board
6
+ // existence), then reduce them to a structured result the doctor (and, later,
7
+ // the build/test gates) can present uniformly. The check is side-effect-free
8
+ // and never throws — it never installs or mutates anything.
9
+ //
10
+ // Two parity checks vs. framework-arduino's doctor:
11
+ // 1. west (the Zephyr build tool) is discoverable + responsive — the direct
12
+ // analog of "arduino-cli is installed". discoverWest() already confirms
13
+ // responsiveness via `west --version`; we additionally capture the version
14
+ // string to report it.
15
+ // 2. the configured board target exists in the Zephyr checkout
16
+ // ($ZEPHYR_BASE/boards/) — the analog of "the required core is installed".
17
+ //
18
+ // The existing compat-range check (compat.ts) is folded in as a third check so
19
+ // the doctor reports everything through one entry point.
20
+ // ---------------------------------------------------------------------------
21
+ import { spawnSync } from 'node:child_process';
22
+ import { existsSync, readdirSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import { discoverWest, resetWestDiscoveryCache } from './west-discover.js';
25
+ import { westSpawn } from './west-spawn.js';
26
+ import { detectZephyrVersion, checkZephyrCompat, resolveBoardTarget, } from './compat.js';
27
+ // ---- west probe (impure; isolated + cached + overridable) ------------------
28
+ let cachedProbe;
29
+ /** Clear the west-probe cache (for tests). Also resets discovery cache. */
30
+ export function resetWestProbeCacheForTest() {
31
+ cachedProbe = undefined;
32
+ resetWestDiscoveryCache();
33
+ }
34
+ /**
35
+ * Gather west facts: discover a usable install, then run `west --version`
36
+ * through it to capture the version. Memoized for the process lifetime (west
37
+ * installs don't move). Never throws — returns westFound:false on any failure.
38
+ */
39
+ export function probeWestEnv() {
40
+ if (cachedProbe)
41
+ return cachedProbe;
42
+ const install = discoverWest();
43
+ const envBase = process.env.ZEPHYR_BASE || undefined;
44
+ if (!install) {
45
+ const data = {
46
+ westFound: false,
47
+ westVersion: undefined,
48
+ source: undefined,
49
+ zephyrBase: envBase,
50
+ };
51
+ cachedProbe = data;
52
+ return data;
53
+ }
54
+ // Run `west --version` through the discovered install to capture the version.
55
+ // discoverWest() already confirmed responsiveness, so a parse failure here is
56
+ // not "unresponsive" — it just means we couldn't read a version token.
57
+ let westVersion;
58
+ try {
59
+ const inv = westSpawn(['--version'], {
60
+ encoding: 'utf8',
61
+ timeout: 15_000,
62
+ windowsHide: true,
63
+ });
64
+ const r = spawnSync(inv.command, inv.args, inv.options);
65
+ if (r.status === 0) {
66
+ // inv.options is a generic SpawnSyncOptions (no encoding literal), so
67
+ // coerce stdout to a string before matching.
68
+ const out = typeof r.stdout === 'string' ? r.stdout : '';
69
+ const m = out.match(/v?(\d+\.\d+\.\d+)/);
70
+ westVersion = m ? m[1] : undefined;
71
+ }
72
+ }
73
+ catch {
74
+ // westSpawn throws only when discovery fails — but discovery already
75
+ // succeeded (install is non-null). Defensive: treat as no version read.
76
+ westVersion = undefined;
77
+ }
78
+ const data = {
79
+ westFound: true,
80
+ westVersion,
81
+ source: install.source,
82
+ zephyrBase: envBase ?? install.zephyrBase,
83
+ };
84
+ cachedProbe = data;
85
+ return data;
86
+ }
87
+ // ---- board existence (pure-ish fs probe) -----------------------------------
88
+ /**
89
+ * Does `boardId` exist as a board directory in the Zephyr checkout? Checks the
90
+ * HWMv2 vendor layout used by Zephyr 4.x: $ZEPHYR_BASE/boards/<vendor>/<boardId>.
91
+ * Returns true/false when determinable; undefined when the base is unknown or
92
+ * the boards/ tree can't be read (so callers never fail on an inconclusive
93
+ * lookup — they just skip the board check).
94
+ */
95
+ export function boardExistsInCheckout(boardId, zephyrBase) {
96
+ if (!zephyrBase)
97
+ return undefined;
98
+ const boards = join(zephyrBase, 'boards');
99
+ try {
100
+ const entries = readdirSync(boards, { withFileTypes: true });
101
+ for (const entry of entries) {
102
+ if (entry.isDirectory() && existsSync(join(boards, entry.name, boardId))) {
103
+ return true;
104
+ }
105
+ }
106
+ return false;
107
+ }
108
+ catch {
109
+ return undefined;
110
+ }
111
+ }
112
+ // ---- main entry point -------------------------------------------------------
113
+ /**
114
+ * Verify the environment can build for `buildTarget`. Cheap and
115
+ * side-effect-free: discovers west, reads the Zephyr version, checks the compat
116
+ * range, and — when a target is configured — verifies the board exists in the
117
+ * checkout. Reports what (if anything) is wrong.
118
+ *
119
+ * - If `buildTarget` is undefined/empty, the board check is skipped (not a
120
+ * failure), mirroring Arduino's no-FQBN path.
121
+ * - Never installs anything. Never mutates the user environment.
122
+ * - Never throws — always returns a result. Callers decide how to react.
123
+ *
124
+ * `options` is for-test only (injects fake probe data / board lookup).
125
+ */
126
+ export function checkZephyrEnv(buildTarget, options) {
127
+ const probe = options?.fakeWestProbe ?? probeWestEnv();
128
+ const boardLookup = options?.fakeBoardExists ?? boardExistsInCheckout;
129
+ const zephyrVersion = detectZephyrVersion();
130
+ const compat = checkZephyrCompat(zephyrVersion);
131
+ const resolvedBoardTarget = buildTarget ? resolveBoardTarget(buildTarget, zephyrVersion) : undefined;
132
+ const boardId = resolvedBoardTarget ? resolvedBoardTarget.split('/')[0] : undefined;
133
+ const boardTargetSupported = boardId !== undefined ? boardLookup(boardId, probe.zephyrBase) : undefined;
134
+ const check = {
135
+ westFound: probe.westFound,
136
+ westVersion: probe.westVersion,
137
+ westSource: probe.source,
138
+ zephyrBase: probe.zephyrBase,
139
+ zephyrVersion,
140
+ compatRange: compat.range,
141
+ compatStatus: compat.status,
142
+ buildTarget,
143
+ resolvedBoardTarget,
144
+ boardTargetSupported,
145
+ };
146
+ // 1. west (the build tool) missing entirely — nothing else can run.
147
+ if (!probe.westFound) {
148
+ return {
149
+ ok: false,
150
+ reason: 'west-not-found',
151
+ check,
152
+ messages: [
153
+ "west (the Zephyr build tool) was not found.",
154
+ " Run the typeCAD Zephyr installer, activate an existing Zephyr venv,",
155
+ " set ZEPHYR_BASE to a Zephyr SDK root, or `pip install west`.",
156
+ ],
157
+ fixCommand: undefined,
158
+ };
159
+ }
160
+ // 2. west healthy but the Zephyr RTOS is outside the supported range.
161
+ if (compat.status === 'out-of-range') {
162
+ return {
163
+ ok: false,
164
+ reason: 'zephyr-out-of-range',
165
+ check,
166
+ messages: [
167
+ `Zephyr ${zephyrVersion} is outside the supported range (${compat.range}) for @typecad/framework-zephyr.`,
168
+ " Set ZEPHYR_BASE to a compatible Zephyr checkout, or install one via '@typecad/zephyr-installer'.",
169
+ ],
170
+ fixCommand: undefined,
171
+ };
172
+ }
173
+ // 3. west + version OK — only check the board when a target is configured and
174
+ // the lookup was able to answer. A missing/absent target is not a board
175
+ // problem; an inconclusive lookup (no base) is reported as a skip, not a fail.
176
+ if (buildTarget && boardTargetSupported === false) {
177
+ return {
178
+ ok: false,
179
+ reason: 'board-not-supported',
180
+ check,
181
+ messages: [
182
+ `Board target '${resolvedBoardTarget}' was not found in this Zephyr checkout` +
183
+ (probe.zephyrBase ? ` (${join(probe.zephyrBase, 'boards')}).` : '.'),
184
+ " Check the board id, or run `west boards` to list boards in this checkout.",
185
+ ],
186
+ fixCommand: 'west boards',
187
+ };
188
+ }
189
+ return { ok: true, check };
190
+ }
@@ -185,12 +185,17 @@ export const Toolchain = {
185
185
  // for either driver. Thread a non-default profile here only if a future
186
186
  // board carries a display node under a different nodelabel.
187
187
  const displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
188
+ // Touch controller kind comes from which DT nodelabel the emitted adapter
189
+ // references (FT6336U on I2C, XPT2046 on the display's SPI bus).
190
+ const usesTouch = uses('ft6336u') || uses('touch_');
191
+ const usesXpt = uses('xpt2046');
188
192
  const overlay = generateOverlay(chip, {
189
193
  usesI2c: uses('i2c_'),
190
194
  usesSpi: uses('spi_'),
191
195
  usesUart: uses('uart_'),
192
196
  usesDisplay,
193
- usesTouch: uses('ft6336u') || uses('touch_'),
197
+ usesTouch: usesTouch || usesXpt,
198
+ touchController: usesXpt ? 'xpt2046' : 'ft6336u',
194
199
  }, displayProfile);
195
200
  const overlayDir = join(projectRoot, 'boards');
196
201
  mkdirSync(overlayDir, { recursive: true });
@@ -274,23 +279,48 @@ export const Toolchain = {
274
279
  backlightPin: typeof dispCfg.backlightPin === 'number' ? dispCfg.backlightPin : undefined,
275
280
  }
276
281
  : undefined;
277
- // Extract touch pin wiring (irq/resetPin/sda/scl) from the config
278
- // display.touch section so the DT overlay wires the I2C bus + touch node.
282
+ // Extract touch pin wiring from the config display.touch section so the
283
+ // DT overlay wires the bus + touch node. I2C (FT6336U) carries
284
+ // irq/resetPin/sda/scl; SPI (XPT2046) carries irq/cs + the calibration
285
+ // range the xptek,xpt2046 binding requires.
279
286
  const touchCfg = dispCfg?.touch;
280
- const touchWiring = touchCfg
287
+ const isXpt = touchCfg?.library === 'XPT2046_Touchscreen';
288
+ const touchCal = touchCfg?.calibration;
289
+ const num = (v) => (typeof v === 'number' ? v : undefined);
290
+ let touchWiring = touchCfg
281
291
  ? {
282
- irq: typeof touchCfg.irq === 'number' ? touchCfg.irq : undefined,
283
- resetPin: typeof touchCfg.resetPin === 'number' ? touchCfg.resetPin : undefined,
284
- sda: typeof touchCfg.sda === 'number' ? touchCfg.sda : undefined,
285
- scl: typeof touchCfg.scl === 'number' ? touchCfg.scl : undefined,
292
+ controller: isXpt ? 'xpt2046' : 'ft6336u',
293
+ irq: num(touchCfg.irq),
294
+ resetPin: num(touchCfg.resetPin),
295
+ sda: num(touchCfg.sda),
296
+ scl: num(touchCfg.scl),
297
+ cs: num(touchCfg.cs),
298
+ calibration: touchCal
299
+ ? {
300
+ xMin: num(touchCal.xMin) ?? 0,
301
+ xMax: num(touchCal.xMax) ?? 4095,
302
+ yMin: num(touchCal.yMin) ?? 0,
303
+ yMax: num(touchCal.yMax) ?? 4095,
304
+ }
305
+ : undefined,
306
+ minPressure: num(touchCfg.minPressure),
286
307
  }
287
308
  : undefined;
309
+ // Touch controller kind for Kconfig (bus driver selection) and the DT
310
+ // node shape: from the config when available, else from the DT nodelabel
311
+ // the emitted adapter references. Forced onto touchWiring so a source
312
+ // scan match without a config section still emits the right node.
313
+ const usesXpt = isXpt || uses('xpt2046');
314
+ if (usesXpt) {
315
+ touchWiring = { controller: 'xpt2046', ...(touchWiring ?? {}) };
316
+ }
288
317
  const overlay = generateOverlay(chip, {
289
318
  usesI2c: uses('i2c_'),
290
319
  usesSpi: uses('spi_'),
291
320
  usesUart: uses('uart_'),
292
321
  usesDisplay,
293
- usesTouch: uses('ft6336u') || uses('touch_'),
322
+ usesTouch: uses('ft6336u') || uses('touch_') || usesXpt,
323
+ touchController: usesXpt ? 'xpt2046' : 'ft6336u',
294
324
  psram: o.psram,
295
325
  }, displayProfile, wiring, touchWiring);
296
326
  const overlayDir = join(projectRoot, 'boards');
@@ -85,6 +85,9 @@ export function scaffoldZephyrProject(projectRoot, debug = false, userKconfig, p
85
85
  const usage = {
86
86
  usesAdc: uses('adc_'),
87
87
  usesPwm: uses('pwm_'),
88
+ usesDac: uses('dac_') || uses('__tc_dac'),
89
+ usesFS: uses('__tc_fs'),
90
+ usesHwtimer: uses('counter_') || uses('__tc_hw'),
88
91
  usesI2c: uses('i2c_'),
89
92
  usesSpi: uses('spi_'),
90
93
  usesUart: uses('uart_'),
@@ -125,8 +128,10 @@ export function scaffoldZephyrProject(projectRoot, debug = false, userKconfig, p
125
128
  '',
126
129
  'project(zephyr_app)',
127
130
  '',
128
- '# Collect cuttlefish-emitted sources.',
129
- 'file(GLOB app_sources src/*.cpp src/*.c)',
131
+ '# Collect cuttlefish-emitted sources. CONFIGURE_DEPENDS makes CMake re-',
132
+ '# check the glob when the source set changes (e.g. the transpiler removes',
133
+ '# a stale entry), instead of linking a file list from the last configure.',
134
+ 'file(GLOB app_sources CONFIGURE_DEPENDS src/*.cpp src/*.c)',
130
135
  '',
131
136
  'target_sources(app PRIVATE ${app_sources})',
132
137
  // When PSRAM is configured, define BOARD_HAS_PSRAM so the UI runtime's
@@ -4,15 +4,21 @@
4
4
  * `pythonExecutable -m west`.
5
5
  */
6
6
  export interface WestInstall {
7
- mode: 'launcher' | 'module';
7
+ mode: 'launcher' | 'module' | 'micromamba';
8
8
  /** Absolute path to a `west` launcher (mode 'launcher') or undefined. */
9
9
  westExecutable?: string;
10
10
  /** Absolute path to a Python interpreter with west installed (mode 'module'). */
11
11
  pythonExecutable?: string;
12
12
  /** Absolute path to the Zephyr SDK root (for $ZEPHYR_BASE), if found. */
13
13
  zephyrBase?: string;
14
+ /** mode 'micromamba': path to the micromamba binary (for `micromamba run -n …`). */
15
+ micromambaExe?: string;
16
+ /** mode 'micromamba': the conda env name (default 'zephyr'). */
17
+ envName?: string;
18
+ /** mode 'micromamba': MAMBA_ROOT_PREFIX, injected so micromamba finds its envs. */
19
+ mambaRootPrefix?: string;
14
20
  /** Which discovery strategy found this install. */
15
- source: 'path' | 'zephyr-base-venv' | 'well-known' | 'system-python';
21
+ source: 'path' | 'zephyr-base-venv' | 'well-known' | 'system-python' | 'micromamba';
16
22
  }
17
23
  /** True if `dir` looks like a Zephyr SDK root: has CMakeLists.txt and the
18
24
  * kernel header. */
@@ -21,6 +27,7 @@ export declare function discoverFromPath(): WestInstall | null;
21
27
  /** The canonical Zephyr workspace layout puts the venv beside the SDK:
22
28
  * <workspace>/{.venv, zephyr}. So ${ZEPHYR_BASE}/../.venv is the venv. */
23
29
  export declare function discoverFromZephyrBase(): WestInstall | null;
30
+ export declare function discoverFromMicromamba(envName?: string): WestInstall | null;
24
31
  /** Candidate Zephyr workspace directories. Each may contain both `.venv/`
25
32
  * and `zephyr/` (the SDK). Exported for test injection. */
26
33
  export declare function wellKnownWorkspaces(): string[];
@@ -34,7 +41,8 @@ export declare function resetWestDiscoveryCache(): void;
34
41
  * Try each discovery strategy in order. The first usable install wins.
35
42
  * Result is memoized for the process lifetime (west installs don't move).
36
43
  *
37
- * Order: PATH → $ZEPHYR_BASE venv → well-known workspaces → system pythons.
44
+ * Order: PATH → $ZEPHYR_BASE venv → micromamba env → well-known workspaces →
45
+ * system pythons.
38
46
  * Returns null when no usable west install is found.
39
47
  */
40
48
  export declare function discoverWest(): WestInstall | null;
@@ -10,14 +10,16 @@
10
10
  // Discovery cascade (first usable wins):
11
11
  // 1. `west` already on PATH (env already activated / global install).
12
12
  // 2. $ZEPHYR_BASE venv: ${ZEPHYR_BASE}/../.venv/<python> -m west.
13
- // 3. Well-known workspace layouts: ~/zephyrproject/.venv, /opt/zephyrproject/.
13
+ // 3. micromamba env from @typecad/zephyr-installer (invoked via `micromamba run`,
14
+ // so cuttlefish builds work with NO manual activation).
15
+ // 4. Well-known workspace layouts: ~/zephyrproject/.venv, /opt/zephyrproject/.
14
16
  // venv, etc.
15
- // 4. System pythons (`python`, `python3`, `py`) via `-m west`.
17
+ // 5. System pythons (`python`, `python3`, `py`) via `-m west`.
16
18
  //
17
19
  // Leaner than the ESP-IDF equivalent: west needs no env sourcing (no 15s
18
20
  // export.sh) — only the right interpreter + ZEPHYR_BASE.
19
21
  // ---------------------------------------------------------------------------
20
- import { existsSync } from 'node:fs';
22
+ import { existsSync, readFileSync } from 'node:fs';
21
23
  import { dirname, join } from 'node:path';
22
24
  import { homedir } from 'node:os';
23
25
  import { spawnSync } from 'node:child_process';
@@ -65,9 +67,12 @@ export function isZephyrBase(dir) {
65
67
  }
66
68
  // ── Strategy 1: `west` on PATH ──────────────────────────────────────────────
67
69
  export function discoverFromPath() {
70
+ // shell only on Windows (where.exe resolution through cmd) — an args array
71
+ // with shell: true triggers Node's DEP0190 deprecation warning on Linux/
72
+ // macOS, where `which` is a plain executable that needs no shell.
68
73
  const which = spawnSync(IS_WIN ? 'where' : 'which', ['west'], {
69
74
  encoding: 'utf8',
70
- shell: true,
75
+ shell: IS_WIN,
71
76
  windowsHide: true,
72
77
  });
73
78
  if (which.status !== 0)
@@ -106,7 +111,77 @@ export function discoverFromZephyrBase() {
106
111
  source: 'zephyr-base-venv',
107
112
  };
108
113
  }
109
- // ── Strategy 3: well-known workspace layouts ───────────────────────────────
114
+ // ── Strategy 3: micromamba env (the @typecad/zephyr-installer install) ─────
115
+ // Locate the micromamba binary + root prefix. The installer downloads
116
+ // micromamba to $MAMBA_ROOT_PREFIX/bin (POSIX) or Library/bin (Windows); the
117
+ // root defaults to ~/micromamba. Returns null if the binary isn't present
118
+ // (the installer hasn't run on this machine).
119
+ function findMicromamba() {
120
+ const root = process.env.MAMBA_ROOT_PREFIX || join(homedir(), 'micromamba');
121
+ const exe = IS_WIN
122
+ ? join(root, 'Library', 'bin', 'micromamba.exe')
123
+ : join(root, 'bin', 'micromamba');
124
+ return existsSync(exe) ? { exe, rootPrefix: root } : null;
125
+ }
126
+ /**
127
+ * The micromamba env created by `@typecad/zephyr-installer`. The env's west
128
+ * lives at envs/<name>/bin/west (POSIX) or Scripts/west.exe (Windows). Found
129
+ * installs are invoked via `micromamba run -n <name> west …` (see
130
+ * west-spawn.ts), which sets up the env's full PATH (cmake/ninja/dtc) AND runs
131
+ * the activation hook (ZEPHYR_BASE / ZEPHYR_SDK_INSTALL_DIR) — so cuttlefish
132
+ * builds work with NO manual `micromamba activate`. This is what makes a fresh
133
+ * `cuttlefish build` succeed in any project without the user activating.
134
+ *
135
+ * Env name defaults to "zephyr"; override via TYPECAD_ZEPHYR_ENV. File-check
136
+ * based (no spawn) so it's cheap to run on every cuttlefish invocation.
137
+ */
138
+ /** Read a TYPECAD_ZEPHYR_* value from the installer-written env-vars file in
139
+ * a micromamba env. Handles .sh (export VAR="val"), .bat (set "VAR=val"),
140
+ * and .ps1 ($env:VAR = "val"). Returns undefined if absent/unreadable. */
141
+ function readMicromambaEnvVar(envDir, varName) {
142
+ const candidates = IS_WIN
143
+ ? [join(envDir, 'etc', 'conda', 'env-vars.ps1'), join(envDir, 'etc', 'conda', 'env-vars.bat')]
144
+ : [join(envDir, 'etc', 'conda', 'env-vars.sh')];
145
+ for (const f of candidates) {
146
+ if (!existsSync(f))
147
+ continue;
148
+ try {
149
+ const text = readFileSync(f, 'utf8');
150
+ // .sh/.ps1: VAR = "value" (quoted value after =).
151
+ let m = text.match(new RegExp(`${varName}\\s*=\\s*"([^"]+)"`));
152
+ if (m)
153
+ return m[1];
154
+ // .bat: set "VAR=value" (value after VAR= inside quotes).
155
+ m = text.match(new RegExp(`${varName}=([^"\\r\\n]+)"`));
156
+ if (m)
157
+ return m[1].trim();
158
+ }
159
+ catch { /* ignore unreadable */ }
160
+ }
161
+ return undefined;
162
+ }
163
+ export function discoverFromMicromamba(envName = process.env.TYPECAD_ZEPHYR_ENV || 'zephyr') {
164
+ const mm = findMicromamba();
165
+ if (!mm)
166
+ return null;
167
+ const envDir = join(mm.rootPrefix, 'envs', envName);
168
+ const westExe = join(envDir, IS_WIN ? 'Scripts' : 'bin', IS_WIN ? 'west.exe' : 'west');
169
+ if (!existsSync(envDir) || !existsSync(westExe))
170
+ return null;
171
+ // Read ZEPHYR_BASE from the installer's env-vars so the compat check (and
172
+ // anything else in the cuttlefish process) can detect the Zephyr version
173
+ // WITHOUT activation — micromamba run sets it only inside the west subprocess.
174
+ const zb = readMicromambaEnvVar(envDir, 'TYPECAD_ZEPHYR_BASE');
175
+ return {
176
+ mode: 'micromamba',
177
+ micromambaExe: mm.exe,
178
+ envName,
179
+ mambaRootPrefix: mm.rootPrefix,
180
+ zephyrBase: zb && isZephyrBase(zb) ? zb : undefined,
181
+ source: 'micromamba',
182
+ };
183
+ }
184
+ // ── Strategy 4: well-known workspace layouts ───────────────────────────────
110
185
  /** Candidate Zephyr workspace directories. Each may contain both `.venv/`
111
186
  * and `zephyr/` (the SDK). Exported for test injection. */
112
187
  export function wellKnownWorkspaces() {
@@ -143,7 +218,7 @@ export function discoverFromWellKnown(workspaces = wellKnownWorkspaces()) {
143
218
  }
144
219
  return null;
145
220
  }
146
- // ── Strategy 4: system pythons via `-m west` ────────────────────────────────
221
+ // ── Strategy 5: system pythons via `-m west` ────────────────────────────────
147
222
  /** Candidate system Python interpreters to probe with `-m west`. */
148
223
  export function systemPythons() {
149
224
  if (IS_WIN)
@@ -173,7 +248,8 @@ export function resetWestDiscoveryCache() {
173
248
  * Try each discovery strategy in order. The first usable install wins.
174
249
  * Result is memoized for the process lifetime (west installs don't move).
175
250
  *
176
- * Order: PATH → $ZEPHYR_BASE venv → well-known workspaces → system pythons.
251
+ * Order: PATH → $ZEPHYR_BASE venv → micromamba env → well-known workspaces →
252
+ * system pythons.
177
253
  * Returns null when no usable west install is found.
178
254
  */
179
255
  export function discoverWest() {
@@ -182,6 +258,7 @@ export function discoverWest() {
182
258
  const strategies = [
183
259
  discoverFromPath,
184
260
  discoverFromZephyrBase,
261
+ discoverFromMicromamba,
185
262
  discoverFromWellKnown,
186
263
  discoverFromSystemPython,
187
264
  ];
@@ -98,6 +98,21 @@ export function westSpawn(westArgs, baseOptions) {
98
98
  // Strip `shell` if present — we pass absolute paths / known commands, and
99
99
  // an explicit shell changes arg-quoting semantics on Windows.
100
100
  const { shell: _drop, ...optsWithoutShell } = baseOptions;
101
+ if (install.mode === 'micromamba' && install.micromambaExe) {
102
+ // `micromamba run -n <env> west …` sets up the env's full PATH
103
+ // (cmake/ninja/dtc) and runs the activation hook (ZEPHYR_BASE /
104
+ // ZEPHYR_SDK_INSTALL_DIR), so cuttlefish builds work WITHOUT the user
105
+ // activating the env. Inject MAMBA_ROOT_PREFIX so micromamba finds envs.
106
+ const mmEnv = { ...env };
107
+ if (install.mambaRootPrefix)
108
+ mmEnv.MAMBA_ROOT_PREFIX = install.mambaRootPrefix;
109
+ return {
110
+ command: install.micromambaExe,
111
+ args: ['run', '-n', install.envName ?? 'zephyr', 'west', ...westArgs],
112
+ options: { ...optsWithoutShell, env: mmEnv },
113
+ install,
114
+ };
115
+ }
101
116
  if (install.mode === 'launcher' && install.westExecutable) {
102
117
  return {
103
118
  command: install.westExecutable,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typecad/framework-zephyr",
3
- "version": "1.0.0-alpha.10",
3
+ "version": "1.0.0-alpha.12",
4
4
  "description": "TypeCAD framework package for the Zephyr RTOS — west/CMake build, devicetree-driven GPIO",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,11 +38,11 @@
38
38
  "test:hw:mqtt": "cd ../../tests/hardware && npm exec -- cuttlefish-test mqtt-client.test.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@typecad/cuttlefish": "1.0.0-alpha.10"
41
+ "@typecad/cuttlefish": "1.0.0-alpha.12"
42
42
  },
43
43
  "devDependencies": {
44
- "@typecad/expect": "1.0.0-alpha.10",
45
- "@typecad/board-xiao-nrf52840": "1.0.0-alpha.10",
44
+ "@typecad/expect": "1.0.0-alpha.12",
45
+ "@typecad/board-xiao-nrf52840": "1.0.0-alpha.12",
46
46
  "typescript": "^5.7.3"
47
47
  },
48
48
  "license": "MIT",
@@ -59,4 +59,16 @@ export const ESP32_DEVKITC: ZephyrChipDescriptor = {
59
59
  // the ESP32 is AMP (dual-image procpu/appcpu), not SMP, by default — so the
60
60
  // dependency is satisfied. Omitted on radioless targets.
61
61
  wifi: { supported: true },
62
+ // DAC: the ESP32 has two 8-bit DAC channels on GPIO25 (channel 1) and GPIO26
63
+ // (channel 2). The Zephyr esp32 DAC driver (drivers/dac/dac_esp32.c) exposes
64
+ // them via the `dac0` node; the lowering emits dac_channel_setup +
65
+ // dac_write_value against DEVICE_DT_GET(DT_NODELABEL(dac0)). The overlay
66
+ // enables the node when the program uses dac.*. ESP32-S3 has no DAC.
67
+ dac: {
68
+ device: 'dac0',
69
+ channels: [
70
+ { pin: 25, channel: 1, resolution: 8 },
71
+ { pin: 26, channel: 2, resolution: 8 },
72
+ ],
73
+ },
62
74
  };
@@ -101,6 +101,19 @@ export interface ZephyrAdcChannel {
101
101
  readonly channel: number;
102
102
  }
103
103
 
104
+ /**
105
+ * A DAC channel: which DAC output a given HAL pin maps to. The lowering emits
106
+ * `dac_channel_setup` + `dac_write_value` against the DAC device node.
107
+ */
108
+ export interface ZephyrDacChannel {
109
+ /** GPIO number (matches the HAL op `pin` field). */
110
+ readonly pin: number;
111
+ /** DAC channel index (ESP32: GPIO25 → 1, GPIO26 → 2). */
112
+ readonly channel: number;
113
+ /** DAC resolution in bits (ESP32 DAC is 8-bit). */
114
+ readonly resolution: number;
115
+ }
116
+
104
117
  /**
105
118
  * Pure-data descriptor for a Zephyr board + its SoC's peripheral layout.
106
119
  */
@@ -151,8 +164,24 @@ export interface ZephyrChipDescriptor {
151
164
  /** ADC resolution in bits. */
152
165
  readonly resolution: number;
153
166
  };
167
+ /**
168
+ * DAC: the DAC device node label + the pin→channel map. Present only on chips
169
+ * with a DAC (ESP32 has 2 channels on GPIO25/26; ESP32-S3 and nRF52840 have
170
+ * none). Read by profileDiagnostics to flag dac.* usage on chips without it.
171
+ */
172
+ readonly dac?: {
173
+ readonly device: string;
174
+ readonly channels: readonly ZephyrDacChannel[];
175
+ };
154
176
  /** Watchdog node label, e.g. 'wdt0'. */
155
177
  readonly wdt?: { readonly nodeLabel: string };
178
+ /**
179
+ * Hardware timers exposed as Zephyr counter devices. `instance` (the HAL
180
+ * hwtimer.* op's instance index) maps to `controllers[instance].nodeLabel`.
181
+ * Omit on chips whose counter nodes are kernel-owned or unavailable; the
182
+ * lowering then lowers to a comment and profileDiagnostics flags usage.
183
+ */
184
+ readonly hwtimer?: { readonly controllers: readonly ZephyrBusController[] };
156
185
  /**
157
186
  * WiFi capability marker. Present only on chips with a WiFi radio (ESP32-S3).
158
187
  * Read by profileDiagnostics to flag wifi.* usage on chips without a radio.
@@ -61,4 +61,10 @@ export const XIAO_BLE: ZephyrChipDescriptor = {
61
61
  ],
62
62
  },
63
63
  wdt: { nodeLabel: 'wdt0' },
64
+ // Hardware timer: nRF RTC1 is the free counter (RTC0 is kernel-owned by the
65
+ // softdevice/clock driver). The hwtimer lowering drives it as a Zephyr
66
+ // counter device (counter_start/stop + a top-value alarm for set_frequency).
67
+ // Verified against the nRF52840 SoC dtsi (rtc0/rtc1 nodes). The kernel uses
68
+ // RTC0 for the system tick; RTC1 is available for application use.
69
+ hwtimer: { controllers: [{ nodeLabel: 'rtc1' }] },
64
70
  };