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

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 +6 -6
  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
@@ -104,6 +104,91 @@ export default defineFrameworkManifest({
104
104
  'pwm.set_pulse': 'supported',
105
105
  'pwm.set_duty': 'supported',
106
106
  'pwm.set_period': 'supported',
107
+ // Servo (hal/servo.ts): calibrated 50 Hz sugar on the same channel —
108
+ // clamping and the angle→pulse mapping run in the lowered C++.
109
+ 'pwm.servo_us': 'supported',
110
+ 'pwm.servo_angle': 'supported',
111
+ 'pwm.servo_idle': 'supported',
112
+ },
113
+ },
114
+
115
+ // ── Supported: LED strip (ws2812-spi over led_strip API) ───────────────
116
+ strip: {
117
+ // DAC discipline: lowers on every board with a wired SPI controller;
118
+ // a board with none lowers to a comment (profileDiagnostics flags it).
119
+ supported: true,
120
+ partialCoverage: true,
121
+ ops: {
122
+ // Strip (hal/strip.ts): buffer edits + one-wire-transaction show.
123
+ 'strip.set_pixel': 'supported',
124
+ 'strip.fill': 'supported',
125
+ 'strip.show': 'supported',
126
+ },
127
+ },
128
+
129
+ // ── Board-gated: USB HID keyboard/mouse (zephyr,hid-device) ───────────
130
+ hid: {
131
+ // Same BOARD-GATED shape as usb below: the lowering only fires when
132
+ // the board's manifest carries zephyr.usb.* — the manifest probe runs
133
+ // with no board, so the honest declaration is unsupported here. The
134
+ // board module exports Keyboard/Mouse only on USB-capable boards
135
+ // (the module-resolution gate); one HID interface per program (the v1
136
+ // ceiling — mixing the two classes is a compile-time #error).
137
+ supported: false,
138
+ unsupportedReason: 'Board-gated: HID lowers only on boards whose DTS enables the USB device controller (boardgen emits zephyr.usb.* from the catalog).',
139
+ partialCoverage: false,
140
+ ops: unsupportedOps('hid.'),
141
+ },
142
+
143
+ // ── Supported: CAN bus (harvested can@ controllers) ────────────────────
144
+ can: {
145
+ supported: true,
146
+ partialCoverage: true,
147
+ ops: {
148
+ // Classic CAN (11/29-bit ids, ≤8-byte payloads). Partial: CAN-FD,
149
+ // bitrate data phase, and RTR frames are not surfaced in v1.
150
+ 'can.begin': 'supported',
151
+ 'can.send': 'supported',
152
+ 'can.on_receive': 'supported',
153
+ },
154
+ },
155
+
156
+ // ── Supported: wall-clock time (rtc alias / counter shim) ─────────────
157
+ clock: {
158
+ supported: true,
159
+ partialCoverage: true,
160
+ ops: {
161
+ // set/now in Unix epoch seconds. Partial: v1 semantics are
162
+ // session-scoped on counter-backed boards — only hardware-calendar
163
+ // aliases persist across power loss.
164
+ 'clock.set': 'supported',
165
+ 'clock.now': 'supported',
166
+ },
167
+ },
168
+
169
+ // ── Supported: explicit power-state entry (sys_poweroff) ───────────────
170
+ power: {
171
+ supported: true,
172
+ partialCoverage: true,
173
+ ops: {
174
+ // Soft-off entry — never returns; wake by reset or a board wake
175
+ // source. Partial: the light states belong to the idle policy, not
176
+ // the app (no op surface for them in v1).
177
+ 'power.off': 'supported',
178
+ // off_for arms the RTC wake timer before soft-off — ESP32 family
179
+ // (the harvested rtc_timer fact); boards without the fact lower
180
+ // to a comment naming the limitation (the DAC discipline).
181
+ 'power.off_for': 'supported',
182
+ },
183
+ },
184
+
185
+ // ── Supported: GPIO key matrix (gpio-kbd-matrix, input subsystem) ─────
186
+ matrix: {
187
+ supported: true,
188
+ partialCoverage: false,
189
+ ops: {
190
+ // Matrix (hal/matrix.ts): (row, col, pressed) trampoline.
191
+ 'matrix.on_key': 'supported',
107
192
  },
108
193
  },
109
194
 
@@ -150,6 +235,12 @@ export default defineFrameworkManifest({
150
235
  'i2c.reg_read': 'supported',
151
236
  'i2c.reg_update': 'supported',
152
237
  'i2c.dev_write': 'supported',
238
+ // I2C responder (hal/i2c-responder.ts): this board as an I2C target.
239
+ 'i2c.resp_on_receive': 'supported',
240
+ 'i2c.resp_on_request': 'supported',
241
+ 'i2c.resp_available': 'supported',
242
+ 'i2c.resp_read': 'supported',
243
+ 'i2c.resp_write': 'supported',
153
244
  },
154
245
  },
155
246
  spi: {
@@ -286,11 +377,25 @@ export default defineFrameworkManifest({
286
377
  partialCoverage: true,
287
378
  // Partial: mono profiles (ssd1306-zephyr) drive display.* ops via the
288
379
  // direct GFX runtime only — no CuttlefishGFX UI rendering path. The
289
- // ILI9341 UI adapter shares the ST7796S direct-drive transport with a
290
- // per-controller init table (16-bit RGB565 wire format); hardware-tuned
291
- // on ST7796S only. E-ink panels are out of scope at this time.
292
- 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.',
293
- drivers: ['ili9341-zephyr', 'st7796-zephyr', 'ssd1306-zephyr'],
380
+ // direct-spi UI adapters (st7796-zephyr, ili9341-zephyr) drive the
381
+ // panel over spi_write with per-controller Adafruit init tables;
382
+ // st7796-zephyr is the full-fidelity path on the clone rig
383
+ // (18-bit mode, hardware-verified incl. colors + touch interaction;
384
+ // the demo needs psram:'opi' for full rendering and resetPin for the
385
+ // touch module's power enable). The zephyr-display transport rides
386
+ // in-tree panel drivers through Zephyr's display API: st7796s via an
387
+ // adapter-emitted CS-holding mipi-dbi host — colors hardware-confirmed
388
+ // correct on the rig with the byte-swap + CS-hold knobs (the stock
389
+ // bridge corrupts bursts on this clone; a ladder test observed slight
390
+ // uniform dimness vs the direct path's 18-bit mode, so direct remains
391
+ // the max-fidelity option there); ili9341 via the stock mipi-dbi-spi
392
+ // bridge is compile/link-verified only. Drop-in configs (display.driver
393
+ // = any bound compatible + geometry + quirk flags) ride the same path.
394
+ // Touch: FT6336U through the input subsystem (in-tree driver,
395
+ // polling mode, hardware-verified); XPT2046 stays on the raw adapter
396
+ // (the in-tree driver's DT scaling would double-apply calibration).
397
+ 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.',
398
+ drivers: ['ili9341-zephyr', 'ili9341-zephyr-display', 'st7796-zephyr', 'st7796-zephyr-display', 'ssd1306-zephyr'],
294
399
  colorFormat: 'rgb565',
295
400
  ops: {
296
401
  'display.init': 'supported',
@@ -403,10 +508,13 @@ export default defineFrameworkManifest({
403
508
  },
404
509
  },
405
510
  i2s: {
406
- supported: false,
407
- unsupportedReason: 'No I2S / digital audio lowering on Zephyr.',
408
- partialCoverage: false,
409
- ops: unsupportedOps('i2s.'),
511
+ supported: true,
512
+ partialCoverage: true,
513
+ ops: {
514
+ 'i2s.write': 'supported',
515
+ 'i2s.read': 'supported',
516
+ 'i2s.read_at': 'supported',
517
+ },
410
518
  },
411
519
  twai: {
412
520
  supported: false,
@@ -533,6 +641,19 @@ export default defineFrameworkManifest({
533
641
  doctor: { available: true },
534
642
 
535
643
  // `typecad-hal licenses` enumerates the Zephyr kernel + west manifest projects
536
- // and resolves each one's SPDX license (mirrors framework-arduino).
644
+ // and resolves each one's SPDX license.
537
645
  licenses: { available: true },
646
+
647
+ // `typecad-hal sbom` emits a build-true SBOM (CycloneDX 1.6 / SPDX 2.3):
648
+ // linked west modules + kernel with commit SHAs and licenses, the hashed
649
+ // firmware artifact, the board, and the toolchain in `formulation`;
650
+ // --check/--diff gate the record against the build.
651
+ sbom: { available: true },
652
+
653
+ // `typecad-hal audit` evaluates the last build's merged Kconfig against a
654
+ // security baseline (shell/console exposure, entropy, watchdog, hard-coded
655
+ // credentials...), with committed waivers (.typecad-hal/audit-waivers.json)
656
+ // recorded as deviations and a machine-readable sidecar
657
+ // (<buildDir>/security-audit.json).
658
+ audit: { available: true },
538
659
  });
package/src/index.ts CHANGED
@@ -26,9 +26,19 @@ export { runDoctor as doctor } from './doctor.js';
26
26
  // `typecad-hal licenses` — enumerate the Zephyr kernel + west manifest projects
27
27
  // and resolve each one's SPDX license. Re-exported under the dispatcher-facing
28
28
  // alias `licenses` so the loader picks it up as mod.licenses (see
29
- // framework-package.ts). Mirrors framework-arduino's presenter.
29
+ // framework-package.ts).
30
30
  export { runLicensesPresenter as licenses } from './licenses.js';
31
31
 
32
+ // `typecad-hal sbom` — emit/verify a build-true software bill of materials
33
+ // (CycloneDX 1.6 / SPDX 2.3). Re-exported under the dispatcher-facing alias
34
+ // `sbom` so the loader picks it up as mod.sbom (see framework-package.ts).
35
+ export { runSbomPresenter as sbom } from './sbom.js';
36
+
37
+ // `typecad-hal audit` — evaluate the last build's merged Kconfig against the
38
+ // security baseline, with committed waivers recorded as deviations.
39
+ // Re-exported under the dispatcher-facing alias `audit` (same loader pattern).
40
+ export { runAuditPresenter as audit } from './audit.js';
41
+
32
42
  // Chip descriptor cache (the lowering's resolved-board view; no curated
33
43
  // registry — every board reconstructs from its generated manifest).
34
44
  export {
package/src/licenses.ts CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  } from '@typecad/cuttlefish/api/shared';
38
38
  import { discoverWest } from './toolchain/west-discover.js';
39
39
  import { westSpawn } from './toolchain/west-spawn.js';
40
+ import { filterLinkedModules, findCompileCommandsPath } from './west-inventory.js';
40
41
 
41
42
  // ---------------------------------------------------------------------------
42
43
  // Outcome + runner types
@@ -72,40 +73,6 @@ export interface ZephyrLicensesRunner {
72
73
  readdir: ReadDir;
73
74
  }
74
75
 
75
- // ---------------------------------------------------------------------------
76
- // Linked-module filtering (project scope)
77
- // ---------------------------------------------------------------------------
78
-
79
- /** Normalize a path for case-insensitive, separator-agnostic comparison. */
80
- function norm(p: string): string {
81
- return p.split('\\').join('/').toLowerCase();
82
- }
83
-
84
- /**
85
- * Filter the west manifest to the modules whose sources were compiled in the
86
- * last build. A module is "linked" iff some compiled translation unit lives
87
- * under its abspath. If the build data is unparseable, no filtering is applied
88
- * (degrade to listing all modules rather than reporting nothing).
89
- */
90
- function filterLinkedModules(
91
- ccText: string,
92
- modules: { name: string; abspath: string }[],
93
- ): { name: string; abspath: string }[] {
94
- let arr: unknown;
95
- try {
96
- arr = JSON.parse(ccText);
97
- } catch {
98
- return modules;
99
- }
100
- if (!Array.isArray(arr)) return modules;
101
- // One normalized blob of every compiled source path; membership is then a
102
- // substring check per module (O(modules) after an O(TUs) join).
103
- const blob = arr
104
- .map((e) => norm(typeof (e as { file?: unknown })?.file === 'string' ? (e as { file: string }).file : ''))
105
- .join('\n');
106
- return modules.filter((m) => blob.includes(norm(m.abspath) + '/'));
107
- }
108
-
109
76
  // ---------------------------------------------------------------------------
110
77
  // scanZephyrLicenses — enumerate + resolve the workspace dependency set
111
78
  // ---------------------------------------------------------------------------
@@ -205,55 +172,6 @@ export function scanZephyrLicenses(
205
172
  // Default runner — wires seams to discoverWest + westSpawn + fs
206
173
  // ---------------------------------------------------------------------------
207
174
 
208
- /**
209
- * Find the build's compile_commands.json under cwd. Cuttlefish's Zephyr project
210
- * root is the transpile output dir (e.g. `<cwd>/src/out`), so the build dir is
211
- * `<root>/build` — not necessarily `<cwd>/build`. Check the common layouts,
212
- * then fall back to a bounded search (skipping node_modules/.git/dist).
213
- */
214
- function findCompileCommandsText(readFile: ReadFile, readdir: ReadDir): string | null {
215
- const direct = [
216
- path.join(process.cwd(), 'build', 'compile_commands.json'),
217
- path.join(process.cwd(), 'out', 'build', 'compile_commands.json'),
218
- path.join(process.cwd(), 'src', 'out', 'build', 'compile_commands.json'),
219
- ];
220
- for (const c of direct) {
221
- const text = readFile(c);
222
- if (text) return text;
223
- }
224
- // Bounded recursive search for a build/compile_commands.json.
225
- const isCc = (p: string) => {
226
- const n = norm(p);
227
- return n.endsWith('/build/compile_commands.json') || n.endsWith('\\build\\compile_commands.json');
228
- };
229
- let found: string | undefined;
230
- const seen = new Set<string>();
231
- const walk = (dir: string, depth: number): void => {
232
- if (found || depth > 4 || seen.has(dir)) return;
233
- seen.add(dir);
234
- let entries: string[];
235
- try {
236
- entries = readdir(dir);
237
- } catch {
238
- return;
239
- }
240
- for (const e of entries) {
241
- const full = path.join(dir, e);
242
- if (e === 'compile_commands.json' && isCc(full)) {
243
- found = full;
244
- return;
245
- }
246
- }
247
- for (const e of entries) {
248
- if (found) return;
249
- if (e === 'node_modules' || e === '.git' || e === 'dist' || e.startsWith('.')) continue;
250
- walk(path.join(dir, e), depth + 1);
251
- }
252
- };
253
- walk(process.cwd(), 0);
254
- return found ? (readFile(found) ?? null) : null;
255
- }
256
-
257
175
  function defaultRunner(): ZephyrLicensesRunner | null {
258
176
  const install = discoverWest();
259
177
  if (!install) return null;
@@ -315,7 +233,10 @@ function defaultRunner(): ZephyrLicensesRunner | null {
315
233
  topdir,
316
234
  listModules,
317
235
  zephyrBase,
318
- compileCommands: () => findCompileCommandsText(readFile, readdir),
236
+ compileCommands: () => {
237
+ const p = findCompileCommandsPath(readFile, readdir);
238
+ return p ? (readFile(p) ?? null) : null;
239
+ },
319
240
  readFile,
320
241
  readdir,
321
242
  };
@@ -0,0 +1,140 @@
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
+
13
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
14
+ import type { ZephyrChipDescriptor } from '../chips/types.js';
15
+
16
+ /** The C variable names for a CAN controller's state. */
17
+ function prefix(idx: number): string {
18
+ return `__tc_can${idx}`;
19
+ }
20
+
21
+ /** Which can.* verbs the program actually uses on a controller — the
22
+ * init block emits only the state those verbs reference (Zephyr's -Werror
23
+ * turns an unreferenced static function/variable into a build failure:
24
+ * a send-only program must not carry the rx trampoline). */
25
+ export interface CanUsage {
26
+ begin: boolean;
27
+ send: boolean;
28
+ onReceive: boolean;
29
+ }
30
+
31
+ /**
32
+ * Emit the per-controller CAN state: device handle, rx trampoline +
33
+ * handler slot, and the accept-all filters. Called from shimLines when the
34
+ * program uses can.* on this controller and the chip declares it.
35
+ */
36
+ export function canInitLines(chip: ZephyrChipDescriptor, controllerIndex: number, usage: CanUsage): string[] {
37
+ const ctrl = chip.can?.controllers[controllerIndex];
38
+ if (!ctrl) return [];
39
+ const p = prefix(controllerIndex);
40
+ const lines: string[] = [
41
+ '// CUTTLEFISH_CAN_BEGIN',
42
+ `static const struct device* ${p}_dev = DEVICE_DT_GET(DT_NODELABEL(${ctrl.nodeLabel}));`,
43
+ ];
44
+ if (usage.begin) {
45
+ // Shim-scoped (not per call site): a second begin() anywhere in the
46
+ // program must not re-fire can_start on a started controller.
47
+ lines.push(`static bool ${p}_started = false;`);
48
+ }
49
+ if (usage.onReceive) {
50
+ lines.push(
51
+ `static void (*${p}_rx)(double, double, double, double, double, double, double, double, double, double) = NULL;`,
52
+ // dlc-bounded: drivers fill only data[0..dlc-1] — copy into a zeroed
53
+ // local so the handler's b0..b7 tail is zero, as the class documents.
54
+ 'static void __tc_can_rx_trampoline(const struct device* dev, struct can_frame* frame, void* user_data) {',
55
+ ' (void)dev;',
56
+ ' (void)user_data;',
57
+ ` if (${p}_rx != NULL) {`,
58
+ ' uint8_t __tc_d[8] = { 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U };',
59
+ ' 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]; }',
60
+ ` ${p}_rx(static_cast<double>(frame->id), static_cast<double>(frame->dlc),`,
61
+ ` static_cast<double>(__tc_d[0]), static_cast<double>(__tc_d[1]),`,
62
+ ` static_cast<double>(__tc_d[2]), static_cast<double>(__tc_d[3]),`,
63
+ ` static_cast<double>(__tc_d[4]), static_cast<double>(__tc_d[5]),`,
64
+ ` static_cast<double>(__tc_d[6]), static_cast<double>(__tc_d[7]));`,
65
+ ' }',
66
+ '}',
67
+ // Two accept-all filters: can_filter_matches_filter never delivers an
68
+ // IDE (29-bit) frame to a filter without CAN_FILTER_IDE — one filter
69
+ // per frame format, so extended sends are receivable too.
70
+ `static const struct can_filter ${p}_filter = { .id = 0U, .mask = 0U, .flags = 0U };`,
71
+ `static const struct can_filter ${p}_filter_ext = { .id = 0U, .mask = 0U, .flags = CAN_FILTER_IDE };`,
72
+ `static bool ${p}_filter_added = false;`,
73
+ );
74
+ }
75
+ if (usage.send) {
76
+ lines.push(
77
+ `static void __tc_can_tx_done(const struct device* dev, int error, void* user_data) {`,
78
+ ' (void)dev; (void)error; (void)user_data;',
79
+ '}',
80
+ );
81
+ }
82
+ lines.push('// CUTTLEFISH_CAN_END');
83
+ return lines;
84
+ }
85
+
86
+ /**
87
+ * Resolve a HAL can.* op to Zephyr C++.
88
+ * Returns `{ code }` for statement ops.
89
+ */
90
+ export function lowerCan(
91
+ op: HALOpIR,
92
+ chip: ZephyrChipDescriptor,
93
+ ): { code?: string; expression?: string } {
94
+ const o = op as any;
95
+ // DAC discipline: a board with no harvested can@ node lowers to a comment
96
+ // naming the constraint (the board gate already refuses the CAN import;
97
+ // this keeps the manifest probe and direct-lowering paths honest).
98
+ if (!chip.can || chip.can.controllers.length === 0) {
99
+ return { code: `/* ${op.operation}: this board's chip data declares no CAN controller (no harvested can@ node) */` };
100
+ }
101
+ const idx = Math.min(Number(o.instance ?? 0), chip.can.controllers.length - 1);
102
+ const p = prefix(idx);
103
+ const started = `${p}_started`;
104
+
105
+ switch (op.operation) {
106
+ case 'can.begin': {
107
+ // Mode and bitrate require a STOPPED controller — apply them before
108
+ // the start, and start exactly once. Failures printk (the loud-
109
+ // diagnostics policy): a CAN bus that never comes up is otherwise a
110
+ // silent hang at the first send.
111
+ return {
112
+ 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); } } }`,
113
+ };
114
+ }
115
+ case 'can.send': {
116
+ const bytes = (o.data ?? []) as (number | string)[];
117
+ if (bytes.length > 8) {
118
+ throw new Error(
119
+ `CAN send: classic frames carry at most 8 bytes — got ${bytes.length}.`,
120
+ );
121
+ }
122
+ const assigns = bytes
123
+ .map((b, i) => `__tc_frame.data[${i}] = static_cast<uint8_t>(${b});`)
124
+ .join(' ');
125
+ return {
126
+ 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); } }`,
127
+ };
128
+ }
129
+ case 'can.on_receive': {
130
+ return {
131
+ 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); } }`,
132
+ };
133
+ }
134
+ default:
135
+ throw new Error(
136
+ `framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
137
+ `Open an issue or use rawCpp() to emit it manually.`,
138
+ );
139
+ }
140
+ }
@@ -0,0 +1,91 @@
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
+
15
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
16
+
17
+ const RTC_DEV = 'DEVICE_DT_GET(DT_ALIAS(rtc))';
18
+
19
+ /** The conversion helpers + device handle. Called from shimLines when the
20
+ * program uses clock.* — each helper only when its consuming op appears
21
+ * (a now()-only program must not carry the unused set() helper: -Werror
22
+ * fails the build on unreferenced static functions). */
23
+ export function clockInitLines(usage: { set: boolean; now: boolean }): string[] {
24
+ const lines: string[] = [
25
+ '// CUTTLEFISH_CLOCK_BEGIN',
26
+ `static const struct device* __tc_rtc = ${RTC_DEV};`,
27
+ ];
28
+ if (usage.now) {
29
+ lines.push(
30
+ 'static int64_t __tc_days_from_civil(int32_t yy, int32_t mm, int32_t dd) {',
31
+ ' yy -= (mm <= 2) ? 1 : 0;',
32
+ ' const int32_t era = (yy >= 0 ? yy : yy - 399) / 400;',
33
+ ' const int32_t yoe = yy - era * 400;',
34
+ ' const int32_t doy = (153 * (mm + (mm > 2 ? -3 : 9)) + 2) / 5 + dd - 1;',
35
+ ' const int32_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;',
36
+ ' return (static_cast<int64_t>(era) * 146097) + static_cast<int64_t>(doe) - 719468;',
37
+ '}',
38
+ );
39
+ }
40
+ if (usage.set) {
41
+ lines.push(
42
+ 'static void __tc_civil_from_days(int64_t z, int32_t* yy, int32_t* mm, int32_t* dd) {',
43
+ ' z += 719468;',
44
+ ' const int64_t era = (z >= 0 ? z : z - 146096) / 146097;',
45
+ ' const int64_t doe = z - (era * 146097);',
46
+ ' const int64_t yoe = (doe - (doe / 1460) + (doe / 36524) - (doe / 146096)) / 365;',
47
+ ' const int64_t y = yoe + (era * 400);',
48
+ ' const int64_t doy = doe - ((365 * yoe) + (yoe / 4) - (yoe / 100));',
49
+ ' const int64_t mp = ((5 * doy) + 2) / 153;',
50
+ ' const int64_t d = doy - (((153 * mp) + 2) / 5) + 1;',
51
+ ' const int64_t m = mp + (mp < 10 ? 3 : -9);',
52
+ ' *yy = static_cast<int32_t>(y + (m <= 2 ? 1 : 0));',
53
+ ' *mm = static_cast<int32_t>(m);',
54
+ ' *dd = static_cast<int32_t>(d);',
55
+ '}',
56
+ );
57
+ }
58
+ lines.push('// CUTTLEFISH_CLOCK_END');
59
+ return lines;
60
+ }
61
+
62
+ /** The all-zero/unknown rtc_time initializer (wday/yday unknown per the
63
+ * struct's contract). */
64
+ const TM_INIT = 'struct rtc_time __tc_tm = { 0, 0, 0, 0, 0, 0, -1, -1, -1, 0 }';
65
+
66
+ /**
67
+ * Resolve a HAL clock.* op to Zephyr C++.
68
+ * Returns `{ code }` for set, `{ expression }` for now.
69
+ */
70
+ export function lowerClock(op: HALOpIR): { code?: string; expression?: string } {
71
+ const o = op as any;
72
+
73
+ switch (op.operation) {
74
+ case 'clock.set': {
75
+ // epoch (runtime text) → civil date → rtc_set_time.
76
+ return {
77
+ 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); }`,
78
+ };
79
+ }
80
+ case 'clock.now': {
81
+ return {
82
+ 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; })`,
83
+ };
84
+ }
85
+ default:
86
+ throw new Error(
87
+ `framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
88
+ `Open an issue or use rawCpp() to emit it manually.`,
89
+ );
90
+ }
91
+ }