@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.
Files changed (56) 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.js +13 -0
  7. package/dist/display/ui-adapter.js +6 -0
  8. package/dist/doctor.d.ts +3 -3
  9. package/dist/doctor.js +56 -29
  10. package/dist/dt-config/kconfig.d.ts +3 -0
  11. package/dist/dt-config/kconfig.js +18 -0
  12. package/dist/dt-config/overlay.js +5 -0
  13. package/dist/framework.manifest.js +37 -14
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +5 -0
  16. package/dist/licenses.d.ts +59 -0
  17. package/dist/licenses.js +347 -0
  18. package/dist/lowering/dac.d.ts +15 -0
  19. package/dist/lowering/dac.js +69 -0
  20. package/dist/lowering/fs.d.ts +16 -0
  21. package/dist/lowering/fs.js +121 -0
  22. package/dist/lowering/hwtimer.d.ts +15 -0
  23. package/dist/lowering/hwtimer.js +84 -0
  24. package/dist/lowering/index.d.ts +4 -1
  25. package/dist/lowering/index.js +12 -3
  26. package/dist/strategy.js +186 -14
  27. package/dist/toolchain/compat.js +10 -1
  28. package/dist/toolchain/env-check.d.ts +93 -0
  29. package/dist/toolchain/env-check.js +190 -0
  30. package/dist/toolchain/scaffold.js +3 -0
  31. package/dist/toolchain/west-discover.d.ts +11 -3
  32. package/dist/toolchain/west-discover.js +80 -6
  33. package/dist/toolchain/west-spawn.js +15 -0
  34. package/package.json +4 -4
  35. package/src/chips/esp32.ts +12 -0
  36. package/src/chips/types.ts +29 -0
  37. package/src/chips/xiao-ble.ts +6 -0
  38. package/src/display/gfx.ts +135 -19
  39. package/src/display/profiles.ts +13 -0
  40. package/src/display/ui-adapter.ts +5 -0
  41. package/src/doctor.ts +77 -56
  42. package/src/dt-config/kconfig.ts +19 -0
  43. package/src/dt-config/overlay.ts +5 -0
  44. package/src/framework.manifest.ts +38 -14
  45. package/src/index.ts +6 -0
  46. package/src/licenses.ts +425 -0
  47. package/src/lowering/dac.ts +82 -0
  48. package/src/lowering/fs.ts +127 -0
  49. package/src/lowering/hwtimer.ts +101 -0
  50. package/src/lowering/index.ts +9 -2
  51. package/src/strategy.ts +180 -14
  52. package/src/toolchain/compat.ts +154 -145
  53. package/src/toolchain/env-check.ts +285 -0
  54. package/src/toolchain/scaffold.ts +3 -0
  55. package/src/toolchain/west-discover.ts +88 -8
  56. package/src/toolchain/west-spawn.ts +15 -0
@@ -0,0 +1,59 @@
1
+ import { type LibraryLicenseEntry, type ReadFile, type ReadDir } from '@typecad/cuttlefish/api/shared';
2
+ export type ZephyrLicensesOutcome = {
3
+ ok: true;
4
+ entries: LibraryLicenseEntry[];
5
+ needsBuild?: boolean;
6
+ } | {
7
+ ok: false;
8
+ reason: 'west-not-found' | 'no-workspace' | 'no-dependencies';
9
+ message: string;
10
+ };
11
+ /**
12
+ * Injected enumeration seams so scanZephyrLicenses is unit-testable without
13
+ * spawning west or touching disk. The production runner (defaultRunner) wires
14
+ * these to discoverWest + westSpawn + fs.
15
+ */
16
+ export interface ZephyrLicensesRunner {
17
+ /** Absolute workspace topdir, or null if cwd is not inside a west workspace. */
18
+ topdir: () => string | null;
19
+ /** west manifest projects `[{ name, abspath }]`, or null if `west list` failed. */
20
+ listModules: () => {
21
+ name: string;
22
+ abspath: string;
23
+ }[] | null;
24
+ /** Discovered $ZEPHYR_BASE (absolute), or undefined. */
25
+ zephyrBase?: string;
26
+ /**
27
+ * The build's compile_commands.json text, or null when no build exists. The
28
+ * project scope uses it to determine which modules the firmware actually
29
+ * links (a module is "linked" iff one of its sources was compiled).
30
+ */
31
+ compileCommands?: () => string | null;
32
+ readFile: ReadFile;
33
+ readdir: ReadDir;
34
+ }
35
+ /**
36
+ * Resolve the Zephyr kernel + west manifest projects to license entries.
37
+ *
38
+ * The Zephyr kernel is always included (when ZEPHYR_BASE is known) — a
39
+ * cuttlefish app always links it via `find_package(Zephyr)`.
40
+ *
41
+ * `all === false` (default, project scope) filters the west modules to those
42
+ * whose sources were compiled in the last build (via `compileCommands`). When
43
+ * no build is available, only the kernel is returned and `needsBuild` is set so
44
+ * the presenter can hint the user to build first. Never throws.
45
+ */
46
+ export declare function scanZephyrLicenses(runner: ZephyrLicensesRunner, all?: boolean): ZephyrLicensesOutcome;
47
+ /** @internal Test-only override of the default runner. Pass null to simulate west-not-found. */
48
+ export declare function __setLicensesRunnerForTest(runner: ZephyrLicensesRunner | null | undefined): void;
49
+ /**
50
+ * `cuttlefish licenses` (Zephyr) presenter. Enumerates the Zephyr kernel +
51
+ * west manifest projects, resolves each one's license, classifies copyleft
52
+ * risk, and renders a sorted table. Warns on unknown licenses; sets
53
+ * process.exitCode under --strict when any strong-copyleft dependency is
54
+ * present. Never calls process.exit().
55
+ *
56
+ * Default scope = only the modules the firmware actually links (from the last
57
+ * `cuttlefish build`); `--all` lists every west manifest module.
58
+ */
59
+ export declare function runLicensesPresenter(strict: boolean, all: boolean): void;
@@ -0,0 +1,347 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/framework-zephyr — Zephyr license scanner
3
+ //
4
+ // Zephyr-specific enumeration + presenter for the `cuttlefish licenses`
5
+ // subcommand. The framework-agnostic SPDX detection engine lives in the shared
6
+ // cuttlefish core (`@typecad/cuttlefish/api/shared`); this module owns only the
7
+ // Zephyr pieces: discovering the west workspace, enumerating the Zephyr kernel
8
+ // + west manifest projects (`west list`), and resolving each one's LICENSE.
9
+ //
10
+ // Scoping mirrors framework-arduino's `--all` distinction:
11
+ // - default (project scope): only the dependencies the firmware ACTUALLY
12
+ // links, derived from the build's `compile_commands.json` (a module is
13
+ // listed iff its sources were compiled). A west manifest carries every
14
+ // vendor HAL/library; almost none are linked by a single project, so the
15
+ // default filters them out. Requires a prior `cuttlefish build` — without
16
+ // one, only the kernel is reported with a hint to build first.
17
+ // - `--all`: every west manifest project (the whole workspace).
18
+ //
19
+ // Mirrors framework-arduino's presenter shape (runLicensesPresenter(strict,
20
+ // all); never calls process.exit(); sets process.exitCode under --strict).
21
+ // ---------------------------------------------------------------------------
22
+ import { spawnSync } from 'node:child_process';
23
+ import { readFileSync, readdirSync } from 'node:fs';
24
+ import * as path from 'node:path';
25
+ import * as ui from '@typecad/cuttlefish/utils/ui';
26
+ import { loadCuttlefishConfig } from '@typecad/cuttlefish/config-loader';
27
+ import { resolveLibraryLicense, RISK_RANK, riskBracket, statusMark, countByRisk, } from '@typecad/cuttlefish/api/shared';
28
+ import { discoverWest } from './toolchain/west-discover.js';
29
+ import { westSpawn } from './toolchain/west-spawn.js';
30
+ // ---------------------------------------------------------------------------
31
+ // Linked-module filtering (project scope)
32
+ // ---------------------------------------------------------------------------
33
+ /** Normalize a path for case-insensitive, separator-agnostic comparison. */
34
+ function norm(p) {
35
+ return p.split('\\').join('/').toLowerCase();
36
+ }
37
+ /**
38
+ * Filter the west manifest to the modules whose sources were compiled in the
39
+ * last build. A module is "linked" iff some compiled translation unit lives
40
+ * under its abspath. If the build data is unparseable, no filtering is applied
41
+ * (degrade to listing all modules rather than reporting nothing).
42
+ */
43
+ function filterLinkedModules(ccText, modules) {
44
+ let arr;
45
+ try {
46
+ arr = JSON.parse(ccText);
47
+ }
48
+ catch {
49
+ return modules;
50
+ }
51
+ if (!Array.isArray(arr))
52
+ return modules;
53
+ // One normalized blob of every compiled source path; membership is then a
54
+ // substring check per module (O(modules) after an O(TUs) join).
55
+ const blob = arr
56
+ .map((e) => norm(typeof e?.file === 'string' ? e.file : ''))
57
+ .join('\n');
58
+ return modules.filter((m) => blob.includes(norm(m.abspath) + '/'));
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // scanZephyrLicenses — enumerate + resolve the workspace dependency set
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * Resolve the Zephyr kernel + west manifest projects to license entries.
65
+ *
66
+ * The Zephyr kernel is always included (when ZEPHYR_BASE is known) — a
67
+ * cuttlefish app always links it via `find_package(Zephyr)`.
68
+ *
69
+ * `all === false` (default, project scope) filters the west modules to those
70
+ * whose sources were compiled in the last build (via `compileCommands`). When
71
+ * no build is available, only the kernel is returned and `needsBuild` is set so
72
+ * the presenter can hint the user to build first. Never throws.
73
+ */
74
+ export function scanZephyrLicenses(runner, all = false) {
75
+ const { readFile, readdir } = runner;
76
+ const entries = [];
77
+ // 1. The Zephyr kernel ($ZEPHYR_BASE). Apache-2.0; resolved from its LICENSE.
78
+ if (runner.zephyrBase) {
79
+ entries.push(resolveLibraryLicense({ name: 'zephyr (kernel)', installDir: runner.zephyrBase }, readFile, readdir,
80
+ // Zephyr's top-level LICENSE is the authoritative source; no manifest
81
+ // license field to consult, and scanning its source headers is noise.
82
+ { subdirs: [] }));
83
+ }
84
+ // 2. west manifest projects.
85
+ const modules = runner.listModules();
86
+ if (modules === null) {
87
+ if (entries.length > 0)
88
+ return { ok: true, entries };
89
+ return {
90
+ ok: false,
91
+ reason: 'no-workspace',
92
+ message: '`west list` did not return a project list (not inside a west workspace?).',
93
+ };
94
+ }
95
+ // Project scope: keep only the modules the build actually linked.
96
+ let projectModules = modules;
97
+ let needsBuild = false;
98
+ if (!all) {
99
+ const cc = runner.compileCommands ? runner.compileCommands() : null;
100
+ if (cc === null) {
101
+ // No build — can't determine the linked set. Report the kernel only and
102
+ // flag it so the presenter prints a "build first" hint.
103
+ projectModules = [];
104
+ needsBuild = true;
105
+ }
106
+ else {
107
+ projectModules = filterLinkedModules(cc, modules);
108
+ }
109
+ }
110
+ for (const m of projectModules) {
111
+ // Skip the Zephyr kernel itself (already added above by name) to avoid a
112
+ // duplicate row when it is also a manifest project.
113
+ if (runner.zephyrBase && path.resolve(m.abspath) === path.resolve(runner.zephyrBase)) {
114
+ continue;
115
+ }
116
+ entries.push(resolveLibraryLicense({ name: m.name, installDir: m.abspath }, readFile, readdir, {
117
+ // Zephyr HAL modules commonly keep their LICENSE under a `zephyr/` or
118
+ // `src/` subdir (e.g. hal_nordic ships zephyr/LICENSE.txt). Check both
119
+ // alongside the root. (Source-header scanning is bounded — it only runs
120
+ // when no LICENSE file is found, capped at 6 files / 120 lines.)
121
+ subdirs: ['zephyr', 'src'],
122
+ }));
123
+ }
124
+ if (entries.length === 0) {
125
+ return {
126
+ ok: false,
127
+ reason: 'no-dependencies',
128
+ message: 'No Zephyr dependencies found to scan.',
129
+ };
130
+ }
131
+ entries.sort((a, b) => {
132
+ const r = RISK_RANK[a.risk] - RISK_RANK[b.risk];
133
+ if (r !== 0)
134
+ return r;
135
+ return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
136
+ });
137
+ return { ok: true, entries, needsBuild };
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // Default runner — wires seams to discoverWest + westSpawn + fs
141
+ // ---------------------------------------------------------------------------
142
+ /**
143
+ * Find the build's compile_commands.json under cwd. Cuttlefish's Zephyr project
144
+ * root is the transpile output dir (e.g. `<cwd>/src/out`), so the build dir is
145
+ * `<root>/build` — not necessarily `<cwd>/build`. Check the common layouts,
146
+ * then fall back to a bounded search (skipping node_modules/.git/dist).
147
+ */
148
+ function findCompileCommandsText(readFile, readdir) {
149
+ const direct = [
150
+ path.join(process.cwd(), 'build', 'compile_commands.json'),
151
+ path.join(process.cwd(), 'out', 'build', 'compile_commands.json'),
152
+ path.join(process.cwd(), 'src', 'out', 'build', 'compile_commands.json'),
153
+ ];
154
+ for (const c of direct) {
155
+ const text = readFile(c);
156
+ if (text)
157
+ return text;
158
+ }
159
+ // Bounded recursive search for a build/compile_commands.json.
160
+ const isCc = (p) => {
161
+ const n = norm(p);
162
+ return n.endsWith('/build/compile_commands.json') || n.endsWith('\\build\\compile_commands.json');
163
+ };
164
+ let found;
165
+ const seen = new Set();
166
+ const walk = (dir, depth) => {
167
+ if (found || depth > 4 || seen.has(dir))
168
+ return;
169
+ seen.add(dir);
170
+ let entries;
171
+ try {
172
+ entries = readdir(dir);
173
+ }
174
+ catch {
175
+ return;
176
+ }
177
+ for (const e of entries) {
178
+ const full = path.join(dir, e);
179
+ if (e === 'compile_commands.json' && isCc(full)) {
180
+ found = full;
181
+ return;
182
+ }
183
+ }
184
+ for (const e of entries) {
185
+ if (found)
186
+ return;
187
+ if (e === 'node_modules' || e === '.git' || e === 'dist' || e.startsWith('.'))
188
+ continue;
189
+ walk(path.join(dir, e), depth + 1);
190
+ }
191
+ };
192
+ walk(process.cwd(), 0);
193
+ return found ? (readFile(found) ?? null) : null;
194
+ }
195
+ function defaultRunner() {
196
+ const install = discoverWest();
197
+ if (!install)
198
+ return null;
199
+ const zephyrBase = install.zephyrBase;
200
+ // `west topdir` prints the workspace root (one path line). Returns null when
201
+ // cwd is not inside a west workspace (west exits non-zero).
202
+ const topdir = () => {
203
+ try {
204
+ const inv = westSpawn(['topdir'], { encoding: 'utf8', timeout: 15_000 });
205
+ const r = spawnSync(inv.command, inv.args, inv.options);
206
+ const out = typeof r.stdout === 'string' ? r.stdout.trim() : '';
207
+ return r.status === 0 && out ? out : null;
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ };
213
+ // `west list --format '{name}\t{abspath}'` → one project per line. Returns
214
+ // null on any spawn failure.
215
+ const listModules = () => {
216
+ try {
217
+ const inv = westSpawn(['list', '--format', '{name}\t{abspath}'], {
218
+ encoding: 'utf8',
219
+ timeout: 30_000,
220
+ });
221
+ const r = spawnSync(inv.command, inv.args, inv.options);
222
+ if (r.status !== 0)
223
+ return null;
224
+ const out = typeof r.stdout === 'string' ? r.stdout : '';
225
+ const mods = [];
226
+ for (const line of out.split(/\r?\n/)) {
227
+ const trimmed = line.trim();
228
+ if (!trimmed)
229
+ continue;
230
+ const [name, abspath] = trimmed.split('\t');
231
+ if (name && abspath)
232
+ mods.push({ name, abspath });
233
+ }
234
+ return mods;
235
+ }
236
+ catch {
237
+ return null;
238
+ }
239
+ };
240
+ const readFile = (p) => {
241
+ try {
242
+ return readFileSync(p, 'utf8');
243
+ }
244
+ catch {
245
+ return undefined;
246
+ }
247
+ };
248
+ const readdir = (d) => {
249
+ try {
250
+ return readdirSync(d);
251
+ }
252
+ catch {
253
+ return [];
254
+ }
255
+ };
256
+ return {
257
+ topdir,
258
+ listModules,
259
+ zephyrBase,
260
+ compileCommands: () => findCompileCommandsText(readFile, readdir),
261
+ readFile,
262
+ readdir,
263
+ };
264
+ }
265
+ // ---------------------------------------------------------------------------
266
+ // CLI presenter
267
+ // ---------------------------------------------------------------------------
268
+ let testRunner;
269
+ /** @internal Test-only override of the default runner. Pass null to simulate west-not-found. */
270
+ export function __setLicensesRunnerForTest(runner) {
271
+ testRunner = runner;
272
+ }
273
+ /**
274
+ * `cuttlefish licenses` (Zephyr) presenter. Enumerates the Zephyr kernel +
275
+ * west manifest projects, resolves each one's license, classifies copyleft
276
+ * risk, and renders a sorted table. Warns on unknown licenses; sets
277
+ * process.exitCode under --strict when any strong-copyleft dependency is
278
+ * present. Never calls process.exit().
279
+ *
280
+ * Default scope = only the modules the firmware actually links (from the last
281
+ * `cuttlefish build`); `--all` lists every west manifest module.
282
+ */
283
+ export function runLicensesPresenter(strict, all) {
284
+ ui.printHeader();
285
+ ui.printStep(all
286
+ ? 'Checking licenses for every west manifest module'
287
+ : 'Checking licenses for this Zephyr project (linked dependencies only)');
288
+ // Surface the config (informational). Best-effort: a missing/malformed config
289
+ // never blocks the license scan.
290
+ let buildTarget;
291
+ try {
292
+ buildTarget = loadCuttlefishConfig(process.cwd())?.buildTarget;
293
+ }
294
+ catch {
295
+ /* best-effort */
296
+ }
297
+ if (buildTarget) {
298
+ ui.printInfo(`Board target ... ${buildTarget}`);
299
+ }
300
+ const runner = testRunner !== undefined ? testRunner : defaultRunner();
301
+ if (!runner) {
302
+ ui.printError('west ............. NOT FOUND');
303
+ ui.printInfo(" → install west (pip install west) or run the typeCAD Zephyr installer.");
304
+ process.exitCode = 1;
305
+ return;
306
+ }
307
+ const result = scanZephyrLicenses(runner, all);
308
+ if (!result.ok) {
309
+ if (result.reason === 'no-workspace' || result.reason === 'no-dependencies') {
310
+ ui.printInfo(`(${result.message})`);
311
+ }
312
+ return;
313
+ }
314
+ // Project scope with no build: only the kernel is reported. Hint the user to
315
+ // build (which records which modules actually link) or use --all.
316
+ if (result.needsBuild) {
317
+ ui.printInfo('(no build found — only the Zephyr kernel is shown. Run `cuttlefish build` to scope ' +
318
+ 'this report to the modules your firmware actually links, or use `cuttlefish licenses --all` ' +
319
+ 'for every west module.)');
320
+ }
321
+ const counts = countByRisk(result.entries);
322
+ for (const e of result.entries) {
323
+ if (e.risk === 'unknown') {
324
+ ui.printWarning(`${e.name} .................. UNKNOWN`);
325
+ }
326
+ else {
327
+ ui.printInfo(`${e.name} .................. ${e.spdx ?? 'UNKNOWN'}${riskBracket(e.risk)}${statusMark(e.risk)}`);
328
+ }
329
+ }
330
+ ui.printSuccess(`${counts.permissive} permissive, ${counts['weak-copyleft']} weak copyleft, ` +
331
+ `${counts['strong-copyleft']} strong copyleft, ${counts.unknown} unknown`);
332
+ const unknowns = result.entries.filter((e) => e.risk === 'unknown');
333
+ if (unknowns.length > 0) {
334
+ ui.printWarning(`License could not be determined for ${unknowns.length} ${unknowns.length === 1 ? 'dependency' : 'dependencies'}:`);
335
+ for (const u of unknowns) {
336
+ ui.printInfo(` ${u.name} (check LICENSE in ${u.path})`);
337
+ }
338
+ }
339
+ const strongCopyleft = result.entries.filter((e) => e.risk === 'strong-copyleft');
340
+ if (strongCopyleft.length > 0) {
341
+ ui.printWarning(`${strongCopyleft.length} ${strongCopyleft.length === 1 ? 'dependency carries' : 'dependencies carry'} strong-copyleft terms — review before shipping.`);
342
+ }
343
+ // --strict fails the build on any unknown OR strong-copyleft dependency.
344
+ if ((unknowns.length > 0 || strongCopyleft.length > 0) && strict) {
345
+ process.exitCode = 1;
346
+ }
347
+ }
@@ -0,0 +1,15 @@
1
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
2
+ import type { ZephyrChipDescriptor } from '../chips/types.js';
3
+ /**
4
+ * Emit the DAC device handle. Called from shimLines when the program uses the
5
+ * DAC (the chip must declare a `dac` entry, or nothing is emitted).
6
+ */
7
+ export declare function dacInitLines(chip: ZephyrChipDescriptor): string[];
8
+ /**
9
+ * Resolve a HAL dac.* op to Zephyr C++.
10
+ * Returns `{ code }` for statement ops.
11
+ */
12
+ export declare function lowerDac(op: HALOpIR, chip: ZephyrChipDescriptor): {
13
+ code?: string;
14
+ expression?: string;
15
+ };
@@ -0,0 +1,69 @@
1
+ // ---------------------------------------------------------------------------
2
+ // DAC lowering — Zephyr DAC driver via dac_channel_setup / dac_write_value
3
+ //
4
+ // Targets with a DAC (ESP32: 2× 8-bit channels on GPIO25/26) declare a `dac`
5
+ // entry in the chip descriptor: the DT device node label + the pin→channel map.
6
+ // The lowering emits a `dac_channel_setup` (lazy, on first write) + a
7
+ // `dac_write_value` against DEVICE_DT_GET(DT_NODELABEL(<device>)).
8
+ //
9
+ // Targets without a DAC (nRF52840, ESP32-S3) omit `dac`; usage there lowers to a
10
+ // comment and profileDiagnostics flags it (mirror of the ADC pin-validity gate).
11
+ // ---------------------------------------------------------------------------
12
+ /** Look up a DAC channel by HAL pin number. */
13
+ function findDacChannel(chip, pin) {
14
+ return chip.dac?.channels.find((c) => c.pin === pin);
15
+ }
16
+ /**
17
+ * Emit the DAC device handle. Called from shimLines when the program uses the
18
+ * DAC (the chip must declare a `dac` entry, or nothing is emitted).
19
+ */
20
+ export function dacInitLines(chip) {
21
+ if (!chip.dac)
22
+ return [];
23
+ return [
24
+ '// CUTTLEFISH_DAC_BEGIN',
25
+ `static const struct device* __tc_dac_dev = DEVICE_DT_GET(DT_NODELABEL(${chip.dac.device}));`,
26
+ '// CUTTLEFISH_DAC_END',
27
+ ];
28
+ }
29
+ /**
30
+ * Resolve a HAL dac.* op to Zephyr C++.
31
+ * Returns `{ code }` for statement ops.
32
+ */
33
+ export function lowerDac(op, chip) {
34
+ const o = op;
35
+ // No DAC on this target (e.g. nRF52840). Return a comment so the resolver
36
+ // reports non-undefined (the manifest validator's probe sends pin:25 on the
37
+ // default chip, which has no DAC). profileDiagnostics flags real misuse.
38
+ if (!chip.dac) {
39
+ return { code: `/* dac on pin ${o.pin}: no DAC on ${chip.id} */` };
40
+ }
41
+ const ch = findDacChannel(chip, o.pin);
42
+ if (!ch) {
43
+ const valid = [...chip.dac.channels].map((c) => c.pin).sort((a, b) => a - b).join(', ');
44
+ return {
45
+ code: `/* dac on pin ${o.pin}: not a DAC pin on ${chip.id} (valid: ${valid}) */`,
46
+ };
47
+ }
48
+ switch (op.operation) {
49
+ case 'dac.write': {
50
+ // Lazy one-time channel setup on first write (the HAL DAC surface has no
51
+ // begin()), then output the value. Arduino analogWrite is 0–255 against
52
+ // the channel's resolution; dac_write_value takes the raw code.
53
+ return {
54
+ code: [
55
+ `{ static bool __tc_dac_done = false;`,
56
+ ` if (!__tc_dac_done) {`,
57
+ ` static const struct dac_channel_cfg __tc_dac_cfg = { .channel_id = ${ch.channel}, .resolution = ${ch.resolution} };`,
58
+ ` (void)dac_channel_setup(__tc_dac_dev, &__tc_dac_cfg);`,
59
+ ` __tc_dac_done = true;`,
60
+ ` }`,
61
+ ` (void)dac_write_value(__tc_dac_dev, ${ch.channel}, ${o.value}); }`,
62
+ ].join(' '),
63
+ };
64
+ }
65
+ default:
66
+ throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
67
+ `Open an issue or use rawCpp() to emit it manually.`);
68
+ }
69
+ }
@@ -0,0 +1,16 @@
1
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
2
+ /**
3
+ * Emit the littlefs mount + the typed helper API the lowering calls into.
4
+ * Called from shimLines when the program uses the filesystem. The helpers are
5
+ * `static` so multiple TUs that carry the shim (guarded by the include guard)
6
+ * do not collide.
7
+ */
8
+ export declare function fsInitLines(): string[];
9
+ /**
10
+ * Resolve a HAL fs.* op to Zephyr C++.
11
+ * Returns `{ code }` — each op's code is the body of the HAL FS class method.
12
+ */
13
+ export declare function lowerFs(op: HALOpIR): {
14
+ code?: string;
15
+ expression?: string;
16
+ };
@@ -0,0 +1,121 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Filesystem lowering — Zephyr FS API (littlefs on the storage partition)
3
+ //
4
+ // HAL `fs.*` (begin/read_text/write_text/exists/remove) maps onto Zephyr's
5
+ // <zephyr/fs/fs.h>. A littlefs filesystem is mounted at "/lfs" on the board's
6
+ // `storage_partition` fixed-partition (the same partition the ZMS settings
7
+ // backend uses for preferences — a program should not use both surfaces at
8
+ // once without dedicating separate partitions). The mount is lazy: it formats
9
+ // + remounts on first use when the partition is unformatted.
10
+ //
11
+ // The HAL paths are treated as paths WITHIN the filesystem: the shim joins the
12
+ // mount point, so fs.read_text("/data.txt") reads "/lfs/data.txt". This keeps
13
+ // the mount point an implementation detail.
14
+ //
15
+ // Requires Kconfig CONFIG_FILE_SYSTEM + CONFIG_FILE_SYSTEM_LITTLEFS (+ FLASH /
16
+ // FLASH_MAP) — emitted by the scaffold when the program uses fs.* — and the
17
+ // storage_partition node (added by the overlay).
18
+ // ---------------------------------------------------------------------------
19
+ /**
20
+ * Emit the littlefs mount + the typed helper API the lowering calls into.
21
+ * Called from shimLines when the program uses the filesystem. The helpers are
22
+ * `static` so multiple TUs that carry the shim (guarded by the include guard)
23
+ * do not collide.
24
+ */
25
+ export function fsInitLines() {
26
+ return [
27
+ '// CUTTLEFISH_FS_BEGIN',
28
+ 'static struct fs_mount_t __tc_fs_mp;',
29
+ 'static bool __tc_fs_mounted = false;',
30
+ '',
31
+ 'FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(__tc_fs_data);',
32
+ 'static struct fs_mount_t __tc_fs_mp_init = {',
33
+ ' .type = FS_LITTLEFS,',
34
+ ' .fs_data = &__tc_fs_data,',
35
+ ' .storage_dev = (void *)FIXED_PARTITION_ID(storage_partition),',
36
+ ' .mnt_point = "/lfs",',
37
+ '};',
38
+ '',
39
+ 'static void __tc_fs_ensure_mount(void) {',
40
+ ' if (__tc_fs_mounted) return;',
41
+ ' __tc_fs_mp = __tc_fs_mp_init;',
42
+ ' int rc = fs_mount(&__tc_fs_mp);',
43
+ ' if (rc == -ENODATA || rc == -EINVAL) {',
44
+ ' // Unformatted partition — format it for littlefs, then remount.',
45
+ ' (void)fs_mkfs(FS_LITTLEFS, static_cast<uintptr_t>(FIXED_PARTITION_ID(storage_partition)), NULL, 0);',
46
+ ' (void)fs_mount(&__tc_fs_mp);',
47
+ ' }',
48
+ ' __tc_fs_mounted = true;',
49
+ '}',
50
+ '',
51
+ '// Join the mount point with the user path (paths are within the FS).',
52
+ 'static const char* __tc_fs_path(const char* p) {',
53
+ ' static char buf[128];',
54
+ ' if (!p) return "/lfs";',
55
+ ' const char* s = (p[0] == \'/\') ? p + 1 : p;',
56
+ ' (void)snprintf(buf, sizeof(buf), "/lfs/%s", s);',
57
+ ' return buf;',
58
+ '}',
59
+ '',
60
+ 'static bool __tc_fs_begin(void) { __tc_fs_ensure_mount(); return true; }',
61
+ '',
62
+ 'static const char* __tc_fs_read_text(const char* path) {',
63
+ ' __tc_fs_ensure_mount();',
64
+ ' static char buf[256];',
65
+ ' buf[0] = 0;',
66
+ ' struct fs_file_t f;',
67
+ ' fs_file_t_init(&f);',
68
+ ' if (fs_open(&f, __tc_fs_path(path), FS_O_READ) == 0) {',
69
+ ' (void)fs_read(&f, buf, sizeof(buf) - 1);',
70
+ ' buf[sizeof(buf) - 1] = 0;',
71
+ ' (void)fs_close(&f);',
72
+ ' }',
73
+ ' return buf;',
74
+ '}',
75
+ '',
76
+ 'static void __tc_fs_write_text(const char* path, const char* content) {',
77
+ ' __tc_fs_ensure_mount();',
78
+ ' struct fs_file_t f;',
79
+ ' fs_file_t_init(&f);',
80
+ ' if (fs_open(&f, __tc_fs_path(path), FS_O_WRITE | FS_O_CREATE | FS_O_TRUNC) == 0) {',
81
+ ' (void)fs_write(&f, content, (content ? strlen(content) : 0));',
82
+ ' (void)fs_close(&f);',
83
+ ' }',
84
+ '}',
85
+ '',
86
+ 'static bool __tc_fs_exists(const char* path) {',
87
+ ' __tc_fs_ensure_mount();',
88
+ ' struct fs_dirent ent;',
89
+ ' return fs_stat(__tc_fs_path(path), &ent) == 0;',
90
+ '}',
91
+ '',
92
+ 'static bool __tc_fs_remove(const char* path) {',
93
+ ' __tc_fs_ensure_mount();',
94
+ ' return fs_unlink(__tc_fs_path(path)) == 0;',
95
+ '}',
96
+ '// CUTTLEFISH_FS_END',
97
+ ];
98
+ }
99
+ /**
100
+ * Resolve a HAL fs.* op to Zephyr C++.
101
+ * Returns `{ code }` — each op's code is the body of the HAL FS class method.
102
+ */
103
+ export function lowerFs(op) {
104
+ const o = op;
105
+ switch (op.operation) {
106
+ case 'fs.begin':
107
+ // No payload (the op carries only its discriminator).
108
+ return { code: 'return __tc_fs_begin();' };
109
+ case 'fs.read_text':
110
+ return { code: `return __tc_fs_read_text(${o.path});` };
111
+ case 'fs.write_text':
112
+ return { code: `__tc_fs_write_text(${o.path}, ${o.content});` };
113
+ case 'fs.exists':
114
+ return { code: `return __tc_fs_exists(${o.path});` };
115
+ case 'fs.remove':
116
+ return { code: `return __tc_fs_remove(${o.path});` };
117
+ default:
118
+ throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
119
+ `Open an issue or use rawCpp() to emit it manually.`);
120
+ }
121
+ }
@@ -0,0 +1,15 @@
1
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
2
+ import type { ZephyrChipDescriptor } from '../chips/types.js';
3
+ /**
4
+ * Emit the per-instance counter device handles + frequency/callback state.
5
+ * Called from shimLines when the program uses hardware timers.
6
+ */
7
+ export declare function hwtimerInitLines(chip: ZephyrChipDescriptor): string[];
8
+ /**
9
+ * Resolve a HAL hwtimer.* op to Zephyr C++.
10
+ * Returns `{ code }` for statement ops.
11
+ */
12
+ export declare function lowerHwtimer(op: HALOpIR, chip: ZephyrChipDescriptor): {
13
+ code?: string;
14
+ expression?: string;
15
+ };