@typecad/framework-zephyr 1.0.0-alpha.15 → 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.
- package/LICENSE +202 -21
- package/README.md +49 -87
- package/dist/as-built.d.ts +2 -2
- package/dist/as-built.js +2 -2
- package/dist/boardgen.d.ts +1 -9
- package/dist/boardgen.js +228 -45
- package/dist/chips/types.d.ts +1 -1
- package/dist/debug-codegen.js +1 -1
- package/dist/display/touch-adapter.js +1 -1
- package/dist/display/ui-adapter.js +549 -549
- package/dist/doctor.js +4 -4
- package/dist/dt-config/custom-board.js +2 -2
- package/dist/dt-config/kconfig.js +18 -12
- package/dist/dt-config/overlay.d.ts +2 -2
- package/dist/dt-config/overlay.js +2 -2
- package/dist/framework.manifest.d.ts +3 -3
- package/dist/framework.manifest.js +10 -7
- package/dist/index.js +5 -5
- package/dist/licenses.d.ts +2 -2
- package/dist/licenses.js +8 -8
- package/dist/lowering/fs.js +1 -1
- package/dist/lowering/gpio.js +0 -32
- package/dist/lowering/http.js +264 -32
- package/dist/lowering/i2c.js +0 -8
- package/dist/lowering/interrupts.js +6 -0
- package/dist/lowering/mqtt.js +110 -8
- package/dist/lowering/usb.js +11 -11
- package/dist/lowering/wdt.js +2 -29
- package/dist/sdk/board-catalog-sync.d.ts +1 -3
- package/dist/sdk/board-catalog-sync.js +4 -10
- package/dist/strategy.d.ts +22 -44
- package/dist/strategy.js +152 -155
- package/dist/tmp-probe.d.ts +2 -0
- package/dist/tmp-probe.js +9 -0
- package/dist/toolchain/debug-config.d.ts +50 -90
- package/dist/toolchain/debug-config.js +241 -502
- package/dist/toolchain/env-check.d.ts +1 -3
- package/dist/toolchain/env-check.js +2 -7
- package/dist/toolchain/index.d.ts +65 -2
- package/dist/toolchain/index.js +432 -55
- package/dist/toolchain/runners.d.ts +16 -0
- package/dist/toolchain/runners.js +75 -0
- package/dist/toolchain/scaffold.d.ts +1 -1
- package/dist/toolchain/scaffold.js +12 -12
- package/dist/toolchain/west-discover.d.ts +6 -0
- package/dist/toolchain/west-discover.js +36 -13
- package/dist/toolchain/west-spawn.js +8 -2
- package/installer/README.md +328 -328
- package/installer/install.sh +2 -2
- package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
- package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
- package/installer/templates/project/.vscode/settings.json +1 -1
- package/installer/templates/project/README.md +2 -2
- package/package.json +5 -5
- package/src/as-built.ts +206 -206
- package/src/boardgen.ts +214 -48
- package/src/chips/types.ts +567 -567
- package/src/display/touch-adapter.ts +204 -204
- package/src/display/ui-adapter.ts +781 -781
- package/src/doctor.ts +4 -4
- package/src/dt-config/custom-board.ts +2 -2
- package/src/dt-config/kconfig.ts +18 -12
- package/src/dt-config/overlay.ts +1058 -1058
- package/src/framework.manifest.ts +538 -535
- package/src/index.ts +5 -5
- package/src/licenses.ts +425 -425
- package/src/lowering/fs.ts +135 -135
- package/src/lowering/gpio.ts +0 -33
- package/src/lowering/http.ts +264 -32
- package/src/lowering/i2c.ts +0 -9
- package/src/lowering/interrupts.ts +6 -0
- package/src/lowering/mqtt.ts +109 -8
- package/src/lowering/usb.ts +221 -221
- package/src/lowering/wdt.ts +2 -25
- package/src/sdk/board-catalog-sync.ts +4 -25
- package/src/strategy.ts +2265 -2309
- package/src/toolchain/debug-config.ts +264 -514
- package/src/toolchain/env-check.ts +279 -285
- package/src/toolchain/index.ts +1703 -1286
- package/src/toolchain/runners.ts +80 -0
- package/src/toolchain/scaffold.ts +296 -296
- package/src/toolchain/west-discover.ts +35 -13
- package/src/toolchain/west-spawn.ts +174 -168
- package/dist/async/timer-polyfill.d.ts +0 -10
- package/dist/async/timer-polyfill.js +0 -95
- package/dist/chips/board-overrides.d.ts +0 -7
- package/dist/chips/board-overrides.js +0 -11
- package/dist/chips/esp32.d.ts +0 -2
- package/dist/chips/esp32.js +0 -71
- package/dist/chips/esp32s3.d.ts +0 -2
- package/dist/chips/esp32s3.js +0 -103
- package/dist/chips/soc/.d.ts +0 -2
- package/dist/chips/soc/.js +0 -129
- package/dist/chips/soc/esp32.d.ts +0 -2
- package/dist/chips/soc/esp32.js +0 -120
- package/dist/chips/soc/esp32c3.d.ts +0 -2
- package/dist/chips/soc/esp32c3.js +0 -90
- package/dist/chips/soc/esp32c6.d.ts +0 -2
- package/dist/chips/soc/esp32c6.js +0 -109
- package/dist/chips/soc/esp32s3.d.ts +0 -2
- package/dist/chips/soc/esp32s3.js +0 -189
- package/dist/chips/soc/index.d.ts +0 -2
- package/dist/chips/soc/index.js +0 -23
- package/dist/chips/soc/nrf52840.d.ts +0 -2
- package/dist/chips/soc/nrf52840.js +0 -130
- package/dist/chips/soc/rp2040.d.ts +0 -2
- package/dist/chips/soc/rp2040.js +0 -141
- package/dist/chips/soc/rp2350a.d.ts +0 -2
- package/dist/chips/soc/rp2350a.js +0 -145
- package/dist/chips/soc/samd21g18a.d.ts +0 -2
- package/dist/chips/soc/samd21g18a.js +0 -143
- package/dist/chips/soc/stm32f411xe.d.ts +0 -2
- package/dist/chips/soc/stm32f411xe.js +0 -251
- package/dist/chips/xiao-ble.d.ts +0 -2
- package/dist/chips/xiao-ble.js +0 -100
- package/dist/lowering/power.d.ts +0 -9
- package/dist/lowering/power.js +0 -60
- package/dist/lowering/pulse.d.ts +0 -7
- package/dist/lowering/pulse.js +0 -51
- package/dist/lowering/tone.d.ts +0 -10
- package/dist/lowering/tone.js +0 -63
- package/dist/lowering/worker-backing.d.ts +0 -14
- package/dist/lowering/worker-backing.js +0 -79
- package/dist/lowering/worker.d.ts +0 -6
- package/dist/lowering/worker.js +0 -14
- package/dist/sdk/board-data.generated.d.ts +0 -2
- package/dist/sdk/board-data.generated.js +0 -4
- package/dist/sdk/catalog-walker.d.ts +0 -90
- package/dist/sdk/catalog-walker.js +0 -682
- package/dist/sdk/dts-reader.d.ts +0 -83
- package/dist/sdk/dts-reader.js +0 -596
- package/src/debug-codegen.ts +0 -207
package/src/lowering/http.ts
CHANGED
|
@@ -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):
|
|
12
|
-
// begin (
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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[
|
|
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][
|
|
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.
|
|
205
|
-
` //
|
|
206
|
-
` //
|
|
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
|
-
`
|
|
209
|
-
`
|
|
210
|
-
`
|
|
211
|
-
`
|
|
212
|
-
`
|
|
213
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`/**
|
|
252
|
-
`
|
|
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
|
|
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
|
|
427
|
-
`//
|
|
428
|
-
`//
|
|
429
|
-
|
|
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
|
|
462
|
-
//
|
|
463
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/lowering/i2c.ts
CHANGED
|
@@ -43,15 +43,6 @@ function speedForHz(hz: number): string {
|
|
|
43
43
|
return 'I2C_SPEED_HIGH';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
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).
|
|
50
|
-
*/
|
|
51
|
-
function i2cKeepAlive(p: string): string {
|
|
52
|
-
return `(void)${p}_dev;`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
46
|
/** The thin-device bus-speed preamble: apply the construction hz once
|
|
56
47
|
* (i2c_configure with Zephyr's I2C_SPEED_SET tier mapping), or '' when the
|
|
57
48
|
* target constructed without hz. Guarded per controller, so the cost after
|
|
@@ -116,6 +116,12 @@ export function collectInterruptPins(program: unknown): Set<number> {
|
|
|
116
116
|
}
|
|
117
117
|
};
|
|
118
118
|
visit(program);
|
|
119
|
+
// Ops resolved to text while inlining one HAL call into another never become
|
|
120
|
+
// IR nodes — they ride on ProgramIR.resolvedHalOps (see strategy.ts's
|
|
121
|
+
// visitResolvedHalOps for the full story).
|
|
122
|
+
for (const op of (program as { resolvedHalOps?: unknown[] })?.resolvedHalOps ?? []) {
|
|
123
|
+
visit({ operation: op });
|
|
124
|
+
}
|
|
119
125
|
return pins;
|
|
120
126
|
}
|
|
121
127
|
|
package/src/lowering/mqtt.ts
CHANGED
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
// shim getaddrinfo-resolves the broker host itself (like the HTTP shim).
|
|
14
14
|
// URI scheme: mqtt:// → 1883 plain TCP, mqtts:// → 8883 TLS
|
|
15
15
|
// (MQTT_TRANSPORT_SECURE + mqtt_sec_config, reusing the same mbedTLS matrix
|
|
16
|
-
// the HTTP TLS path established).
|
|
16
|
+
// the HTTP TLS path established). caCert pins the broker's CA — the PEM is
|
|
17
|
+
// DER-decoded at emit time and registered via tls_credential_add as a
|
|
18
|
+
// CA_CERTIFICATE sec tag with TLS_PEER_VERIFY_REQUIRED (verified TLS, the
|
|
19
|
+
// MQTT analogue of Request's caCert); mqtts:// without a CA stays
|
|
20
|
+
// encrypted-but-unverified (TLS_PEER_VERIFY_NONE).
|
|
17
21
|
//
|
|
18
22
|
// One client slot (HAL singleton). on_message callbacks are dispatched on the
|
|
19
23
|
// poll thread.
|
|
@@ -49,6 +53,8 @@ export function mqttInitLines(): string[] {
|
|
|
49
53
|
`// at connect time; host drives both DNS resolution and the TLS SNI/hostname.`,
|
|
50
54
|
`// on_message is the user callback (set via mqtt.on_message); it fires on the`,
|
|
51
55
|
`// poll thread for every incoming PUBLISH on a subscribed topic.`,
|
|
56
|
+
`// ca_der is the pinned CA (DER, user-owned from the caCert fact); nullptr`,
|
|
57
|
+
`// means mqtts:// runs encrypted-but-unverified.`,
|
|
52
58
|
`typedef void (*__tc_mqtt_msg_cb_t)(const char* topic, const char* payload);`,
|
|
53
59
|
`static struct {`,
|
|
54
60
|
` struct mqtt_client client;`,
|
|
@@ -59,6 +65,16 @@ export function mqttInitLines(): string[] {
|
|
|
59
65
|
` uint16_t port; // 1883 / 8883 unless overridden in the URI`,
|
|
60
66
|
` char client_id[64];`,
|
|
61
67
|
` bool tls; // scheme == "mqtts"`,
|
|
68
|
+
` const uint8_t* ca_der; // pinned CA (DER), nullptr = no CA`,
|
|
69
|
+
` size_t ca_der_len;`,
|
|
70
|
+
` // What this shim last registered under __TC_MQTT_SEC_TAG (DER arrays`,
|
|
71
|
+
` // are function-static, so pointer identity is stable). Zephyr's`,
|
|
72
|
+
` // credential store never REPLACES a tag — repeats are skipped, a`,
|
|
73
|
+
` // different CA at an occupied tag is reported, not silent.`,
|
|
74
|
+
` const uint8_t* ca_added;`,
|
|
75
|
+
` size_t ca_added_len;`,
|
|
76
|
+
` bool ca_added_set;`,
|
|
77
|
+
` sec_tag_t sec_tags[1]; // backs tls.config.sec_tag_list (static lifetime)`,
|
|
62
78
|
` volatile bool connected;`,
|
|
63
79
|
` volatile bool stop_poll; // set by disconnect to terminate the poll thread`,
|
|
64
80
|
` __tc_mqtt_msg_cb_t on_message;`,
|
|
@@ -200,6 +216,35 @@ export function mqttInitLines(): string[] {
|
|
|
200
216
|
` return host_len > 0U;`,
|
|
201
217
|
`}`,
|
|
202
218
|
``,
|
|
219
|
+
`// Strict IPv4 dotted-quad predicate: exactly four 0-255 groups. A`,
|
|
220
|
+
`// digit-and-dot scan would also swallow numeric-only DNS names ("123"),`,
|
|
221
|
+
`// silently skipping the TLS hostname check for them (same helper as the`,
|
|
222
|
+
`// HTTP shim).`,
|
|
223
|
+
`static bool __tc_mqtt_host_is_ipv4(const char* h) {`,
|
|
224
|
+
` uint32_t quads = 0U;`,
|
|
225
|
+
` uint32_t val = 0U;`,
|
|
226
|
+
` uint32_t digits = 0U;`,
|
|
227
|
+
` for (const char* p = h; ; p++) {`,
|
|
228
|
+
` if (*p >= '0' && *p <= '9') {`,
|
|
229
|
+
` val = (val * 10U) + static_cast<uint32_t>(*p - '0');`,
|
|
230
|
+
` digits++;`,
|
|
231
|
+
` if (digits > 3U || val > 255U) return false;`,
|
|
232
|
+
` } else if (*p == '.') {`,
|
|
233
|
+
` if (digits == 0U || quads >= 3U) return false;`,
|
|
234
|
+
` quads++;`,
|
|
235
|
+
` val = 0U;`,
|
|
236
|
+
` digits = 0U;`,
|
|
237
|
+
` } else if (*p == '\\0') {`,
|
|
238
|
+
` if (digits == 0U) return false;`,
|
|
239
|
+
` quads++;`,
|
|
240
|
+
` break;`,
|
|
241
|
+
` } else {`,
|
|
242
|
+
` return false;`,
|
|
243
|
+
` }`,
|
|
244
|
+
` }`,
|
|
245
|
+
` return quads == 4U;`,
|
|
246
|
+
`}`,
|
|
247
|
+
``,
|
|
203
248
|
`// ── connect ─────────────────────────────────────────────────────────────`,
|
|
204
249
|
`// Parse URI → resolve broker → init client → mqtt_connect → spawn poll thread.`,
|
|
205
250
|
`// Blocks until CONNACK (the poll thread isn't running yet, so we drive a few`,
|
|
@@ -248,14 +293,47 @@ export function mqttInitLines(): string[] {
|
|
|
248
293
|
` __tc_mqtt.client.transport.type = MQTT_TRANSPORT_SECURE;`,
|
|
249
294
|
` struct mqtt_sec_config* tls_cfg = &__tc_mqtt.client.transport.tls.config;`,
|
|
250
295
|
` (void)memset(tls_cfg, 0, sizeof(*tls_cfg));`,
|
|
251
|
-
`
|
|
252
|
-
`
|
|
253
|
-
`
|
|
254
|
-
`
|
|
255
|
-
`
|
|
256
|
-
`
|
|
296
|
+
` if (__tc_mqtt.ca_der != nullptr) {`,
|
|
297
|
+
` // Pinned CA (the caCert construction fact): register it as a`,
|
|
298
|
+
` // CA_CERTIFICATE sec tag and REQUIRE peer verification — the MQTT`,
|
|
299
|
+
` // analogue of the HTTPS caCert path. Zephyr's credential store`,
|
|
300
|
+
` // never REPLACES a tag+type pair: re-registering the same DER`,
|
|
301
|
+
` // (pointer identity — the arrays are function-static) is skipped`,
|
|
302
|
+
` // silently; a DIFFERENT CA hitting an occupied tag is reported so`,
|
|
303
|
+
` // a wrong-CA verify is never silent; any other failure keeps the`,
|
|
304
|
+
` // tag list empty and verification required — fail closed.`,
|
|
305
|
+
` if (!__tc_mqtt.ca_added_set || __tc_mqtt.ca_added != __tc_mqtt.ca_der`,
|
|
306
|
+
` || __tc_mqtt.ca_added_len != __tc_mqtt.ca_der_len) {`,
|
|
307
|
+
` int cred_rc = tls_credential_add(__TC_MQTT_SEC_TAG,`,
|
|
308
|
+
` TLS_CREDENTIAL_CA_CERTIFICATE,`,
|
|
309
|
+
` __tc_mqtt.ca_der, __tc_mqtt.ca_der_len);`,
|
|
310
|
+
` __tc_mqtt.ca_added = __tc_mqtt.ca_der;`,
|
|
311
|
+
` __tc_mqtt.ca_added_len = __tc_mqtt.ca_der_len;`,
|
|
312
|
+
` __tc_mqtt.ca_added_set = true;`,
|
|
313
|
+
` if (cred_rc == -17 /* -EEXIST */) {`,
|
|
314
|
+
` printk("tc-mqtt: sec tag %d already holds a CA; the first one stays active (rc=%d)\\n", __TC_MQTT_SEC_TAG, cred_rc);`,
|
|
315
|
+
` } else if (cred_rc != 0) {`,
|
|
316
|
+
` printk("tc-mqtt: CA registration failed (rc=%d) - verifying against no credentials\\n", cred_rc);`,
|
|
317
|
+
` }`,
|
|
318
|
+
` }`,
|
|
319
|
+
` if (__tc_mqtt.ca_added_set) {`,
|
|
320
|
+
` __tc_mqtt.sec_tags[0] = __TC_MQTT_SEC_TAG;`,
|
|
321
|
+
` tls_cfg->sec_tag_list = __tc_mqtt.sec_tags;`,
|
|
322
|
+
` tls_cfg->sec_tag_count = 1U;`,
|
|
323
|
+
` }`,
|
|
324
|
+
` tls_cfg->peer_verify = TLS_PEER_VERIFY_REQUIRED;`,
|
|
325
|
+
` } else {`,
|
|
326
|
+
` // No CA pinned: the session is encrypted but the broker's identity`,
|
|
327
|
+
` // is not verified — the strongest the no-caCert form can express.`,
|
|
328
|
+
` tls_cfg->peer_verify = TLS_PEER_VERIFY_NONE;`,
|
|
329
|
+
` }`,
|
|
257
330
|
` tls_cfg->cipher_list = nullptr;`,
|
|
258
|
-
`
|
|
331
|
+
` // mbedtls does not match IP literals against SAN entries — set the`,
|
|
332
|
+
` // hostname (SNI + identity check) only for name hosts; IP brokers`,
|
|
333
|
+
` // verify chain-only (same policy as the HTTPS path).`,
|
|
334
|
+
` if (!__tc_mqtt_host_is_ipv4(__tc_mqtt.host)) {`,
|
|
335
|
+
` tls_cfg->hostname = __tc_mqtt.host;`,
|
|
336
|
+
` }`,
|
|
259
337
|
`#else`,
|
|
260
338
|
` printk("tc-mqtt: mqtts:// requested but CONFIG_MQTT_LIB_TLS is off\\n");`,
|
|
261
339
|
` return;`,
|
|
@@ -307,6 +385,10 @@ export function mqttInitLines(): string[] {
|
|
|
307
385
|
` __tc_mqtt.on_message = fn;`,
|
|
308
386
|
`}`,
|
|
309
387
|
``,
|
|
388
|
+
`// ca_cert is the DER-decoded PEM from the caCert construction fact`,
|
|
389
|
+
`// (user-owned static array; nullptr = no pinned CA).`,
|
|
390
|
+
`static inline void __tc_mqtt_set_ca_cert_der(const uint8_t* der, size_t len) { __tc_mqtt.ca_der = der; __tc_mqtt.ca_der_len = len; }`,
|
|
391
|
+
``,
|
|
310
392
|
`static inline void __tc_mqtt_subscribe(const char* topic) {`,
|
|
311
393
|
` static struct mqtt_topic topics[4];`,
|
|
312
394
|
` static uint16_t msg_id = 1;`,
|
|
@@ -361,6 +443,25 @@ export function lowerMqtt(op: HALOpIR): { code?: string; expression?: string } {
|
|
|
361
443
|
switch (op.operation) {
|
|
362
444
|
case 'mqtt.connect':
|
|
363
445
|
return { code: `__tc_mqtt_connect(${s(o.brokerUri)}, ${s(o.clientId)});` };
|
|
446
|
+
case 'mqtt.set_ca_cert': {
|
|
447
|
+
// PEM → DER at emit time (this tree's tf-psa-crypto mbedTLS has no PEM
|
|
448
|
+
// parser — same decode as http.set_ca_cert; tls_credential_add takes
|
|
449
|
+
// the DER directly).
|
|
450
|
+
const pem = String(o.pem ?? '').replace(/^"|"$/g, '').replace(/\\n/g, '\n');
|
|
451
|
+
if (pem.replace(/\s+/g, '') === '') {
|
|
452
|
+
// No-CA sentinel (Mqtt always emits the op from connect(); opts.caCert
|
|
453
|
+
// defaults to '') — elide silently so plain-mqtt:// carries no comment.
|
|
454
|
+
return { code: '' };
|
|
455
|
+
}
|
|
456
|
+
const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\s+/g, '');
|
|
457
|
+
const der = Buffer.from(b64, 'base64');
|
|
458
|
+
// Every X.509 DER starts with the ASN.1 SEQUENCE tag (0x30); a long
|
|
459
|
+
// but non-cert payload fails here instead of at a confusing handshake
|
|
460
|
+
// error on the target (same check as http.set_ca_cert).
|
|
461
|
+
if (der.length < 100 || der[0] !== 0x30) return { code: `// tc-mqtt: caCert PEM failed to decode` };
|
|
462
|
+
const hex = [...der].map((b) => `0x${b.toString(16).padStart(2, '0')}`).join(', ');
|
|
463
|
+
return { code: `{ static const uint8_t __tc_mqtt_ca_der[] = { ${hex} }; __tc_mqtt_set_ca_cert_der(__tc_mqtt_ca_der, sizeof(__tc_mqtt_ca_der)); }` };
|
|
464
|
+
}
|
|
364
465
|
case 'mqtt.on_message':
|
|
365
466
|
return { code: `__tc_mqtt_set_on_message(${s(o.handler)});` };
|
|
366
467
|
case 'mqtt.subscribe':
|