@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
@@ -104,9 +104,13 @@ export default defineFrameworkManifest({
104
104
  },
105
105
  },
106
106
  dac: {
107
- supported: false,
108
- unsupportedReason: 'No DAC lowering implemented in the framework (not applicable on nRF52840; ESP32 variants with DAC not yet wired).',
109
- ops: { 'dac.write': 'unsupported' },
107
+ // ESP32 DAC (2× 8-bit channels on GPIO25/26) via the Zephyr DAC driver
108
+ // (dac_channel_setup + dac_write_value). nRF52840 / ESP32-S3 have no DAC;
109
+ // usage there lowers to a comment and profileDiagnostics flags it
110
+ // (zephyr-dac-pin-unavailable).
111
+ supported: true,
112
+ partialCoverage: true,
113
+ ops: { 'dac.write': 'supported' },
110
114
  },
111
115
  interrupts: {
112
116
  supported: true,
@@ -276,9 +280,14 @@ export default defineFrameworkManifest({
276
280
  },
277
281
  display: {
278
282
  supported: true,
279
- partialCoverage: false,
280
- unsupportedReason: undefined,
281
- drivers: ['ili9341-zephyr', 'st7796-zephyr'],
283
+ partialCoverage: true,
284
+ // Partial: mono profiles (ssd1306-zephyr) drive display.* ops via the
285
+ // direct GFX runtime only — no CuttlefishGFX UI rendering path. The
286
+ // ILI9341 UI adapter shares the ST7796S direct-drive transport with a
287
+ // per-controller init table (16-bit RGB565 wire format); hardware-tuned
288
+ // on ST7796S only. E-ink panels are out of scope at this time.
289
+ unsupportedReason: 'Mono panels (ssd1306) are direct-op only (no UI rendering); ili9341 UI path is ported but not yet hardware-verified; e-ink is out of scope at this time.',
290
+ drivers: ['ili9341-zephyr', 'st7796-zephyr', 'ssd1306-zephyr'],
282
291
  colorFormat: 'rgb565',
283
292
  ops: {
284
293
  'display.init': 'supported',
@@ -356,10 +365,17 @@ export default defineFrameworkManifest({
356
365
  },
357
366
  },
358
367
  fs: {
359
- supported: false,
360
- unsupportedReason: 'No filesystem lowering on Zephyr (Zephyr has its own FS API; not wired).',
368
+ // littlefs on the board's storage_partition, via <zephyr/fs/fs.h>. The
369
+ // shim mounts at /lfs lazily (formats on first use) and the HAL paths are
370
+ // treated as paths within the filesystem. Requires CONFIG_FILE_SYSTEM +
371
+ // CONFIG_FILE_SYSTEM_LITTLEFS (emitted by the scaffold when fs.* is used)
372
+ // and the storage_partition node.
373
+ supported: true,
361
374
  partialCoverage: false,
362
- ops: unsupportedOps('fs.'),
375
+ ops: {
376
+ 'fs.begin': 'supported', 'fs.read_text': 'supported', 'fs.write_text': 'supported',
377
+ 'fs.exists': 'supported', 'fs.remove': 'supported',
378
+ },
363
379
  },
364
380
  mdns: {
365
381
  supported: false,
@@ -396,10 +412,17 @@ export default defineFrameworkManifest({
396
412
  ops: { 'temp.read': 'unsupported' },
397
413
  },
398
414
  hwtimer: {
399
- supported: false,
400
- unsupportedReason: 'No hardware-timer lowering on Zephyr (timers are handled via the k_timer polyfill, not hwtimer.*).',
401
- partialCoverage: false,
402
- ops: unsupportedOps('hwtimer.'),
415
+ // Hardware timers via the Zephyr counter driver (<zephyr/drivers/counter.h>).
416
+ // set_frequency top value (counter_freq/hz) + on_overflow callback;
417
+ // start arms both; stop halts. A chip declares its free counters
418
+ // (e.g. nRF RTC1; RTC0 is kernel-owned). This is distinct from the JS
419
+ // setInterval/setTimeout k_timer polyfill, which is unaffected.
420
+ supported: true,
421
+ partialCoverage: true,
422
+ ops: {
423
+ 'hwtimer.set_frequency': 'supported', 'hwtimer.on_overflow': 'supported',
424
+ 'hwtimer.start': 'supported', 'hwtimer.stop': 'supported',
425
+ },
403
426
  },
404
427
  capacitive: {
405
428
  // FT6336U capacitive touch is handled via the strategy-owned touch adapter
@@ -466,6 +489,9 @@ export default defineFrameworkManifest({
466
489
  polyfills: {
467
490
  emitted: [
468
491
  { id: 'cuttlefish_halt', domain: 'standard', notes: 'Mapped to a k_msleep halt loop (exceptions disabled)' },
492
+ { id: 'wiring_compat', domain: 'standard', notes: 'HIGH/LOW/digitalRead/etc. macros routing Wiring tokens (referenced unconditionally by the UI runtime header) to the __tc_gpio_* helpers' },
493
+ { id: 'string_methods', domain: 'embedded', notes: 'STL-free __tc_* string helpers (const char*, inline ASCII case conv, <cstring> only)' },
494
+ { id: 'static_array', domain: 'embedded', notes: 'STL-free __tc_StaticArray<T,N> wrapper for no-<vector> mutated/struct array literals' },
469
495
  { id: 'timer_methods', domain: 'embedded', notes: 'k_timer + k_work pool (system workqueue); callbacks run in thread context' },
470
496
  { id: 'async_runtime', domain: 'embedded', notes: 'Heap-free static Promise/microtask runtime (generateStaticAsyncRuntime), pumped in loop()' },
471
497
  ],
@@ -473,7 +499,7 @@ export default defineFrameworkManifest({
473
499
  },
474
500
  toolchain: {
475
501
  backend: 'west',
476
- operations: { prepare: true, compile: true, upload: true, monitor: true },
502
+ operations: { prepare: true, compile: true, upload: true, monitor: true, debug: true },
477
503
  },
478
504
  libraryResolution: {
479
505
  isFrameworkLibraryImport: false,
@@ -517,9 +543,9 @@ export default defineFrameworkManifest({
517
543
  // pure string-snapshot tests (no hardware); they are the safety net that
518
544
  // catches regressions like silent pull-resistor / interrupt no-ops.
519
545
  halResolutionTests: [
520
- 'adc', 'ble', 'board', 'dac', 'gpio', 'http', 'i2c', 'interrupts', 'mqtt',
521
- 'power', 'preferences', 'pulse', 'pwm', 'random', 'spi', 'timing', 'tone',
522
- 'uart', 'wdt', 'worker',
546
+ 'adc', 'ble', 'board', 'dac', 'fs', 'gpio', 'http', 'hwtimer', 'i2c',
547
+ 'interrupts', 'mqtt', 'power', 'preferences', 'pulse', 'pwm', 'random',
548
+ 'spi', 'timing', 'tone', 'uart', 'wdt', 'worker',
523
549
  ],
524
550
  },
525
551
  // Declared compatibility range for the installed Zephyr RTOS. The framework's
@@ -533,4 +559,7 @@ export default defineFrameworkManifest({
533
559
  // `cuttlefish doctor` prints the detected Zephyr version + compat result and
534
560
  // previews how the configured board target resolves for that version.
535
561
  doctor: { available: true },
562
+ // `cuttlefish licenses` enumerates the Zephyr kernel + west manifest projects
563
+ // and resolves each one's SPDX license (mirrors framework-arduino).
564
+ licenses: { available: true },
536
565
  });
package/dist/index.d.ts CHANGED
@@ -2,5 +2,6 @@ export { ZephyrStrategy as FrameworkStrategy } from './strategy.js';
2
2
  export { ZephyrStrategy } from './strategy.js';
3
3
  export { Toolchain } from './toolchain/index.js';
4
4
  export { runDoctor as doctor } from './doctor.js';
5
+ export { runLicensesPresenter as licenses } from './licenses.js';
5
6
  export { chipForTarget, setActiveChip, getActiveChip, XIAO_BLE, } from './chips/index.js';
6
7
  export type { ZephyrChipDescriptor, ZephyrGpioDtSpec, } from './chips/types.js';
package/dist/index.js CHANGED
@@ -13,5 +13,10 @@ export { Toolchain } from './toolchain/index.js';
13
13
  // under the dispatcher-facing alias `doctor` so the loader picks it up as
14
14
  // mod.doctor (see framework-package.ts).
15
15
  export { runDoctor as doctor } from './doctor.js';
16
+ // `cuttlefish licenses` — enumerate the Zephyr kernel + west manifest projects
17
+ // and resolve each one's SPDX license. Re-exported under the dispatcher-facing
18
+ // alias `licenses` so the loader picks it up as mod.licenses (see
19
+ // framework-package.ts). Mirrors framework-arduino's presenter.
20
+ export { runLicensesPresenter as licenses } from './licenses.js';
16
21
  // Chip descriptor registry (for downstream tooling / additional boards).
17
22
  export { chipForTarget, setActiveChip, getActiveChip, XIAO_BLE, } from './chips/index.js';
@@ -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
+ }
@@ -388,7 +388,9 @@ export function lowerBle(op) {
388
388
  case 'ble.on_read':
389
389
  // Store the typed read handler as void*; __tc_ble_attr_read casts it back
390
390
  // to the right signature based on the char's type field. reinterpret_cast
391
- // (not a C-style cast) keeps this AUTOSAR-compliant under --autosar=strict.
391
+ // (not a C-style cast) avoids M5-0-7, but M5-0-10 still flags it — the
392
+ // whole type-erased table is covered by a knownPatterns deviation on
393
+ // that rule (see rules.ts, "BLE type-erased callback table").
392
394
  return { code: `__tc_ble.on_read[__tc_ble.current_char] = reinterpret_cast<void*>(${s(o.handler)});` };
393
395
  case 'ble.on_write':
394
396
  return { code: `__tc_ble.on_write[__tc_ble.current_char] = (${s(o.handler)});` };
@@ -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
+ };