@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)
@@ -55,7 +58,7 @@ export function httpInitLines() {
55
58
  ` char scheme[8]; // "http" or "https"`,
56
59
  ` char host[128];`,
57
60
  ` uint16_t port; // 80 / 443 unless overridden in the url`,
58
- ` char path[256]; // resource path beginning with '/'`,
61
+ ` char path[384]; // resource path beginning with '/'`,
59
62
  ` enum http_method method;`,
60
63
  ` int32_t timeout_ms;`,
61
64
  ` size_t max_body;`,
@@ -64,6 +67,13 @@ export function httpInitLines() {
64
67
  ` bool insecure;`,
65
68
  ` const uint8_t* ca_der; // DER, user-owned; nullptr = no pinned CA`,
66
69
  ` size_t ca_der_len;`,
70
+ ` // What this shim last registered under __TC_HTTP_SEC_TAG (the DER arrays`,
71
+ ` // are function-static, so pointer identity is stable). Zephyr's`,
72
+ ` // credential store never REPLACES a tag — repeats are skipped silently,`,
73
+ ` // a different CA arriving at an occupied tag is reported, not hidden.`,
74
+ ` const uint8_t* ca_added;`,
75
+ ` size_t ca_added_len;`,
76
+ ` bool ca_added_set;`,
67
77
  ` const char* hdr_name[__TC_HTTP_MAX_HEADERS];`,
68
78
  ` const char* hdr_value[__TC_HTTP_MAX_HEADERS];`,
69
79
  ` int32_t hdr_count;`,
@@ -80,15 +90,25 @@ export function httpInitLines() {
80
90
  ` // is still reading → EBADMSG (-77) on chunked/larger responses.`,
81
91
  ` uint8_t recvbuf[1024];`,
82
92
  ` int64_t content_length; // -1 = unknown`,
93
+ ` // Response headers: captured as raw "Name: Value\\r\\n" lines by the`,
94
+ ` // http_parser on_header_field/on_header_value hooks (req.http_cb) and`,
95
+ ` // scanned by __tc_http_response_header(). hdr_parse_name is the name`,
96
+ ` // fragment accumulator; hdr_stage tracks name(0)/value(1) parsing.`,
97
+ ` char resp_headers[768];`,
98
+ ` size_t resp_headers_len;`,
99
+ ` char hdr_parse_name[64];`,
100
+ ` size_t hdr_parse_name_len;`,
101
+ ` uint8_t hdr_stage; // 0 = accumulating name, 1 = accumulating value`,
83
102
  ` char resp_header[128]; // scratch for response_header()`,
84
103
  ` // staged header fields for http_client_req: a NULL-terminated list of`,
85
104
  ` // "Name: Value\\r\\n" strings built from hdr_name/hdr_value at perform.`,
86
105
  ` const char* hdr_fields[__TC_HTTP_MAX_HEADERS + 1];`,
87
- ` char hdr_field_buf[__TC_HTTP_MAX_HEADERS][64];`,
106
+ ` char hdr_field_buf[__TC_HTTP_MAX_HEADERS][128];`,
88
107
  ` // async completion signal (memory barrier between worker and poller)`,
89
108
  ` struct k_sem done_sem;`,
90
109
  ` bool sem_inited;`,
91
110
  ` struct k_work perform_work;`,
111
+ ` bool work_inited;`,
92
112
  `} __tc_http;`,
93
113
  ``,
94
114
  `// ── URL parser ──────────────────────────────────────────────────────────`,
@@ -115,6 +135,13 @@ export function httpInitLines() {
115
135
  ` __tc_http.host[host_len++] = *p++;`,
116
136
  ` }`,
117
137
  ` __tc_http.host[host_len] = '\\0';`,
138
+ ` // A host that filled the buffer means the URL is not what the user`,
139
+ ` // typed — a silently truncated host resolves the wrong server. Refuse.`,
140
+ ` if (host_len == (sizeof(__tc_http.host) - 1U) && *p != '\\0' && *p != ':' && *p != '/') {`,
141
+ ` printk("tc-http: host longer than %u bytes - refused\\n",`,
142
+ ` static_cast<unsigned int>(sizeof(__tc_http.host) - 1U));`,
143
+ ` return false;`,
144
+ ` }`,
118
145
  ` // optional :port`,
119
146
  ` if (*p == ':') {`,
120
147
  ` p++;`,
@@ -132,6 +159,13 @@ export function httpInitLines() {
132
159
  ` while (*p != '\\0' && path_len < (sizeof(__tc_http.path) - 1U)) {`,
133
160
  ` __tc_http.path[path_len++] = *p++;`,
134
161
  ` }`,
162
+ ` // Same honesty rule as the host: a truncated path (long query`,
163
+ ` // string) requests the wrong resource — refuse instead of misfetch.`,
164
+ ` if (path_len == (sizeof(__tc_http.path) - 1U) && *p != '\\0') {`,
165
+ ` printk("tc-http: path longer than %u bytes - refused\\n",`,
166
+ ` static_cast<unsigned int>(sizeof(__tc_http.path) - 1U));`,
167
+ ` return false;`,
168
+ ` }`,
135
169
  ` __tc_http.path[path_len] = '\\0';`,
136
170
  ` } else {`,
137
171
  ` __tc_http.path[0] = '/';`,
@@ -140,6 +174,53 @@ export function httpInitLines() {
140
174
  ` return host_len > 0U;`,
141
175
  `}`,
142
176
  ``,
177
+ `// ── response-header capture (http_parser hooks via req.http_cb) ─────────`,
178
+ `// The client's internal parser settings forward on_header_field/`,
179
+ `// on_header_value fragments here while keeping their own status + body`,
180
+ `// handling. Fragments accumulate into raw "Name: Value\\r\\n" lines in`,
181
+ `// resp_headers (bounded — overflow drops the rest of the block, and`,
182
+ `// lookups of the dropped headers return ""). __tc_http_hdr_finish closes`,
183
+ `// the last line after http_client_req returns.`,
184
+ `static void __tc_http_hdr_append(const char* at, size_t len) {`,
185
+ ` size_t room = sizeof(__tc_http.resp_headers) - __tc_http.resp_headers_len;`,
186
+ ` size_t n = (len < room) ? len : room;`,
187
+ ` (void)memcpy(__tc_http.resp_headers + __tc_http.resp_headers_len, at, n);`,
188
+ ` __tc_http.resp_headers_len += n;`,
189
+ `}`,
190
+ ``,
191
+ `static int __tc_http_on_hdr_field(struct http_parser* parser, const char* at, size_t len) {`,
192
+ ` (void)parser;`,
193
+ ` if (__tc_http.hdr_stage == 1U) {`,
194
+ ` __tc_http_hdr_append("\\r\\n", 2U);`,
195
+ ` __tc_http.hdr_stage = 0U;`,
196
+ ` __tc_http.hdr_parse_name_len = 0U;`,
197
+ ` }`,
198
+ ` size_t room = sizeof(__tc_http.hdr_parse_name) - 1U - __tc_http.hdr_parse_name_len;`,
199
+ ` size_t n = (len < room) ? len : room;`,
200
+ ` (void)memcpy(__tc_http.hdr_parse_name + __tc_http.hdr_parse_name_len, at, n);`,
201
+ ` __tc_http.hdr_parse_name_len += n;`,
202
+ ` return 0;`,
203
+ `}`,
204
+ ``,
205
+ `static int __tc_http_on_hdr_value(struct http_parser* parser, const char* at, size_t len) {`,
206
+ ` (void)parser;`,
207
+ ` if (__tc_http.hdr_stage == 0U) {`,
208
+ ` __tc_http_hdr_append(__tc_http.hdr_parse_name, __tc_http.hdr_parse_name_len);`,
209
+ ` __tc_http_hdr_append(": ", 2U);`,
210
+ ` __tc_http.hdr_stage = 1U;`,
211
+ ` }`,
212
+ ` __tc_http_hdr_append(at, len);`,
213
+ ` return 0;`,
214
+ `}`,
215
+ ``,
216
+ `static void __tc_http_hdr_finish(void) {`,
217
+ ` if (__tc_http.hdr_stage == 1U) {`,
218
+ ` __tc_http_hdr_append("\\r\\n", 2U);`,
219
+ ` }`,
220
+ ` __tc_http.hdr_stage = 0U;`,
221
+ ` __tc_http.hdr_parse_name_len = 0U;`,
222
+ `}`,
223
+ ``,
143
224
  `// ── response callback ───────────────────────────────────────────────────`,
144
225
  `// http_response_cb_t: accumulate each body fragment into resp up to max_body,`,
145
226
  `// and capture the status code + content length from the first response.`,
@@ -163,6 +244,34 @@ export function httpInitLines() {
163
244
  ` return 0;`,
164
245
  `}`,
165
246
  ``,
247
+ `// Strict IPv4 dotted-quad predicate: exactly four 0-255 groups. A`,
248
+ `// digit-and-dot scan would also swallow numeric-only DNS names ("123"),`,
249
+ `// silently skipping the TLS hostname check for them.`,
250
+ `static bool __tc_http_host_is_ipv4(const char* h) {`,
251
+ ` uint32_t quads = 0U;`,
252
+ ` uint32_t val = 0U;`,
253
+ ` uint32_t digits = 0U;`,
254
+ ` for (const char* p = h; ; p++) {`,
255
+ ` if (*p >= '0' && *p <= '9') {`,
256
+ ` val = (val * 10U) + static_cast<uint32_t>(*p - '0');`,
257
+ ` digits++;`,
258
+ ` if (digits > 3U || val > 255U) return false;`,
259
+ ` } else if (*p == '.') {`,
260
+ ` if (digits == 0U || quads >= 3U) return false;`,
261
+ ` quads++;`,
262
+ ` val = 0U;`,
263
+ ` digits = 0U;`,
264
+ ` } else if (*p == '\\0') {`,
265
+ ` if (digits == 0U) return false;`,
266
+ ` quads++;`,
267
+ ` break;`,
268
+ ` } else {`,
269
+ ` return false;`,
270
+ ` }`,
271
+ ` }`,
272
+ ` return quads == 4U;`,
273
+ `}`,
274
+ ``,
166
275
  `// ── socket connect ──────────────────────────────────────────────────────`,
167
276
  `// zsock_getaddrinfo → zsock_socket(TCP | TLS_1_2) → TLS sockopts →`,
168
277
  `// zsock_connect. Uses the zsock_* (native Zephyr socket) API directly rather`,
@@ -197,24 +306,47 @@ export function httpInitLines() {
197
306
  `#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)`,
198
307
  ` int sock_tls = zsock_socket(res->ai_family, res->ai_socktype, IPPROTO_TLS_1_2);`,
199
308
  ` if (sock_tls >= 0) {`,
200
- ` // Register the pinned CA as the active sec tag. tls_credential_add`,
201
- ` // is idempotent-enough for the single-slot model: a duplicate tag`,
202
- ` // is an error we ignore, and the most recently added cert wins.`,
309
+ ` // Register the pinned CA as the active sec tag. Zephyr's`,
310
+ ` // credential store never REPLACES a tag+type pair: a duplicate`,
311
+ ` // add fails and the FIRST cert stays active. Re-registering the`,
312
+ ` // same DER (pointer identity — the arrays are function-static)`,
313
+ ` // is skipped silently; a DIFFERENT CA hitting an occupied tag`,
314
+ ` // is reported so a wrong-CA verify is never silent. Any other`,
315
+ ` // failure skips the tag list: verification stays required with`,
316
+ ` // no credentials, so the handshake fails closed, not open.`,
203
317
  ` if (__tc_http.ca_der != nullptr) {`,
204
- ` int cred_rc = tls_credential_add(__TC_HTTP_SEC_TAG,`,
205
- ` TLS_CREDENTIAL_CA_CERTIFICATE,`,
206
- ` __tc_http.ca_der, __tc_http.ca_der_len);`,
207
- ` sec_tag_t tags[1] = { __TC_HTTP_SEC_TAG };`,
208
- ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_SEC_TAG_LIST, tags, sizeof(tags));`,
209
- ` (void)cred_rc;`,
318
+ ` if (!__tc_http.ca_added_set || __tc_http.ca_added != __tc_http.ca_der`,
319
+ ` || __tc_http.ca_added_len != __tc_http.ca_der_len) {`,
320
+ ` int cred_rc = tls_credential_add(__TC_HTTP_SEC_TAG,`,
321
+ ` TLS_CREDENTIAL_CA_CERTIFICATE,`,
322
+ ` __tc_http.ca_der, __tc_http.ca_der_len);`,
323
+ ` if (cred_rc == 0) {`,
324
+ ` __tc_http.ca_added = __tc_http.ca_der;`,
325
+ ` __tc_http.ca_added_len = __tc_http.ca_der_len;`,
326
+ ` __tc_http.ca_added_set = true;`,
327
+ ` sec_tag_t tags[1] = { __TC_HTTP_SEC_TAG };`,
328
+ ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_SEC_TAG_LIST, tags, sizeof(tags));`,
329
+ ` } else if (cred_rc == -17 /* -EEXIST */) {`,
330
+ ` 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);`,
331
+ ` __tc_http.ca_added = __tc_http.ca_der;`,
332
+ ` __tc_http.ca_added_len = __tc_http.ca_der_len;`,
333
+ ` __tc_http.ca_added_set = true;`,
334
+ ` sec_tag_t tags[1] = { __TC_HTTP_SEC_TAG };`,
335
+ ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_SEC_TAG_LIST, tags, sizeof(tags));`,
336
+ ` } else {`,
337
+ ` printk("tc-http: CA registration failed (rc=%d) - verifying against no credentials\\n", cred_rc);`,
338
+ ` }`,
339
+ ` } else {`,
340
+ ` sec_tag_t tags[1] = { __TC_HTTP_SEC_TAG };`,
341
+ ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_SEC_TAG_LIST, tags, sizeof(tags));`,
342
+ ` }`,
210
343
  ` }`,
211
344
  ` // Hostname SNI + verification. insecure() disables verification;`,
212
345
  ` // the default (no caCert, no insecure) leaves verification required`,
213
346
  ` // with no sec tag, so the handshake fails safely until a CA is set.`,
214
347
  ` // mbedtls does not match IP literals against SAN entries —`,
215
348
  ` // skip the hostname check for IP hosts (chain-only verify).`,
216
- ` 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; } }`,
217
- ` if (!host_is_ip) {`,
349
+ ` if (!__tc_http_host_is_ipv4(__tc_http.host)) {`,
218
350
  ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_HOSTNAME, __tc_http.host, strlen(__tc_http.host) + 1U);`,
219
351
  ` }`,
220
352
  ` int32_t verify = __tc_http.insecure ? TLS_PEER_VERIFY_NONE : TLS_PEER_VERIFY_REQUIRED;`,
@@ -244,8 +376,29 @@ export function httpInitLines() {
244
376
  ` return sock;`,
245
377
  `}`,
246
378
  ``,
247
- `/** Clear request options + response; called when Http.get/post/… starts a new request. */`,
248
- `static inline void __tc_http_reset(void) {`,
379
+ `/** Block until an async perform has finished (single-slot contract: a new`,
380
+ ` * request may not clear state the workqueue is still writing). Bounded at`,
381
+ ` * 20 s — longer than any sane timeout_ms; past that it reports and the`,
382
+ ` * caller proceeds without clearing (the in-flight response stays intact).`,
383
+ ` * Cheap no-op when the async path was never used. */`,
384
+ `static void __tc_http_wait_async_idle(void) {`,
385
+ ` if (!__tc_http.work_inited) return;`,
386
+ ` int32_t waited = 0;`,
387
+ ` while (k_work_is_pending(&__tc_http.perform_work)) {`,
388
+ ` if (waited >= 20000) {`,
389
+ ` printk("tc-http: async request still busy after 20s\\n");`,
390
+ ` return;`,
391
+ ` }`,
392
+ ` k_msleep(10);`,
393
+ ` waited += 10;`,
394
+ ` }`,
395
+ `}`,
396
+ ``,
397
+ `/** Clear request options + response. Rides the head of every request (the`,
398
+ ` * begin op from send()): reset FIRST, then this request's fact setters —`,
399
+ ` * so nothing a previous request staged can leak into this one. */`,
400
+ `static void __tc_http_reset(void) {`,
401
+ ` __tc_http_wait_async_idle();`,
249
402
  ` if (__tc_http.resp != nullptr) { delete[] __tc_http.resp; __tc_http.resp = nullptr; }`,
250
403
  ` __tc_http.timeout_ms = 15000;`,
251
404
  ` __tc_http.max_body = __TC_HTTP_DEFAULT_MAX_BODY;`,
@@ -260,12 +413,24 @@ export function httpInitLines() {
260
413
  ` __tc_http.ok = false;`,
261
414
  ` __tc_http.resp_len = 0;`,
262
415
  ` __tc_http.content_length = -1;`,
416
+ ` __tc_http.resp_headers_len = 0;`,
417
+ ` __tc_http.hdr_parse_name_len = 0;`,
418
+ ` __tc_http.hdr_stage = 0U;`,
263
419
  ` __tc_http.resp_header[0] = '\\0';`,
264
420
  ` __tc_http.url[0] = '\\0';`,
265
421
  `}`,
266
422
  ``,
267
423
  `/** Stage the request. Setters (timeout/max_body/body/tls/headers) survive begin. */`,
268
424
  `static inline void __tc_http_begin(enum http_method method, const char* url) {`,
425
+ ` if (strlen(url) >= sizeof(__tc_http.url)) {`,
426
+ ` // A silently truncated URL fetches the wrong resource — refuse; the`,
427
+ ` // empty url makes perform's parse fail loudly ("bad url").`,
428
+ ` printk("tc-http: url longer than %u bytes - refused\\n",`,
429
+ ` static_cast<unsigned int>(sizeof(__tc_http.url) - 1U));`,
430
+ ` __tc_http.url[0] = '\\0';`,
431
+ ` __tc_http.method = method;`,
432
+ ` return;`,
433
+ ` }`,
269
434
  ` (void)strncpy(__tc_http.url, url, sizeof(__tc_http.url) - 1U);`,
270
435
  ` __tc_http.url[sizeof(__tc_http.url) - 1U] = '\\0';`,
271
436
  ` __tc_http.method = method;`,
@@ -276,6 +441,9 @@ export function httpInitLines() {
276
441
  ` __tc_http.ok = false;`,
277
442
  ` __tc_http.resp_len = 0;`,
278
443
  ` __tc_http.content_length = -1;`,
444
+ ` __tc_http.resp_headers_len = 0;`,
445
+ ` __tc_http.hdr_parse_name_len = 0;`,
446
+ ` __tc_http.hdr_stage = 0U;`,
279
447
  ` __tc_http.resp_header[0] = '\\0';`,
280
448
  `}`,
281
449
  ``,
@@ -327,15 +495,34 @@ export function httpInitLines() {
327
495
  ` int sock = __tc_http_open_socket();`,
328
496
  ` if (sock < 0) { __tc_http.done = true; return; }`,
329
497
  ` // Build the NULL-terminated header-fields list from staged headers.`,
498
+ ` // Format capped 2 bytes short of the buffer so the CRLF can ALWAYS be`,
499
+ ` // appended — a truncated value stays a syntactically valid header line`,
500
+ ` // instead of swallowing the next one (snprintk alone can cut the \\r\\n).`,
330
501
  ` for (int32_t i = 0; i < __tc_http.hdr_count && i < __TC_HTTP_MAX_HEADERS; i++) {`,
331
- ` (void)snprintk(__tc_http.hdr_field_buf[i], sizeof(__tc_http.hdr_field_buf[i]),`,
332
- ` "%s: %s\\r\\n", __tc_http.hdr_name[i], __tc_http.hdr_value[i]);`,
502
+ ` (void)snprintk(__tc_http.hdr_field_buf[i], sizeof(__tc_http.hdr_field_buf[i]) - 2U,`,
503
+ ` "%s: %s", __tc_http.hdr_name[i], __tc_http.hdr_value[i]);`,
504
+ ` size_t flen = strlen(__tc_http.hdr_field_buf[i]);`,
505
+ ` __tc_http.hdr_field_buf[i][flen] = '\\r';`,
506
+ ` __tc_http.hdr_field_buf[i][flen + 1U] = '\\n';`,
507
+ ` __tc_http.hdr_field_buf[i][flen + 2U] = '\\0';`,
333
508
  ` __tc_http.hdr_fields[i] = __tc_http.hdr_field_buf[i];`,
334
509
  ` }`,
335
510
  ` __tc_http.hdr_fields[(__tc_http.hdr_count < __TC_HTTP_MAX_HEADERS)`,
336
511
  ` ? __tc_http.hdr_count : __TC_HTTP_MAX_HEADERS] = nullptr;`,
512
+ ` // Response-header capture: the client's internal parser hooks forward`,
513
+ ` // to these settings (on_header_field/on_header_value) while keeping`,
514
+ ` // their own status/body handling — no interference with the body path.`,
515
+ ` static struct http_parser_settings __tc_http_parse_settings;`,
516
+ ` static bool __tc_http_parse_settings_inited;`,
517
+ ` if (!__tc_http_parse_settings_inited) {`,
518
+ ` http_parser_settings_init(&__tc_http_parse_settings);`,
519
+ ` __tc_http_parse_settings.on_header_field = __tc_http_on_hdr_field;`,
520
+ ` __tc_http_parse_settings.on_header_value = __tc_http_on_hdr_value;`,
521
+ ` __tc_http_parse_settings_inited = true;`,
522
+ ` }`,
337
523
  ` struct http_request req = { 0 };`,
338
524
  ` req.method = __tc_http.method;`,
525
+ ` req.http_cb = &__tc_http_parse_settings;`,
339
526
  ` req.url = __tc_http.path;`,
340
527
  ` req.host = __tc_http.host;`,
341
528
  ` req.protocol = "HTTP/1.1";`,
@@ -348,6 +535,7 @@ export function httpInitLines() {
348
535
  ` req.payload_len = static_cast<size_t>(__tc_http.body_len);`,
349
536
  ` }`,
350
537
  ` int sent = http_client_req(sock, &req, __tc_http.timeout_ms, nullptr);`,
538
+ ` __tc_http_hdr_finish();`,
351
539
  ` // The response callback (__tc_http_resp_cb) captures the status, but Zephyr's`,
352
540
  ` // parser aborts on 5xx responses (on_headers_complete returns 1 for status`,
353
541
  ` // >= 500, which http_parser treats as an error → http_client_req returns`,
@@ -391,12 +579,23 @@ export function httpInitLines() {
391
579
  ` k_sem_init(&__tc_http.done_sem, 0, 1);`,
392
580
  ` __tc_http.sem_inited = true;`,
393
581
  ` }`,
582
+ ` // Init the work item ONCE — re-initializing a queued/running item is`,
583
+ ` // undefined in Zephyr, and the busy check below makes the single-slot`,
584
+ ` // contract explicit instead of corrupting the in-flight request.`,
585
+ ` if (!__tc_http.work_inited) {`,
586
+ ` k_work_init(&__tc_http.perform_work, __tc_http_perform_work);`,
587
+ ` __tc_http.work_inited = true;`,
588
+ ` }`,
589
+ ` if (k_work_is_pending(&__tc_http.perform_work)) {`,
590
+ ` printk("tc-http: previous async request still in flight - send refused\\n");`,
591
+ ` __tc_http.done = true;`,
592
+ ` return;`,
593
+ ` }`,
394
594
  ` __tc_http.done = false;`,
395
595
  ` // Drain any completion signal left from a previous request (e.g. the`,
396
596
  ` // prior done() saw the volatile flag before taking the sem), so the first`,
397
597
  ` // poll of this request doesn't fire on a stale signal.`,
398
598
  ` (void)k_sem_take(&__tc_http.done_sem, K_NO_WAIT);`,
399
- ` k_work_init(&__tc_http.perform_work, __tc_http_perform_work);`,
400
599
  ` (void)k_work_submit(&__tc_http.perform_work);`,
401
600
  `}`,
402
601
  ``,
@@ -419,13 +618,39 @@ export function httpInitLines() {
419
618
  ` return static_cast<int32_t>(__tc_http.content_length);`,
420
619
  `}`,
421
620
  ``,
422
- `// Response-header lookup. The Zephyr http_client_req surface does not expose`,
423
- `// arbitrary response headers via the recv buffer; this returns the last`,
424
- `// header captured by the response callback path. With no per-header hook`,
425
- `// it returns "" until a richer parser setting is wired (http_cb).`,
621
+ `// Response-header lookup over the block captured by the parser hooks.`,
622
+ `// ASCII-case-insensitive name match at a line start; returns the value`,
623
+ `// ("" when absent or when the block was truncated past it).`,
624
+ `static inline char __tc_http_ascii_lower(char c) {`,
625
+ ` return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + 32) : c;`,
626
+ `}`,
627
+ ``,
426
628
  `static inline const char* __tc_http_response_header(const char* name) {`,
427
- ` (void)name;`,
428
629
  ` __tc_http.resp_header[0] = '\\0';`,
630
+ ` const char* block = __tc_http.resp_headers;`,
631
+ ` const char* end = block + __tc_http.resp_headers_len;`,
632
+ ` const char* line = block;`,
633
+ ` while (line < end) {`,
634
+ ` const char* eol = line;`,
635
+ ` while (eol < end && *eol != '\\r' && *eol != '\\n') { eol++; }`,
636
+ ` const char* c = line;`,
637
+ ` const char* n = name;`,
638
+ ` while (c < eol && *n != '\\0' && __tc_http_ascii_lower(*c) == __tc_http_ascii_lower(*n)) {`,
639
+ ` c++;`,
640
+ ` n++;`,
641
+ ` }`,
642
+ ` if (*n == '\\0' && c < eol && *c == ':') {`,
643
+ ` c++;`,
644
+ ` if (c < eol && *c == ' ') { c++; }`,
645
+ ` size_t vlen = static_cast<size_t>(eol - c);`,
646
+ ` if (vlen >= sizeof(__tc_http.resp_header)) { vlen = sizeof(__tc_http.resp_header) - 1U; }`,
647
+ ` (void)memcpy(__tc_http.resp_header, c, vlen);`,
648
+ ` __tc_http.resp_header[vlen] = '\\0';`,
649
+ ` break;`,
650
+ ` }`,
651
+ ` line = eol;`,
652
+ ` while (line < end && (*line == '\\r' || *line == '\\n')) { line++; }`,
653
+ ` }`,
429
654
  ` return __tc_http.resp_header;`,
430
655
  `}`,
431
656
  `// CUTTLEFISH_HTTP_END`,
@@ -452,9 +677,13 @@ export function lowerHttp(op) {
452
677
  case 'http.begin': {
453
678
  const methodName = String(o.method).replace(/^["']|["']$/g, '').toUpperCase();
454
679
  const method = METHOD_MAP[methodName] ?? 'HTTP_GET';
455
- // Fresh shim state per request — the reset used to ride the deleted
456
- // factory op (http.reset); construction now owns it.
457
- return { code: `__tc_http_reset(); __tc_http_begin(${method}, ${s(o.url)});` };
680
+ // Fresh shim state per request, THEN this request's header() pairs:
681
+ // the reset must precede them (the call-site emissions ran before
682
+ // send() and are wiped by the reset — re-staging here also keeps
683
+ // headers alive across send() loops).
684
+ const pairs = Array.isArray(o.headers) ? o.headers : [];
685
+ const staged = pairs.length > 0 ? `${pairs.map(([n, v]) => `__tc_http_set_header(${s(n)}, ${s(v)});`).join(' ')} ` : '';
686
+ return { code: `__tc_http_reset(); ${staged}__tc_http_begin(${method}, ${s(o.url)});` };
458
687
  }
459
688
  case 'http.set_header':
460
689
  return { code: `__tc_http_set_header(${s(o.name)}, ${s(o.value)});` };
@@ -480,7 +709,10 @@ export function lowerHttp(op) {
480
709
  }
481
710
  const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\s+/g, '');
482
711
  const der = Buffer.from(b64, 'base64');
483
- if (der.length < 100)
712
+ // Every X.509 DER starts with the ASN.1 SEQUENCE tag (0x30); a long
713
+ // but non-cert payload fails here instead of at a confusing handshake
714
+ // error on the target.
715
+ if (der.length < 100 || der[0] !== 0x30)
484
716
  return { code: `// tc-http: caCert PEM failed to decode` };
485
717
  const hex = [...der].map((b) => `0x${b.toString(16).padStart(2, '0')}`).join(', ');
486
718
  return { code: `{ static const uint8_t __tc_ca_der[] = { ${hex} }; __tc_http_set_ca_cert_der(__tc_ca_der, sizeof(__tc_ca_der)); }` };
@@ -5,6 +5,14 @@ import type { ZephyrChipDescriptor } from '../chips/types.js';
5
5
  * controller the program uses (gated on ctx.analysis.usesI2C).
6
6
  */
7
7
  export declare function i2cInitLines(chip: ZephyrChipDescriptor, controllerIndex: number): string[];
8
+ /**
9
+ * Emit one responder's state: receive ring (UART discipline — free-running
10
+ * head/tail, overflow-drop), response buffer with its served position, the
11
+ * two handler slots, the five i2c_target callbacks, and the config the ops'
12
+ * registration guard passes to i2c_target_register. Called from shimLines
13
+ * for each constructed responder (collectI2cResponders).
14
+ */
15
+ export declare function i2cResponderStateLines(chip: ZephyrChipDescriptor, controllerIndex: number, address: number, rx: number, tx: number): string[];
8
16
  /**
9
17
  * Resolve a HAL i2c.* op to Zephyr C++.
10
18
  * Returns `{ code }` for statement ops, `{ expression }` for value-returning ops.
@@ -40,13 +40,105 @@ function speedForHz(hz) {
40
40
  return 'I2C_SPEED_FAST_PLUS';
41
41
  return 'I2C_SPEED_HIGH';
42
42
  }
43
+ // ── I2C responder (hal/i2c-responder.ts) — this board as an I2C target ────
44
+ //
45
+ // One state block per constructed responder (bus + address), the i2c_target
46
+ // vocabulary verbatim: the five driver callbacks behind two user-facing ones
47
+ // (onReceive fires at STOP with the receive ring's new byte count; onRequest
48
+ // fires at read start — the write() refill point). Registration
49
+ // (i2c_target_register) is once-guarded at the first op, against the
50
+ // controller handle the master-side init block emitted (__tc_i2c<N>_dev).
51
+ /** The C symbol prefix for one responder's state. */
52
+ function respPrefix(idx, address) {
53
+ return `__tc_i2cresp${idx}_a${address.toString(16)}`;
54
+ }
55
+ /** The once-guarded registration every responder op carries — the UART arm
56
+ * discipline: the first emitted call registers the address, so the
57
+ * callbacks installed early in setup make it answer from boot. An
58
+ * unsupported controller prints loudly (the CAN policy: -ENOSYS from a
59
+ * driver with no target mode must not be a silent empty responder). */
60
+ function respRegisterGuard(p, idx) {
61
+ 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); } } } `;
62
+ }
43
63
  /**
44
- * The (void) comma expression begin/end lower to — a no-op that references
45
- * the controller's whole state block (device + transaction buffers) so a
46
- * program calling only begin() stays -Werror clean (-Wunused-variable).
64
+ * Emit one responder's state: receive ring (UART discipline — free-running
65
+ * head/tail, overflow-drop), response buffer with its served position, the
66
+ * two handler slots, the five i2c_target callbacks, and the config the ops'
67
+ * registration guard passes to i2c_target_register. Called from shimLines
68
+ * for each constructed responder (collectI2cResponders).
47
69
  */
48
- function i2cKeepAlive(p) {
49
- return `(void)${p}_dev;`;
70
+ export function i2cResponderStateLines(chip, controllerIndex, address, rx, tx) {
71
+ const ctrl = chip.i2c?.controllers[controllerIndex];
72
+ if (!ctrl)
73
+ return [];
74
+ const p = respPrefix(controllerIndex, address);
75
+ return [
76
+ '// CUTTLEFISH_I2CRESP_BEGIN',
77
+ `static uint8_t ${p}_rx[${rx}];`,
78
+ `static volatile uint32_t ${p}_head = 0;`,
79
+ `static volatile uint32_t ${p}_tail = 0;`,
80
+ `static uint8_t ${p}_tx[${tx}];`,
81
+ `static volatile uint32_t ${p}_txlen = 0;`,
82
+ `static volatile uint32_t ${p}_txpos = 0;`,
83
+ `static void (*${p}_on_rx)(double) = NULL;`,
84
+ `static void (*${p}_on_rq)(void) = NULL;`,
85
+ // A controller write transaction: reset the ring, then take each byte.
86
+ // A byte arriving with a full ring is dropped — the embedded-honest
87
+ // answer (no unbounded buffering), same as the UART RX ring.
88
+ `static int ${p}_write_requested(struct i2c_target_config* cfg) {`,
89
+ ' (void)cfg;',
90
+ ` ${p}_head = 0;`,
91
+ ` ${p}_tail = 0;`,
92
+ ' return 0;',
93
+ '}',
94
+ `static int ${p}_write_received(struct i2c_target_config* cfg, uint8_t val) {`,
95
+ ' (void)cfg;',
96
+ ` if ((${p}_head - ${p}_tail) < ${rx}U) {`,
97
+ ` ${p}_rx[${p}_head % ${rx}U] = val;`,
98
+ ` ${p}_head++;`,
99
+ ' }',
100
+ ' return 0;',
101
+ '}',
102
+ // A controller read transaction: the user's refill point first, then the
103
+ // first byte. Past the buffer's end the controller reads 0xFF (the bus
104
+ // has no NACK for a too-long read; the value is the honest filler).
105
+ `static int ${p}_read_requested(struct i2c_target_config* cfg, uint8_t* val) {`,
106
+ ' (void)cfg;',
107
+ ` if (${p}_on_rq != NULL) { ${p}_on_rq(); }`,
108
+ ` ${p}_txpos = 1U;`,
109
+ ` *val = (${p}_txlen > 0U) ? ${p}_tx[0] : 0xFFU;`,
110
+ ' return 0;',
111
+ '}',
112
+ `static int ${p}_read_processed(struct i2c_target_config* cfg, uint8_t* val) {`,
113
+ ' (void)cfg;',
114
+ ` *val = (${p}_txpos < ${p}_txlen) ? ${p}_tx[${p}_txpos] : 0xFFU;`,
115
+ ` ${p}_txpos++;`,
116
+ ' return 0;',
117
+ '}',
118
+ // End of transaction: announce a completed controller write with the
119
+ // count now waiting in the ring (the Wire onReceive(len) semantic).
120
+ `static int ${p}_stop(struct i2c_target_config* cfg) {`,
121
+ ' (void)cfg;',
122
+ ` if ((${p}_on_rx != NULL) && (${p}_head > ${p}_tail)) {`,
123
+ ` ${p}_on_rx(static_cast<double>(${p}_head - ${p}_tail));`,
124
+ ' }',
125
+ ' return 0;',
126
+ '}',
127
+ `static const struct i2c_target_callbacks ${p}_cbs = {`,
128
+ ` .write_requested = ${p}_write_requested,`,
129
+ ` .read_requested = ${p}_read_requested,`,
130
+ ` .write_received = ${p}_write_received,`,
131
+ ` .read_processed = ${p}_read_processed,`,
132
+ ` .stop = ${p}_stop,`,
133
+ '};',
134
+ `static struct i2c_target_config ${p}_cfg = {`,
135
+ ' .flags = 0U,',
136
+ ` .address = ${address}U,`,
137
+ ` .callbacks = &${p}_cbs,`,
138
+ '};',
139
+ `static bool ${p}_registered = false;`,
140
+ '// CUTTLEFISH_I2CRESP_END',
141
+ ];
50
142
  }
51
143
  /** The thin-device bus-speed preamble: apply the construction hz once
52
144
  * (i2c_configure with Zephyr's I2C_SPEED_SET tier mapping), or '' when the
@@ -99,6 +191,46 @@ export function lowerI2c(op, chip) {
99
191
  const arr = `static const uint8_t __tc_i2cw[] = { ${bytes.join(', ')} };`;
100
192
  return { code: `${pre} ${arr} (void)i2c_write(${p}_dev, __tc_i2cw, sizeof(__tc_i2cw), static_cast<uint16_t>(${o.address}));` };
101
193
  }
194
+ // ── I2C responder (hal/i2c-responder.ts) — target-mode ops ───────────
195
+ // Every op carries the once-guarded registration; the state it touches
196
+ // is the per-responder block i2cResponderStateLines emitted.
197
+ case 'i2c.resp_on_receive': {
198
+ const rp = respPrefix(idx, o.address);
199
+ return { code: `${respRegisterGuard(rp, idx)} ${rp}_on_rx = ${o.handler};` };
200
+ }
201
+ case 'i2c.resp_on_request': {
202
+ const rp = respPrefix(idx, o.address);
203
+ return { code: `${respRegisterGuard(rp, idx)} ${rp}_on_rq = ${o.handler};` };
204
+ }
205
+ case 'i2c.resp_available': {
206
+ const rp = respPrefix(idx, o.address);
207
+ return { expression: `({ ${respRegisterGuard(rp, idx)} (${rp}_head - ${rp}_tail); })` };
208
+ }
209
+ case 'i2c.resp_read': {
210
+ const rp = respPrefix(idx, o.address);
211
+ return { expression: `({ ${respRegisterGuard(rp, idx)} (${rp}_tail < ${rp}_head ? ${rp}_rx[(${rp}_tail)++ % ${o.rx}U] : -1); })` };
212
+ }
213
+ case 'i2c.resp_write': {
214
+ const rp = respPrefix(idx, o.address);
215
+ const bytes = o.bytes ?? [];
216
+ const isBuffer = bytes.length === 1 && typeof bytes[0] === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(bytes[0]);
217
+ if (isBuffer) {
218
+ const name = bytes[0];
219
+ // A named user buffer: copy its storage into the response buffer,
220
+ // clamped to the constructed size; the length lands last so an
221
+ // in-flight read never sees a torn buffer.
222
+ return {
223
+ 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; }`,
224
+ };
225
+ }
226
+ if (bytes.length > Number(o.tx)) {
227
+ throw new Error(`I2C responder write: the response buffer holds ${o.tx} bytes — got ${bytes.length}. Construct with a larger txBufferBytes.`);
228
+ }
229
+ const assigns = bytes
230
+ .map((b, i) => `${rp}_tx[${i}] = static_cast<uint8_t>(${b});`)
231
+ .join(' ');
232
+ return { code: `${respRegisterGuard(rp, idx)} { ${rp}_txlen = 0U; ${assigns} ${rp}_txlen = ${bytes.length}U; }` };
233
+ }
102
234
  default:
103
235
  throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
104
236
  `Open an issue or use rawCpp() to emit it manually.`);