@typecad/framework-zephyr 1.0.0-alpha.16 → 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 (210) hide show
  1. package/LICENSE +202 -21
  2. package/README.md +49 -87
  3. package/dist/as-built.d.ts +2 -2
  4. package/dist/as-built.js +2 -2
  5. package/dist/audit.d.ts +111 -0
  6. package/dist/audit.js +416 -0
  7. package/dist/boardgen.d.ts +1 -9
  8. package/dist/boardgen.js +288 -47
  9. package/dist/chips/resolve.js +20 -0
  10. package/dist/chips/types.d.ts +22 -1
  11. package/dist/debug-codegen.js +1 -1
  12. package/dist/display/bindings.d.ts +55 -0
  13. package/dist/display/bindings.js +316 -0
  14. package/dist/display/gfx.d.ts +2 -3
  15. package/dist/display/gfx.js +166 -154
  16. package/dist/display/index.js +20 -1
  17. package/dist/display/mipi-dbi-host.d.ts +9 -0
  18. package/dist/display/mipi-dbi-host.js +174 -0
  19. package/dist/display/profiles.d.ts +109 -4
  20. package/dist/display/profiles.js +270 -7
  21. package/dist/display/touch-adapter.js +119 -49
  22. package/dist/display/ui-adapter-eink.d.ts +2 -0
  23. package/dist/display/ui-adapter-eink.js +4 -0
  24. package/dist/display/ui-adapter-gray.d.ts +8 -0
  25. package/dist/display/ui-adapter-gray.js +170 -0
  26. package/dist/display/ui-adapter-mono.d.ts +13 -0
  27. package/dist/display/ui-adapter-mono.js +230 -0
  28. package/dist/display/ui-adapter-native.d.ts +10 -0
  29. package/dist/display/ui-adapter-native.js +295 -0
  30. package/dist/display/ui-adapter-shared.d.ts +11 -0
  31. package/dist/display/ui-adapter-shared.js +122 -0
  32. package/dist/display/ui-adapter.js +510 -558
  33. package/dist/doctor.js +4 -4
  34. package/dist/dt-config/custom-board.js +2 -2
  35. package/dist/dt-config/kconfig.d.ts +62 -1
  36. package/dist/dt-config/kconfig.js +141 -38
  37. package/dist/dt-config/overlay.d.ts +15 -2
  38. package/dist/dt-config/overlay.js +433 -18
  39. package/dist/framework.manifest.d.ts +10 -4
  40. package/dist/framework.manifest.js +133 -17
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +14 -6
  43. package/dist/licenses.d.ts +2 -2
  44. package/dist/licenses.js +13 -92
  45. package/dist/lowering/can.d.ts +25 -0
  46. package/dist/lowering/can.js +97 -0
  47. package/dist/lowering/clock.d.ts +17 -0
  48. package/dist/lowering/clock.js +58 -0
  49. package/dist/lowering/fs.js +1 -1
  50. package/dist/lowering/gpio.js +0 -32
  51. package/dist/lowering/hid.d.ts +27 -0
  52. package/dist/lowering/hid.js +244 -0
  53. package/dist/lowering/http.js +264 -32
  54. package/dist/lowering/i2c.d.ts +8 -0
  55. package/dist/lowering/i2c.js +137 -5
  56. package/dist/lowering/i2s.d.ts +27 -0
  57. package/dist/lowering/i2s.js +98 -0
  58. package/dist/lowering/index.d.ts +9 -1
  59. package/dist/lowering/index.js +25 -1
  60. package/dist/lowering/interrupts.js +6 -0
  61. package/dist/lowering/matrix.d.ts +15 -0
  62. package/dist/lowering/matrix.js +63 -0
  63. package/dist/lowering/mqtt.js +110 -8
  64. package/dist/lowering/power.d.ts +3 -2
  65. package/dist/lowering/power.js +20 -45
  66. package/dist/lowering/pwm.js +25 -0
  67. package/dist/lowering/sensor.d.ts +2 -2
  68. package/dist/lowering/sensor.js +8 -4
  69. package/dist/lowering/strip.d.ts +16 -0
  70. package/dist/lowering/strip.js +70 -0
  71. package/dist/lowering/thread.js +5 -1
  72. package/dist/lowering/trace.d.ts +44 -0
  73. package/dist/lowering/trace.js +239 -0
  74. package/dist/lowering/uart.js +6 -1
  75. package/dist/lowering/usb.d.ts +3 -1
  76. package/dist/lowering/usb.js +16 -13
  77. package/dist/lowering/wdt.js +2 -29
  78. package/dist/sbom.d.ts +181 -0
  79. package/dist/sbom.js +901 -0
  80. package/dist/sdk/board-catalog-sync.d.ts +1 -3
  81. package/dist/sdk/board-catalog-sync.js +4 -10
  82. package/dist/strategy.d.ts +82 -46
  83. package/dist/strategy.js +639 -182
  84. package/dist/tmp-probe.d.ts +2 -0
  85. package/dist/tmp-probe.js +9 -0
  86. package/dist/toolchain/debug-config.d.ts +50 -90
  87. package/dist/toolchain/debug-config.js +239 -510
  88. package/dist/toolchain/env-check.d.ts +1 -3
  89. package/dist/toolchain/env-check.js +2 -7
  90. package/dist/toolchain/index.d.ts +28 -3
  91. package/dist/toolchain/index.js +498 -62
  92. package/dist/toolchain/runners.d.ts +16 -0
  93. package/dist/toolchain/runners.js +75 -0
  94. package/dist/toolchain/scaffold.d.ts +5 -2
  95. package/dist/toolchain/scaffold.js +79 -15
  96. package/dist/toolchain/west-discover.d.ts +6 -0
  97. package/dist/toolchain/west-discover.js +36 -13
  98. package/dist/toolchain/west-spawn.js +8 -2
  99. package/dist/west-inventory.d.ts +25 -0
  100. package/dist/west-inventory.js +97 -0
  101. package/installer/README.md +328 -328
  102. package/installer/install.sh +2 -2
  103. package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
  104. package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
  105. package/installer/templates/project/.vscode/settings.json +1 -1
  106. package/installer/templates/project/README.md +2 -2
  107. package/package.json +5 -5
  108. package/src/as-built.ts +206 -206
  109. package/src/audit.ts +529 -0
  110. package/src/boardgen.ts +265 -50
  111. package/src/chips/resolve.ts +21 -0
  112. package/src/chips/types.ts +10 -1
  113. package/src/display/bindings.ts +347 -0
  114. package/src/display/gfx.ts +318 -306
  115. package/src/display/index.ts +87 -70
  116. package/src/display/mipi-dbi-host.ts +183 -0
  117. package/src/display/profiles.ts +458 -139
  118. package/src/display/touch-adapter.ts +119 -49
  119. package/src/display/ui-adapter-eink.ts +13 -0
  120. package/src/display/ui-adapter-gray.ts +178 -0
  121. package/src/display/ui-adapter-mono.ts +238 -0
  122. package/src/display/ui-adapter-native.ts +304 -0
  123. package/src/display/ui-adapter-shared.ts +125 -0
  124. package/src/display/ui-adapter.ts +732 -781
  125. package/src/doctor.ts +4 -4
  126. package/src/dt-config/custom-board.ts +2 -2
  127. package/src/dt-config/kconfig.ts +647 -505
  128. package/src/dt-config/overlay.ts +1475 -1058
  129. package/src/framework.manifest.ts +659 -535
  130. package/src/index.ts +16 -6
  131. package/src/licenses.ts +346 -425
  132. package/src/lowering/can.ts +140 -0
  133. package/src/lowering/clock.ts +91 -0
  134. package/src/lowering/fs.ts +135 -135
  135. package/src/lowering/gpio.ts +0 -33
  136. package/src/lowering/hid.ts +261 -0
  137. package/src/lowering/http.ts +264 -32
  138. package/src/lowering/i2c.ts +142 -5
  139. package/src/lowering/i2s.ts +143 -0
  140. package/src/lowering/index.ts +18 -1
  141. package/src/lowering/interrupts.ts +6 -0
  142. package/src/lowering/matrix.ts +70 -0
  143. package/src/lowering/mqtt.ts +109 -8
  144. package/src/lowering/power.ts +41 -0
  145. package/src/lowering/pwm.ts +192 -167
  146. package/src/lowering/sensor.ts +159 -155
  147. package/src/lowering/strip.ts +81 -0
  148. package/src/lowering/thread.ts +5 -1
  149. package/src/lowering/trace.ts +270 -0
  150. package/src/lowering/uart.ts +6 -1
  151. package/src/lowering/usb.ts +16 -13
  152. package/src/lowering/wdt.ts +2 -25
  153. package/src/sbom.ts +1117 -0
  154. package/src/sdk/board-catalog-sync.ts +4 -25
  155. package/src/strategy.ts +2680 -2309
  156. package/src/toolchain/debug-config.ts +262 -522
  157. package/src/toolchain/env-check.ts +279 -285
  158. package/src/toolchain/index.ts +1792 -1359
  159. package/src/toolchain/runners.ts +80 -0
  160. package/src/toolchain/scaffold.ts +355 -296
  161. package/src/toolchain/west-discover.ts +35 -13
  162. package/src/toolchain/west-spawn.ts +174 -168
  163. package/src/west-inventory.ts +102 -0
  164. package/dist/async/timer-polyfill.d.ts +0 -10
  165. package/dist/async/timer-polyfill.js +0 -95
  166. package/dist/chips/board-overrides.d.ts +0 -7
  167. package/dist/chips/board-overrides.js +0 -11
  168. package/dist/chips/esp32.d.ts +0 -2
  169. package/dist/chips/esp32.js +0 -71
  170. package/dist/chips/esp32s3.d.ts +0 -2
  171. package/dist/chips/esp32s3.js +0 -103
  172. package/dist/chips/soc/.d.ts +0 -2
  173. package/dist/chips/soc/.js +0 -129
  174. package/dist/chips/soc/esp32.d.ts +0 -2
  175. package/dist/chips/soc/esp32.js +0 -120
  176. package/dist/chips/soc/esp32c3.d.ts +0 -2
  177. package/dist/chips/soc/esp32c3.js +0 -90
  178. package/dist/chips/soc/esp32c6.d.ts +0 -2
  179. package/dist/chips/soc/esp32c6.js +0 -109
  180. package/dist/chips/soc/esp32s3.d.ts +0 -2
  181. package/dist/chips/soc/esp32s3.js +0 -189
  182. package/dist/chips/soc/index.d.ts +0 -2
  183. package/dist/chips/soc/index.js +0 -23
  184. package/dist/chips/soc/nrf52840.d.ts +0 -2
  185. package/dist/chips/soc/nrf52840.js +0 -130
  186. package/dist/chips/soc/rp2040.d.ts +0 -2
  187. package/dist/chips/soc/rp2040.js +0 -141
  188. package/dist/chips/soc/rp2350a.d.ts +0 -2
  189. package/dist/chips/soc/rp2350a.js +0 -145
  190. package/dist/chips/soc/samd21g18a.d.ts +0 -2
  191. package/dist/chips/soc/samd21g18a.js +0 -143
  192. package/dist/chips/soc/stm32f411xe.d.ts +0 -2
  193. package/dist/chips/soc/stm32f411xe.js +0 -251
  194. package/dist/chips/xiao-ble.d.ts +0 -2
  195. package/dist/chips/xiao-ble.js +0 -100
  196. package/dist/lowering/pulse.d.ts +0 -7
  197. package/dist/lowering/pulse.js +0 -51
  198. package/dist/lowering/tone.d.ts +0 -10
  199. package/dist/lowering/tone.js +0 -63
  200. package/dist/lowering/worker-backing.d.ts +0 -14
  201. package/dist/lowering/worker-backing.js +0 -79
  202. package/dist/lowering/worker.d.ts +0 -6
  203. package/dist/lowering/worker.js +0 -14
  204. package/dist/sdk/board-data.generated.d.ts +0 -2
  205. package/dist/sdk/board-data.generated.js +0 -4
  206. package/dist/sdk/catalog-walker.d.ts +0 -90
  207. package/dist/sdk/catalog-walker.js +0 -682
  208. package/dist/sdk/dts-reader.d.ts +0 -83
  209. package/dist/sdk/dts-reader.js +0 -596
  210. package/src/debug-codegen.ts +0 -207
@@ -8,11 +8,14 @@
8
8
  // (IPPROTO_TCP or IPPROTO_TLS_1_2 for https) → setsockopt TLS sec tags /
9
9
  // hostname / peer-verify → connect() → http_client_req → accumulate body.
10
10
  //
11
- // One request slot at a time (embedded-friendly): factory/reset → setters →
12
- // begin (inside send) → perform. Setters may run before begin and must survive
13
- // it (req.timeout(ms); req.send()); options are cleared by __tc_http_reset
14
- // when a new request starts (Http.get/post/…). Response body is heap-capped
15
- // (default 8 KB), NUL-terminated, valid until the next begin/perform/reset.
11
+ // One request slot at a time (embedded-friendly): send() lowers
12
+ // reset+begin (method/url) FIRST, then the fact setters (timeout/max_body/
13
+ // body/tls), then perform — the reset-before-setters protocol; header()
14
+ // pairs recorded on the request instance are re-staged by begin (after its
15
+ // reset) so call-site header emissions and send() loops both survive.
16
+ // Options are cleared by __tc_http_reset when a new request starts; response
17
+ // body is heap-capped (default 8 KB, opts.maxBody), NUL-terminated, valid
18
+ // until the next begin/perform/reset.
16
19
  //
17
20
  // TLS: https selects IPPROTO_TLS_1_2 + NET_SOCKETS_SOCKOPT_TLS (which selects
18
21
  // mbedTLS in Kconfig). insecure() sets TLS_PEER_VERIFY_NONE; caCert(pem)
@@ -59,7 +62,7 @@ export function httpInitLines(): string[] {
59
62
  ` char scheme[8]; // "http" or "https"`,
60
63
  ` char host[128];`,
61
64
  ` uint16_t port; // 80 / 443 unless overridden in the url`,
62
- ` char path[256]; // resource path beginning with '/'`,
65
+ ` char path[384]; // resource path beginning with '/'`,
63
66
  ` enum http_method method;`,
64
67
  ` int32_t timeout_ms;`,
65
68
  ` size_t max_body;`,
@@ -68,6 +71,13 @@ export function httpInitLines(): string[] {
68
71
  ` bool insecure;`,
69
72
  ` const uint8_t* ca_der; // DER, user-owned; nullptr = no pinned CA`,
70
73
  ` size_t ca_der_len;`,
74
+ ` // What this shim last registered under __TC_HTTP_SEC_TAG (the DER arrays`,
75
+ ` // are function-static, so pointer identity is stable). Zephyr's`,
76
+ ` // credential store never REPLACES a tag — repeats are skipped silently,`,
77
+ ` // a different CA arriving at an occupied tag is reported, not hidden.`,
78
+ ` const uint8_t* ca_added;`,
79
+ ` size_t ca_added_len;`,
80
+ ` bool ca_added_set;`,
71
81
  ` const char* hdr_name[__TC_HTTP_MAX_HEADERS];`,
72
82
  ` const char* hdr_value[__TC_HTTP_MAX_HEADERS];`,
73
83
  ` int32_t hdr_count;`,
@@ -84,15 +94,25 @@ export function httpInitLines(): string[] {
84
94
  ` // is still reading → EBADMSG (-77) on chunked/larger responses.`,
85
95
  ` uint8_t recvbuf[1024];`,
86
96
  ` int64_t content_length; // -1 = unknown`,
97
+ ` // Response headers: captured as raw "Name: Value\\r\\n" lines by the`,
98
+ ` // http_parser on_header_field/on_header_value hooks (req.http_cb) and`,
99
+ ` // scanned by __tc_http_response_header(). hdr_parse_name is the name`,
100
+ ` // fragment accumulator; hdr_stage tracks name(0)/value(1) parsing.`,
101
+ ` char resp_headers[768];`,
102
+ ` size_t resp_headers_len;`,
103
+ ` char hdr_parse_name[64];`,
104
+ ` size_t hdr_parse_name_len;`,
105
+ ` uint8_t hdr_stage; // 0 = accumulating name, 1 = accumulating value`,
87
106
  ` char resp_header[128]; // scratch for response_header()`,
88
107
  ` // staged header fields for http_client_req: a NULL-terminated list of`,
89
108
  ` // "Name: Value\\r\\n" strings built from hdr_name/hdr_value at perform.`,
90
109
  ` const char* hdr_fields[__TC_HTTP_MAX_HEADERS + 1];`,
91
- ` char hdr_field_buf[__TC_HTTP_MAX_HEADERS][64];`,
110
+ ` char hdr_field_buf[__TC_HTTP_MAX_HEADERS][128];`,
92
111
  ` // async completion signal (memory barrier between worker and poller)`,
93
112
  ` struct k_sem done_sem;`,
94
113
  ` bool sem_inited;`,
95
114
  ` struct k_work perform_work;`,
115
+ ` bool work_inited;`,
96
116
  `} __tc_http;`,
97
117
  ``,
98
118
  `// ── URL parser ──────────────────────────────────────────────────────────`,
@@ -119,6 +139,13 @@ export function httpInitLines(): string[] {
119
139
  ` __tc_http.host[host_len++] = *p++;`,
120
140
  ` }`,
121
141
  ` __tc_http.host[host_len] = '\\0';`,
142
+ ` // A host that filled the buffer means the URL is not what the user`,
143
+ ` // typed — a silently truncated host resolves the wrong server. Refuse.`,
144
+ ` if (host_len == (sizeof(__tc_http.host) - 1U) && *p != '\\0' && *p != ':' && *p != '/') {`,
145
+ ` printk("tc-http: host longer than %u bytes - refused\\n",`,
146
+ ` static_cast<unsigned int>(sizeof(__tc_http.host) - 1U));`,
147
+ ` return false;`,
148
+ ` }`,
122
149
  ` // optional :port`,
123
150
  ` if (*p == ':') {`,
124
151
  ` p++;`,
@@ -136,6 +163,13 @@ export function httpInitLines(): string[] {
136
163
  ` while (*p != '\\0' && path_len < (sizeof(__tc_http.path) - 1U)) {`,
137
164
  ` __tc_http.path[path_len++] = *p++;`,
138
165
  ` }`,
166
+ ` // Same honesty rule as the host: a truncated path (long query`,
167
+ ` // string) requests the wrong resource — refuse instead of misfetch.`,
168
+ ` if (path_len == (sizeof(__tc_http.path) - 1U) && *p != '\\0') {`,
169
+ ` printk("tc-http: path longer than %u bytes - refused\\n",`,
170
+ ` static_cast<unsigned int>(sizeof(__tc_http.path) - 1U));`,
171
+ ` return false;`,
172
+ ` }`,
139
173
  ` __tc_http.path[path_len] = '\\0';`,
140
174
  ` } else {`,
141
175
  ` __tc_http.path[0] = '/';`,
@@ -144,6 +178,53 @@ export function httpInitLines(): string[] {
144
178
  ` return host_len > 0U;`,
145
179
  `}`,
146
180
  ``,
181
+ `// ── response-header capture (http_parser hooks via req.http_cb) ─────────`,
182
+ `// The client's internal parser settings forward on_header_field/`,
183
+ `// on_header_value fragments here while keeping their own status + body`,
184
+ `// handling. Fragments accumulate into raw "Name: Value\\r\\n" lines in`,
185
+ `// resp_headers (bounded — overflow drops the rest of the block, and`,
186
+ `// lookups of the dropped headers return ""). __tc_http_hdr_finish closes`,
187
+ `// the last line after http_client_req returns.`,
188
+ `static void __tc_http_hdr_append(const char* at, size_t len) {`,
189
+ ` size_t room = sizeof(__tc_http.resp_headers) - __tc_http.resp_headers_len;`,
190
+ ` size_t n = (len < room) ? len : room;`,
191
+ ` (void)memcpy(__tc_http.resp_headers + __tc_http.resp_headers_len, at, n);`,
192
+ ` __tc_http.resp_headers_len += n;`,
193
+ `}`,
194
+ ``,
195
+ `static int __tc_http_on_hdr_field(struct http_parser* parser, const char* at, size_t len) {`,
196
+ ` (void)parser;`,
197
+ ` if (__tc_http.hdr_stage == 1U) {`,
198
+ ` __tc_http_hdr_append("\\r\\n", 2U);`,
199
+ ` __tc_http.hdr_stage = 0U;`,
200
+ ` __tc_http.hdr_parse_name_len = 0U;`,
201
+ ` }`,
202
+ ` size_t room = sizeof(__tc_http.hdr_parse_name) - 1U - __tc_http.hdr_parse_name_len;`,
203
+ ` size_t n = (len < room) ? len : room;`,
204
+ ` (void)memcpy(__tc_http.hdr_parse_name + __tc_http.hdr_parse_name_len, at, n);`,
205
+ ` __tc_http.hdr_parse_name_len += n;`,
206
+ ` return 0;`,
207
+ `}`,
208
+ ``,
209
+ `static int __tc_http_on_hdr_value(struct http_parser* parser, const char* at, size_t len) {`,
210
+ ` (void)parser;`,
211
+ ` if (__tc_http.hdr_stage == 0U) {`,
212
+ ` __tc_http_hdr_append(__tc_http.hdr_parse_name, __tc_http.hdr_parse_name_len);`,
213
+ ` __tc_http_hdr_append(": ", 2U);`,
214
+ ` __tc_http.hdr_stage = 1U;`,
215
+ ` }`,
216
+ ` __tc_http_hdr_append(at, len);`,
217
+ ` return 0;`,
218
+ `}`,
219
+ ``,
220
+ `static void __tc_http_hdr_finish(void) {`,
221
+ ` if (__tc_http.hdr_stage == 1U) {`,
222
+ ` __tc_http_hdr_append("\\r\\n", 2U);`,
223
+ ` }`,
224
+ ` __tc_http.hdr_stage = 0U;`,
225
+ ` __tc_http.hdr_parse_name_len = 0U;`,
226
+ `}`,
227
+ ``,
147
228
  `// ── response callback ───────────────────────────────────────────────────`,
148
229
  `// http_response_cb_t: accumulate each body fragment into resp up to max_body,`,
149
230
  `// and capture the status code + content length from the first response.`,
@@ -167,6 +248,34 @@ export function httpInitLines(): string[] {
167
248
  ` return 0;`,
168
249
  `}`,
169
250
  ``,
251
+ `// Strict IPv4 dotted-quad predicate: exactly four 0-255 groups. A`,
252
+ `// digit-and-dot scan would also swallow numeric-only DNS names ("123"),`,
253
+ `// silently skipping the TLS hostname check for them.`,
254
+ `static bool __tc_http_host_is_ipv4(const char* h) {`,
255
+ ` uint32_t quads = 0U;`,
256
+ ` uint32_t val = 0U;`,
257
+ ` uint32_t digits = 0U;`,
258
+ ` for (const char* p = h; ; p++) {`,
259
+ ` if (*p >= '0' && *p <= '9') {`,
260
+ ` val = (val * 10U) + static_cast<uint32_t>(*p - '0');`,
261
+ ` digits++;`,
262
+ ` if (digits > 3U || val > 255U) return false;`,
263
+ ` } else if (*p == '.') {`,
264
+ ` if (digits == 0U || quads >= 3U) return false;`,
265
+ ` quads++;`,
266
+ ` val = 0U;`,
267
+ ` digits = 0U;`,
268
+ ` } else if (*p == '\\0') {`,
269
+ ` if (digits == 0U) return false;`,
270
+ ` quads++;`,
271
+ ` break;`,
272
+ ` } else {`,
273
+ ` return false;`,
274
+ ` }`,
275
+ ` }`,
276
+ ` return quads == 4U;`,
277
+ `}`,
278
+ ``,
170
279
  `// ── socket connect ──────────────────────────────────────────────────────`,
171
280
  `// zsock_getaddrinfo → zsock_socket(TCP | TLS_1_2) → TLS sockopts →`,
172
281
  `// zsock_connect. Uses the zsock_* (native Zephyr socket) API directly rather`,
@@ -201,24 +310,47 @@ export function httpInitLines(): string[] {
201
310
  `#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)`,
202
311
  ` int sock_tls = zsock_socket(res->ai_family, res->ai_socktype, IPPROTO_TLS_1_2);`,
203
312
  ` if (sock_tls >= 0) {`,
204
- ` // Register the pinned CA as the active sec tag. tls_credential_add`,
205
- ` // is idempotent-enough for the single-slot model: a duplicate tag`,
206
- ` // is an error we ignore, and the most recently added cert wins.`,
313
+ ` // Register the pinned CA as the active sec tag. Zephyr's`,
314
+ ` // credential store never REPLACES a tag+type pair: a duplicate`,
315
+ ` // add fails and the FIRST cert stays active. Re-registering the`,
316
+ ` // same DER (pointer identity — the arrays are function-static)`,
317
+ ` // is skipped silently; a DIFFERENT CA hitting an occupied tag`,
318
+ ` // is reported so a wrong-CA verify is never silent. Any other`,
319
+ ` // failure skips the tag list: verification stays required with`,
320
+ ` // no credentials, so the handshake fails closed, not open.`,
207
321
  ` if (__tc_http.ca_der != nullptr) {`,
208
- ` int cred_rc = tls_credential_add(__TC_HTTP_SEC_TAG,`,
209
- ` TLS_CREDENTIAL_CA_CERTIFICATE,`,
210
- ` __tc_http.ca_der, __tc_http.ca_der_len);`,
211
- ` sec_tag_t tags[1] = { __TC_HTTP_SEC_TAG };`,
212
- ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_SEC_TAG_LIST, tags, sizeof(tags));`,
213
- ` (void)cred_rc;`,
322
+ ` if (!__tc_http.ca_added_set || __tc_http.ca_added != __tc_http.ca_der`,
323
+ ` || __tc_http.ca_added_len != __tc_http.ca_der_len) {`,
324
+ ` int cred_rc = tls_credential_add(__TC_HTTP_SEC_TAG,`,
325
+ ` TLS_CREDENTIAL_CA_CERTIFICATE,`,
326
+ ` __tc_http.ca_der, __tc_http.ca_der_len);`,
327
+ ` if (cred_rc == 0) {`,
328
+ ` __tc_http.ca_added = __tc_http.ca_der;`,
329
+ ` __tc_http.ca_added_len = __tc_http.ca_der_len;`,
330
+ ` __tc_http.ca_added_set = true;`,
331
+ ` sec_tag_t tags[1] = { __TC_HTTP_SEC_TAG };`,
332
+ ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_SEC_TAG_LIST, tags, sizeof(tags));`,
333
+ ` } else if (cred_rc == -17 /* -EEXIST */) {`,
334
+ ` printk("tc-http: sec tag %d already holds a CA; the first one stays active (add rc=%d)\\n", __TC_HTTP_SEC_TAG, cred_rc);`,
335
+ ` __tc_http.ca_added = __tc_http.ca_der;`,
336
+ ` __tc_http.ca_added_len = __tc_http.ca_der_len;`,
337
+ ` __tc_http.ca_added_set = true;`,
338
+ ` sec_tag_t tags[1] = { __TC_HTTP_SEC_TAG };`,
339
+ ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_SEC_TAG_LIST, tags, sizeof(tags));`,
340
+ ` } else {`,
341
+ ` printk("tc-http: CA registration failed (rc=%d) - verifying against no credentials\\n", cred_rc);`,
342
+ ` }`,
343
+ ` } else {`,
344
+ ` sec_tag_t tags[1] = { __TC_HTTP_SEC_TAG };`,
345
+ ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_SEC_TAG_LIST, tags, sizeof(tags));`,
346
+ ` }`,
214
347
  ` }`,
215
348
  ` // Hostname SNI + verification. insecure() disables verification;`,
216
349
  ` // the default (no caCert, no insecure) leaves verification required`,
217
350
  ` // with no sec tag, so the handshake fails safely until a CA is set.`,
218
351
  ` // mbedtls does not match IP literals against SAN entries —`,
219
352
  ` // skip the hostname check for IP hosts (chain-only verify).`,
220
- ` bool host_is_ip = true; for (const char* p3 = __tc_http.host; *p3 != 0; p3++) { if (!((*p3 >= '0' && *p3 <= '9') || *p3 == '.')) { host_is_ip = false; break; } }`,
221
- ` if (!host_is_ip) {`,
353
+ ` if (!__tc_http_host_is_ipv4(__tc_http.host)) {`,
222
354
  ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_HOSTNAME, __tc_http.host, strlen(__tc_http.host) + 1U);`,
223
355
  ` }`,
224
356
  ` int32_t verify = __tc_http.insecure ? TLS_PEER_VERIFY_NONE : TLS_PEER_VERIFY_REQUIRED;`,
@@ -248,8 +380,29 @@ export function httpInitLines(): string[] {
248
380
  ` return sock;`,
249
381
  `}`,
250
382
  ``,
251
- `/** Clear request options + response; called when Http.get/post/… starts a new request. */`,
252
- `static inline void __tc_http_reset(void) {`,
383
+ `/** Block until an async perform has finished (single-slot contract: a new`,
384
+ ` * request may not clear state the workqueue is still writing). Bounded at`,
385
+ ` * 20 s — longer than any sane timeout_ms; past that it reports and the`,
386
+ ` * caller proceeds without clearing (the in-flight response stays intact).`,
387
+ ` * Cheap no-op when the async path was never used. */`,
388
+ `static void __tc_http_wait_async_idle(void) {`,
389
+ ` if (!__tc_http.work_inited) return;`,
390
+ ` int32_t waited = 0;`,
391
+ ` while (k_work_is_pending(&__tc_http.perform_work)) {`,
392
+ ` if (waited >= 20000) {`,
393
+ ` printk("tc-http: async request still busy after 20s\\n");`,
394
+ ` return;`,
395
+ ` }`,
396
+ ` k_msleep(10);`,
397
+ ` waited += 10;`,
398
+ ` }`,
399
+ `}`,
400
+ ``,
401
+ `/** Clear request options + response. Rides the head of every request (the`,
402
+ ` * begin op from send()): reset FIRST, then this request's fact setters —`,
403
+ ` * so nothing a previous request staged can leak into this one. */`,
404
+ `static void __tc_http_reset(void) {`,
405
+ ` __tc_http_wait_async_idle();`,
253
406
  ` if (__tc_http.resp != nullptr) { delete[] __tc_http.resp; __tc_http.resp = nullptr; }`,
254
407
  ` __tc_http.timeout_ms = 15000;`,
255
408
  ` __tc_http.max_body = __TC_HTTP_DEFAULT_MAX_BODY;`,
@@ -264,12 +417,24 @@ export function httpInitLines(): string[] {
264
417
  ` __tc_http.ok = false;`,
265
418
  ` __tc_http.resp_len = 0;`,
266
419
  ` __tc_http.content_length = -1;`,
420
+ ` __tc_http.resp_headers_len = 0;`,
421
+ ` __tc_http.hdr_parse_name_len = 0;`,
422
+ ` __tc_http.hdr_stage = 0U;`,
267
423
  ` __tc_http.resp_header[0] = '\\0';`,
268
424
  ` __tc_http.url[0] = '\\0';`,
269
425
  `}`,
270
426
  ``,
271
427
  `/** Stage the request. Setters (timeout/max_body/body/tls/headers) survive begin. */`,
272
428
  `static inline void __tc_http_begin(enum http_method method, const char* url) {`,
429
+ ` if (strlen(url) >= sizeof(__tc_http.url)) {`,
430
+ ` // A silently truncated URL fetches the wrong resource — refuse; the`,
431
+ ` // empty url makes perform's parse fail loudly ("bad url").`,
432
+ ` printk("tc-http: url longer than %u bytes - refused\\n",`,
433
+ ` static_cast<unsigned int>(sizeof(__tc_http.url) - 1U));`,
434
+ ` __tc_http.url[0] = '\\0';`,
435
+ ` __tc_http.method = method;`,
436
+ ` return;`,
437
+ ` }`,
273
438
  ` (void)strncpy(__tc_http.url, url, sizeof(__tc_http.url) - 1U);`,
274
439
  ` __tc_http.url[sizeof(__tc_http.url) - 1U] = '\\0';`,
275
440
  ` __tc_http.method = method;`,
@@ -280,6 +445,9 @@ export function httpInitLines(): string[] {
280
445
  ` __tc_http.ok = false;`,
281
446
  ` __tc_http.resp_len = 0;`,
282
447
  ` __tc_http.content_length = -1;`,
448
+ ` __tc_http.resp_headers_len = 0;`,
449
+ ` __tc_http.hdr_parse_name_len = 0;`,
450
+ ` __tc_http.hdr_stage = 0U;`,
283
451
  ` __tc_http.resp_header[0] = '\\0';`,
284
452
  `}`,
285
453
  ``,
@@ -331,15 +499,34 @@ export function httpInitLines(): string[] {
331
499
  ` int sock = __tc_http_open_socket();`,
332
500
  ` if (sock < 0) { __tc_http.done = true; return; }`,
333
501
  ` // Build the NULL-terminated header-fields list from staged headers.`,
502
+ ` // Format capped 2 bytes short of the buffer so the CRLF can ALWAYS be`,
503
+ ` // appended — a truncated value stays a syntactically valid header line`,
504
+ ` // instead of swallowing the next one (snprintk alone can cut the \\r\\n).`,
334
505
  ` for (int32_t i = 0; i < __tc_http.hdr_count && i < __TC_HTTP_MAX_HEADERS; i++) {`,
335
- ` (void)snprintk(__tc_http.hdr_field_buf[i], sizeof(__tc_http.hdr_field_buf[i]),`,
336
- ` "%s: %s\\r\\n", __tc_http.hdr_name[i], __tc_http.hdr_value[i]);`,
506
+ ` (void)snprintk(__tc_http.hdr_field_buf[i], sizeof(__tc_http.hdr_field_buf[i]) - 2U,`,
507
+ ` "%s: %s", __tc_http.hdr_name[i], __tc_http.hdr_value[i]);`,
508
+ ` size_t flen = strlen(__tc_http.hdr_field_buf[i]);`,
509
+ ` __tc_http.hdr_field_buf[i][flen] = '\\r';`,
510
+ ` __tc_http.hdr_field_buf[i][flen + 1U] = '\\n';`,
511
+ ` __tc_http.hdr_field_buf[i][flen + 2U] = '\\0';`,
337
512
  ` __tc_http.hdr_fields[i] = __tc_http.hdr_field_buf[i];`,
338
513
  ` }`,
339
514
  ` __tc_http.hdr_fields[(__tc_http.hdr_count < __TC_HTTP_MAX_HEADERS)`,
340
515
  ` ? __tc_http.hdr_count : __TC_HTTP_MAX_HEADERS] = nullptr;`,
516
+ ` // Response-header capture: the client's internal parser hooks forward`,
517
+ ` // to these settings (on_header_field/on_header_value) while keeping`,
518
+ ` // their own status/body handling — no interference with the body path.`,
519
+ ` static struct http_parser_settings __tc_http_parse_settings;`,
520
+ ` static bool __tc_http_parse_settings_inited;`,
521
+ ` if (!__tc_http_parse_settings_inited) {`,
522
+ ` http_parser_settings_init(&__tc_http_parse_settings);`,
523
+ ` __tc_http_parse_settings.on_header_field = __tc_http_on_hdr_field;`,
524
+ ` __tc_http_parse_settings.on_header_value = __tc_http_on_hdr_value;`,
525
+ ` __tc_http_parse_settings_inited = true;`,
526
+ ` }`,
341
527
  ` struct http_request req = { 0 };`,
342
528
  ` req.method = __tc_http.method;`,
529
+ ` req.http_cb = &__tc_http_parse_settings;`,
343
530
  ` req.url = __tc_http.path;`,
344
531
  ` req.host = __tc_http.host;`,
345
532
  ` req.protocol = "HTTP/1.1";`,
@@ -352,6 +539,7 @@ export function httpInitLines(): string[] {
352
539
  ` req.payload_len = static_cast<size_t>(__tc_http.body_len);`,
353
540
  ` }`,
354
541
  ` int sent = http_client_req(sock, &req, __tc_http.timeout_ms, nullptr);`,
542
+ ` __tc_http_hdr_finish();`,
355
543
  ` // The response callback (__tc_http_resp_cb) captures the status, but Zephyr's`,
356
544
  ` // parser aborts on 5xx responses (on_headers_complete returns 1 for status`,
357
545
  ` // >= 500, which http_parser treats as an error → http_client_req returns`,
@@ -395,12 +583,23 @@ export function httpInitLines(): string[] {
395
583
  ` k_sem_init(&__tc_http.done_sem, 0, 1);`,
396
584
  ` __tc_http.sem_inited = true;`,
397
585
  ` }`,
586
+ ` // Init the work item ONCE — re-initializing a queued/running item is`,
587
+ ` // undefined in Zephyr, and the busy check below makes the single-slot`,
588
+ ` // contract explicit instead of corrupting the in-flight request.`,
589
+ ` if (!__tc_http.work_inited) {`,
590
+ ` k_work_init(&__tc_http.perform_work, __tc_http_perform_work);`,
591
+ ` __tc_http.work_inited = true;`,
592
+ ` }`,
593
+ ` if (k_work_is_pending(&__tc_http.perform_work)) {`,
594
+ ` printk("tc-http: previous async request still in flight - send refused\\n");`,
595
+ ` __tc_http.done = true;`,
596
+ ` return;`,
597
+ ` }`,
398
598
  ` __tc_http.done = false;`,
399
599
  ` // Drain any completion signal left from a previous request (e.g. the`,
400
600
  ` // prior done() saw the volatile flag before taking the sem), so the first`,
401
601
  ` // poll of this request doesn't fire on a stale signal.`,
402
602
  ` (void)k_sem_take(&__tc_http.done_sem, K_NO_WAIT);`,
403
- ` k_work_init(&__tc_http.perform_work, __tc_http_perform_work);`,
404
603
  ` (void)k_work_submit(&__tc_http.perform_work);`,
405
604
  `}`,
406
605
  ``,
@@ -423,13 +622,39 @@ export function httpInitLines(): string[] {
423
622
  ` return static_cast<int32_t>(__tc_http.content_length);`,
424
623
  `}`,
425
624
  ``,
426
- `// Response-header lookup. The Zephyr http_client_req surface does not expose`,
427
- `// arbitrary response headers via the recv buffer; this returns the last`,
428
- `// header captured by the response callback path. With no per-header hook`,
429
- `// it returns "" until a richer parser setting is wired (http_cb).`,
625
+ `// Response-header lookup over the block captured by the parser hooks.`,
626
+ `// ASCII-case-insensitive name match at a line start; returns the value`,
627
+ `// ("" when absent or when the block was truncated past it).`,
628
+ `static inline char __tc_http_ascii_lower(char c) {`,
629
+ ` return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + 32) : c;`,
630
+ `}`,
631
+ ``,
430
632
  `static inline const char* __tc_http_response_header(const char* name) {`,
431
- ` (void)name;`,
432
633
  ` __tc_http.resp_header[0] = '\\0';`,
634
+ ` const char* block = __tc_http.resp_headers;`,
635
+ ` const char* end = block + __tc_http.resp_headers_len;`,
636
+ ` const char* line = block;`,
637
+ ` while (line < end) {`,
638
+ ` const char* eol = line;`,
639
+ ` while (eol < end && *eol != '\\r' && *eol != '\\n') { eol++; }`,
640
+ ` const char* c = line;`,
641
+ ` const char* n = name;`,
642
+ ` while (c < eol && *n != '\\0' && __tc_http_ascii_lower(*c) == __tc_http_ascii_lower(*n)) {`,
643
+ ` c++;`,
644
+ ` n++;`,
645
+ ` }`,
646
+ ` if (*n == '\\0' && c < eol && *c == ':') {`,
647
+ ` c++;`,
648
+ ` if (c < eol && *c == ' ') { c++; }`,
649
+ ` size_t vlen = static_cast<size_t>(eol - c);`,
650
+ ` if (vlen >= sizeof(__tc_http.resp_header)) { vlen = sizeof(__tc_http.resp_header) - 1U; }`,
651
+ ` (void)memcpy(__tc_http.resp_header, c, vlen);`,
652
+ ` __tc_http.resp_header[vlen] = '\\0';`,
653
+ ` break;`,
654
+ ` }`,
655
+ ` line = eol;`,
656
+ ` while (line < end && (*line == '\\r' || *line == '\\n')) { line++; }`,
657
+ ` }`,
433
658
  ` return __tc_http.resp_header;`,
434
659
  `}`,
435
660
  `// CUTTLEFISH_HTTP_END`,
@@ -458,9 +683,13 @@ export function lowerHttp(op: HALOpIR): { code?: string; expression?: string } {
458
683
  case 'http.begin': {
459
684
  const methodName = String(o.method).replace(/^["']|["']$/g, '').toUpperCase();
460
685
  const method = METHOD_MAP[methodName] ?? 'HTTP_GET';
461
- // Fresh shim state per request — the reset used to ride the deleted
462
- // factory op (http.reset); construction now owns it.
463
- return { code: `__tc_http_reset(); __tc_http_begin(${method}, ${s(o.url)});` };
686
+ // Fresh shim state per request, THEN this request's header() pairs:
687
+ // the reset must precede them (the call-site emissions ran before
688
+ // send() and are wiped by the reset — re-staging here also keeps
689
+ // headers alive across send() loops).
690
+ const pairs = Array.isArray(o.headers) ? (o.headers as string[][]) : [];
691
+ const staged = pairs.length > 0 ? `${pairs.map(([n, v]) => `__tc_http_set_header(${s(n)}, ${s(v)});`).join(' ')} ` : '';
692
+ return { code: `__tc_http_reset(); ${staged}__tc_http_begin(${method}, ${s(o.url)});` };
464
693
  }
465
694
  case 'http.set_header':
466
695
  return { code: `__tc_http_set_header(${s(o.name)}, ${s(o.value)});` };
@@ -486,7 +715,10 @@ export function lowerHttp(op: HALOpIR): { code?: string; expression?: string } {
486
715
  }
487
716
  const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\s+/g, '');
488
717
  const der = Buffer.from(b64, 'base64');
489
- if (der.length < 100) return { code: `// tc-http: caCert PEM failed to decode` };
718
+ // Every X.509 DER starts with the ASN.1 SEQUENCE tag (0x30); a long
719
+ // but non-cert payload fails here instead of at a confusing handshake
720
+ // error on the target.
721
+ if (der.length < 100 || der[0] !== 0x30) return { code: `// tc-http: caCert PEM failed to decode` };
490
722
  const hex = [...der].map((b) => `0x${b.toString(16).padStart(2, '0')}`).join(', ');
491
723
  return { code: `{ static const uint8_t __tc_ca_der[] = { ${hex} }; __tc_http_set_ca_cert_der(__tc_ca_der, sizeof(__tc_ca_der)); }` };
492
724
  }
@@ -43,13 +43,107 @@ 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
+
46
69
  /**
47
- * The (void) comma expression begin/end lower to — a no-op that references
48
- * the controller's whole state block (device + transaction buffers) so a
49
- * program calling only begin() stays -Werror clean (-Wunused-variable).
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).
50
75
  */
51
- function i2cKeepAlive(p: string): string {
52
- return `(void)${p}_dev;`;
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
+ ];
53
147
  }
54
148
 
55
149
  /** The thin-device bus-speed preamble: apply the construction hz once
@@ -107,6 +201,49 @@ export function lowerI2c(
107
201
  const arr = `static const uint8_t __tc_i2cw[] = { ${bytes.join(', ')} };`;
108
202
  return { code: `${pre} ${arr} (void)i2c_write(${p}_dev, __tc_i2cw, sizeof(__tc_i2cw), static_cast<uint16_t>(${o.address}));` };
109
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
+ }
110
247
  default:
111
248
  throw new Error(
112
249
  `framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +