@typecad/framework-zephyr 1.0.0-alpha.10 → 1.0.0-alpha.11
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.
- package/dist/chips/esp32.js +12 -0
- package/dist/chips/types.d.ts +30 -0
- package/dist/chips/xiao-ble.js +6 -0
- package/dist/display/gfx.d.ts +12 -3
- package/dist/display/gfx.js +130 -17
- package/dist/display/profiles.js +13 -0
- package/dist/display/ui-adapter.js +6 -0
- package/dist/doctor.d.ts +3 -3
- package/dist/doctor.js +56 -29
- package/dist/dt-config/kconfig.d.ts +3 -0
- package/dist/dt-config/kconfig.js +18 -0
- package/dist/dt-config/overlay.js +5 -0
- package/dist/framework.manifest.js +37 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/licenses.d.ts +59 -0
- package/dist/licenses.js +347 -0
- package/dist/lowering/dac.d.ts +15 -0
- package/dist/lowering/dac.js +69 -0
- package/dist/lowering/fs.d.ts +16 -0
- package/dist/lowering/fs.js +121 -0
- package/dist/lowering/hwtimer.d.ts +15 -0
- package/dist/lowering/hwtimer.js +84 -0
- package/dist/lowering/index.d.ts +4 -1
- package/dist/lowering/index.js +12 -3
- package/dist/strategy.js +186 -14
- package/dist/toolchain/compat.js +10 -1
- package/dist/toolchain/env-check.d.ts +93 -0
- package/dist/toolchain/env-check.js +190 -0
- package/dist/toolchain/scaffold.js +3 -0
- package/dist/toolchain/west-discover.d.ts +11 -3
- package/dist/toolchain/west-discover.js +80 -6
- package/dist/toolchain/west-spawn.js +15 -0
- package/package.json +4 -4
- package/src/chips/esp32.ts +12 -0
- package/src/chips/types.ts +29 -0
- package/src/chips/xiao-ble.ts +6 -0
- package/src/display/gfx.ts +135 -19
- package/src/display/profiles.ts +13 -0
- package/src/display/ui-adapter.ts +5 -0
- package/src/doctor.ts +77 -56
- package/src/dt-config/kconfig.ts +19 -0
- package/src/dt-config/overlay.ts +5 -0
- package/src/framework.manifest.ts +38 -14
- package/src/index.ts +6 -0
- package/src/licenses.ts +425 -0
- package/src/lowering/dac.ts +82 -0
- package/src/lowering/fs.ts +127 -0
- package/src/lowering/hwtimer.ts +101 -0
- package/src/lowering/index.ts +9 -2
- package/src/strategy.ts +180 -14
- package/src/toolchain/compat.ts +154 -145
- package/src/toolchain/env-check.ts +285 -0
- package/src/toolchain/scaffold.ts +3 -0
- package/src/toolchain/west-discover.ts +88 -8
- 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
|
+
}
|
|
@@ -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_'),
|
|
@@ -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 →
|
|
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.
|
|
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
|
-
//
|
|
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';
|
|
@@ -106,7 +108,77 @@ export function discoverFromZephyrBase() {
|
|
|
106
108
|
source: 'zephyr-base-venv',
|
|
107
109
|
};
|
|
108
110
|
}
|
|
109
|
-
// ── Strategy 3:
|
|
111
|
+
// ── Strategy 3: micromamba env (the @typecad/zephyr-installer install) ─────
|
|
112
|
+
// Locate the micromamba binary + root prefix. The installer downloads
|
|
113
|
+
// micromamba to $MAMBA_ROOT_PREFIX/bin (POSIX) or Library/bin (Windows); the
|
|
114
|
+
// root defaults to ~/micromamba. Returns null if the binary isn't present
|
|
115
|
+
// (the installer hasn't run on this machine).
|
|
116
|
+
function findMicromamba() {
|
|
117
|
+
const root = process.env.MAMBA_ROOT_PREFIX || join(homedir(), 'micromamba');
|
|
118
|
+
const exe = IS_WIN
|
|
119
|
+
? join(root, 'Library', 'bin', 'micromamba.exe')
|
|
120
|
+
: join(root, 'bin', 'micromamba');
|
|
121
|
+
return existsSync(exe) ? { exe, rootPrefix: root } : null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The micromamba env created by `@typecad/zephyr-installer`. The env's west
|
|
125
|
+
* lives at envs/<name>/bin/west (POSIX) or Scripts/west.exe (Windows). Found
|
|
126
|
+
* installs are invoked via `micromamba run -n <name> west …` (see
|
|
127
|
+
* west-spawn.ts), which sets up the env's full PATH (cmake/ninja/dtc) AND runs
|
|
128
|
+
* the activation hook (ZEPHYR_BASE / ZEPHYR_SDK_INSTALL_DIR) — so cuttlefish
|
|
129
|
+
* builds work with NO manual `micromamba activate`. This is what makes a fresh
|
|
130
|
+
* `cuttlefish build` succeed in any project without the user activating.
|
|
131
|
+
*
|
|
132
|
+
* Env name defaults to "zephyr"; override via TYPECAD_ZEPHYR_ENV. File-check
|
|
133
|
+
* based (no spawn) so it's cheap to run on every cuttlefish invocation.
|
|
134
|
+
*/
|
|
135
|
+
/** Read a TYPECAD_ZEPHYR_* value from the installer-written env-vars file in
|
|
136
|
+
* a micromamba env. Handles .sh (export VAR="val"), .bat (set "VAR=val"),
|
|
137
|
+
* and .ps1 ($env:VAR = "val"). Returns undefined if absent/unreadable. */
|
|
138
|
+
function readMicromambaEnvVar(envDir, varName) {
|
|
139
|
+
const candidates = IS_WIN
|
|
140
|
+
? [join(envDir, 'etc', 'conda', 'env-vars.ps1'), join(envDir, 'etc', 'conda', 'env-vars.bat')]
|
|
141
|
+
: [join(envDir, 'etc', 'conda', 'env-vars.sh')];
|
|
142
|
+
for (const f of candidates) {
|
|
143
|
+
if (!existsSync(f))
|
|
144
|
+
continue;
|
|
145
|
+
try {
|
|
146
|
+
const text = readFileSync(f, 'utf8');
|
|
147
|
+
// .sh/.ps1: VAR = "value" (quoted value after =).
|
|
148
|
+
let m = text.match(new RegExp(`${varName}\\s*=\\s*"([^"]+)"`));
|
|
149
|
+
if (m)
|
|
150
|
+
return m[1];
|
|
151
|
+
// .bat: set "VAR=value" (value after VAR= inside quotes).
|
|
152
|
+
m = text.match(new RegExp(`${varName}=([^"\\r\\n]+)"`));
|
|
153
|
+
if (m)
|
|
154
|
+
return m[1].trim();
|
|
155
|
+
}
|
|
156
|
+
catch { /* ignore unreadable */ }
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
export function discoverFromMicromamba(envName = process.env.TYPECAD_ZEPHYR_ENV || 'zephyr') {
|
|
161
|
+
const mm = findMicromamba();
|
|
162
|
+
if (!mm)
|
|
163
|
+
return null;
|
|
164
|
+
const envDir = join(mm.rootPrefix, 'envs', envName);
|
|
165
|
+
const westExe = join(envDir, IS_WIN ? 'Scripts' : 'bin', IS_WIN ? 'west.exe' : 'west');
|
|
166
|
+
if (!existsSync(envDir) || !existsSync(westExe))
|
|
167
|
+
return null;
|
|
168
|
+
// Read ZEPHYR_BASE from the installer's env-vars so the compat check (and
|
|
169
|
+
// anything else in the cuttlefish process) can detect the Zephyr version
|
|
170
|
+
// WITHOUT activation — micromamba run sets it only inside the west subprocess.
|
|
171
|
+
const zb = readMicromambaEnvVar(envDir, 'TYPECAD_ZEPHYR_BASE');
|
|
172
|
+
return {
|
|
173
|
+
mode: 'micromamba',
|
|
174
|
+
micromambaExe: mm.exe,
|
|
175
|
+
envName,
|
|
176
|
+
mambaRootPrefix: mm.rootPrefix,
|
|
177
|
+
zephyrBase: zb && isZephyrBase(zb) ? zb : undefined,
|
|
178
|
+
source: 'micromamba',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// ── Strategy 4: well-known workspace layouts ───────────────────────────────
|
|
110
182
|
/** Candidate Zephyr workspace directories. Each may contain both `.venv/`
|
|
111
183
|
* and `zephyr/` (the SDK). Exported for test injection. */
|
|
112
184
|
export function wellKnownWorkspaces() {
|
|
@@ -143,7 +215,7 @@ export function discoverFromWellKnown(workspaces = wellKnownWorkspaces()) {
|
|
|
143
215
|
}
|
|
144
216
|
return null;
|
|
145
217
|
}
|
|
146
|
-
// ── Strategy
|
|
218
|
+
// ── Strategy 5: system pythons via `-m west` ────────────────────────────────
|
|
147
219
|
/** Candidate system Python interpreters to probe with `-m west`. */
|
|
148
220
|
export function systemPythons() {
|
|
149
221
|
if (IS_WIN)
|
|
@@ -173,7 +245,8 @@ export function resetWestDiscoveryCache() {
|
|
|
173
245
|
* Try each discovery strategy in order. The first usable install wins.
|
|
174
246
|
* Result is memoized for the process lifetime (west installs don't move).
|
|
175
247
|
*
|
|
176
|
-
* Order: PATH → $ZEPHYR_BASE venv → well-known workspaces →
|
|
248
|
+
* Order: PATH → $ZEPHYR_BASE venv → micromamba env → well-known workspaces →
|
|
249
|
+
* system pythons.
|
|
177
250
|
* Returns null when no usable west install is found.
|
|
178
251
|
*/
|
|
179
252
|
export function discoverWest() {
|
|
@@ -182,6 +255,7 @@ export function discoverWest() {
|
|
|
182
255
|
const strategies = [
|
|
183
256
|
discoverFromPath,
|
|
184
257
|
discoverFromZephyrBase,
|
|
258
|
+
discoverFromMicromamba,
|
|
185
259
|
discoverFromWellKnown,
|
|
186
260
|
discoverFromSystemPython,
|
|
187
261
|
];
|
|
@@ -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.
|
|
3
|
+
"version": "1.0.0-alpha.11",
|
|
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.
|
|
41
|
+
"@typecad/cuttlefish": "1.0.0-alpha.11"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@typecad/expect": "1.0.0-alpha.
|
|
45
|
-
"@typecad/board-xiao-nrf52840": "1.0.0-alpha.
|
|
44
|
+
"@typecad/expect": "1.0.0-alpha.11",
|
|
45
|
+
"@typecad/board-xiao-nrf52840": "1.0.0-alpha.11",
|
|
46
46
|
"typescript": "^5.7.3"
|
|
47
47
|
},
|
|
48
48
|
"license": "MIT",
|
package/src/chips/esp32.ts
CHANGED
|
@@ -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
|
};
|
package/src/chips/types.ts
CHANGED
|
@@ -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.
|
package/src/chips/xiao-ble.ts
CHANGED
|
@@ -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
|
};
|
package/src/display/gfx.ts
CHANGED
|
@@ -86,18 +86,23 @@ function fontTableCpp(): string {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* Build the display runtime C++ for a profile.
|
|
90
|
-
*
|
|
91
|
-
*
|
|
89
|
+
* Build the display runtime C++ for a profile.
|
|
90
|
+
*
|
|
91
|
+
* Two rendering models, selected by `profile.colorFormat`:
|
|
92
|
+
* - `'rgb565'` (TFT): a one-row line buffer (no full framebuffer — see
|
|
93
|
+
* AGENTS.md rendering guardrails); fill_rect/draw_rect/draw_text stream
|
|
94
|
+
* each row via display_write.
|
|
95
|
+
* - `'mono'` (OLED, e.g. SSD1306): a full framebuffer — the standard model
|
|
96
|
+
* for page-buffered monochrome panels (the AGENTS.md "no full framebuffer"
|
|
97
|
+
* guardrail targets RGB SPI TFTs, not mono OLEDs). draw ops set bits;
|
|
98
|
+
* display_flush pushes the whole framebuffer. The MONO01 packing is
|
|
99
|
+
* horizontal, MSB-first (Zephyr convention): byte = (y*rowBytes)+(x>>3),
|
|
100
|
+
* bit = 0x80>>(x&7).
|
|
92
101
|
*/
|
|
93
102
|
export function buildDisplayRuntime(profile: ZephyrDisplayProfile): DisplayRuntimeResult {
|
|
94
103
|
const w = profile.width;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
`static const struct device* __tc_display = DEVICE_DT_GET(DT_NODELABEL(${profile.dtLabel}));`,
|
|
98
|
-
`static uint16_t __tc_display_line[${w}]; // one-row line buffer (rgb565)`,
|
|
99
|
-
'// CUTTLEFISH_DISPLAY_END',
|
|
100
|
-
];
|
|
104
|
+
const h = profile.height;
|
|
105
|
+
const isMono = profile.colorFormat === 'mono';
|
|
101
106
|
|
|
102
107
|
// Backlight is optional: the overlay emits the DT alias only when a backlight
|
|
103
108
|
// GPIO is configured. Guard with DT_HAS_ALIAS so this compiles whether or not
|
|
@@ -107,7 +112,127 @@ export function buildDisplayRuntime(profile: ZephyrDisplayProfile): DisplayRunti
|
|
|
107
112
|
? `#if DT_HAS_ALIAS(${profile.backlight})\n const struct device* __bl = DEVICE_DT_GET(DT_ALIAS(${profile.backlight}));\n gpio_pin_configure(__bl, 0, GPIO_OUTPUT); gpio_pin_set(__bl, 0, 1);\n#endif`
|
|
108
113
|
: '';
|
|
109
114
|
|
|
110
|
-
const
|
|
115
|
+
const stateLines = isMono
|
|
116
|
+
? [
|
|
117
|
+
'// CUTTLEFISH_DISPLAY_BEGIN',
|
|
118
|
+
`static const struct device* __tc_display = DEVICE_DT_GET(DT_NODELABEL(${profile.dtLabel}));`,
|
|
119
|
+
`// Mono framebuffer (Zephyr MONO01: horizontal, MSB-first). ${(w + 7) >> 3} bytes/row x ${h} rows.`,
|
|
120
|
+
`static uint8_t __tc_display_fb[((${w} * ${h}) + 7) / 8];`,
|
|
121
|
+
'// CUTTLEFISH_DISPLAY_END',
|
|
122
|
+
]
|
|
123
|
+
: [
|
|
124
|
+
'// CUTTLEFISH_DISPLAY_BEGIN',
|
|
125
|
+
`static const struct device* __tc_display = DEVICE_DT_GET(DT_NODELABEL(${profile.dtLabel}));`,
|
|
126
|
+
`static uint16_t __tc_display_line[${w}]; // one-row line buffer (rgb565)`,
|
|
127
|
+
'// CUTTLEFISH_DISPLAY_END',
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const helpers = isMono
|
|
131
|
+
? monoHelpers(w, h, blInit)
|
|
132
|
+
: rgb565Helpers(w, blInit);
|
|
133
|
+
|
|
134
|
+
const fontTable = fontTableCpp();
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
includes: ['<zephyr/drivers/display.h>'],
|
|
138
|
+
stateLines,
|
|
139
|
+
helpers,
|
|
140
|
+
fontTable,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Mono (OLED) helpers: a full framebuffer + bit-packing. SSD1306-class panels
|
|
146
|
+
* are page-buffered, so draw ops set bits in the framebuffer and display_flush
|
|
147
|
+
* pushes the whole buffer. color != 0 ⇒ lit.
|
|
148
|
+
*/
|
|
149
|
+
function monoHelpers(w: number, h: number, blInit: string): string {
|
|
150
|
+
const rowBytes = (w + 7) >> 3; // bytes per row (w is byte-aligned for 128-wide panels)
|
|
151
|
+
return `
|
|
152
|
+
// MONO01 pixel packing: byte = (y * ${rowBytes}) + (x >> 3), bit = 0x80 >> (x & 7).
|
|
153
|
+
static inline void __tc_set_pixel(uint16_t x, uint16_t y, uint8_t on) {
|
|
154
|
+
if ((x >= ${w}U) || (y >= ${h}U)) { return; }
|
|
155
|
+
uint16_t idx = static_cast<uint16_t>((static_cast<uint32_t>(y) * ${rowBytes}U) + (x >> 3));
|
|
156
|
+
uint8_t mask = static_cast<uint8_t>(0x80U >> (x & 7U));
|
|
157
|
+
if (on != 0U) {
|
|
158
|
+
__tc_display_fb[idx] = static_cast<uint8_t>(__tc_display_fb[idx] | mask);
|
|
159
|
+
} else {
|
|
160
|
+
__tc_display_fb[idx] = static_cast<uint8_t>(__tc_display_fb[idx] & static_cast<uint8_t>(~mask));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static inline void display_init(void) {
|
|
165
|
+
if (!device_is_ready(__tc_display)) { for (;;) { k_msleep(1000); } }
|
|
166
|
+
${blInit}
|
|
167
|
+
for (uint16_t i = 0; i < static_cast<uint16_t>(sizeof(__tc_display_fb)); i++) { __tc_display_fb[i] = 0U; }
|
|
168
|
+
display_blanking_off(__tc_display);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static inline void display_fill_rect(uint16_t x, uint16_t y, uint16_t rw, uint16_t rh, uint16_t color) {
|
|
172
|
+
uint8_t on = (color != 0U) ? 1U : 0U;
|
|
173
|
+
for (uint16_t row = 0; row < rh; row++) {
|
|
174
|
+
for (uint16_t i = 0; i < rw; i++) {
|
|
175
|
+
__tc_set_pixel(static_cast<uint16_t>(x + i), static_cast<uint16_t>(y + row), on);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static inline void display_draw_rect(uint16_t x, uint16_t y, uint16_t rw, uint16_t rh, uint16_t color) {
|
|
181
|
+
uint8_t on = (color != 0U) ? 1U : 0U;
|
|
182
|
+
for (uint16_t i = 0; i < rw; i++) {
|
|
183
|
+
__tc_set_pixel(static_cast<uint16_t>(x + i), y, on);
|
|
184
|
+
__tc_set_pixel(static_cast<uint16_t>(x + i), static_cast<uint16_t>(y + rh - 1U), on);
|
|
185
|
+
}
|
|
186
|
+
for (uint16_t row = 1U; row < rh - 1U; row++) {
|
|
187
|
+
__tc_set_pixel(x, static_cast<uint16_t>(y + row), on);
|
|
188
|
+
__tc_set_pixel(static_cast<uint16_t>(x + rw - 1U), static_cast<uint16_t>(y + row), on);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
static inline void display_draw_text(uint16_t x, uint16_t y, const char* text, uint16_t color) {
|
|
193
|
+
uint8_t on = (color != 0U) ? 1U : 0U;
|
|
194
|
+
uint16_t cx = x;
|
|
195
|
+
for (const char* p = text; *p != 0; p++) {
|
|
196
|
+
uint8_t uc = static_cast<uint8_t>(*p);
|
|
197
|
+
if (uc >= 128U) { uc = static_cast<uint8_t>(' '); }
|
|
198
|
+
const uint8_t* glyph = &__tc_font5x7[uc][0];
|
|
199
|
+
if (uc >= static_cast<uint8_t>('a') && uc <= static_cast<uint8_t>('z')) {
|
|
200
|
+
glyph = &__tc_font5x7[uc - 32U][0];
|
|
201
|
+
} else if (uc < static_cast<uint8_t>('0')
|
|
202
|
+
|| (uc > static_cast<uint8_t>('9') && uc < static_cast<uint8_t>('A'))
|
|
203
|
+
|| uc > static_cast<uint8_t>('Z')) {
|
|
204
|
+
glyph = &__tc_font5x7[static_cast<uint8_t>(' ')][0];
|
|
205
|
+
}
|
|
206
|
+
for (uint16_t col = 0; col < 5U; col++) {
|
|
207
|
+
uint8_t bits = glyph[col];
|
|
208
|
+
for (uint16_t row = 0; row < 7U; row++) {
|
|
209
|
+
if ((bits & static_cast<uint8_t>(1U << row)) != 0U) {
|
|
210
|
+
__tc_set_pixel(static_cast<uint16_t>(cx + col), static_cast<uint16_t>(y + row), on);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
cx = static_cast<uint16_t>(cx + 6U);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
static inline void display_flush(void) {
|
|
219
|
+
struct display_buffer_descriptor __desc;
|
|
220
|
+
__desc.buf_size = sizeof(__tc_display_fb); // ${rowBytes} * ${h} bytes
|
|
221
|
+
__desc.width = ${w}U;
|
|
222
|
+
__desc.height = ${h}U;
|
|
223
|
+
__desc.pitch = ${rowBytes}U; // bytes per row
|
|
224
|
+
__desc.frame_incomplete = false;
|
|
225
|
+
(void)display_write(__tc_display, 0, 0, &__desc, __tc_display_fb);
|
|
226
|
+
}
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* RGB565 (TFT) helpers: a one-row line buffer; each op streams rows via
|
|
232
|
+
* display_write. No full framebuffer (AGENTS.md rendering guardrails).
|
|
233
|
+
*/
|
|
234
|
+
function rgb565Helpers(w: number, blInit: string): string {
|
|
235
|
+
return `
|
|
111
236
|
// Write a single row of \`rw\` rgb565 pixels at (x,y). Builds the
|
|
112
237
|
// display_buffer_descriptor the Zephyr display_write API requires (rgb565 =
|
|
113
238
|
// 2 bytes/pixel) and pushes the one-row line buffer.
|
|
@@ -178,13 +303,4 @@ static inline void display_flush(void) {
|
|
|
178
303
|
// No-op: writes are immediate via display_write; there is no framebuffer to push.
|
|
179
304
|
}
|
|
180
305
|
`;
|
|
181
|
-
|
|
182
|
-
const fontTable = fontTableCpp();
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
includes: ['<zephyr/drivers/display.h>'],
|
|
186
|
-
stateLines,
|
|
187
|
-
helpers,
|
|
188
|
-
fontTable,
|
|
189
|
-
};
|
|
190
306
|
}
|