@typecad/framework-zephyr 1.0.0-alpha.10

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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/dist/async/timer-polyfill.d.ts +10 -0
  4. package/dist/async/timer-polyfill.js +95 -0
  5. package/dist/chips/controllers.d.ts +21 -0
  6. package/dist/chips/controllers.js +66 -0
  7. package/dist/chips/esp32.d.ts +2 -0
  8. package/dist/chips/esp32.js +59 -0
  9. package/dist/chips/esp32s3.d.ts +2 -0
  10. package/dist/chips/esp32s3.js +54 -0
  11. package/dist/chips/index.d.ts +15 -0
  12. package/dist/chips/index.js +45 -0
  13. package/dist/chips/resolve.d.ts +9 -0
  14. package/dist/chips/resolve.js +121 -0
  15. package/dist/chips/types.d.ts +154 -0
  16. package/dist/chips/types.js +14 -0
  17. package/dist/chips/xiao-ble.d.ts +2 -0
  18. package/dist/chips/xiao-ble.js +61 -0
  19. package/dist/debug-codegen.d.ts +33 -0
  20. package/dist/debug-codegen.js +162 -0
  21. package/dist/display/gfx.d.ts +17 -0
  22. package/dist/display/gfx.js +171 -0
  23. package/dist/display/index.d.ts +20 -0
  24. package/dist/display/index.js +53 -0
  25. package/dist/display/profiles.d.ts +25 -0
  26. package/dist/display/profiles.js +41 -0
  27. package/dist/display/touch-adapter.d.ts +9 -0
  28. package/dist/display/touch-adapter.js +95 -0
  29. package/dist/display/ui-adapter.d.ts +21 -0
  30. package/dist/display/ui-adapter.js +534 -0
  31. package/dist/doctor.d.ts +6 -0
  32. package/dist/doctor.js +48 -0
  33. package/dist/dt-config/kconfig.d.ts +27 -0
  34. package/dist/dt-config/kconfig.js +283 -0
  35. package/dist/dt-config/overlay.d.ts +35 -0
  36. package/dist/dt-config/overlay.js +246 -0
  37. package/dist/framework.manifest.d.ts +101 -0
  38. package/dist/framework.manifest.js +536 -0
  39. package/dist/index.d.ts +6 -0
  40. package/dist/index.js +17 -0
  41. package/dist/lowering/adc.d.ts +19 -0
  42. package/dist/lowering/adc.js +76 -0
  43. package/dist/lowering/ble.d.ts +20 -0
  44. package/dist/lowering/ble.js +419 -0
  45. package/dist/lowering/board.d.ts +9 -0
  46. package/dist/lowering/board.js +33 -0
  47. package/dist/lowering/gpio.d.ts +12 -0
  48. package/dist/lowering/gpio.js +123 -0
  49. package/dist/lowering/http.d.ts +23 -0
  50. package/dist/lowering/http.js +482 -0
  51. package/dist/lowering/i2c.d.ts +15 -0
  52. package/dist/lowering/i2c.js +114 -0
  53. package/dist/lowering/index.d.ts +29 -0
  54. package/dist/lowering/index.js +93 -0
  55. package/dist/lowering/interrupts.d.ts +21 -0
  56. package/dist/lowering/interrupts.js +91 -0
  57. package/dist/lowering/mqtt.d.ts +19 -0
  58. package/dist/lowering/mqtt.js +366 -0
  59. package/dist/lowering/power.d.ts +9 -0
  60. package/dist/lowering/power.js +64 -0
  61. package/dist/lowering/preferences.d.ts +23 -0
  62. package/dist/lowering/preferences.js +345 -0
  63. package/dist/lowering/pulse.d.ts +7 -0
  64. package/dist/lowering/pulse.js +82 -0
  65. package/dist/lowering/pwm.d.ts +15 -0
  66. package/dist/lowering/pwm.js +59 -0
  67. package/dist/lowering/random.d.ts +18 -0
  68. package/dist/lowering/random.js +129 -0
  69. package/dist/lowering/spi.d.ts +17 -0
  70. package/dist/lowering/spi.js +113 -0
  71. package/dist/lowering/timing.d.ts +9 -0
  72. package/dist/lowering/timing.js +63 -0
  73. package/dist/lowering/tone.d.ts +10 -0
  74. package/dist/lowering/tone.js +50 -0
  75. package/dist/lowering/uart.d.ts +15 -0
  76. package/dist/lowering/uart.js +102 -0
  77. package/dist/lowering/util.d.ts +4 -0
  78. package/dist/lowering/util.js +12 -0
  79. package/dist/lowering/wdt.d.ts +15 -0
  80. package/dist/lowering/wdt.js +80 -0
  81. package/dist/lowering/wifi.d.ts +21 -0
  82. package/dist/lowering/wifi.js +447 -0
  83. package/dist/lowering/worker-backing.d.ts +14 -0
  84. package/dist/lowering/worker-backing.js +79 -0
  85. package/dist/lowering/worker.d.ts +6 -0
  86. package/dist/lowering/worker.js +14 -0
  87. package/dist/strategy.d.ts +168 -0
  88. package/dist/strategy.js +1094 -0
  89. package/dist/toolchain/compat.d.ts +38 -0
  90. package/dist/toolchain/compat.js +133 -0
  91. package/dist/toolchain/debug-config.d.ts +82 -0
  92. package/dist/toolchain/debug-config.js +359 -0
  93. package/dist/toolchain/index.d.ts +73 -0
  94. package/dist/toolchain/index.js +444 -0
  95. package/dist/toolchain/scaffold.d.ts +15 -0
  96. package/dist/toolchain/scaffold.js +196 -0
  97. package/dist/toolchain/west-discover.d.ts +40 -0
  98. package/dist/toolchain/west-discover.js +203 -0
  99. package/dist/toolchain/west-spawn.d.ts +39 -0
  100. package/dist/toolchain/west-spawn.js +117 -0
  101. package/package.json +82 -0
  102. package/src/async/timer-polyfill.ts +107 -0
  103. package/src/chips/controllers.ts +74 -0
  104. package/src/chips/esp32.ts +62 -0
  105. package/src/chips/esp32s3.ts +57 -0
  106. package/src/chips/index.ts +54 -0
  107. package/src/chips/resolve.ts +146 -0
  108. package/src/chips/types.ts +163 -0
  109. package/src/chips/xiao-ble.ts +64 -0
  110. package/src/debug-codegen.ts +207 -0
  111. package/src/display/gfx.ts +190 -0
  112. package/src/display/index.ts +70 -0
  113. package/src/display/profiles.ts +63 -0
  114. package/src/display/touch-adapter.ts +100 -0
  115. package/src/display/ui-adapter.ts +558 -0
  116. package/src/doctor.ts +56 -0
  117. package/src/dt-config/kconfig.ts +308 -0
  118. package/src/dt-config/overlay.ts +293 -0
  119. package/src/framework.manifest.ts +558 -0
  120. package/src/index.ts +29 -0
  121. package/src/lowering/adc.ts +102 -0
  122. package/src/lowering/ble.ts +426 -0
  123. package/src/lowering/board.ts +38 -0
  124. package/src/lowering/gpio.ts +148 -0
  125. package/src/lowering/http.ts +488 -0
  126. package/src/lowering/i2c.ts +126 -0
  127. package/src/lowering/index.ts +86 -0
  128. package/src/lowering/interrupts.ts +114 -0
  129. package/src/lowering/mqtt.ts +371 -0
  130. package/src/lowering/power.ts +70 -0
  131. package/src/lowering/preferences.ts +354 -0
  132. package/src/lowering/pulse.ts +93 -0
  133. package/src/lowering/pwm.ts +74 -0
  134. package/src/lowering/random.ts +135 -0
  135. package/src/lowering/spi.ts +124 -0
  136. package/src/lowering/timing.ts +72 -0
  137. package/src/lowering/tone.ts +60 -0
  138. package/src/lowering/uart.ts +111 -0
  139. package/src/lowering/util.ts +12 -0
  140. package/src/lowering/wdt.ts +84 -0
  141. package/src/lowering/wifi.ts +452 -0
  142. package/src/lowering/worker-backing.ts +94 -0
  143. package/src/lowering/worker.ts +17 -0
  144. package/src/strategy.ts +1265 -0
  145. package/src/toolchain/compat.ts +145 -0
  146. package/src/toolchain/debug-config.ts +399 -0
  147. package/src/toolchain/index.ts +482 -0
  148. package/src/toolchain/scaffold.ts +196 -0
  149. package/src/toolchain/west-discover.ts +233 -0
  150. package/src/toolchain/west-spawn.ts +141 -0
@@ -0,0 +1,148 @@
1
+ // ---------------------------------------------------------------------------
2
+ // GPIO lowering — devicetree-spec bridge
3
+ //
4
+ // Translates HAL gpio.* ops to Zephyr driver C++. Two addressing modes:
5
+ //
6
+ // 1. Devicetree spec (preferred): if the HAL pin matches a dtSpec in the
7
+ // active chip descriptor, emit gpio_pin_*_dt() calls against a
8
+ // `__tc_dt_<alias>` gpio_dt_spec. This honors the node's polarity flags
9
+ // (GPIO_ACTIVE_LOW), so logical value 1 = LED on for an active-low LED.
10
+ //
11
+ // 2. Raw fallback: pins without a dtSpec are addressed via the SoC's gpio
12
+ // controller node (DEVICE_DT_GET(DT_NODELABEL(gpio0))) and the
13
+ // gpio_pin_*_raw() API. Polarity is physical (raw bypasses DT flags).
14
+ // The manifest validator's probe (which sends {operation, pin:0} with no
15
+ // port) hits this path, so it must return a lowered result, not undefined.
16
+ // ---------------------------------------------------------------------------
17
+
18
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
19
+ import type { ZephyrChipDescriptor } from '../chips/types.js';
20
+ import { controllerNodelabelForPin } from '../chips/controllers.js';
21
+
22
+ /** The C identifier emitted for a pin's gpio_dt_spec variable. */
23
+ export function dtSpecVarName(dtSpec: string): string {
24
+ return `__tc_dt_${dtSpec}`;
25
+ }
26
+
27
+ /** Look up a dtSpec by GPIO pin number; undefined if the pin has none. */
28
+ function findDtSpec(chip: ZephyrChipDescriptor, pin: number) {
29
+ return chip.gpio.dtSpecs.find((s) => s.pin === pin);
30
+ }
31
+
32
+ // HAL passes UPPERCASE modes ("OUTPUT") while docs say lowercase ("output").
33
+ // The *_pullup / *_pulldown modes map to GPIO_INPUT (Zephyr does not have
34
+ // separate input+pull mode flags) and OR in a GPIO_PULL_UP / GPIO_PULL_DOWN
35
+ // bit via `dtFlagsForMode` — without that bit the pin floats, so INPUT_PULLUP
36
+ // was a silent no-op (bug B1). Mirrors the gpio_pullup_en/gpio_pulldown_en
37
+ // extras framework-esp32 emits for the same modes.
38
+ const MODE_MAP: Record<string, string> = {
39
+ output: 'GPIO_OUTPUT',
40
+ OUTPUT: 'GPIO_OUTPUT',
41
+ input: 'GPIO_INPUT',
42
+ INPUT: 'GPIO_INPUT',
43
+ input_pullup: 'GPIO_INPUT',
44
+ INPUT_PULLUP: 'GPIO_INPUT',
45
+ input_pulldown: 'GPIO_INPUT',
46
+ INPUT_PULLDOWN: 'GPIO_INPUT',
47
+ };
48
+
49
+ /** Additional DT flag bits for a HAL mode, OR'd into the configure flags.
50
+ * Returns '' for modes with no extra bits so the join leaves the mode alone. */
51
+ function dtFlagsForMode(mode: string): string {
52
+ const m = (mode ?? '').toLowerCase();
53
+ if (m === 'input_pullup') return ' | GPIO_PULL_UP';
54
+ if (m === 'input_pulldown') return ' | GPIO_PULL_DOWN';
55
+ return '';
56
+ }
57
+
58
+ /** Combined mode + pull flags for a HAL mode string, e.g.
59
+ * 'INPUT_PULLUP' → 'GPIO_INPUT | GPIO_PULL_UP'. */
60
+ function flagsForMode(mode: string): string {
61
+ const base = MODE_MAP[mode] ?? 'GPIO_INPUT';
62
+ return base + dtFlagsForMode(mode);
63
+ }
64
+
65
+ /**
66
+ * Resolve a HAL gpio.* op to Zephyr C++.
67
+ * Returns `{ code }` for statement ops, `{ expression }` for value-returning ops.
68
+ */
69
+ export function lowerGpio(
70
+ op: HALOpIR,
71
+ chip: ZephyrChipDescriptor,
72
+ ): { code?: string; expression?: string } {
73
+ const o = op as any;
74
+ const pin: number = o.pin;
75
+ const spec = findDtSpec(chip, pin);
76
+
77
+ if (spec) {
78
+ return lowerGpioDtSpec(op, spec.dtSpec);
79
+ }
80
+ return lowerGpioRaw(op, chip);
81
+ }
82
+
83
+ /** Devicetree-spec path — polarity-correct via gpio_pin_*_dt(). */
84
+ function lowerGpioDtSpec(
85
+ op: HALOpIR,
86
+ dtSpec: string,
87
+ ): { code?: string; expression?: string } {
88
+ const o = op as any;
89
+ const varName = dtSpecVarName(dtSpec);
90
+
91
+ switch (op.operation) {
92
+ case 'gpio.set_mode': {
93
+ return { code: `gpio_pin_configure_dt(&${varName}, ${flagsForMode(o.mode)});` };
94
+ }
95
+ case 'gpio.write': {
96
+ // Literal 0/1 stays as-is; runtime expression coerced to int via ternary.
97
+ const v = o.value;
98
+ const rhs = typeof v === 'string' ? `((${v}) ? 1 : 0)` : v ? 1 : 0;
99
+ return { code: `gpio_pin_set_dt(&${varName}, ${rhs});` };
100
+ }
101
+ case 'gpio.read':
102
+ return { expression: `gpio_pin_get_dt(&${varName})` };
103
+ case 'gpio.toggle':
104
+ return { code: `gpio_pin_toggle_dt(&${varName});` };
105
+ default:
106
+ throw new Error(
107
+ `framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
108
+ `Open an issue or use rawCpp() to emit it manually.`,
109
+ );
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Raw-controller path — for pins without a DT spec (and the manifest probe).
115
+ * Uses the SoC's primary GPIO controller node. Polarity is physical.
116
+ */
117
+ function lowerGpioRaw(
118
+ op: HALOpIR,
119
+ chip: ZephyrChipDescriptor,
120
+ ): { code?: string; expression?: string } {
121
+ const o = op as any;
122
+ const pin: number = o.pin;
123
+ // Resolve the owning controller by pin range (ESP32-S3 splits GPIO across
124
+ // gpio0/gpio1). For single-controller SoCs this is just chip.gpioController.
125
+ const controller = `DEVICE_DT_GET(DT_NODELABEL(${controllerNodelabelForPin(chip, pin)}))`;
126
+
127
+ switch (op.operation) {
128
+ case 'gpio.set_mode': {
129
+ return { code: `gpio_pin_configure(${controller}, ${pin}, ${flagsForMode(o.mode)});` };
130
+ }
131
+ case 'gpio.write': {
132
+ const v = o.value;
133
+ const rhs = typeof v === 'string' ? `((${v}) ? 1 : 0)` : v ? 1 : 0;
134
+ return { code: `gpio_pin_set_raw(${controller}, ${pin}, ${rhs});` };
135
+ }
136
+ case 'gpio.read':
137
+ return { expression: `gpio_pin_get_raw(${controller}, ${pin})` };
138
+ case 'gpio.toggle':
139
+ return {
140
+ code: `gpio_pin_set_raw(${controller}, ${pin}, !gpio_pin_get_raw(${controller}, ${pin}));`,
141
+ };
142
+ default:
143
+ throw new Error(
144
+ `framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
145
+ `Open an issue or use rawCpp() to emit it manually.`,
146
+ );
147
+ }
148
+ }
@@ -0,0 +1,488 @@
1
+ // ---------------------------------------------------------------------------
2
+ // HTTP/S client lowering — Zephyr socket + http_client_req
3
+ //
4
+ // Unlike ESP-IDF's esp_http_client (which folds URL parse + DNS + connect +
5
+ // TLS into one perform call), Zephyr's <zephyr/net/http/client.h> runs
6
+ // http_client_req() over a *pre-connected* socket. The shim therefore owns the
7
+ // full connect path: parse scheme://host[:port]/path → getaddrinfo → socket
8
+ // (IPPROTO_TCP or IPPROTO_TLS_1_2 for https) → setsockopt TLS sec tags /
9
+ // hostname / peer-verify → connect() → http_client_req → accumulate body.
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.
16
+ //
17
+ // TLS: https selects IPPROTO_TLS_1_2 + NET_SOCKETS_SOCKOPT_TLS (which selects
18
+ // mbedTLS in Kconfig). insecure() sets TLS_PEER_VERIFY_NONE; caCert(pem)
19
+ // registers the PEM via tls_credential_add as a CA_CERTIFICATE sec tag and
20
+ // keeps verification required. No cert bundle attach (Zephyr has no ESP-IDF
21
+ // crt_bundle equivalent); https without caCert/insecure still requires the
22
+ // caller to have registered a CA, otherwise the handshake fails — that is the
23
+ // safe default.
24
+ //
25
+ // EMIT BOUNDARY: emitted bytes land in user firmware. Covered by the TypeCAD
26
+ // Runtime Exception (RUNTIME_EXCEPTION.md at the repo root).
27
+ // ---------------------------------------------------------------------------
28
+
29
+ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
30
+
31
+ /** Render a HAL field: pass through (already rendered by the resolver). */
32
+ function s(v: unknown): string {
33
+ return String(v);
34
+ }
35
+
36
+ /**
37
+ * The HTTP runtime shim. All helpers are `static`/`inline` so unused ones
38
+ * don't trip -Wunused-function in the single generated TU (mirrors wifi.ts).
39
+ *
40
+ * The connect/perform path runs on the calling thread for the blocking send,
41
+ * and on the system workqueue for the async send_start/done split. Completion
42
+ * of the async path is signaled with a k_sem (give/take) which issues a full
43
+ * memory barrier, so the consumer observes the resp/status/ok writes the
44
+ * worker made before signaling — the same handoff guarantee ESP32's
45
+ * xTaskNotifyGive provides.
46
+ */
47
+ export function httpInitLines(): string[] {
48
+ return [
49
+ `// CUTTLEFISH_HTTP_BEGIN`,
50
+ `#define __TC_HTTP_DEFAULT_MAX_BODY 8192`,
51
+ `#define __TC_HTTP_MAX_HEADERS 8`,
52
+ `#define __TC_HTTP_SEC_TAG 1`,
53
+ ``,
54
+ `// Single-slot request/response state. scheme/host/port/path are parsed`,
55
+ `// from the url at perform time and drive the socket family + TLS choice.`,
56
+ `// ca_cert is user-owned PEM; insecure skips peer verification entirely.`,
57
+ `static struct {`,
58
+ ` char url[512];`,
59
+ ` char scheme[8]; // "http" or "https"`,
60
+ ` char host[128];`,
61
+ ` uint16_t port; // 80 / 443 unless overridden in the url`,
62
+ ` char path[256]; // resource path beginning with '/'`,
63
+ ` enum http_method method;`,
64
+ ` int32_t timeout_ms;`,
65
+ ` size_t max_body;`,
66
+ ` const char* body; // request body (user-owned)`,
67
+ ` int32_t body_len;`,
68
+ ` bool insecure;`,
69
+ ` const char* ca_cert; // PEM, user-owned; nullptr = no pinned CA`,
70
+ ` const char* hdr_name[__TC_HTTP_MAX_HEADERS];`,
71
+ ` const char* hdr_value[__TC_HTTP_MAX_HEADERS];`,
72
+ ` int32_t hdr_count;`,
73
+ ` // response`,
74
+ ` int32_t status; // HTTP status code (0 = none/error)`,
75
+ ` volatile bool done; // set when a perform completes (ok or fail)`,
76
+ ` bool ok; // transport ok AND 2xx`,
77
+ ` char* resp; // heap body buffer (max_body + 1, NUL-terminated)`,
78
+ ` size_t resp_len;`,
79
+ ` // Dedicated recv buffer for http_client_req. MUST be separate from resp:`,
80
+ ` // http_client_req writes the raw response (headers + body) into recv_buf`,
81
+ ` // and the parser walks it; the body callback then memcpy's body_frag into`,
82
+ ` // resp. Sharing the two means the callback clobbers the buffer the parser`,
83
+ ` // is still reading → EBADMSG (-77) on chunked/larger responses.`,
84
+ ` uint8_t recvbuf[1024];`,
85
+ ` int64_t content_length; // -1 = unknown`,
86
+ ` char resp_header[128]; // scratch for response_header()`,
87
+ ` // staged header fields for http_client_req: a NULL-terminated list of`,
88
+ ` // "Name: Value\\r\\n" strings built from hdr_name/hdr_value at perform.`,
89
+ ` const char* hdr_fields[__TC_HTTP_MAX_HEADERS + 1];`,
90
+ ` char hdr_field_buf[__TC_HTTP_MAX_HEADERS][64];`,
91
+ ` // async completion signal (memory barrier between worker and poller)`,
92
+ ` struct k_sem done_sem;`,
93
+ ` bool sem_inited;`,
94
+ ` struct k_work perform_work;`,
95
+ `} __tc_http;`,
96
+ ``,
97
+ `// ── URL parser ──────────────────────────────────────────────────────────`,
98
+ `// Splits "scheme://host[:port]/path" into the struct fields. http→80,`,
99
+ `// https→443. Missing path becomes "/". Returns false on an unparseable url.`,
100
+ `static bool __tc_http_parse_url(const char* url) {`,
101
+ ` const char* p = url;`,
102
+ ` // scheme up to "://"`,
103
+ ` const char* sep = strstr(p, "://");`,
104
+ ` if (sep == nullptr) return false;`,
105
+ ` size_t scheme_len = static_cast<size_t>(sep - p);`,
106
+ ` if (scheme_len >= sizeof(__tc_http.scheme)) scheme_len = sizeof(__tc_http.scheme) - 1U;`,
107
+ ` for (size_t i = 0U; i < scheme_len; i++) {`,
108
+ ` char c = p[i];`,
109
+ ` __tc_http.scheme[i] = (c >= 'A' && c <= 'Z') ? static_cast<char>(c + 32) : c;`,
110
+ ` }`,
111
+ ` __tc_http.scheme[scheme_len] = '\\0';`,
112
+ ` p = sep + 3; // past "://"`,
113
+ ` // host up to ':' (port), '/' (path), or end`,
114
+ ` size_t host_len = 0U;`,
115
+ ` bool is_https = (strcmp(__tc_http.scheme, "https") == 0);`,
116
+ ` __tc_http.port = is_https ? 443U : 80U;`,
117
+ ` while (*p != '\\0' && *p != ':' && *p != '/' && host_len < (sizeof(__tc_http.host) - 1U)) {`,
118
+ ` __tc_http.host[host_len++] = *p++;`,
119
+ ` }`,
120
+ ` __tc_http.host[host_len] = '\\0';`,
121
+ ` // optional :port`,
122
+ ` if (*p == ':') {`,
123
+ ` p++;`,
124
+ ` uint32_t portval = 0U;`,
125
+ ` while (*p >= '0' && *p <= '9') {`,
126
+ ` portval = (portval * 10U) + static_cast<uint32_t>(*p - '0');`,
127
+ ` p++;`,
128
+ ` if (portval > 65535U) { portval = 65535U; }`,
129
+ ` }`,
130
+ ` if (portval > 0U) __tc_http.port = static_cast<uint16_t>(portval);`,
131
+ ` }`,
132
+ ` // path (defaults to "/")`,
133
+ ` if (*p == '/') {`,
134
+ ` size_t path_len = 0U;`,
135
+ ` while (*p != '\\0' && path_len < (sizeof(__tc_http.path) - 1U)) {`,
136
+ ` __tc_http.path[path_len++] = *p++;`,
137
+ ` }`,
138
+ ` __tc_http.path[path_len] = '\\0';`,
139
+ ` } else {`,
140
+ ` __tc_http.path[0] = '/';`,
141
+ ` __tc_http.path[1] = '\\0';`,
142
+ ` }`,
143
+ ` return host_len > 0U;`,
144
+ `}`,
145
+ ``,
146
+ `// ── response callback ───────────────────────────────────────────────────`,
147
+ `// http_response_cb_t: accumulate each body fragment into resp up to max_body,`,
148
+ `// and capture the status code + content length from the first response.`,
149
+ `static int __tc_http_resp_cb(struct http_response* rsp,`,
150
+ ` enum http_final_call final_data,`,
151
+ ` void* user_data) {`,
152
+ ` (void)final_data;`,
153
+ ` (void)user_data;`,
154
+ ` if (rsp->http_status_code != 0U && __tc_http.status == 0) {`,
155
+ ` __tc_http.status = static_cast<int32_t>(rsp->http_status_code);`,
156
+ ` }`,
157
+ ` if (rsp->cl_present != 0 && __tc_http.content_length < 0) {`,
158
+ ` __tc_http.content_length = static_cast<int64_t>(rsp->content_length);`,
159
+ ` }`,
160
+ ` if (__tc_http.resp != nullptr && rsp->body_frag_start != nullptr && rsp->body_frag_len > 0U) {`,
161
+ ` size_t room = __tc_http.max_body - __tc_http.resp_len;`,
162
+ ` size_t n = (rsp->body_frag_len < room) ? rsp->body_frag_len : room;`,
163
+ ` (void)memcpy(__tc_http.resp + __tc_http.resp_len, rsp->body_frag_start, n);`,
164
+ ` __tc_http.resp_len += n;`,
165
+ ` }`,
166
+ ` return 0;`,
167
+ `}`,
168
+ ``,
169
+ `// ── socket connect ──────────────────────────────────────────────────────`,
170
+ `// zsock_getaddrinfo → zsock_socket(TCP | TLS_1_2) → TLS sockopts →`,
171
+ `// zsock_connect. Uses the zsock_* (native Zephyr socket) API directly rather`,
172
+ `// than the bare POSIX aliases: the POSIX socket() dispatcher (under`,
173
+ `// CONFIG_POSIX_API) returns ENOENT when the net backend isn't registered as`,
174
+ `// a POSIX socket family, whereas zsock_socket talks straight to the net`,
175
+ `// socket service registered by NET_SOCKETS. zsock_* calls return negative`,
176
+ `// errno on failure (not -1 + errno). For https, a pinned CA (ca_cert) is`,
177
+ `// registered as a sec tag and verification kept required; insecure()`,
178
+ `// relaxes it to NONE. Returns the connected fd or -1.`,
179
+ `static int __tc_http_open_socket(void) {`,
180
+ ` bool is_https = (strcmp(__tc_http.scheme, "https") == 0);`,
181
+ ` char port_str[8];`,
182
+ ` (void)snprintk(port_str, sizeof(port_str), "%u", static_cast<unsigned int>(__tc_http.port));`,
183
+ ` struct zsock_addrinfo hints = { 0 };`,
184
+ ` hints.ai_family = AF_INET;`,
185
+ ` hints.ai_socktype = SOCK_STREAM;`,
186
+ ` struct zsock_addrinfo* res = nullptr;`,
187
+ ` int gai = zsock_getaddrinfo(__tc_http.host, port_str, &hints, &res);`,
188
+ ` if (gai != 0 || res == nullptr) {`,
189
+ ` printk("tc-http: DNS failed for %s (gai=%d)\\n", __tc_http.host, gai);`,
190
+ ` return -1;`,
191
+ ` }`,
192
+ ` // IPPROTO_TLS_1_2 + the SOL_TLS sockopts are gated on`,
193
+ ` // CONFIG_NET_SOCKETS_SOCKOPT_TLS (which selects mbedTLS). A plain-HTTP`,
194
+ ` // build (the default — TLS isn't in the kconfig block because mbedTLS`,
195
+ ` // needs a full user-config symbol matrix to link) compiles the TLS branch`,
196
+ ` // out entirely, so it never references the unlinked tls_credential_add /`,
197
+ ` // mbedtls symbols. HTTPS targets opt in via a per-program kconfig override.`,
198
+ ` int sock = -1;`,
199
+ ` if (is_https) {`,
200
+ `#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS)`,
201
+ ` int sock_tls = zsock_socket(res->ai_family, res->ai_socktype, IPPROTO_TLS_1_2);`,
202
+ ` if (sock_tls >= 0) {`,
203
+ ` // Register the pinned CA as the active sec tag. tls_credential_add`,
204
+ ` // is idempotent-enough for the single-slot model: a duplicate tag`,
205
+ ` // is an error we ignore, and the most recently added cert wins.`,
206
+ ` if (__tc_http.ca_cert != nullptr) {`,
207
+ ` (void)tls_credential_add(__TC_HTTP_SEC_TAG,`,
208
+ ` TLS_CREDENTIAL_CA_CERTIFICATE,`,
209
+ ` __tc_http.ca_cert, strlen(__tc_http.ca_cert) + 1U);`,
210
+ ` sec_tag_t tags[1] = { __TC_HTTP_SEC_TAG };`,
211
+ ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_SEC_TAG_LIST, tags, sizeof(tags));`,
212
+ ` }`,
213
+ ` // Hostname SNI + verification. insecure() disables verification;`,
214
+ ` // the default (no caCert, no insecure) leaves verification required`,
215
+ ` // with no sec tag, so the handshake fails safely until a CA is set.`,
216
+ ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_HOSTNAME, __tc_http.host, strlen(__tc_http.host) + 1U);`,
217
+ ` int32_t verify = __tc_http.insecure ? TLS_PEER_VERIFY_NONE : TLS_PEER_VERIFY_REQUIRED;`,
218
+ ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_PEER_VERIFY, &verify, sizeof(verify));`,
219
+ ` }`,
220
+ ` sock = sock_tls;`,
221
+ `#else`,
222
+ ` printk("tc-http: https requested but CONFIG_NET_SOCKETS_SOCKOPT_TLS is off\\n");`,
223
+ ` sock = -1;`,
224
+ `#endif`,
225
+ ` } else {`,
226
+ ` sock = zsock_socket(res->ai_family, res->ai_socktype, IPPROTO_TCP);`,
227
+ ` }`,
228
+ ` if (sock < 0) {`,
229
+ ` printk("tc-http: socket() failed (errno=%d)\\n", -sock);`,
230
+ ` zsock_freeaddrinfo(res);`,
231
+ ` return -1;`,
232
+ ` }`,
233
+ ` int cret = zsock_connect(sock, res->ai_addr, res->ai_addrlen);`,
234
+ ` zsock_freeaddrinfo(res);`,
235
+ ` if (cret < 0) {`,
236
+ ` printk("tc-http: connect(%s:%u) failed (errno=%d)\\n", __tc_http.host,`,
237
+ ` static_cast<unsigned int>(__tc_http.port), -cret);`,
238
+ ` zsock_close(sock);`,
239
+ ` return -1;`,
240
+ ` }`,
241
+ ` return sock;`,
242
+ `}`,
243
+ ``,
244
+ `/** Clear request options + response; called when Http.get/post/… starts a new request. */`,
245
+ `static inline void __tc_http_reset(void) {`,
246
+ ` if (__tc_http.resp != nullptr) { delete[] __tc_http.resp; __tc_http.resp = nullptr; }`,
247
+ ` __tc_http.timeout_ms = 15000;`,
248
+ ` __tc_http.max_body = __TC_HTTP_DEFAULT_MAX_BODY;`,
249
+ ` __tc_http.body = nullptr;`,
250
+ ` __tc_http.body_len = 0;`,
251
+ ` __tc_http.insecure = false;`,
252
+ ` __tc_http.ca_cert = nullptr;`,
253
+ ` __tc_http.hdr_count = 0;`,
254
+ ` __tc_http.status = 0;`,
255
+ ` __tc_http.done = false;`,
256
+ ` __tc_http.ok = false;`,
257
+ ` __tc_http.resp_len = 0;`,
258
+ ` __tc_http.content_length = -1;`,
259
+ ` __tc_http.resp_header[0] = '\\0';`,
260
+ ` __tc_http.url[0] = '\\0';`,
261
+ `}`,
262
+ ``,
263
+ `/** Stage the request. Setters (timeout/max_body/body/tls/headers) survive begin. */`,
264
+ `static inline void __tc_http_begin(enum http_method method, const char* url) {`,
265
+ ` (void)strncpy(__tc_http.url, url, sizeof(__tc_http.url) - 1U);`,
266
+ ` __tc_http.url[sizeof(__tc_http.url) - 1U] = '\\0';`,
267
+ ` __tc_http.method = method;`,
268
+ ` // Clear only the per-request response state — keep options set before send().`,
269
+ ` if (__tc_http.resp != nullptr) { delete[] __tc_http.resp; __tc_http.resp = nullptr; }`,
270
+ ` __tc_http.status = 0;`,
271
+ ` __tc_http.done = false;`,
272
+ ` __tc_http.ok = false;`,
273
+ ` __tc_http.resp_len = 0;`,
274
+ ` __tc_http.content_length = -1;`,
275
+ ` __tc_http.resp_header[0] = '\\0';`,
276
+ `}`,
277
+ ``,
278
+ `static inline void __tc_http_set_header(const char* name, const char* value) {`,
279
+ ` if (__tc_http.hdr_count < __TC_HTTP_MAX_HEADERS) {`,
280
+ ` __tc_http.hdr_name[__tc_http.hdr_count] = name;`,
281
+ ` __tc_http.hdr_value[__tc_http.hdr_count] = value;`,
282
+ ` __tc_http.hdr_count++;`,
283
+ ` }`,
284
+ `}`,
285
+ ``,
286
+ `static inline void __tc_http_set_timeout(int32_t ms) { __tc_http.timeout_ms = ms; }`,
287
+ ``,
288
+ `static inline void __tc_http_set_max_body(size_t bytes) { __tc_http.max_body = bytes; }`,
289
+ ``,
290
+ `// json=true stages Content-Type: application/json as an extra header so the`,
291
+ `// server treats the body as JSON. The body itself is the caller's string.`,
292
+ `static inline void __tc_http_set_body(const char* data, bool json) {`,
293
+ ` __tc_http.body = data;`,
294
+ ` __tc_http.body_len = (data != nullptr) ? static_cast<int32_t>(strlen(data)) : 0;`,
295
+ ` if (json) __tc_http_set_header("Content-Type", "application/json");`,
296
+ `}`,
297
+ ``,
298
+ `static inline void __tc_http_set_insecure(void) { __tc_http.insecure = true; }`,
299
+ ``,
300
+ `static inline void __tc_http_set_ca_cert(const char* pem) { __tc_http.ca_cert = pem; }`,
301
+ ``,
302
+ `// ── perform (the shared blocking core) ──────────────────────────────────`,
303
+ `// Parse url → open socket → build http_request → http_client_req → close.`,
304
+ `// Sets status/ok/done. On a fresh resp buffer (heap, max_body+1, NUL-terminated).`,
305
+ `static void __tc_http_perform(void) {`,
306
+ ` __tc_http.ok = false;`,
307
+ ` __tc_http.done = false;`,
308
+ ` if (!__tc_http_parse_url(__tc_http.url)) {`,
309
+ ` printk("tc-http: bad url\\n");`,
310
+ ` __tc_http.done = true;`,
311
+ ` return;`,
312
+ ` }`,
313
+ ` if (__tc_http.resp != nullptr) { delete[] __tc_http.resp; __tc_http.resp = nullptr; }`,
314
+ ` __tc_http.resp_len = 0;`,
315
+ ` __tc_http.resp = new (std::nothrow) char[__tc_http.max_body + 1U];`,
316
+ ` if (__tc_http.resp == nullptr) {`,
317
+ ` printk("tc-http: out of memory for %u-byte body\\n",`,
318
+ ` static_cast<unsigned int>(__tc_http.max_body));`,
319
+ ` __tc_http.done = true;`,
320
+ ` return;`,
321
+ ` }`,
322
+ ` __tc_http.resp[0] = '\\0';`,
323
+ ` int sock = __tc_http_open_socket();`,
324
+ ` if (sock < 0) { __tc_http.done = true; return; }`,
325
+ ` // Build the NULL-terminated header-fields list from staged headers.`,
326
+ ` for (int32_t i = 0; i < __tc_http.hdr_count && i < __TC_HTTP_MAX_HEADERS; i++) {`,
327
+ ` (void)snprintk(__tc_http.hdr_field_buf[i], sizeof(__tc_http.hdr_field_buf[i]),`,
328
+ ` "%s: %s\\r\\n", __tc_http.hdr_name[i], __tc_http.hdr_value[i]);`,
329
+ ` __tc_http.hdr_fields[i] = __tc_http.hdr_field_buf[i];`,
330
+ ` }`,
331
+ ` __tc_http.hdr_fields[(__tc_http.hdr_count < __TC_HTTP_MAX_HEADERS)`,
332
+ ` ? __tc_http.hdr_count : __TC_HTTP_MAX_HEADERS] = nullptr;`,
333
+ ` struct http_request req = { 0 };`,
334
+ ` req.method = __tc_http.method;`,
335
+ ` req.url = __tc_http.path;`,
336
+ ` req.host = __tc_http.host;`,
337
+ ` req.protocol = "HTTP/1.1";`,
338
+ ` req.response = __tc_http_resp_cb;`,
339
+ ` req.recv_buf = __tc_http.recvbuf;`,
340
+ ` req.recv_buf_len = sizeof(__tc_http.recvbuf);`,
341
+ ` req.header_fields = (__tc_http.hdr_count > 0) ? __tc_http.hdr_fields : nullptr;`,
342
+ ` if (__tc_http.body != nullptr) {`,
343
+ ` req.payload = __tc_http.body;`,
344
+ ` req.payload_len = static_cast<size_t>(__tc_http.body_len);`,
345
+ ` }`,
346
+ ` int sent = http_client_req(sock, &req, __tc_http.timeout_ms, nullptr);`,
347
+ ` // The response callback (__tc_http_resp_cb) captures the status, but Zephyr's`,
348
+ ` // parser aborts on 5xx responses (on_headers_complete returns 1 for status`,
349
+ ` // >= 500, which http_parser treats as an error → http_client_req returns`,
350
+ ` // -EBADMSG and the response callback may not fire). The on_status parser`,
351
+ ` // hook still runs first and sets http_status_code, so read it back directly`,
352
+ ` // from the response struct when the callback path didn't capture it.`,
353
+ ` if (__tc_http.status == 0 && req.internal.response.http_status_code != 0U) {`,
354
+ ` __tc_http.status = static_cast<int32_t>(req.internal.response.http_status_code);`,
355
+ ` }`,
356
+ ` // Full shutdown before close so the net_context is reclaimed promptly: a`,
357
+ ` // bare zsock_close on Zephyr leaves the TCP context in a deferred-release`,
358
+ ` // state, and rapid sequential requests exhaust NET_MAX_CONTEXTS (socket/`,
359
+ ` // connect then return -EPERM). SHUT_RDWR triggers an immediate FIN exchange.`,
360
+ ` (void)zsock_shutdown(sock, SHUT_RDWR);`,
361
+ ` (void)zsock_close(sock);`,
362
+ ` // NUL-terminate the accumulated body at the cap.`,
363
+ ` size_t end = (__tc_http.resp_len < __tc_http.max_body) ? __tc_http.resp_len : __tc_http.max_body;`,
364
+ ` __tc_http.resp[end] = '\\0';`,
365
+ ` __tc_http.ok = (sent >= 0) && __tc_http.status >= 200 && __tc_http.status < 300;`,
366
+ ` __tc_http.done = true;`,
367
+ `}`,
368
+ ``,
369
+ `/** Blocking send: perform on the calling thread, return ok. */`,
370
+ `static inline bool __tc_http_send(void) {`,
371
+ ` __tc_http_perform();`,
372
+ ` return __tc_http.ok;`,
373
+ `}`,
374
+ ``,
375
+ `// ── async split (workqueue + k_sem) ──────────────────────────────────────`,
376
+ `// perform on the system workqueue; poll __tc_http_done. k_sem_give issues a`,
377
+ `// full memory barrier on completion, so the poller observes resp/status/ok`,
378
+ `// before done flips — the same handoff ESP32's xTaskNotifyGive provides.`,
379
+ `static void __tc_http_perform_work(struct k_work* w) {`,
380
+ ` (void)w;`,
381
+ ` __tc_http_perform();`,
382
+ ` k_sem_give(&__tc_http.done_sem);`,
383
+ `}`,
384
+ ``,
385
+ `static inline void __tc_http_send_start(void) {`,
386
+ ` if (!__tc_http.sem_inited) {`,
387
+ ` k_sem_init(&__tc_http.done_sem, 0, 1);`,
388
+ ` __tc_http.sem_inited = true;`,
389
+ ` }`,
390
+ ` __tc_http.done = false;`,
391
+ ` // Drain any completion signal left from a previous request (e.g. the`,
392
+ ` // prior done() saw the volatile flag before taking the sem), so the first`,
393
+ ` // poll of this request doesn't fire on a stale signal.`,
394
+ ` (void)k_sem_take(&__tc_http.done_sem, K_NO_WAIT);`,
395
+ ` k_work_init(&__tc_http.perform_work, __tc_http_perform_work);`,
396
+ ` (void)k_work_submit(&__tc_http.perform_work);`,
397
+ `}`,
398
+ ``,
399
+ `// Poll predicate: true once the worker has signaled completion. The`,
400
+ `// k_sem_take(K_NO_WAIT) is non-blocking and consumes the signal on the first`,
401
+ `// observed completion — its barrier makes resp/status/ok safe to read.`,
402
+ `static inline bool __tc_http_done(void) {`,
403
+ ` if (__tc_http.done) return true;`,
404
+ ` if (k_sem_take(&__tc_http.done_sem, K_NO_WAIT) == 0) { __tc_http.done = true; return true; }`,
405
+ ` return false;`,
406
+ `}`,
407
+ ``,
408
+ `// ── response readers ────────────────────────────────────────────────────`,
409
+ `static inline int32_t __tc_http_status(void) { return __tc_http.status; }`,
410
+ `static inline bool __tc_http_ok(void) { return __tc_http.ok; }`,
411
+ `static inline const char* __tc_http_body(void) {`,
412
+ ` return (__tc_http.resp != nullptr) ? __tc_http.resp : "";`,
413
+ `}`,
414
+ `static inline int32_t __tc_http_content_length(void) {`,
415
+ ` return static_cast<int32_t>(__tc_http.content_length);`,
416
+ `}`,
417
+ ``,
418
+ `// Response-header lookup. The Zephyr http_client_req surface does not expose`,
419
+ `// arbitrary response headers via the recv buffer; this returns the last`,
420
+ `// header captured by the response callback path. With no per-header hook`,
421
+ `// it returns "" until a richer parser setting is wired (http_cb).`,
422
+ `static inline const char* __tc_http_response_header(const char* name) {`,
423
+ ` (void)name;`,
424
+ ` __tc_http.resp_header[0] = '\\0';`,
425
+ ` return __tc_http.resp_header;`,
426
+ `}`,
427
+ `// CUTTLEFISH_HTTP_END`,
428
+ ``,
429
+ ];
430
+ }
431
+
432
+ const METHOD_MAP: Record<string, string> = {
433
+ GET: 'HTTP_GET',
434
+ POST: 'HTTP_POST',
435
+ PUT: 'HTTP_PUT',
436
+ DELETE: 'HTTP_DELETE',
437
+ HEAD: 'HTTP_HEAD',
438
+ PATCH: 'HTTP_PATCH',
439
+ };
440
+
441
+ /**
442
+ * Resolve a HAL http.* op to Zephyr C++ via the __tc_http_* shim. Returns
443
+ * `{ code }` for statement ops, `{ expression }` for value-returning ops.
444
+ * The method string on http.begin may arrive quoted ("GET"); strip quotes and
445
+ * normalize to the Zephyr enum constant (GET→HTTP_GET, etc.).
446
+ */
447
+ export function lowerHttp(op: HALOpIR): { code?: string; expression?: string } {
448
+ const o = op as any;
449
+ switch (op.operation) {
450
+ case 'http.reset':
451
+ return { code: `__tc_http_reset();` };
452
+ case 'http.begin': {
453
+ const methodName = String(o.method).replace(/^["']|["']$/g, '').toUpperCase();
454
+ const method = METHOD_MAP[methodName] ?? 'HTTP_GET';
455
+ return { code: `__tc_http_begin(${method}, ${s(o.url)});` };
456
+ }
457
+ case 'http.set_header':
458
+ return { code: `__tc_http_set_header(${s(o.name)}, ${s(o.value)});` };
459
+ case 'http.set_timeout':
460
+ return { code: `__tc_http_set_timeout(${s(o.ms)});` };
461
+ case 'http.set_max_body':
462
+ return { code: `__tc_http_set_max_body(${s(o.bytes)});` };
463
+ case 'http.set_body':
464
+ return { code: `__tc_http_set_body(${s(o.data)}, ${o.json ? 'true' : 'false'});` };
465
+ case 'http.set_insecure':
466
+ return { code: `__tc_http_set_insecure();` };
467
+ case 'http.set_ca_cert':
468
+ return { code: `__tc_http_set_ca_cert(${s(o.pem)});` };
469
+ case 'http.send':
470
+ return { expression: `__tc_http_send()` };
471
+ case 'http.send_start':
472
+ return { code: `__tc_http_send_start();` };
473
+ case 'http.done':
474
+ return { expression: `__tc_http_done()` };
475
+ case 'http.status':
476
+ return { expression: `__tc_http_status()` };
477
+ case 'http.ok':
478
+ return { expression: `__tc_http_ok()` };
479
+ case 'http.body':
480
+ return { expression: `__tc_http_body()` };
481
+ case 'http.content_length':
482
+ return { expression: `__tc_http_content_length()` };
483
+ case 'http.response_header':
484
+ return { expression: `__tc_http_response_header(${s(o.name)})` };
485
+ default:
486
+ throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`.`);
487
+ }
488
+ }