@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
@@ -0,0 +1,261 @@
1
+ // ---------------------------------------------------------------------------
2
+ // HID lowering — USB keyboard/mouse over the Zephyr "next" device stack
3
+ //
4
+ // One HID interface per program (Zephyr's zephyr,hid-device node — a v1
5
+ // ceiling like the nRF PWM matrix: Keyboard OR Mouse, not both). The shim
6
+ // block declares the interface device + the boot report buffer + the report
7
+ // descriptor macro; the overlay generator synthesizes the DT node with the
8
+ // protocol the program uses (key verbs → keyboard, mouse verbs → mouse).
9
+ // Key/button arguments arrive as KEY.*/MOUSE.* token text (or runtime
10
+ // expressions) — the token map below translates name-for-name onto Zephyr's
11
+ // HID_KEY_* / button macros, with a build error naming the valid spellings.
12
+ // ----------------------------------------------------------------------------
13
+
14
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
15
+ import type { ZephyrChipDescriptor } from '../chips/types.js';
16
+
17
+ /** KEY.* token → Zephyr usage macro. Digits and the four modifiers are the
18
+ * spellings that differ; everything else maps name-for-name. */
19
+ const KEY_TOKENS: Record<string, string> = {
20
+ A: 'HID_KEY_A', B: 'HID_KEY_B', C: 'HID_KEY_C', D: 'HID_KEY_D', E: 'HID_KEY_E',
21
+ F: 'HID_KEY_F', G: 'HID_KEY_G', H: 'HID_KEY_H', I: 'HID_KEY_I', J: 'HID_KEY_J',
22
+ K: 'HID_KEY_K', L: 'HID_KEY_L', M: 'HID_KEY_M', N: 'HID_KEY_N', O: 'HID_KEY_O',
23
+ P: 'HID_KEY_P', Q: 'HID_KEY_Q', R: 'HID_KEY_R', S: 'HID_KEY_S', T: 'HID_KEY_T',
24
+ U: 'HID_KEY_U', V: 'HID_KEY_V', W: 'HID_KEY_W', X: 'HID_KEY_X', Y: 'HID_KEY_Y',
25
+ Z: 'HID_KEY_Z',
26
+ N1: 'HID_KEY_1', N2: 'HID_KEY_2', N3: 'HID_KEY_3', N4: 'HID_KEY_4', N5: 'HID_KEY_5',
27
+ N6: 'HID_KEY_6', N7: 'HID_KEY_7', N8: 'HID_KEY_8', N9: 'HID_KEY_9', N0: 'HID_KEY_0',
28
+ ENTER: 'HID_KEY_ENTER', ESC: 'HID_KEY_ESC', BACKSPACE: 'HID_KEY_BACKSPACE',
29
+ TAB: 'HID_KEY_TAB', SPACE: 'HID_KEY_SPACE',
30
+ MINUS: 'HID_KEY_MINUS', EQUAL: 'HID_KEY_EQUAL',
31
+ LEFTBRACE: 'HID_KEY_LEFTBRACE', RIGHTBRACE: 'HID_KEY_RIGHTBRACE',
32
+ BACKSLASH: 'HID_KEY_BACKSLASH', SEMICOLON: 'HID_KEY_SEMICOLON',
33
+ APOSTROPHE: 'HID_KEY_APOSTROPHE', GRAVE: 'HID_KEY_GRAVE',
34
+ COMMA: 'HID_KEY_COMMA', DOT: 'HID_KEY_DOT', SLASH: 'HID_KEY_SLASH',
35
+ CAPSLOCK: 'HID_KEY_CAPSLOCK',
36
+ F1: 'HID_KEY_F1', F2: 'HID_KEY_F2', F3: 'HID_KEY_F3', F4: 'HID_KEY_F4',
37
+ F5: 'HID_KEY_F5', F6: 'HID_KEY_F6', F7: 'HID_KEY_F7', F8: 'HID_KEY_F8',
38
+ F9: 'HID_KEY_F9', F10: 'HID_KEY_F10', F11: 'HID_KEY_F11', F12: 'HID_KEY_F12',
39
+ // Zephyr names usage 70 HID_KEY_SYSRQ (the /* PRINTSCREEN */ comment is
40
+ // its only mention of the name) — the HAL token keeps the datasheet word.
41
+ PRINTSCREEN: 'HID_KEY_SYSRQ', SCROLLLOCK: 'HID_KEY_SCROLLLOCK',
42
+ PAUSE: 'HID_KEY_PAUSE', INSERT: 'HID_KEY_INSERT', HOME: 'HID_KEY_HOME',
43
+ PAGEUP: 'HID_KEY_PAGEUP', DEL: 'HID_KEY_DELETE', END: 'HID_KEY_END',
44
+ PAGEDOWN: 'HID_KEY_PAGEDOWN', RIGHT: 'HID_KEY_RIGHT', LEFT: 'HID_KEY_LEFT',
45
+ DOWN: 'HID_KEY_DOWN', UP: 'HID_KEY_UP',
46
+ };
47
+
48
+ /** Modifier tokens — a separate bitmask enum in Zephyr (usage codes start
49
+ * at 4, which collides with modifier bits, so the two kinds CANNOT share
50
+ * one runtime branch: the lowering emits different code per kind).
51
+ * Exported for the hid.h drift-guard test (every mapped macro must exist
52
+ * in Zephyr's header). */
53
+ export const KEY_MODIFIER_TOKENS: Record<string, string> = {
54
+ CTRL: 'HID_KBD_MODIFIER_LEFT_CTRL',
55
+ SHIFT: 'HID_KBD_MODIFIER_LEFT_SHIFT',
56
+ ALT: 'HID_KBD_MODIFIER_LEFT_ALT',
57
+ // HID usage tables call the Windows/Apple/GUI key "UI" — Zephyr follows.
58
+ GUI: 'HID_KBD_MODIFIER_LEFT_UI',
59
+ };
60
+
61
+ /** Is this KEY.* token a modifier (bitmask) rather than a usage code? */
62
+ export function isHidModifierToken(token: string): boolean {
63
+ return token.startsWith('KEY.') && KEY_MODIFIER_TOKENS[token.slice(4)] !== undefined;
64
+ }
65
+
66
+ /** Map a modifier token to its Zephyr bitmask macro. */
67
+ function hidModifierMacro(token: string): string {
68
+ return KEY_MODIFIER_TOKENS[token.slice(4)]!;
69
+ }
70
+
71
+ const MOUSE_TOKENS: Record<string, string> = {
72
+ LEFT: 'BIT(0)', RIGHT: 'BIT(1)', MIDDLE: 'BIT(2)',
73
+ };
74
+
75
+ /** Map a KEY or MOUSE token (or pass a runtime expression through) to C. */
76
+ export function hidTokenToMacro(token: string, kind: 'key' | 'button'): string {
77
+ const prefix = kind === 'key' ? 'KEY.' : 'MOUSE.';
78
+ if (!token.startsWith(prefix)) return token; // runtime expression
79
+ const name = token.slice(prefix.length);
80
+ const table = kind === 'key' ? KEY_TOKENS : MOUSE_TOKENS;
81
+ const mapped = table[name];
82
+ if (!mapped) {
83
+ const valid = Object.keys(table).map((k) => prefix + k).join(', ');
84
+ throw new Error(
85
+ `Unknown ${kind} token '${token}'. Valid spellings: ${valid} (or a runtime number expression).`,
86
+ );
87
+ }
88
+ return mapped;
89
+ }
90
+
91
+ /**
92
+ * Emit the HID interface state for one protocol. Called from shimLines when
93
+ * the program uses hid.* — one device handle, one report buffer, the boot
94
+ * report descriptor, and the guarded register helper that also starts the
95
+ * shared usbd context (the same one CDC rides).
96
+ */
97
+ export function hidInitLines(protocol: 'keyboard' | 'mouse'): string[] {
98
+ // The boot descriptors: keyboard takes no args; mouse takes its button
99
+ // count (3 — the MOUSE.LEFT/RIGHT/MIDDLE surface).
100
+ const desc = protocol === 'keyboard' ? 'HID_KEYBOARD_REPORT_DESC()' : 'HID_MOUSE_REPORT_DESC(3)';
101
+ // Driver-contract buffers: USB_STATIC_BUF_DEFINE gives pointer alignment
102
+ // (the DWC2 driver rejects unaligned IN buffers with "Buffer not aligned"
103
+ // and the completion still fires — every submit silently failed before
104
+ // this) and the nocache section on builds that require it.
105
+ const buf = protocol === 'keyboard'
106
+ ? 'USB_STATIC_BUF_DEFINE(__tc_kb_report, 8);'
107
+ : 'USB_STATIC_BUF_DEFINE(__tc_mouse_report, 4);';
108
+ const pragmaWrap = protocol === 'mouse'
109
+ ? [
110
+ '#pragma GCC diagnostic push',
111
+ '#pragma GCC diagnostic ignored "-Wnarrowing"',
112
+ ]
113
+ : [];
114
+ const pragmaUnwrap = protocol === 'mouse' ? ['#pragma GCC diagnostic pop'] : [];
115
+ return [
116
+ '// CUTTLEFISH_HID_BEGIN',
117
+ 'static const struct device* __tc_hid_dev = DEVICE_DT_GET_ONE(zephyr_hid_device);',
118
+ ...pragmaWrap,
119
+ `static const uint8_t __tc_hid_desc[] = ${desc};`,
120
+ ...pragmaUnwrap,
121
+ buf,
122
+ 'static bool __tc_hid_registered = false;',
123
+ // The API contract: get_report is required for every HID device type
124
+ // (hid_dev_register rejects NULL ops with -EINVAL), set_protocol is
125
+ // required for boot-subclass devices (both descriptors here), and
126
+ // set_report when an output report is declared. Minimal stubs — the
127
+ // verbs never read host->device state in v1.
128
+ 'static int __tc_hid_get_report(const struct device* dev, const uint8_t type, const uint8_t id, const uint16_t len, uint8_t* const buf) {',
129
+ ' (void)dev; (void)type; (void)id; (void)len; (void)buf;',
130
+ ' return -ENOTSUP;',
131
+ '}',
132
+ 'static int __tc_hid_set_report(const struct device* dev, const uint8_t type, const uint8_t id, const uint16_t len, const uint8_t* const buf) {',
133
+ ' (void)dev; (void)type; (void)id; (void)len; (void)buf;',
134
+ ' return 0;',
135
+ '}',
136
+ 'static void __tc_hid_set_protocol(const struct device* dev, const uint8_t protocol) {',
137
+ ' (void)dev; (void)protocol;',
138
+ '}',
139
+ 'static bool __tc_hid_ready = false;',
140
+ 'static void __tc_hid_iface_ready(const struct device* dev, const bool ready) {',
141
+ ' (void)dev;',
142
+ ' __tc_hid_ready = ready;',
143
+ '}',
144
+ 'static uint32_t __tc_hid_idle = 0;',
145
+ 'static void __tc_hid_set_idle(const struct device* dev, const uint8_t id, const uint32_t duration) {',
146
+ ' (void)dev; (void)id;',
147
+ ' __tc_hid_idle = duration;',
148
+ '}',
149
+ 'static uint32_t __tc_hid_get_idle(const struct device* dev, const uint8_t id) {',
150
+ ' (void)dev; (void)id;',
151
+ ' return __tc_hid_idle;',
152
+ '}',
153
+ 'static void __tc_hid_output_report(const struct device* dev, const uint16_t len, const uint8_t* const buf) {',
154
+ ' (void)dev; (void)len; (void)buf;',
155
+ '}',
156
+ 'static const struct hid_device_ops __tc_hid_ops = {',
157
+ ' .iface_ready = __tc_hid_iface_ready,',
158
+ ' .get_report = __tc_hid_get_report,',
159
+ ' .set_report = __tc_hid_set_report,',
160
+ ' .set_idle = __tc_hid_set_idle,',
161
+ ' .get_idle = __tc_hid_get_idle,',
162
+ ' .set_protocol = __tc_hid_set_protocol,',
163
+ ' .output_report = __tc_hid_output_report,',
164
+ '};',
165
+ 'static void __tc_hid_register(void) {',
166
+ ' if (__tc_hid_registered) { return; }',
167
+ ' __tc_hid_registered = true;',
168
+ ' int err = hid_device_register(__tc_hid_dev, __tc_hid_desc, sizeof(__tc_hid_desc), &__tc_hid_ops);',
169
+ ' if (err != 0) { printk("typecad-hal hid: register failed: %d\\n", err); return; }',
170
+ ' __tc_usbd_start();',
171
+ '}',
172
+ 'static void __tc_hid_submit(const uint8_t* rep, uint16_t len) {',
173
+ ' int err = hid_device_submit_report(__tc_hid_dev, len, rep);',
174
+ ' // -EACCES is the normal pre-enumeration race (submit before the',
175
+ ' // host configures the interface) — not worth a printk; every other',
176
+ ' // errno (e.g. the alignment error that silently ate reports once)',
177
+ ' // deserves to be loud.',
178
+ ' if (err != 0 && err != -EACCES) { printk("typecad-hal hid: submit failed: %d\\n", err); }',
179
+ '}',
180
+ '// CUTTLEFISH_HID_END',
181
+ ];
182
+ }
183
+
184
+ /**
185
+ * Resolve a HAL hid.* op to Zephyr C++.
186
+ * Returns `{ code }` for statement ops.
187
+ */
188
+ export function lowerHid(op: HALOpIR, chip: ZephyrChipDescriptor): { code?: string; expression?: string } {
189
+ if (!chip.usb) {
190
+ throw new Error(
191
+ `framework-zephyr: \`${op.operation}\` needs a USB device stack, but this board's chip data declares no \`zephyr.usb\` ` +
192
+ `(controller + cdcInstances). Boards whose Zephyr DTS has no enabled UDC controller node ` +
193
+ `(zephyr_udc0) cannot provide USB HID.`,
194
+ );
195
+ }
196
+ const o = op as any;
197
+
198
+ switch (op.operation) {
199
+ case 'hid.kb_begin':
200
+ return { code: '__tc_hid_register();' };
201
+ case 'hid.kb_press': {
202
+ // Modifier tokens are compile-time bitmasks — a different emit than
203
+ // the usage-code slot logic (runtime expressions take the key path).
204
+ if (isHidModifierToken(String(o.key))) {
205
+ const mod = hidModifierMacro(String(o.key));
206
+ return {
207
+ code: `{ __tc_hid_register(); __tc_kb_report[0] = static_cast<uint8_t>(__tc_kb_report[0] | ${mod}); __tc_hid_submit(__tc_kb_report, 8U); }`,
208
+ };
209
+ }
210
+ const key = hidTokenToMacro(String(o.key), 'key');
211
+ return {
212
+ code: `{ __tc_hid_register(); uint8_t __tc_k = static_cast<uint8_t>(${key}); for (uint32_t __tc_slot = 2U; __tc_slot < 8U; ++__tc_slot) { if (__tc_kb_report[__tc_slot] == 0U) { __tc_kb_report[__tc_slot] = __tc_k; break; } } __tc_hid_submit(__tc_kb_report, 8U); }`,
213
+ };
214
+ }
215
+ case 'hid.kb_release': {
216
+ if (isHidModifierToken(String(o.key))) {
217
+ const mod = hidModifierMacro(String(o.key));
218
+ return {
219
+ code: `{ __tc_hid_register(); __tc_kb_report[0] = static_cast<uint8_t>(__tc_kb_report[0] & static_cast<uint8_t>(~${mod})); __tc_hid_submit(__tc_kb_report, 8U); }`,
220
+ };
221
+ }
222
+ const key = hidTokenToMacro(String(o.key), 'key');
223
+ return {
224
+ code: `{ __tc_hid_register(); uint8_t __tc_k = static_cast<uint8_t>(${key}); for (uint32_t __tc_slot = 2U; __tc_slot < 8U; ++__tc_slot) { if (__tc_kb_report[__tc_slot] == __tc_k) { __tc_kb_report[__tc_slot] = 0U; } } __tc_hid_submit(__tc_kb_report, 8U); }`,
225
+ };
226
+ }
227
+ case 'hid.kb_release_all':
228
+ return {
229
+ code: `{ __tc_hid_register(); for (uint32_t __tc_slot = 0U; __tc_slot < 8U; ++__tc_slot) { __tc_kb_report[__tc_slot] = 0U; } __tc_hid_submit(__tc_kb_report, 8U); }`,
230
+ };
231
+ case 'hid.mouse_begin':
232
+ return { code: '__tc_hid_register();' };
233
+ case 'hid.mouse_move': {
234
+ return {
235
+ code: `{ __tc_hid_register(); int32_t __tc_mx = static_cast<int32_t>(${o.dx}); int32_t __tc_my = static_cast<int32_t>(${o.dy}); int32_t __tc_mw = static_cast<int32_t>(${o.wheel}); if (__tc_mx > 127) { __tc_mx = 127; } if (__tc_mx < -127) { __tc_mx = -127; } if (__tc_my > 127) { __tc_my = 127; } if (__tc_my < -127) { __tc_my = -127; } if (__tc_mw > 127) { __tc_mw = 127; } if (__tc_mw < -127) { __tc_mw = -127; } __tc_mouse_report[1] = static_cast<uint8_t>(__tc_mx); __tc_mouse_report[2] = static_cast<uint8_t>(__tc_my); __tc_mouse_report[3] = static_cast<uint8_t>(__tc_mw); __tc_hid_submit(__tc_mouse_report, 4U); }`,
236
+ };
237
+ }
238
+ case 'hid.mouse_press':
239
+ case 'hid.mouse_release': {
240
+ const btn = hidTokenToMacro(String(o.button), 'button');
241
+ const opText = op.operation === 'hid.mouse_press' ? '|' : '&';
242
+ const mask = op.operation === 'hid.mouse_press'
243
+ ? btn
244
+ : `static_cast<uint8_t>(~${btn})`;
245
+ return {
246
+ code: `{ __tc_hid_register(); __tc_mouse_report[0] = static_cast<uint8_t>(__tc_mouse_report[0] ${opText} ${mask}); __tc_hid_submit(__tc_mouse_report, 4U); }`,
247
+ };
248
+ }
249
+ case 'hid.mouse_click': {
250
+ const btn = hidTokenToMacro(String(o.button), 'button');
251
+ return {
252
+ code: `{ __tc_hid_register(); __tc_mouse_report[0] = static_cast<uint8_t>(__tc_mouse_report[0] | ${btn}); __tc_hid_submit(__tc_mouse_report, 4U); __tc_mouse_report[0] = static_cast<uint8_t>(__tc_mouse_report[0] & static_cast<uint8_t>(~${btn})); __tc_hid_submit(__tc_mouse_report, 4U); }`,
253
+ };
254
+ }
255
+ default:
256
+ throw new Error(
257
+ `framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
258
+ `Open an issue or use rawCpp() to emit it manually.`,
259
+ );
260
+ }
261
+ }
@@ -43,6 +43,109 @@ function speedForHz(hz: number): string {
43
43
  return 'I2C_SPEED_HIGH';
44
44
  }
45
45
 
46
+ // ── I2C responder (hal/i2c-responder.ts) — this board as an I2C target ────
47
+ //
48
+ // One state block per constructed responder (bus + address), the i2c_target
49
+ // vocabulary verbatim: the five driver callbacks behind two user-facing ones
50
+ // (onReceive fires at STOP with the receive ring's new byte count; onRequest
51
+ // fires at read start — the write() refill point). Registration
52
+ // (i2c_target_register) is once-guarded at the first op, against the
53
+ // controller handle the master-side init block emitted (__tc_i2c<N>_dev).
54
+
55
+ /** The C symbol prefix for one responder's state. */
56
+ function respPrefix(idx: number, address: number): string {
57
+ return `__tc_i2cresp${idx}_a${address.toString(16)}`;
58
+ }
59
+
60
+ /** The once-guarded registration every responder op carries — the UART arm
61
+ * discipline: the first emitted call registers the address, so the
62
+ * callbacks installed early in setup make it answer from boot. An
63
+ * unsupported controller prints loudly (the CAN policy: -ENOSYS from a
64
+ * driver with no target mode must not be a silent empty responder). */
65
+ function respRegisterGuard(p: string, idx: number): string {
66
+ return `{ if (!${p}_registered) { ${p}_registered = true; int __tc_err = i2c_target_register(__tc_i2c${idx}_dev, &${p}_cfg); if (__tc_err != 0) { printk("typecad-hal i2c responder: target_register failed: %d\\n", __tc_err); } } } `;
67
+ }
68
+
69
+ /**
70
+ * Emit one responder's state: receive ring (UART discipline — free-running
71
+ * head/tail, overflow-drop), response buffer with its served position, the
72
+ * two handler slots, the five i2c_target callbacks, and the config the ops'
73
+ * registration guard passes to i2c_target_register. Called from shimLines
74
+ * for each constructed responder (collectI2cResponders).
75
+ */
76
+ export function i2cResponderStateLines(chip: ZephyrChipDescriptor, controllerIndex: number, address: number, rx: number, tx: number): string[] {
77
+ const ctrl = chip.i2c?.controllers[controllerIndex];
78
+ if (!ctrl) return [];
79
+ const p = respPrefix(controllerIndex, address);
80
+ return [
81
+ '// CUTTLEFISH_I2CRESP_BEGIN',
82
+ `static uint8_t ${p}_rx[${rx}];`,
83
+ `static volatile uint32_t ${p}_head = 0;`,
84
+ `static volatile uint32_t ${p}_tail = 0;`,
85
+ `static uint8_t ${p}_tx[${tx}];`,
86
+ `static volatile uint32_t ${p}_txlen = 0;`,
87
+ `static volatile uint32_t ${p}_txpos = 0;`,
88
+ `static void (*${p}_on_rx)(double) = NULL;`,
89
+ `static void (*${p}_on_rq)(void) = NULL;`,
90
+ // A controller write transaction: reset the ring, then take each byte.
91
+ // A byte arriving with a full ring is dropped — the embedded-honest
92
+ // answer (no unbounded buffering), same as the UART RX ring.
93
+ `static int ${p}_write_requested(struct i2c_target_config* cfg) {`,
94
+ ' (void)cfg;',
95
+ ` ${p}_head = 0;`,
96
+ ` ${p}_tail = 0;`,
97
+ ' return 0;',
98
+ '}',
99
+ `static int ${p}_write_received(struct i2c_target_config* cfg, uint8_t val) {`,
100
+ ' (void)cfg;',
101
+ ` if ((${p}_head - ${p}_tail) < ${rx}U) {`,
102
+ ` ${p}_rx[${p}_head % ${rx}U] = val;`,
103
+ ` ${p}_head++;`,
104
+ ' }',
105
+ ' return 0;',
106
+ '}',
107
+ // A controller read transaction: the user's refill point first, then the
108
+ // first byte. Past the buffer's end the controller reads 0xFF (the bus
109
+ // has no NACK for a too-long read; the value is the honest filler).
110
+ `static int ${p}_read_requested(struct i2c_target_config* cfg, uint8_t* val) {`,
111
+ ' (void)cfg;',
112
+ ` if (${p}_on_rq != NULL) { ${p}_on_rq(); }`,
113
+ ` ${p}_txpos = 1U;`,
114
+ ` *val = (${p}_txlen > 0U) ? ${p}_tx[0] : 0xFFU;`,
115
+ ' return 0;',
116
+ '}',
117
+ `static int ${p}_read_processed(struct i2c_target_config* cfg, uint8_t* val) {`,
118
+ ' (void)cfg;',
119
+ ` *val = (${p}_txpos < ${p}_txlen) ? ${p}_tx[${p}_txpos] : 0xFFU;`,
120
+ ` ${p}_txpos++;`,
121
+ ' return 0;',
122
+ '}',
123
+ // End of transaction: announce a completed controller write with the
124
+ // count now waiting in the ring (the Wire onReceive(len) semantic).
125
+ `static int ${p}_stop(struct i2c_target_config* cfg) {`,
126
+ ' (void)cfg;',
127
+ ` if ((${p}_on_rx != NULL) && (${p}_head > ${p}_tail)) {`,
128
+ ` ${p}_on_rx(static_cast<double>(${p}_head - ${p}_tail));`,
129
+ ' }',
130
+ ' return 0;',
131
+ '}',
132
+ `static const struct i2c_target_callbacks ${p}_cbs = {`,
133
+ ` .write_requested = ${p}_write_requested,`,
134
+ ` .read_requested = ${p}_read_requested,`,
135
+ ` .write_received = ${p}_write_received,`,
136
+ ` .read_processed = ${p}_read_processed,`,
137
+ ` .stop = ${p}_stop,`,
138
+ '};',
139
+ `static struct i2c_target_config ${p}_cfg = {`,
140
+ ' .flags = 0U,',
141
+ ` .address = ${address}U,`,
142
+ ` .callbacks = &${p}_cbs,`,
143
+ '};',
144
+ `static bool ${p}_registered = false;`,
145
+ '// CUTTLEFISH_I2CRESP_END',
146
+ ];
147
+ }
148
+
46
149
  /** The thin-device bus-speed preamble: apply the construction hz once
47
150
  * (i2c_configure with Zephyr's I2C_SPEED_SET tier mapping), or '' when the
48
151
  * target constructed without hz. Guarded per controller, so the cost after
@@ -98,6 +201,49 @@ export function lowerI2c(
98
201
  const arr = `static const uint8_t __tc_i2cw[] = { ${bytes.join(', ')} };`;
99
202
  return { code: `${pre} ${arr} (void)i2c_write(${p}_dev, __tc_i2cw, sizeof(__tc_i2cw), static_cast<uint16_t>(${o.address}));` };
100
203
  }
204
+
205
+ // ── I2C responder (hal/i2c-responder.ts) — target-mode ops ───────────
206
+ // Every op carries the once-guarded registration; the state it touches
207
+ // is the per-responder block i2cResponderStateLines emitted.
208
+ case 'i2c.resp_on_receive': {
209
+ const rp = respPrefix(idx, o.address);
210
+ return { code: `${respRegisterGuard(rp, idx)} ${rp}_on_rx = ${o.handler};` };
211
+ }
212
+ case 'i2c.resp_on_request': {
213
+ const rp = respPrefix(idx, o.address);
214
+ return { code: `${respRegisterGuard(rp, idx)} ${rp}_on_rq = ${o.handler};` };
215
+ }
216
+ case 'i2c.resp_available': {
217
+ const rp = respPrefix(idx, o.address);
218
+ return { expression: `({ ${respRegisterGuard(rp, idx)} (${rp}_head - ${rp}_tail); })` };
219
+ }
220
+ case 'i2c.resp_read': {
221
+ const rp = respPrefix(idx, o.address);
222
+ return { expression: `({ ${respRegisterGuard(rp, idx)} (${rp}_tail < ${rp}_head ? ${rp}_rx[(${rp}_tail)++ % ${o.rx}U] : -1); })` };
223
+ }
224
+ case 'i2c.resp_write': {
225
+ const rp = respPrefix(idx, o.address);
226
+ const bytes: unknown[] = o.bytes ?? [];
227
+ const isBuffer = bytes.length === 1 && typeof bytes[0] === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(bytes[0] as string);
228
+ if (isBuffer) {
229
+ const name = bytes[0] as string;
230
+ // A named user buffer: copy its storage into the response buffer,
231
+ // clamped to the constructed size; the length lands last so an
232
+ // in-flight read never sees a torn buffer.
233
+ return {
234
+ code: `${respRegisterGuard(rp, idx)} { ${rp}_txlen = 0U; for (uint32_t __tc_i = 0U; (__tc_i < sizeof(${name})) && (__tc_i < ${o.tx}U); ++__tc_i) { ${rp}_tx[__tc_i] = ${name}[__tc_i]; } ${rp}_txlen = (sizeof(${name}) < ${o.tx}U) ? static_cast<uint32_t>(sizeof(${name})) : ${o.tx}U; }`,
235
+ };
236
+ }
237
+ if (bytes.length > Number(o.tx)) {
238
+ throw new Error(
239
+ `I2C responder write: the response buffer holds ${o.tx} bytes — got ${bytes.length}. Construct with a larger txBufferBytes.`,
240
+ );
241
+ }
242
+ const assigns = bytes
243
+ .map((b, i) => `${rp}_tx[${i}] = static_cast<uint8_t>(${b});`)
244
+ .join(' ');
245
+ return { code: `${respRegisterGuard(rp, idx)} { ${rp}_txlen = 0U; ${assigns} ${rp}_txlen = ${bytes.length}U; }` };
246
+ }
101
247
  default:
102
248
  throw new Error(
103
249
  `framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
@@ -0,0 +1,143 @@
1
+ // ---------------------------------------------------------------------------
2
+ // I2S lowering — Zephyr's i2s API over the harvested controller node
3
+ //
4
+ // The controller is addressed by its DT nodelabel (chip.i2s, the harvested
5
+ // i2s@ node — ESP32 I2S0/1). Each direction lazy-inits on first use (the
6
+ // PWM discipline): configure + trigger START once, then every write/read is
7
+ // one block. The shim owns the mem slab (2 blocks), the TX packing buffer,
8
+ // and the RX receive buffer; blockAt/readAt index the last received block.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
12
+ import type { ZephyrChipDescriptor } from '../chips/types.js';
13
+
14
+ /** The C variable names for an I2S controller's state. */
15
+ function prefix(idx: number): string {
16
+ return `__tc_i2s${idx}`;
17
+ }
18
+
19
+ /** Which i2s.* verbs the program actually uses on a controller — the init
20
+ * block emits only the state those verbs reference (a write-only program
21
+ * must not carry an unused rx buffer: -Werror fails the build). */
22
+ export interface I2sUsage {
23
+ write: boolean;
24
+ read: boolean;
25
+ readAt: boolean;
26
+ }
27
+
28
+ /**
29
+ * Emit the per-controller I2S state: device handle, mem slab, TX/RX block
30
+ * buffers, and the lazy per-direction init helpers. Called from shimLines
31
+ * when the program uses i2s.* on this controller and the chip declares it.
32
+ * The once-flags live HERE (shim scope), not in the lowered call sites — a
33
+ * second write() statement must not reconfigure/re-start a running stream
34
+ * (i2s_configure is legal only before the first trigger).
35
+ */
36
+ export function i2sInitLines(chip: ZephyrChipDescriptor, controllerIndex: number, blockBytes: number, usage: I2sUsage): string[] {
37
+ const ctrl = chip.i2s?.controllers[controllerIndex];
38
+ if (!ctrl) return [];
39
+ const p = prefix(controllerIndex);
40
+ // readAt rides the RX engine too (its lowering lazily starts RX like read
41
+ // does) — a readAt-only program still references the whole engine state.
42
+ const needsEngine = usage.write || usage.read || usage.readAt;
43
+ const lines: string[] = [
44
+ '// CUTTLEFISH_I2S_BEGIN',
45
+ `static const struct device* ${p}_dev = DEVICE_DT_GET(DT_NODELABEL(${ctrl.nodeLabel}));`,
46
+ ];
47
+ if (needsEngine) {
48
+ lines.push(
49
+ `K_MEM_SLAB_DEFINE_STATIC(${p}_slab, ${blockBytes}, 2, 4);`,
50
+ `K_MEM_SLAB_DEFINE_STATIC(${p}_rxslab, ${blockBytes}, 2, 4);`,
51
+ `static void ${p}_init(uint32_t hz, uint8_t channels, uint8_t bits, size_t block_bytes) {`,
52
+ ' struct i2s_config cfg = { 0 };',
53
+ ' cfg.word_size = bits;',
54
+ ' cfg.channels = channels;',
55
+ ' cfg.format = I2S_FMT_DATA_FORMAT_I2S;',
56
+ ' cfg.options = 0;',
57
+ ' cfg.frame_clk_freq = hz;',
58
+ ` cfg.mem_slab = &${p}_slab;`,
59
+ ' cfg.block_size = block_bytes;',
60
+ ' cfg.timeout = 200;',
61
+ ` int err = i2s_configure(${p}_dev, I2S_DIR_TX, &cfg);`,
62
+ ' if (err != 0) { printk("typecad-hal i2s: tx configure failed: %d\\n", err); }',
63
+ ` cfg.mem_slab = &${p}_rxslab;`,
64
+ ` err = i2s_configure(${p}_dev, I2S_DIR_RX, &cfg);`,
65
+ ' if (err != 0) { printk("typecad-hal i2s: rx configure failed: %d\\n", err); }',
66
+ '}',
67
+ `static bool ${p}_init_done = false;`,
68
+ );
69
+ }
70
+ if (usage.write) {
71
+ lines.push(
72
+ `static int16_t ${p}_txbuf[${blockBytes / 2}];`,
73
+ `static bool ${p}_tx_started = false;`,
74
+ );
75
+ }
76
+ if (usage.read || usage.readAt) {
77
+ lines.push(`static int16_t ${p}_rxbuf[${blockBytes / 2}];`);
78
+ lines.push(`static bool ${p}_rx_started = false;`);
79
+ }
80
+ lines.push('// CUTTLEFISH_I2S_END');
81
+ return lines;
82
+ }
83
+
84
+ /**
85
+ * Resolve a HAL i2s.* op to Zephyr C++.
86
+ * Returns `{ code }` for write, `{ expression }` for read/readAt.
87
+ */
88
+ export function lowerI2s(
89
+ op: HALOpIR,
90
+ chip: ZephyrChipDescriptor,
91
+ ): { code?: string; expression?: string } {
92
+ const o = op as any;
93
+ // DAC discipline: no harvested i2s@ node lowers to the honest comment.
94
+ if (!chip.i2s || chip.i2s.controllers.length === 0) {
95
+ return { code: `/* ${op.operation}: this board's chip data declares no I2S controller (no harvested i2s@ node) */` };
96
+ }
97
+ const idx = Math.min(Number(o.instance ?? 0), chip.i2s.controllers.length - 1);
98
+ const p = prefix(idx);
99
+ const hz = Number(o.hz ?? 16000);
100
+ const channels = Number(o.channels ?? 2);
101
+ const bits = Number(o.bits ?? 16);
102
+ const blockFrames = Number(o.blockFrames ?? 64);
103
+ const blockBytes = blockFrames * channels * (bits / 8);
104
+
105
+ switch (op.operation) {
106
+ case 'i2s.write': {
107
+ const samples = (o.samples ?? []) as (number | string)[];
108
+ const cap = blockFrames * channels;
109
+ if (samples.length > cap) {
110
+ throw new Error(
111
+ `I2S write: one block carries at most ${cap} samples (${blockFrames} frames × ${channels} channels) — got ${samples.length}.`,
112
+ );
113
+ }
114
+ // Zero-pad to the full block; samples are 16-bit.
115
+ const writes: string[] = [];
116
+ for (let i = 0; i < cap; i++) {
117
+ const v = i < samples.length ? samples[i] : 0;
118
+ writes.push(`${p}_txbuf[${i}] = static_cast<int16_t>(${v});`);
119
+ }
120
+ return {
121
+ code: `{ if (!${p}_init_done) { ${p}_init_done = true; ${p}_init(${hz}U, ${channels}U, ${bits}U, ${blockBytes}U); } ${writes.join(' ')} int err = i2s_buf_write(${p}_dev, ${p}_txbuf, ${blockBytes}U); if (err != 0) { printk("typecad-hal i2s: write failed: %d\\n", err); } else { if (!${p}_tx_started) { ${p}_tx_started = true; err = i2s_trigger(${p}_dev, I2S_DIR_TX, I2S_TRIGGER_START); if (err != 0) { printk("typecad-hal i2s: tx start failed: %d\\n", err); } } } }`,
122
+ };
123
+ }
124
+ case 'i2s.read': {
125
+ return {
126
+ expression: `({ if (!${p}_init_done) { ${p}_init_done = true; ${p}_init(${hz}U, ${channels}U, ${bits}U, ${blockBytes}U); } if (!${p}_rx_started) { ${p}_rx_started = true; int err = i2s_trigger(${p}_dev, I2S_DIR_RX, I2S_TRIGGER_START); if (err != 0) { printk("typecad-hal i2s: rx start failed: %d\\n", err); } } size_t got = 0U; int err = i2s_buf_read(${p}_dev, ${p}_rxbuf, &got); if (err != 0) { printk("typecad-hal i2s: read failed: %d\\n", err); } static_cast<double>(${p}_rxbuf[0]); })`,
127
+ };
128
+ }
129
+ case 'i2s.read_at': {
130
+ // Same lazy RX start as read(): readAt indexes the last RECEIVED
131
+ // block, so the stream must be running — and a readAt-only program
132
+ // references (and therefore compiles in) the whole engine state.
133
+ return {
134
+ expression: `({ if (!${p}_init_done) { ${p}_init_done = true; ${p}_init(${hz}U, ${channels}U, ${bits}U, ${blockBytes}U); } if (!${p}_rx_started) { ${p}_rx_started = true; int err = i2s_trigger(${p}_dev, I2S_DIR_RX, I2S_TRIGGER_START); if (err != 0) { printk("typecad-hal i2s: rx start failed: %d\\n", err); } } int32_t at = static_cast<int32_t>(${o.index}); double v = 0.0; if (at >= 0 && static_cast<uint32_t>(at) < (sizeof(${p}_rxbuf) / sizeof(${p}_rxbuf[0]))) { v = static_cast<double>(${p}_rxbuf[at]); } v; })`,
135
+ };
136
+ }
137
+ default:
138
+ throw new Error(
139
+ `framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
140
+ `Open an issue or use rawCpp() to emit it manually.`,
141
+ );
142
+ }
143
+ }
@@ -40,12 +40,21 @@ import { lowerFs } from './fs.js';
40
40
  import { lowerSensor } from './sensor.js';
41
41
  import { lowerHwtimer, lowerCounter } from './hwtimer.js';
42
42
  import { lowerThread } from './thread.js';
43
+ import { lowerTrace } from './trace.js';
44
+ import { lowerStrip } from './strip.js';
45
+ import { lowerHid } from './hid.js';
46
+ import { lowerMatrix } from './matrix.js';
47
+ import { lowerPower } from './power.js';
48
+ import { lowerClock } from './clock.js';
49
+ import { lowerCan } from './can.js';
50
+ import { lowerI2s } from './i2s.js';
43
51
 
44
52
  export {
45
53
  lowerGpio, lowerTiming, lowerAdc, lowerPwm, lowerI2c, lowerSpi, lowerUart,
46
54
  lowerUsb, lowerInterrupt, lowerWdt, lowerBle,
47
55
  lowerWifi, lowerHttp, lowerMqtt, lowerPreferences, lowerBoard, lowerRandom,
48
- lowerDac, lowerFs, lowerHwtimer, lowerCounter, lowerSensor, lowerThread,
56
+ lowerDac, lowerFs, lowerHwtimer, lowerCounter, lowerSensor, lowerThread, lowerTrace,
57
+ lowerStrip, lowerHid, lowerMatrix, lowerPower, lowerClock, lowerCan, lowerI2s,
49
58
  };
50
59
 
51
60
  /**
@@ -83,6 +92,14 @@ export function lowerHalOp(
83
92
  if (op.operation.startsWith('random.')) return lowerRandom(op);
84
93
  if (op.operation.startsWith('sensor.')) return lowerSensor(op);
85
94
  if (op.operation.startsWith('thread.')) return lowerThread(op);
95
+ if (op.operation.startsWith('trace.')) return lowerTrace(op);
96
+ if (op.operation.startsWith('strip.')) return lowerStrip(op, chip);
97
+ if (op.operation.startsWith('hid.')) return lowerHid(op, chip);
98
+ if (op.operation.startsWith('matrix.')) return lowerMatrix(op);
99
+ if (op.operation.startsWith('power.')) return lowerPower(op, chip);
100
+ if (op.operation.startsWith('clock.')) return lowerClock(op);
101
+ if (op.operation.startsWith('can.')) return lowerCan(op, chip);
102
+ if (op.operation.startsWith('i2s.')) return lowerI2s(op, chip);
86
103
 
87
104
  // raw / snprintf.emit / display.* / ... — not lowered by this
88
105
  // framework. Return undefined so the transpiler falls back and the manifest