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

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 (132) 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/boardgen.d.ts +1 -9
  6. package/dist/boardgen.js +228 -45
  7. package/dist/chips/types.d.ts +1 -1
  8. package/dist/debug-codegen.js +1 -1
  9. package/dist/display/touch-adapter.js +1 -1
  10. package/dist/display/ui-adapter.js +549 -549
  11. package/dist/doctor.js +4 -4
  12. package/dist/dt-config/custom-board.js +2 -2
  13. package/dist/dt-config/kconfig.js +18 -12
  14. package/dist/dt-config/overlay.d.ts +2 -2
  15. package/dist/dt-config/overlay.js +2 -2
  16. package/dist/framework.manifest.d.ts +3 -3
  17. package/dist/framework.manifest.js +10 -7
  18. package/dist/index.js +5 -5
  19. package/dist/licenses.d.ts +2 -2
  20. package/dist/licenses.js +8 -8
  21. package/dist/lowering/fs.js +1 -1
  22. package/dist/lowering/gpio.js +0 -32
  23. package/dist/lowering/http.js +264 -32
  24. package/dist/lowering/i2c.js +0 -8
  25. package/dist/lowering/interrupts.js +6 -0
  26. package/dist/lowering/mqtt.js +110 -8
  27. package/dist/lowering/usb.js +11 -11
  28. package/dist/lowering/wdt.js +2 -29
  29. package/dist/sdk/board-catalog-sync.d.ts +1 -3
  30. package/dist/sdk/board-catalog-sync.js +4 -10
  31. package/dist/strategy.d.ts +22 -44
  32. package/dist/strategy.js +152 -155
  33. package/dist/tmp-probe.d.ts +2 -0
  34. package/dist/tmp-probe.js +9 -0
  35. package/dist/toolchain/debug-config.d.ts +50 -90
  36. package/dist/toolchain/debug-config.js +239 -510
  37. package/dist/toolchain/env-check.d.ts +1 -3
  38. package/dist/toolchain/env-check.js +2 -7
  39. package/dist/toolchain/index.d.ts +16 -2
  40. package/dist/toolchain/index.js +383 -35
  41. package/dist/toolchain/runners.d.ts +16 -0
  42. package/dist/toolchain/runners.js +75 -0
  43. package/dist/toolchain/scaffold.d.ts +1 -1
  44. package/dist/toolchain/scaffold.js +12 -12
  45. package/dist/toolchain/west-discover.d.ts +6 -0
  46. package/dist/toolchain/west-discover.js +36 -13
  47. package/dist/toolchain/west-spawn.js +8 -2
  48. package/installer/README.md +328 -328
  49. package/installer/install.sh +2 -2
  50. package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
  51. package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
  52. package/installer/templates/project/.vscode/settings.json +1 -1
  53. package/installer/templates/project/README.md +2 -2
  54. package/package.json +5 -5
  55. package/src/as-built.ts +206 -206
  56. package/src/boardgen.ts +214 -48
  57. package/src/chips/types.ts +567 -567
  58. package/src/display/touch-adapter.ts +204 -204
  59. package/src/display/ui-adapter.ts +781 -781
  60. package/src/doctor.ts +4 -4
  61. package/src/dt-config/custom-board.ts +2 -2
  62. package/src/dt-config/kconfig.ts +18 -12
  63. package/src/dt-config/overlay.ts +1058 -1058
  64. package/src/framework.manifest.ts +538 -535
  65. package/src/index.ts +5 -5
  66. package/src/licenses.ts +425 -425
  67. package/src/lowering/fs.ts +135 -135
  68. package/src/lowering/gpio.ts +0 -33
  69. package/src/lowering/http.ts +264 -32
  70. package/src/lowering/i2c.ts +0 -9
  71. package/src/lowering/interrupts.ts +6 -0
  72. package/src/lowering/mqtt.ts +109 -8
  73. package/src/lowering/usb.ts +221 -221
  74. package/src/lowering/wdt.ts +2 -25
  75. package/src/sdk/board-catalog-sync.ts +4 -25
  76. package/src/strategy.ts +2265 -2309
  77. package/src/toolchain/debug-config.ts +262 -522
  78. package/src/toolchain/env-check.ts +279 -285
  79. package/src/toolchain/index.ts +1703 -1359
  80. package/src/toolchain/runners.ts +80 -0
  81. package/src/toolchain/scaffold.ts +296 -296
  82. package/src/toolchain/west-discover.ts +35 -13
  83. package/src/toolchain/west-spawn.ts +174 -168
  84. package/dist/async/timer-polyfill.d.ts +0 -10
  85. package/dist/async/timer-polyfill.js +0 -95
  86. package/dist/chips/board-overrides.d.ts +0 -7
  87. package/dist/chips/board-overrides.js +0 -11
  88. package/dist/chips/esp32.d.ts +0 -2
  89. package/dist/chips/esp32.js +0 -71
  90. package/dist/chips/esp32s3.d.ts +0 -2
  91. package/dist/chips/esp32s3.js +0 -103
  92. package/dist/chips/soc/.d.ts +0 -2
  93. package/dist/chips/soc/.js +0 -129
  94. package/dist/chips/soc/esp32.d.ts +0 -2
  95. package/dist/chips/soc/esp32.js +0 -120
  96. package/dist/chips/soc/esp32c3.d.ts +0 -2
  97. package/dist/chips/soc/esp32c3.js +0 -90
  98. package/dist/chips/soc/esp32c6.d.ts +0 -2
  99. package/dist/chips/soc/esp32c6.js +0 -109
  100. package/dist/chips/soc/esp32s3.d.ts +0 -2
  101. package/dist/chips/soc/esp32s3.js +0 -189
  102. package/dist/chips/soc/index.d.ts +0 -2
  103. package/dist/chips/soc/index.js +0 -23
  104. package/dist/chips/soc/nrf52840.d.ts +0 -2
  105. package/dist/chips/soc/nrf52840.js +0 -130
  106. package/dist/chips/soc/rp2040.d.ts +0 -2
  107. package/dist/chips/soc/rp2040.js +0 -141
  108. package/dist/chips/soc/rp2350a.d.ts +0 -2
  109. package/dist/chips/soc/rp2350a.js +0 -145
  110. package/dist/chips/soc/samd21g18a.d.ts +0 -2
  111. package/dist/chips/soc/samd21g18a.js +0 -143
  112. package/dist/chips/soc/stm32f411xe.d.ts +0 -2
  113. package/dist/chips/soc/stm32f411xe.js +0 -251
  114. package/dist/chips/xiao-ble.d.ts +0 -2
  115. package/dist/chips/xiao-ble.js +0 -100
  116. package/dist/lowering/power.d.ts +0 -9
  117. package/dist/lowering/power.js +0 -60
  118. package/dist/lowering/pulse.d.ts +0 -7
  119. package/dist/lowering/pulse.js +0 -51
  120. package/dist/lowering/tone.d.ts +0 -10
  121. package/dist/lowering/tone.js +0 -63
  122. package/dist/lowering/worker-backing.d.ts +0 -14
  123. package/dist/lowering/worker-backing.js +0 -79
  124. package/dist/lowering/worker.d.ts +0 -6
  125. package/dist/lowering/worker.js +0 -14
  126. package/dist/sdk/board-data.generated.d.ts +0 -2
  127. package/dist/sdk/board-data.generated.js +0 -4
  128. package/dist/sdk/catalog-walker.d.ts +0 -90
  129. package/dist/sdk/catalog-walker.js +0 -682
  130. package/dist/sdk/dts-reader.d.ts +0 -83
  131. package/dist/sdk/dts-reader.js +0 -596
  132. 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)); }` };
@@ -40,14 +40,6 @@ function speedForHz(hz) {
40
40
  return 'I2C_SPEED_FAST_PLUS';
41
41
  return 'I2C_SPEED_HIGH';
42
42
  }
43
- /**
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).
47
- */
48
- function i2cKeepAlive(p) {
49
- return `(void)${p}_dev;`;
50
- }
51
43
  /** The thin-device bus-speed preamble: apply the construction hz once
52
44
  * (i2c_configure with Zephyr's I2C_SPEED_SET tier mapping), or '' when the
53
45
  * target constructed without hz. Guarded per controller, so the cost after
@@ -92,6 +92,12 @@ export function collectInterruptPins(program) {
92
92
  }
93
93
  };
94
94
  visit(program);
95
+ // Ops resolved to text while inlining one HAL call into another never become
96
+ // IR nodes — they ride on ProgramIR.resolvedHalOps (see strategy.ts's
97
+ // visitResolvedHalOps for the full story).
98
+ for (const op of program?.resolvedHalOps ?? []) {
99
+ visit({ operation: op });
100
+ }
95
101
  return pins;
96
102
  }
97
103
  /**