@typecad/framework-zephyr 1.0.0-alpha.18 → 1.0.0-alpha.19

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 (114) hide show
  1. package/dist/audit.d.ts +111 -0
  2. package/dist/audit.js +416 -0
  3. package/dist/boardgen.js +63 -5
  4. package/dist/chips/resolve.js +20 -0
  5. package/dist/chips/types.d.ts +21 -0
  6. package/dist/display/bindings.d.ts +55 -0
  7. package/dist/display/bindings.js +316 -0
  8. package/dist/display/gfx.d.ts +2 -3
  9. package/dist/display/gfx.js +166 -154
  10. package/dist/display/index.js +20 -1
  11. package/dist/display/mipi-dbi-host.d.ts +9 -0
  12. package/dist/display/mipi-dbi-host.js +174 -0
  13. package/dist/display/profiles.d.ts +109 -4
  14. package/dist/display/profiles.js +270 -7
  15. package/dist/display/touch-adapter.js +118 -48
  16. package/dist/display/ui-adapter-eink.d.ts +2 -0
  17. package/dist/display/ui-adapter-eink.js +4 -0
  18. package/dist/display/ui-adapter-gray.d.ts +8 -0
  19. package/dist/display/ui-adapter-gray.js +170 -0
  20. package/dist/display/ui-adapter-mono.d.ts +13 -0
  21. package/dist/display/ui-adapter-mono.js +230 -0
  22. package/dist/display/ui-adapter-native.d.ts +10 -0
  23. package/dist/display/ui-adapter-native.js +295 -0
  24. package/dist/display/ui-adapter-shared.d.ts +11 -0
  25. package/dist/display/ui-adapter-shared.js +122 -0
  26. package/dist/display/ui-adapter.js +53 -101
  27. package/dist/dt-config/kconfig.d.ts +62 -1
  28. package/dist/dt-config/kconfig.js +123 -26
  29. package/dist/dt-config/overlay.d.ts +13 -0
  30. package/dist/dt-config/overlay.js +431 -16
  31. package/dist/framework.manifest.d.ts +9 -3
  32. package/dist/framework.manifest.js +123 -10
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +9 -1
  35. package/dist/licenses.js +5 -84
  36. package/dist/lowering/can.d.ts +25 -0
  37. package/dist/lowering/can.js +97 -0
  38. package/dist/lowering/clock.d.ts +17 -0
  39. package/dist/lowering/clock.js +58 -0
  40. package/dist/lowering/hid.d.ts +27 -0
  41. package/dist/lowering/hid.js +244 -0
  42. package/dist/lowering/i2c.d.ts +8 -0
  43. package/dist/lowering/i2c.js +140 -0
  44. package/dist/lowering/i2s.d.ts +27 -0
  45. package/dist/lowering/i2s.js +98 -0
  46. package/dist/lowering/index.d.ts +9 -1
  47. package/dist/lowering/index.js +25 -1
  48. package/dist/lowering/matrix.d.ts +15 -0
  49. package/dist/lowering/matrix.js +63 -0
  50. package/dist/lowering/power.d.ts +10 -0
  51. package/dist/lowering/power.js +35 -0
  52. package/dist/lowering/pwm.js +25 -0
  53. package/dist/lowering/sensor.d.ts +2 -2
  54. package/dist/lowering/sensor.js +8 -4
  55. package/dist/lowering/strip.d.ts +16 -0
  56. package/dist/lowering/strip.js +70 -0
  57. package/dist/lowering/thread.js +5 -1
  58. package/dist/lowering/trace.d.ts +44 -0
  59. package/dist/lowering/trace.js +239 -0
  60. package/dist/lowering/uart.js +6 -1
  61. package/dist/lowering/usb.d.ts +3 -1
  62. package/dist/lowering/usb.js +6 -3
  63. package/dist/sbom.d.ts +181 -0
  64. package/dist/sbom.js +901 -0
  65. package/dist/strategy.d.ts +60 -2
  66. package/dist/strategy.js +485 -25
  67. package/dist/toolchain/index.d.ts +12 -1
  68. package/dist/toolchain/index.js +115 -27
  69. package/dist/toolchain/scaffold.d.ts +4 -1
  70. package/dist/toolchain/scaffold.js +67 -3
  71. package/dist/west-inventory.d.ts +25 -0
  72. package/dist/west-inventory.js +97 -0
  73. package/package.json +3 -3
  74. package/src/audit.ts +529 -0
  75. package/src/boardgen.ts +54 -5
  76. package/src/chips/resolve.ts +21 -0
  77. package/src/chips/types.ts +576 -567
  78. package/src/display/bindings.ts +347 -0
  79. package/src/display/gfx.ts +318 -306
  80. package/src/display/index.ts +87 -70
  81. package/src/display/mipi-dbi-host.ts +183 -0
  82. package/src/display/profiles.ts +458 -139
  83. package/src/display/touch-adapter.ts +274 -204
  84. package/src/display/ui-adapter-eink.ts +13 -0
  85. package/src/display/ui-adapter-gray.ts +178 -0
  86. package/src/display/ui-adapter-mono.ts +238 -0
  87. package/src/display/ui-adapter-native.ts +304 -0
  88. package/src/display/ui-adapter-shared.ts +125 -0
  89. package/src/display/ui-adapter.ts +51 -100
  90. package/src/dt-config/kconfig.ts +647 -511
  91. package/src/dt-config/overlay.ts +433 -16
  92. package/src/framework.manifest.ts +131 -10
  93. package/src/index.ts +11 -1
  94. package/src/licenses.ts +5 -84
  95. package/src/lowering/can.ts +140 -0
  96. package/src/lowering/clock.ts +91 -0
  97. package/src/lowering/hid.ts +261 -0
  98. package/src/lowering/i2c.ts +146 -0
  99. package/src/lowering/i2s.ts +143 -0
  100. package/src/lowering/index.ts +18 -1
  101. package/src/lowering/matrix.ts +70 -0
  102. package/src/lowering/power.ts +41 -0
  103. package/src/lowering/pwm.ts +192 -167
  104. package/src/lowering/sensor.ts +159 -155
  105. package/src/lowering/strip.ts +81 -0
  106. package/src/lowering/thread.ts +5 -1
  107. package/src/lowering/trace.ts +270 -0
  108. package/src/lowering/uart.ts +6 -1
  109. package/src/lowering/usb.ts +224 -221
  110. package/src/sbom.ts +1117 -0
  111. package/src/strategy.ts +440 -25
  112. package/src/toolchain/index.ts +117 -28
  113. package/src/toolchain/scaffold.ts +62 -3
  114. package/src/west-inventory.ts +102 -0
@@ -96,6 +96,85 @@ export default defineFrameworkManifest({
96
96
  'pwm.set_pulse': 'supported',
97
97
  'pwm.set_duty': 'supported',
98
98
  'pwm.set_period': 'supported',
99
+ // Servo (hal/servo.ts): calibrated 50 Hz sugar on the same channel —
100
+ // clamping and the angle→pulse mapping run in the lowered C++.
101
+ 'pwm.servo_us': 'supported',
102
+ 'pwm.servo_angle': 'supported',
103
+ 'pwm.servo_idle': 'supported',
104
+ },
105
+ },
106
+ // ── Supported: LED strip (ws2812-spi over led_strip API) ───────────────
107
+ strip: {
108
+ // DAC discipline: lowers on every board with a wired SPI controller;
109
+ // a board with none lowers to a comment (profileDiagnostics flags it).
110
+ supported: true,
111
+ partialCoverage: true,
112
+ ops: {
113
+ // Strip (hal/strip.ts): buffer edits + one-wire-transaction show.
114
+ 'strip.set_pixel': 'supported',
115
+ 'strip.fill': 'supported',
116
+ 'strip.show': 'supported',
117
+ },
118
+ },
119
+ // ── Board-gated: USB HID keyboard/mouse (zephyr,hid-device) ───────────
120
+ hid: {
121
+ // Same BOARD-GATED shape as usb below: the lowering only fires when
122
+ // the board's manifest carries zephyr.usb.* — the manifest probe runs
123
+ // with no board, so the honest declaration is unsupported here. The
124
+ // board module exports Keyboard/Mouse only on USB-capable boards
125
+ // (the module-resolution gate); one HID interface per program (the v1
126
+ // ceiling — mixing the two classes is a compile-time #error).
127
+ supported: false,
128
+ unsupportedReason: 'Board-gated: HID lowers only on boards whose DTS enables the USB device controller (boardgen emits zephyr.usb.* from the catalog).',
129
+ partialCoverage: false,
130
+ ops: unsupportedOps('hid.'),
131
+ },
132
+ // ── Supported: CAN bus (harvested can@ controllers) ────────────────────
133
+ can: {
134
+ supported: true,
135
+ partialCoverage: true,
136
+ ops: {
137
+ // Classic CAN (11/29-bit ids, ≤8-byte payloads). Partial: CAN-FD,
138
+ // bitrate data phase, and RTR frames are not surfaced in v1.
139
+ 'can.begin': 'supported',
140
+ 'can.send': 'supported',
141
+ 'can.on_receive': 'supported',
142
+ },
143
+ },
144
+ // ── Supported: wall-clock time (rtc alias / counter shim) ─────────────
145
+ clock: {
146
+ supported: true,
147
+ partialCoverage: true,
148
+ ops: {
149
+ // set/now in Unix epoch seconds. Partial: v1 semantics are
150
+ // session-scoped on counter-backed boards — only hardware-calendar
151
+ // aliases persist across power loss.
152
+ 'clock.set': 'supported',
153
+ 'clock.now': 'supported',
154
+ },
155
+ },
156
+ // ── Supported: explicit power-state entry (sys_poweroff) ───────────────
157
+ power: {
158
+ supported: true,
159
+ partialCoverage: true,
160
+ ops: {
161
+ // Soft-off entry — never returns; wake by reset or a board wake
162
+ // source. Partial: the light states belong to the idle policy, not
163
+ // the app (no op surface for them in v1).
164
+ 'power.off': 'supported',
165
+ // off_for arms the RTC wake timer before soft-off — ESP32 family
166
+ // (the harvested rtc_timer fact); boards without the fact lower
167
+ // to a comment naming the limitation (the DAC discipline).
168
+ 'power.off_for': 'supported',
169
+ },
170
+ },
171
+ // ── Supported: GPIO key matrix (gpio-kbd-matrix, input subsystem) ─────
172
+ matrix: {
173
+ supported: true,
174
+ partialCoverage: false,
175
+ ops: {
176
+ // Matrix (hal/matrix.ts): (row, col, pressed) trampoline.
177
+ 'matrix.on_key': 'supported',
99
178
  },
100
179
  },
101
180
  // ── Supported: ADC (SAADC via adc_read + channel setup) ────────────────
@@ -141,6 +220,12 @@ export default defineFrameworkManifest({
141
220
  'i2c.reg_read': 'supported',
142
221
  'i2c.reg_update': 'supported',
143
222
  'i2c.dev_write': 'supported',
223
+ // I2C responder (hal/i2c-responder.ts): this board as an I2C target.
224
+ 'i2c.resp_on_receive': 'supported',
225
+ 'i2c.resp_on_request': 'supported',
226
+ 'i2c.resp_available': 'supported',
227
+ 'i2c.resp_read': 'supported',
228
+ 'i2c.resp_write': 'supported',
144
229
  },
145
230
  },
146
231
  spi: {
@@ -276,11 +361,25 @@ export default defineFrameworkManifest({
276
361
  partialCoverage: true,
277
362
  // Partial: mono profiles (ssd1306-zephyr) drive display.* ops via the
278
363
  // direct GFX runtime only — no CuttlefishGFX UI rendering path. The
279
- // ILI9341 UI adapter shares the ST7796S direct-drive transport with a
280
- // per-controller init table (16-bit RGB565 wire format); hardware-tuned
281
- // on ST7796S only. E-ink panels are out of scope at this time.
282
- 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.',
283
- drivers: ['ili9341-zephyr', 'st7796-zephyr', 'ssd1306-zephyr'],
364
+ // direct-spi UI adapters (st7796-zephyr, ili9341-zephyr) drive the
365
+ // panel over spi_write with per-controller Adafruit init tables;
366
+ // st7796-zephyr is the full-fidelity path on the clone rig
367
+ // (18-bit mode, hardware-verified incl. colors + touch interaction;
368
+ // the demo needs psram:'opi' for full rendering and resetPin for the
369
+ // touch module's power enable). The zephyr-display transport rides
370
+ // in-tree panel drivers through Zephyr's display API: st7796s via an
371
+ // adapter-emitted CS-holding mipi-dbi host — colors hardware-confirmed
372
+ // correct on the rig with the byte-swap + CS-hold knobs (the stock
373
+ // bridge corrupts bursts on this clone; a ladder test observed slight
374
+ // uniform dimness vs the direct path's 18-bit mode, so direct remains
375
+ // the max-fidelity option there); ili9341 via the stock mipi-dbi-spi
376
+ // bridge is compile/link-verified only. Drop-in configs (display.driver
377
+ // = any bound compatible + geometry + quirk flags) ride the same path.
378
+ // Touch: FT6336U through the input subsystem (in-tree driver,
379
+ // polling mode, hardware-verified); XPT2046 stays on the raw adapter
380
+ // (the in-tree driver's DT scaling would double-apply calibration).
381
+ unsupportedReason: 'Mono panels (ssd1306) are direct-op only (no UI rendering); the direct-spi ili9341 UI path is not yet hardware-verified; the zephyr-display transport is hue-verified but 16-bit-brightness-limited on the clone st7796s (use direct-spi there) and compile-verified on ili9341; e-ink is out of scope at this time.',
382
+ drivers: ['ili9341-zephyr', 'ili9341-zephyr-display', 'st7796-zephyr', 'st7796-zephyr-display', 'ssd1306-zephyr'],
284
383
  colorFormat: 'rgb565',
285
384
  ops: {
286
385
  'display.init': 'supported',
@@ -388,10 +487,13 @@ export default defineFrameworkManifest({
388
487
  },
389
488
  },
390
489
  i2s: {
391
- supported: false,
392
- unsupportedReason: 'No I2S / digital audio lowering on Zephyr.',
393
- partialCoverage: false,
394
- ops: unsupportedOps('i2s.'),
490
+ supported: true,
491
+ partialCoverage: true,
492
+ ops: {
493
+ 'i2s.write': 'supported',
494
+ 'i2s.read': 'supported',
495
+ 'i2s.read_at': 'supported',
496
+ },
395
497
  },
396
498
  twai: {
397
499
  supported: false,
@@ -509,6 +611,17 @@ export default defineFrameworkManifest({
509
611
  // previews how the configured board target resolves for that version.
510
612
  doctor: { available: true },
511
613
  // `typecad-hal licenses` enumerates the Zephyr kernel + west manifest projects
512
- // and resolves each one's SPDX license (mirrors framework-arduino).
614
+ // and resolves each one's SPDX license.
513
615
  licenses: { available: true },
616
+ // `typecad-hal sbom` emits a build-true SBOM (CycloneDX 1.6 / SPDX 2.3):
617
+ // linked west modules + kernel with commit SHAs and licenses, the hashed
618
+ // firmware artifact, the board, and the toolchain in `formulation`;
619
+ // --check/--diff gate the record against the build.
620
+ sbom: { available: true },
621
+ // `typecad-hal audit` evaluates the last build's merged Kconfig against a
622
+ // security baseline (shell/console exposure, entropy, watchdog, hard-coded
623
+ // credentials...), with committed waivers (.typecad-hal/audit-waivers.json)
624
+ // recorded as deviations and a machine-readable sidecar
625
+ // (<buildDir>/security-audit.json).
626
+ audit: { available: true },
514
627
  });
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ export { bossacTouchReset } from './toolchain/bossac-touch.js';
5
5
  export { writeProjectDebugArtifacts } from './toolchain/debug-config.js';
6
6
  export { runDoctor as doctor } from './doctor.js';
7
7
  export { runLicensesPresenter as licenses } from './licenses.js';
8
+ export { runSbomPresenter as sbom } from './sbom.js';
9
+ export { runAuditPresenter as audit } from './audit.js';
8
10
  export { setActiveChip, getActiveChip, NO_BOARD_CHIP, } from './chips/index.js';
9
11
  export type { ZephyrChipDescriptor, ZephyrGpioDtSpec, } from './chips/types.js';
10
12
  export { syncBoardCatalog, ensureFreshBoardCatalog, loadBoardCatalogOverlay, resetBoardCatalogOverlayCache, } from './sdk/board-catalog-sync.js';
package/dist/index.js CHANGED
@@ -22,8 +22,16 @@ export { runDoctor as doctor } from './doctor.js';
22
22
  // `typecad-hal licenses` — enumerate the Zephyr kernel + west manifest projects
23
23
  // and resolve each one's SPDX license. Re-exported under the dispatcher-facing
24
24
  // alias `licenses` so the loader picks it up as mod.licenses (see
25
- // framework-package.ts). Mirrors framework-arduino's presenter.
25
+ // framework-package.ts).
26
26
  export { runLicensesPresenter as licenses } from './licenses.js';
27
+ // `typecad-hal sbom` — emit/verify a build-true software bill of materials
28
+ // (CycloneDX 1.6 / SPDX 2.3). Re-exported under the dispatcher-facing alias
29
+ // `sbom` so the loader picks it up as mod.sbom (see framework-package.ts).
30
+ export { runSbomPresenter as sbom } from './sbom.js';
31
+ // `typecad-hal audit` — evaluate the last build's merged Kconfig against the
32
+ // security baseline, with committed waivers recorded as deviations.
33
+ // Re-exported under the dispatcher-facing alias `audit` (same loader pattern).
34
+ export { runAuditPresenter as audit } from './audit.js';
27
35
  // Chip descriptor cache (the lowering's resolved-board view; no curated
28
36
  // registry — every board reconstructs from its generated manifest).
29
37
  export { setActiveChip, getActiveChip, NO_BOARD_CHIP, } from './chips/index.js';
package/dist/licenses.js CHANGED
@@ -27,36 +27,7 @@ import { loadTypecadConfig } from '@typecad/cuttlefish/config-loader';
27
27
  import { resolveLibraryLicense, RISK_RANK, riskBracket, statusMark, countByRisk, } from '@typecad/cuttlefish/api/shared';
28
28
  import { discoverWest } from './toolchain/west-discover.js';
29
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
- }
30
+ import { filterLinkedModules, findCompileCommandsPath } from './west-inventory.js';
60
31
  // ---------------------------------------------------------------------------
61
32
  // scanZephyrLicenses — enumerate + resolve the workspace dependency set
62
33
  // ---------------------------------------------------------------------------
@@ -139,59 +110,6 @@ export function scanZephyrLicenses(runner, all = false) {
139
110
  // ---------------------------------------------------------------------------
140
111
  // Default runner — wires seams to discoverWest + westSpawn + fs
141
112
  // ---------------------------------------------------------------------------
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
113
  function defaultRunner() {
196
114
  const install = discoverWest();
197
115
  if (!install)
@@ -257,7 +175,10 @@ function defaultRunner() {
257
175
  topdir,
258
176
  listModules,
259
177
  zephyrBase,
260
- compileCommands: () => findCompileCommandsText(readFile, readdir),
178
+ compileCommands: () => {
179
+ const p = findCompileCommandsPath(readFile, readdir);
180
+ return p ? (readFile(p) ?? null) : null;
181
+ },
261
182
  readFile,
262
183
  readdir,
263
184
  };
@@ -0,0 +1,25 @@
1
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
2
+ import type { ZephyrChipDescriptor } from '../chips/types.js';
3
+ /** Which can.* verbs the program actually uses on a controller — the
4
+ * init block emits only the state those verbs reference (Zephyr's -Werror
5
+ * turns an unreferenced static function/variable into a build failure:
6
+ * a send-only program must not carry the rx trampoline). */
7
+ export interface CanUsage {
8
+ begin: boolean;
9
+ send: boolean;
10
+ onReceive: boolean;
11
+ }
12
+ /**
13
+ * Emit the per-controller CAN state: device handle, rx trampoline +
14
+ * handler slot, and the accept-all filters. Called from shimLines when the
15
+ * program uses can.* on this controller and the chip declares it.
16
+ */
17
+ export declare function canInitLines(chip: ZephyrChipDescriptor, controllerIndex: number, usage: CanUsage): string[];
18
+ /**
19
+ * Resolve a HAL can.* op to Zephyr C++.
20
+ * Returns `{ code }` for statement ops.
21
+ */
22
+ export declare function lowerCan(op: HALOpIR, chip: ZephyrChipDescriptor): {
23
+ code?: string;
24
+ expression?: string;
25
+ };
@@ -0,0 +1,97 @@
1
+ // ---------------------------------------------------------------------------
2
+ // CAN lowering — Zephyr's can API over the harvested controller node
3
+ //
4
+ // The controller is addressed by its DT nodelabel (chip.can, the harvested
5
+ // can@ node — ESP32 TWAI, STM32 bxCAN, NXP FlexCAN…). begin() applies
6
+ // mode + bitrate + start in the required order (mode and bitrate need a
7
+ // stopped controller); send() builds one can_frame and submits it with a
8
+ // bounded timeout and no tx callback (blocking-send form); onReceive()
9
+ // installs the accept-all filter whose trampoline carries the frame as
10
+ // ten scalars — the Matrix discipline (ISR context, set-a-variable work).
11
+ // ----------------------------------------------------------------------------
12
+ /** The C variable names for a CAN controller's state. */
13
+ function prefix(idx) {
14
+ return `__tc_can${idx}`;
15
+ }
16
+ /**
17
+ * Emit the per-controller CAN state: device handle, rx trampoline +
18
+ * handler slot, and the accept-all filters. Called from shimLines when the
19
+ * program uses can.* on this controller and the chip declares it.
20
+ */
21
+ export function canInitLines(chip, controllerIndex, usage) {
22
+ const ctrl = chip.can?.controllers[controllerIndex];
23
+ if (!ctrl)
24
+ return [];
25
+ const p = prefix(controllerIndex);
26
+ const lines = [
27
+ '// CUTTLEFISH_CAN_BEGIN',
28
+ `static const struct device* ${p}_dev = DEVICE_DT_GET(DT_NODELABEL(${ctrl.nodeLabel}));`,
29
+ ];
30
+ if (usage.begin) {
31
+ // Shim-scoped (not per call site): a second begin() anywhere in the
32
+ // program must not re-fire can_start on a started controller.
33
+ lines.push(`static bool ${p}_started = false;`);
34
+ }
35
+ if (usage.onReceive) {
36
+ lines.push(`static void (*${p}_rx)(double, double, double, double, double, double, double, double, double, double) = NULL;`,
37
+ // dlc-bounded: drivers fill only data[0..dlc-1] — copy into a zeroed
38
+ // local so the handler's b0..b7 tail is zero, as the class documents.
39
+ 'static void __tc_can_rx_trampoline(const struct device* dev, struct can_frame* frame, void* user_data) {', ' (void)dev;', ' (void)user_data;', ` if (${p}_rx != NULL) {`, ' uint8_t __tc_d[8] = { 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U };', ' for (uint32_t __tc_i = 0U; (__tc_i < 8U) && (__tc_i < static_cast<uint32_t>(frame->dlc)); ++__tc_i) { __tc_d[__tc_i] = frame->data[__tc_i]; }', ` ${p}_rx(static_cast<double>(frame->id), static_cast<double>(frame->dlc),`, ` static_cast<double>(__tc_d[0]), static_cast<double>(__tc_d[1]),`, ` static_cast<double>(__tc_d[2]), static_cast<double>(__tc_d[3]),`, ` static_cast<double>(__tc_d[4]), static_cast<double>(__tc_d[5]),`, ` static_cast<double>(__tc_d[6]), static_cast<double>(__tc_d[7]));`, ' }', '}',
40
+ // Two accept-all filters: can_filter_matches_filter never delivers an
41
+ // IDE (29-bit) frame to a filter without CAN_FILTER_IDE — one filter
42
+ // per frame format, so extended sends are receivable too.
43
+ `static const struct can_filter ${p}_filter = { .id = 0U, .mask = 0U, .flags = 0U };`, `static const struct can_filter ${p}_filter_ext = { .id = 0U, .mask = 0U, .flags = CAN_FILTER_IDE };`, `static bool ${p}_filter_added = false;`);
44
+ }
45
+ if (usage.send) {
46
+ lines.push(`static void __tc_can_tx_done(const struct device* dev, int error, void* user_data) {`, ' (void)dev; (void)error; (void)user_data;', '}');
47
+ }
48
+ lines.push('// CUTTLEFISH_CAN_END');
49
+ return lines;
50
+ }
51
+ /**
52
+ * Resolve a HAL can.* op to Zephyr C++.
53
+ * Returns `{ code }` for statement ops.
54
+ */
55
+ export function lowerCan(op, chip) {
56
+ const o = op;
57
+ // DAC discipline: a board with no harvested can@ node lowers to a comment
58
+ // naming the constraint (the board gate already refuses the CAN import;
59
+ // this keeps the manifest probe and direct-lowering paths honest).
60
+ if (!chip.can || chip.can.controllers.length === 0) {
61
+ return { code: `/* ${op.operation}: this board's chip data declares no CAN controller (no harvested can@ node) */` };
62
+ }
63
+ const idx = Math.min(Number(o.instance ?? 0), chip.can.controllers.length - 1);
64
+ const p = prefix(idx);
65
+ const started = `${p}_started`;
66
+ switch (op.operation) {
67
+ case 'can.begin': {
68
+ // Mode and bitrate require a STOPPED controller — apply them before
69
+ // the start, and start exactly once. Failures printk (the loud-
70
+ // diagnostics policy): a CAN bus that never comes up is otherwise a
71
+ // silent hang at the first send.
72
+ return {
73
+ code: `{ int __tc_err; if (${o.loopback}) { __tc_err = can_set_mode(${p}_dev, CAN_MODE_LOOPBACK); if (__tc_err != 0) { printk("typecad-hal can: set_mode failed: %d\\n", __tc_err); } } __tc_err = can_set_bitrate(${p}_dev, ${o.hz}); if (__tc_err != 0) { printk("typecad-hal can: bitrate failed: %d\\n", __tc_err); } if (!${started}) { ${started} = true; __tc_err = can_start(${p}_dev); if (__tc_err != 0) { printk("typecad-hal can: start failed: %d\\n", __tc_err); } } }`,
74
+ };
75
+ }
76
+ case 'can.send': {
77
+ const bytes = (o.data ?? []);
78
+ if (bytes.length > 8) {
79
+ throw new Error(`CAN send: classic frames carry at most 8 bytes — got ${bytes.length}.`);
80
+ }
81
+ const assigns = bytes
82
+ .map((b, i) => `__tc_frame.data[${i}] = static_cast<uint8_t>(${b});`)
83
+ .join(' ');
84
+ return {
85
+ code: `{ struct can_frame __tc_frame = { 0 }; __tc_frame.id = static_cast<uint32_t>(${o.id}); __tc_frame.dlc = ${bytes.length}U; __tc_frame.flags = ${o.extended ? 'CAN_FRAME_IDE' : '0U'}; ${assigns} int __tc_err = can_send(${p}_dev, &__tc_frame, K_MSEC(100), __tc_can_tx_done, NULL); if (__tc_err != 0) { printk("typecad-hal can: send failed: %d\\n", __tc_err); } }`,
86
+ };
87
+ }
88
+ case 'can.on_receive': {
89
+ return {
90
+ code: `{ ${p}_rx = ${o.handler}; if (!${p}_filter_added) { ${p}_filter_added = true; (void)can_add_rx_filter(${p}_dev, __tc_can_rx_trampoline, NULL, &${p}_filter); (void)can_add_rx_filter(${p}_dev, __tc_can_rx_trampoline, NULL, &${p}_filter_ext); } }`,
91
+ };
92
+ }
93
+ default:
94
+ throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
95
+ `Open an issue or use rawCpp() to emit it manually.`);
96
+ }
97
+ }
@@ -0,0 +1,17 @@
1
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
2
+ /** The conversion helpers + device handle. Called from shimLines when the
3
+ * program uses clock.* — each helper only when its consuming op appears
4
+ * (a now()-only program must not carry the unused set() helper: -Werror
5
+ * fails the build on unreferenced static functions). */
6
+ export declare function clockInitLines(usage: {
7
+ set: boolean;
8
+ now: boolean;
9
+ }): string[];
10
+ /**
11
+ * Resolve a HAL clock.* op to Zephyr C++.
12
+ * Returns `{ code }` for set, `{ expression }` for now.
13
+ */
14
+ export declare function lowerClock(op: HALOpIR): {
15
+ code?: string;
16
+ expression?: string;
17
+ };
@@ -0,0 +1,58 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Clock lowering — rtc_set_time / rtc_get_time on the `rtc` DT alias
3
+ //
4
+ // The alias is Zephyr's own discovery convention. Boards with a hardware
5
+ // calendar RTC alias it themselves; the overlay generator synthesizes a
6
+ // zephyr,rtc-counter shim (a child of the board's first free counter — the
7
+ // counter driver keeps the parent, so Counter and Clock coexist) when they
8
+ // do not.
9
+ //
10
+ // Epoch ↔ civil conversion is Howard Hinnant's days_from_civil pair —
11
+ // pure int64 arithmetic, no libc timezone machinery, valid over the whole
12
+ // Unix range. The helpers live in the shim block so both ops share them.
13
+ // ----------------------------------------------------------------------------
14
+ const RTC_DEV = 'DEVICE_DT_GET(DT_ALIAS(rtc))';
15
+ /** The conversion helpers + device handle. Called from shimLines when the
16
+ * program uses clock.* — each helper only when its consuming op appears
17
+ * (a now()-only program must not carry the unused set() helper: -Werror
18
+ * fails the build on unreferenced static functions). */
19
+ export function clockInitLines(usage) {
20
+ const lines = [
21
+ '// CUTTLEFISH_CLOCK_BEGIN',
22
+ `static const struct device* __tc_rtc = ${RTC_DEV};`,
23
+ ];
24
+ if (usage.now) {
25
+ lines.push('static int64_t __tc_days_from_civil(int32_t yy, int32_t mm, int32_t dd) {', ' yy -= (mm <= 2) ? 1 : 0;', ' const int32_t era = (yy >= 0 ? yy : yy - 399) / 400;', ' const int32_t yoe = yy - era * 400;', ' const int32_t doy = (153 * (mm + (mm > 2 ? -3 : 9)) + 2) / 5 + dd - 1;', ' const int32_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;', ' return (static_cast<int64_t>(era) * 146097) + static_cast<int64_t>(doe) - 719468;', '}');
26
+ }
27
+ if (usage.set) {
28
+ lines.push('static void __tc_civil_from_days(int64_t z, int32_t* yy, int32_t* mm, int32_t* dd) {', ' z += 719468;', ' const int64_t era = (z >= 0 ? z : z - 146096) / 146097;', ' const int64_t doe = z - (era * 146097);', ' const int64_t yoe = (doe - (doe / 1460) + (doe / 36524) - (doe / 146096)) / 365;', ' const int64_t y = yoe + (era * 400);', ' const int64_t doy = doe - ((365 * yoe) + (yoe / 4) - (yoe / 100));', ' const int64_t mp = ((5 * doy) + 2) / 153;', ' const int64_t d = doy - (((153 * mp) + 2) / 5) + 1;', ' const int64_t m = mp + (mp < 10 ? 3 : -9);', ' *yy = static_cast<int32_t>(y + (m <= 2 ? 1 : 0));', ' *mm = static_cast<int32_t>(m);', ' *dd = static_cast<int32_t>(d);', '}');
29
+ }
30
+ lines.push('// CUTTLEFISH_CLOCK_END');
31
+ return lines;
32
+ }
33
+ /** The all-zero/unknown rtc_time initializer (wday/yday unknown per the
34
+ * struct's contract). */
35
+ const TM_INIT = 'struct rtc_time __tc_tm = { 0, 0, 0, 0, 0, 0, -1, -1, -1, 0 }';
36
+ /**
37
+ * Resolve a HAL clock.* op to Zephyr C++.
38
+ * Returns `{ code }` for set, `{ expression }` for now.
39
+ */
40
+ export function lowerClock(op) {
41
+ const o = op;
42
+ switch (op.operation) {
43
+ case 'clock.set': {
44
+ // epoch (runtime text) → civil date → rtc_set_time.
45
+ return {
46
+ code: `{ int64_t __tc_ep = static_cast<int64_t>(${o.epoch}); int32_t __tc_y = 0; int32_t __tc_mo = 0; int32_t __tc_d = 0; __tc_civil_from_days(__tc_ep / 86400, &__tc_y, &__tc_mo, &__tc_d); int64_t __tc_sod = __tc_ep % 86400; ${TM_INIT}; __tc_tm.tm_year = __tc_y - 1900; __tc_tm.tm_mon = __tc_mo - 1; __tc_tm.tm_mday = __tc_d; __tc_tm.tm_hour = static_cast<int32_t>(__tc_sod / 3600); __tc_tm.tm_min = static_cast<int32_t>((__tc_sod % 3600) / 60); __tc_tm.tm_sec = static_cast<int32_t>(__tc_sod % 60); (void)rtc_set_time(__tc_rtc, &__tc_tm); }`,
47
+ };
48
+ }
49
+ case 'clock.now': {
50
+ return {
51
+ expression: `({ ${TM_INIT}; double __tc_epoch = 0.0; if (rtc_get_time(__tc_rtc, &__tc_tm) == 0) { int64_t __tc_days = __tc_days_from_civil(__tc_tm.tm_year + 1900, __tc_tm.tm_mon + 1, __tc_tm.tm_mday); __tc_epoch = static_cast<double>((__tc_days * 86400) + (static_cast<int64_t>(__tc_tm.tm_hour) * 3600) + (static_cast<int64_t>(__tc_tm.tm_min) * 60) + static_cast<int64_t>(__tc_tm.tm_sec)); } __tc_epoch; })`,
52
+ };
53
+ }
54
+ default:
55
+ throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
56
+ `Open an issue or use rawCpp() to emit it manually.`);
57
+ }
58
+ }
@@ -0,0 +1,27 @@
1
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
2
+ import type { ZephyrChipDescriptor } from '../chips/types.js';
3
+ /** Modifier tokens — a separate bitmask enum in Zephyr (usage codes start
4
+ * at 4, which collides with modifier bits, so the two kinds CANNOT share
5
+ * one runtime branch: the lowering emits different code per kind).
6
+ * Exported for the hid.h drift-guard test (every mapped macro must exist
7
+ * in Zephyr's header). */
8
+ export declare const KEY_MODIFIER_TOKENS: Record<string, string>;
9
+ /** Is this KEY.* token a modifier (bitmask) rather than a usage code? */
10
+ export declare function isHidModifierToken(token: string): boolean;
11
+ /** Map a KEY or MOUSE token (or pass a runtime expression through) to C. */
12
+ export declare function hidTokenToMacro(token: string, kind: 'key' | 'button'): string;
13
+ /**
14
+ * Emit the HID interface state for one protocol. Called from shimLines when
15
+ * the program uses hid.* — one device handle, one report buffer, the boot
16
+ * report descriptor, and the guarded register helper that also starts the
17
+ * shared usbd context (the same one CDC rides).
18
+ */
19
+ export declare function hidInitLines(protocol: 'keyboard' | 'mouse'): string[];
20
+ /**
21
+ * Resolve a HAL hid.* op to Zephyr C++.
22
+ * Returns `{ code }` for statement ops.
23
+ */
24
+ export declare function lowerHid(op: HALOpIR, chip: ZephyrChipDescriptor): {
25
+ code?: string;
26
+ expression?: string;
27
+ };