@typecad/framework-zephyr 1.0.0-alpha.8

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 (144) 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/dt-config/kconfig.d.ts +27 -0
  32. package/dist/dt-config/kconfig.js +283 -0
  33. package/dist/dt-config/overlay.d.ts +35 -0
  34. package/dist/dt-config/overlay.js +246 -0
  35. package/dist/framework.manifest.d.ts +98 -0
  36. package/dist/framework.manifest.js +525 -0
  37. package/dist/index.d.ts +5 -0
  38. package/dist/index.js +12 -0
  39. package/dist/lowering/adc.d.ts +19 -0
  40. package/dist/lowering/adc.js +76 -0
  41. package/dist/lowering/ble.d.ts +20 -0
  42. package/dist/lowering/ble.js +419 -0
  43. package/dist/lowering/board.d.ts +9 -0
  44. package/dist/lowering/board.js +33 -0
  45. package/dist/lowering/gpio.d.ts +12 -0
  46. package/dist/lowering/gpio.js +123 -0
  47. package/dist/lowering/http.d.ts +23 -0
  48. package/dist/lowering/http.js +482 -0
  49. package/dist/lowering/i2c.d.ts +15 -0
  50. package/dist/lowering/i2c.js +114 -0
  51. package/dist/lowering/index.d.ts +29 -0
  52. package/dist/lowering/index.js +93 -0
  53. package/dist/lowering/interrupts.d.ts +21 -0
  54. package/dist/lowering/interrupts.js +91 -0
  55. package/dist/lowering/mqtt.d.ts +19 -0
  56. package/dist/lowering/mqtt.js +366 -0
  57. package/dist/lowering/power.d.ts +9 -0
  58. package/dist/lowering/power.js +64 -0
  59. package/dist/lowering/preferences.d.ts +23 -0
  60. package/dist/lowering/preferences.js +345 -0
  61. package/dist/lowering/pulse.d.ts +7 -0
  62. package/dist/lowering/pulse.js +82 -0
  63. package/dist/lowering/pwm.d.ts +15 -0
  64. package/dist/lowering/pwm.js +59 -0
  65. package/dist/lowering/random.d.ts +18 -0
  66. package/dist/lowering/random.js +129 -0
  67. package/dist/lowering/spi.d.ts +17 -0
  68. package/dist/lowering/spi.js +113 -0
  69. package/dist/lowering/timing.d.ts +9 -0
  70. package/dist/lowering/timing.js +63 -0
  71. package/dist/lowering/tone.d.ts +10 -0
  72. package/dist/lowering/tone.js +50 -0
  73. package/dist/lowering/uart.d.ts +15 -0
  74. package/dist/lowering/uart.js +102 -0
  75. package/dist/lowering/util.d.ts +4 -0
  76. package/dist/lowering/util.js +12 -0
  77. package/dist/lowering/wdt.d.ts +15 -0
  78. package/dist/lowering/wdt.js +80 -0
  79. package/dist/lowering/wifi.d.ts +21 -0
  80. package/dist/lowering/wifi.js +447 -0
  81. package/dist/lowering/worker-backing.d.ts +14 -0
  82. package/dist/lowering/worker-backing.js +79 -0
  83. package/dist/lowering/worker.d.ts +6 -0
  84. package/dist/lowering/worker.js +14 -0
  85. package/dist/strategy.d.ts +168 -0
  86. package/dist/strategy.js +1094 -0
  87. package/dist/toolchain/debug-config.d.ts +82 -0
  88. package/dist/toolchain/debug-config.js +359 -0
  89. package/dist/toolchain/index.d.ts +73 -0
  90. package/dist/toolchain/index.js +427 -0
  91. package/dist/toolchain/scaffold.d.ts +15 -0
  92. package/dist/toolchain/scaffold.js +196 -0
  93. package/dist/toolchain/west-discover.d.ts +40 -0
  94. package/dist/toolchain/west-discover.js +203 -0
  95. package/dist/toolchain/west-spawn.d.ts +39 -0
  96. package/dist/toolchain/west-spawn.js +117 -0
  97. package/package.json +82 -0
  98. package/src/async/timer-polyfill.ts +107 -0
  99. package/src/chips/controllers.ts +74 -0
  100. package/src/chips/esp32.ts +62 -0
  101. package/src/chips/esp32s3.ts +57 -0
  102. package/src/chips/index.ts +54 -0
  103. package/src/chips/resolve.ts +146 -0
  104. package/src/chips/types.ts +163 -0
  105. package/src/chips/xiao-ble.ts +64 -0
  106. package/src/debug-codegen.ts +207 -0
  107. package/src/display/gfx.ts +190 -0
  108. package/src/display/index.ts +70 -0
  109. package/src/display/profiles.ts +63 -0
  110. package/src/display/touch-adapter.ts +100 -0
  111. package/src/display/ui-adapter.ts +558 -0
  112. package/src/dt-config/kconfig.ts +308 -0
  113. package/src/dt-config/overlay.ts +293 -0
  114. package/src/framework.manifest.ts +545 -0
  115. package/src/index.ts +23 -0
  116. package/src/lowering/adc.ts +102 -0
  117. package/src/lowering/ble.ts +426 -0
  118. package/src/lowering/board.ts +38 -0
  119. package/src/lowering/gpio.ts +148 -0
  120. package/src/lowering/http.ts +488 -0
  121. package/src/lowering/i2c.ts +126 -0
  122. package/src/lowering/index.ts +86 -0
  123. package/src/lowering/interrupts.ts +114 -0
  124. package/src/lowering/mqtt.ts +371 -0
  125. package/src/lowering/power.ts +70 -0
  126. package/src/lowering/preferences.ts +354 -0
  127. package/src/lowering/pulse.ts +93 -0
  128. package/src/lowering/pwm.ts +74 -0
  129. package/src/lowering/random.ts +135 -0
  130. package/src/lowering/spi.ts +124 -0
  131. package/src/lowering/timing.ts +72 -0
  132. package/src/lowering/tone.ts +60 -0
  133. package/src/lowering/uart.ts +111 -0
  134. package/src/lowering/util.ts +12 -0
  135. package/src/lowering/wdt.ts +84 -0
  136. package/src/lowering/wifi.ts +452 -0
  137. package/src/lowering/worker-backing.ts +94 -0
  138. package/src/lowering/worker.ts +17 -0
  139. package/src/strategy.ts +1265 -0
  140. package/src/toolchain/debug-config.ts +399 -0
  141. package/src/toolchain/index.ts +459 -0
  142. package/src/toolchain/scaffold.ts +196 -0
  143. package/src/toolchain/west-discover.ts +233 -0
  144. package/src/toolchain/west-spawn.ts +141 -0
@@ -0,0 +1,1265 @@
1
+ // ---------------------------------------------------------------------------
2
+ // ZephyrStrategy — Zephyr RTOS target
3
+ //
4
+ // Outputs C++ built with `west` (the Zephyr build tool). Programs use the
5
+ // Arduino-style setup()/loop() pair; a generated main() bridges them into
6
+ // Zephyr's standard C entry point and yields to the scheduler between loops.
7
+ //
8
+ // GPIO is lowered through devicetree specs (gpio_pin_*_dt) so an active-low
9
+ // LED's polarity is honored by the DT flags, not by the generated code. See
10
+ // src/lowering/gpio.ts.
11
+ //
12
+ // EMIT BOUNDARY: This file is a canonical entry point of the framework strategy
13
+ // surface (B) — its main()/shim bytes land in user firmware. The emitted bytes
14
+ // are covered by the TypeCAD Runtime Exception (see RUNTIME_EXCEPTION.md at the
15
+ // repository root) and are not subject to the license of this tool source.
16
+ // ---------------------------------------------------------------------------
17
+
18
+ import type {
19
+ PlatformStrategy,
20
+ ExpressionIR,
21
+ ProgramIR,
22
+ Diagnostic,
23
+ PlatformContext,
24
+ BoardConstants,
25
+ RuntimePolyfillIR,
26
+ StdLibSupport,
27
+ AsyncRuntimeConfig,
28
+ GraphicsCapacity,
29
+ HALOpIR,
30
+ DisplayHALOp,
31
+ ResolvedDisplay,
32
+ DisplayAdapterCode,
33
+ DisplayProfile,
34
+ TouchProfile,
35
+ TouchAdapterCodegen,
36
+ } from '@typecad/cuttlefish/api/shared';
37
+ import { DEFAULT_STDLIB_SUPPORT } from '@typecad/cuttlefish/api/shared';
38
+ import { buildWorkerRuntimePolyfill } from '@typecad/cuttlefish/api/shared';
39
+ import { programUsesSafety } from '@typecad/cuttlefish/api';
40
+ import { chipForTarget, setActiveChip, getActiveChip } from './chips/index.js';
41
+ import { resolveChipFromBoard } from './chips/resolve.js';
42
+ import { emitGpioDevDispatcher } from './chips/controllers.js';
43
+ import { lowerHalOp } from './lowering/index.js';
44
+ import { buildZephyrWorkerBacking } from './lowering/worker-backing.js';
45
+ import { adcInitLines } from './lowering/adc.js';
46
+ import { pwmInitLines } from './lowering/pwm.js';
47
+ import { i2cInitLines } from './lowering/i2c.js';
48
+ import { spiInitLines } from './lowering/spi.js';
49
+ import { uartInitLines } from './lowering/uart.js';
50
+ import { interruptInitLines } from './lowering/interrupts.js';
51
+ import { wdtInitLines } from './lowering/wdt.js';
52
+ import { bleInitLines } from './lowering/ble.js';
53
+ import { wifiInitLines } from './lowering/wifi.js';
54
+ import { httpInitLines } from './lowering/http.js';
55
+ import { mqttInitLines } from './lowering/mqtt.js';
56
+ import { preferencesInitLines } from './lowering/preferences.js';
57
+ import { randomInitLines } from './lowering/random.js';
58
+ import { generateZephyrInitCode, generateZephyrBreakpointCode, generateZephyrLogpointCode } from './debug-codegen.js';
59
+ import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
60
+ import { buildTimerPolyfill } from './async/timer-polyfill.js';
61
+ import { resolveZephyrDisplayOp, newDisplayState, type DisplayState } from './display/index.js';
62
+ import { buildDisplayRuntime } from './display/gfx.js';
63
+ import { ZEPHYR_DISPLAY_PROFILES } from './display/profiles.js';
64
+ import { zephyrDisplayAdapterGenerator } from './display/ui-adapter.js';
65
+ import { zephyrTouchAdapter } from './display/touch-adapter.js';
66
+
67
+ export class ZephyrStrategy implements PlatformStrategy {
68
+ readonly id = 'zephyr';
69
+
70
+ // ── Profile resolution ──────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Resolve + cache the active chip from the platform context. Called lazily
74
+ * by the methods that need the descriptor (shimLines, resolveHALOperation
75
+ * via lowerHalOp).
76
+ *
77
+ * Tries to derive the chip descriptor from the board/MCU package's zephyr
78
+ * fields (via boardConstants) first. Falls back to the hardcoded
79
+ * chipForTarget registry for boards that haven't shipped zephyr config yet.
80
+ */
81
+ private resolveChip(ctx?: PlatformContext, program?: ProgramIR) {
82
+ // 1. Try board/MCU package constants (new path)
83
+ const fromBoard = resolveChipFromBoard(program?.boardConstants);
84
+ if (fromBoard) {
85
+ setActiveChip(fromBoard);
86
+ return fromBoard;
87
+ }
88
+
89
+ // 2. Fall back to frameworkData.buildTarget → hardcoded registry
90
+ const fd = ctx?.frameworkData as Record<string, unknown> | undefined;
91
+ const target =
92
+ (fd?.target as string | undefined) ??
93
+ (fd?.buildTarget as string | undefined);
94
+ const chip = chipForTarget(target);
95
+ setActiveChip(chip);
96
+ return chip;
97
+ }
98
+
99
+ /**
100
+ * Resolve the debug mode for the active target from the platform context.
101
+ * Mirrors resolveChip's target extraction so shimLines/forcedIncludes can
102
+ * gate the printf halt shim + console UART include to printf builds only
103
+ * (gdb builds use VS Code native breakpoints + #line markers, so the
104
+ * __tc_debug_wait_for_continue shim and its <zephyr/drivers/uart.h> include
105
+ * are dead code there).
106
+ */
107
+ private resolveDebugMode(ctx?: PlatformContext): 'gdb' | 'printf' {
108
+ const fd = ctx?.frameworkData as Record<string, unknown> | undefined;
109
+ const target =
110
+ (fd?.target as string | undefined) ??
111
+ (fd?.buildTarget as string | undefined);
112
+ return this.debugMode(target);
113
+ }
114
+
115
+ forcedIncludes(_program?: ProgramIR, ctx?: PlatformContext): string[] {
116
+ const isPrintf = this.resolveDebugMode(ctx) === 'printf';
117
+ // <zephyr/kernel.h> for k_msleep / k_uptime_get_32 / k_busy_wait / printk.
118
+ // <zephyr/drivers/gpio.h> for the gpio_pin_*_dt / gpio_dt_spec API.
119
+ // <cstdint> because DIRECT_CPP_TYPE_MAP passes int32_t/uint8_t through
120
+ // verbatim and Zephyr's minimal C++ lib provides it.
121
+ //
122
+ // Driver headers are usage-gated on ctx.analysis.usesX (same flags
123
+ // shimLines uses to emit the per-peripheral bus state), so an unused
124
+ // peripheral doesn't pull in its header. When analysis is absent (e.g. a
125
+ // capability query before a real build), the uses() helper defaults to
126
+ // true so nothing is stripped — mirrors framework-esp32's forcedIncludes.
127
+ const a = (ctx as any)?.analysis;
128
+ const uses = (f: string): boolean => (a ? !!a[f] : true);
129
+ const inc: string[] = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdio>', '<cstdint>'];
130
+ if (uses('usesI2C')) inc.push('<zephyr/drivers/i2c.h>');
131
+ if (uses('usesSPI')) inc.push('<zephyr/drivers/spi.h>');
132
+ if (uses('usesUart')) inc.push('<zephyr/drivers/uart.h>');
133
+ // uart.h is also needed by the printf-mode debug halt shim
134
+ // (__tc_debug_wait_for_continue polls the console UART) even when the
135
+ // program itself does not use the UART HAL. In gdb mode the shim is not
136
+ // emitted, so skip the include there to avoid pulling in an unused header.
137
+ if (isPrintf && !inc.includes('<zephyr/drivers/uart.h>')) inc.push('<zephyr/drivers/uart.h>');
138
+ if (uses('usesADC')) inc.push('<zephyr/drivers/adc.h>');
139
+ if (uses('usesPWM')) inc.push('<zephyr/drivers/pwm.h>');
140
+ if (uses('usesWDT')) inc.push('<zephyr/drivers/watchdog.h>');
141
+ if (uses('usesPower')) inc.push('<zephyr/pm/pm.h>', '<zephyr/pm/state.h>', '<zephyr/pm/policy.h>');
142
+ // BLE: the bt_* GATT API + the flat-string headers the shim uses. <string>
143
+ // is needed because a Utf8 (BleValueType.Utf8) read handler lowers to a
144
+ // std::string-returning function (the string literal return type), and the
145
+ // program-analysis usesStdString detector doesn't see types generated by
146
+ // the BLE lowering layer — so without forcing <string> here, any BLE server
147
+ // with a Utf8 characteristic fails to compile ('std::string does not name a
148
+ // type'). Uses <string>, not <string.h>: the latter is the C flat-string
149
+ // header (already included for the shim's strncpy/strcmp).
150
+ if (uses('usesBle')) inc.push('<stdlib.h>', '<string.h>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
151
+ // Display: the analyzer's usesDisplay flag (set by display.* hal-ops) drives
152
+ // this include. When ctx.analysis is absent (capability query), uses()
153
+ // defaults to true so a real build never strips it.
154
+ if (uses('usesDisplay')) inc.push('<zephyr/drivers/display.h>');
155
+ // Random: <zephyr/random/random.h> for sys_rand_get (the entropy tap that
156
+ // seeds the __tc_rand_* xorshift32 PRNG). The shim block re-includes it, but
157
+ // force it here too so a split-TU emit still has the symbol available.
158
+ if (uses('usesRandom')) inc.push('<zephyr/random/random.h>');
159
+ if (uses('usesWifi')) inc.push(
160
+ '<zephyr/net/net_mgmt.h>', '<zephyr/net/wifi_mgmt.h>',
161
+ '<zephyr/net/net_if.h>', '<zephyr/net/net_ip.h>',
162
+ '<zephyr/net/conn_mgr_connectivity.h>', '<zephyr/net/conn_mgr_monitor.h>',
163
+ );
164
+ // HTTP/S client: Zephyr's http_client_req runs over a pre-connected socket,
165
+ // so the shim pulls in the BSD socket + POSIX DNS surfaces alongside the
166
+ // http client/parser headers. TLS sec tags need tls_credentials; <cstring>
167
+ // /<cstdlib> back the shim's memcpy/strlen/new-nothrow usage (the core
168
+ // shim only includes <cstdio>/<cstdint>).
169
+ if (uses('usesHttp')) inc.push(
170
+ '<zephyr/net/socket.h>', '<zephyr/net/http/client.h>',
171
+ '<zephyr/net/http/parser.h>', '<zephyr/net/tls_credentials.h>',
172
+ '<zephyr/posix/sys/socket.h>', '<cstring>', '<cstdlib>',
173
+ );
174
+ // MQTT client: <zephyr/net/mqtt.h> for mqtt_connect/publish/subscribe, plus
175
+ // <zephyr/net/socket.h> for the zsock_* poll/getaddrinfo API the shim's poll
176
+ // thread uses, and <zephyr/net/tls_credentials.h> for the mqtts:// path
177
+ // (mqtt_sec_config). <cstring> backs the shim's memcpy/strlen.
178
+ if (uses('usesMqtt')) inc.push(
179
+ '<zephyr/net/mqtt.h>', '<zephyr/net/socket.h>',
180
+ '<zephyr/net/tls_credentials.h>', '<cstring>',
181
+ );
182
+ // Preferences: Zephyr settings subsystem (ZMS backend) — settings_load/
183
+ // settings_save_one/settings_delete + the SETTINGS_STATIC_HANDLER_DEFINE
184
+ // macro. <cstring> backs the shim's memcpy/memmove/strncpy/strncmp (the
185
+ // core shim only includes <cstdio>/<cstdint>). <errno.h> for ENOENT in h_get.
186
+ if (uses('usesPreferences')) inc.push(
187
+ '<zephyr/settings/settings.h>', '<cstring>', '<errno.h>',
188
+ );
189
+ // std::string — Zephyr has no umbrella header that transitively pulls in
190
+ // <string> (unlike framework-arduino's <Arduino.h>), so a program that
191
+ // lowers a std::string parameter/variable must request it explicitly. Uses
192
+ // <string>, not <string.h>: the latter is the C flat-string header.
193
+ if (uses('usesStdString')) inc.push('<string>');
194
+ return inc;
195
+ }
196
+
197
+ symbolAliases(): Record<string, string> {
198
+ return {};
199
+ }
200
+
201
+
202
+ /**
203
+ * Detect async-runtime usage: a program needs the Promise/microtask runtime
204
+ * if it declares an async function OR references an async-runtime symbol
205
+ * (`__cuttlefish_async_`) — e.g. Async.sleep()/.then() called from a non-async
206
+ * function. Mirrors Arduino's programUsesAsyncRuntime walk (that helper is
207
+ * private to framework-arduino and not exported from cuttlefish, so we walk
208
+ * here). The token appears in `raw` expr nodes and in `hal-expr`/`hal-op`
209
+ * nodes whose resolved `raw` code references it.
210
+ */
211
+ private programUsesAsyncRuntime(program?: ProgramIR): boolean {
212
+ if (!program) return false;
213
+ const TOKEN = '__cuttlefish_async_';
214
+ let found = false;
215
+ const visit = (node: any): void => {
216
+ if (found || !node || typeof node !== 'object') return;
217
+ // raw expression node
218
+ if (node.kind === 'raw' && typeof node.value === 'string' && node.value.includes(TOKEN)) {
219
+ found = true; return;
220
+ }
221
+ // hal-expr / hal-op node whose operation is a raw op carrying code
222
+ if (node.operation && typeof node.operation === 'object'
223
+ && node.operation.operation === 'raw'
224
+ && typeof node.operation.code === 'string'
225
+ && node.operation.code.includes(TOKEN)) {
226
+ found = true; return;
227
+ }
228
+ for (const v of Object.values(node)) {
229
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
230
+ else if (v && typeof v === 'object') visit(v);
231
+ }
232
+ };
233
+ visit(program);
234
+ return found;
235
+ }
236
+
237
+ shimLines(program?: ProgramIR, ctx?: PlatformContext): string[] {
238
+ const chip = this.resolveChip(ctx, program);
239
+ const isPrintf = this.resolveDebugMode(ctx) === 'printf';
240
+ const lines: string[] = [
241
+ '// cuttlefish runtime shim. Wrapped in a single include guard so the',
242
+ '// block is safe to emit into multiple headers and .cpp files within',
243
+ '// one translation unit (a .cpp may #include several headers that each',
244
+ '// carry the shim). The guard ensures the definitions are seen exactly',
245
+ '// once per TU.',
246
+ '#ifndef CUTTLEFISH_SHIM_DEFINED',
247
+ '#define CUTTLEFISH_SHIM_DEFINED',
248
+ '#ifndef CUTTLEFISH_UNDEFINED',
249
+ '#define CUTTLEFISH_UNDEFINED 0',
250
+ '#endif',
251
+ 'template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }',
252
+ 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }',
253
+ 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }',
254
+ 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }',
255
+ 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }',
256
+ 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }',
257
+ 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }',
258
+ 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }',
259
+ // millis() backed by the Zephyr uptime counter. uint32_t return matches
260
+ // the Arduino API the shared runtime expects (wraps every ~49.7 days).
261
+ 'inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }',
262
+ // Arduino-compat defines referenced by the shared runtime polyfills.
263
+ '#ifndef HIGH', '#define HIGH 1', '#endif',
264
+ '#ifndef LOW', '#define LOW 0', '#endif',
265
+ '#ifndef PROGMEM', '#define PROGMEM', '#endif',
266
+ 'inline long map(long x, long in_min, long in_max, long out_min, long out_max) { return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; }',
267
+ 'inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }',
268
+ // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
269
+ // for protocol output. Overloaded for string (const char*) and numeric
270
+ // (double) so the same call site works for markers and test values.
271
+ 'inline void __tc_print(const char* s) { printf("%s", s); }',
272
+ 'inline void __tc_print(double v) { printf("%g", v); }',
273
+ 'inline void __tc_println(const char* s) { printf("%s\\n", s); }',
274
+ 'inline void __tc_println(double v) { printf("%g\\n", v); }',
275
+ ];
276
+
277
+ // Devicetree specs for every board-defined GPIO pin. Emitted unconditionally
278
+ // (guarded by the include guard) so any of them is available whether or not
279
+ // a given program uses it. Safe because every spec references a node that
280
+ // exists in the active board's devicetree.
281
+ for (const spec of chip.gpio.dtSpecs) {
282
+ lines.push(
283
+ `static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`,
284
+ );
285
+ }
286
+
287
+ // Per-peripheral bus state — gated on the same ctx.analysis.usesX flags as
288
+ // forcedIncludes, so an unused peripheral emits no state (and its header is
289
+ // not included). Mirrors framework-esp32's shimLines espInit block.
290
+ const a = (ctx as any)?.analysis;
291
+ const uses = (f: string): boolean => (a ? !!a[f] : true);
292
+ if (uses('usesI2C') && chip.i2c) {
293
+ for (let i = 0; i < chip.i2c.controllers.length; i++) lines.push(...i2cInitLines(chip, i));
294
+ }
295
+ if (uses('usesSPI') && chip.spi) {
296
+ for (let i = 0; i < chip.spi.controllers.length; i++) lines.push(...spiInitLines(chip, i));
297
+ }
298
+ if (uses('usesUart') && chip.uart) {
299
+ for (let i = 0; i < chip.uart.controllers.length; i++) lines.push(...uartInitLines(chip, i));
300
+ }
301
+ if (uses('usesADC') && chip.adc) lines.push(...adcInitLines(chip));
302
+ if (uses('usesPWM') && chip.pwm) lines.push(...pwmInitLines(chip));
303
+ if (uses('usesInterrupts')) lines.push(...interruptInitLines(chip));
304
+ if (uses('usesWDT') && chip.wdt) lines.push(...wdtInitLines(chip));
305
+ if (uses('usesBle')) lines.push(...bleInitLines());
306
+ // Display runtime (minimal rect/text renderer): gated on the analyzer's
307
+ // usesDisplay flag (set by display.* hal-ops). This is the DIRECT-call
308
+ // display path (user code calling screen.display.fillRect etc.). It is
309
+ // SUPPRESSED when the UI display adapter is active — the adapter seam
310
+ // (providesDisplayAdapter/resolveDisplayAdapter) emits its own display_*
311
+ // runtime that drives the UI rendering pipeline via CuttlefishGFX, and the
312
+ // two define the same symbols (display_init, __tc_display_line) so emitting
313
+ // both causes redefinition errors. The adapter is active for the
314
+ // strategy-owned drivers (ili9341-zephyr, st7796-zephyr).
315
+ if (uses('usesDisplay') && !this.providesDisplayAdapter()) {
316
+ const rt = buildDisplayRuntime(this._displayState.profile);
317
+ lines.push(...rt.stateLines);
318
+ lines.push(rt.fontTable);
319
+ lines.push(rt.helpers);
320
+ }
321
+ if (uses('usesWifi')) lines.push(...wifiInitLines());
322
+ if (uses('usesHttp')) lines.push(...httpInitLines());
323
+ if (uses('usesMqtt')) lines.push(...mqttInitLines());
324
+ if (uses('usesPreferences')) lines.push(...preferencesInitLines());
325
+ if (uses('usesRandom')) lines.push(...randomInitLines());
326
+
327
+ lines.push('#endif // CUTTLEFISH_SHIM_DEFINED');
328
+
329
+ // --- Debug-mode halt + per-breakpoint disable registry ---
330
+ //
331
+ // Printf mode only. In gdb mode the cuttlefish debug preprocessor is
332
+ // skipped (core emits #line markers + VS Code native breakpoints instead),
333
+ // so __tc_debug_wait_for_continue is never called — skip the shim and its
334
+ // <zephyr/drivers/uart.h> dependency entirely (forcedIncludes mirrors this).
335
+ //
336
+ // The cuttlefish debug preprocessor injects __tc_debug_wait_for_continue(id)
337
+ // calls at each breakpoint; without these definitions the emitted code
338
+ // would not link.
339
+ //
340
+ // Zephyr's minimal libc has no getchar()/EOF, so the halt polls the console
341
+ // UART directly via uart_poll_in on the system console device, yielding to
342
+ // the scheduler with k_msleep between polls so an unattended breakpoint
343
+ // does not starve the system. ENTER (or any non-'s' byte) = continue;
344
+ // 's'/'S' = skip this breakpoint for the rest of the run (records the id).
345
+ if (isPrintf) {
346
+ lines.push(
347
+ '#ifndef __TC_BP_DISABLED_DEFINED',
348
+ '#define __TC_BP_DISABLED_DEFINED',
349
+ 'static bool __tc_bp_disabled[256] = {0};',
350
+ 'static inline bool __tc_bp_is_disabled(int id) { return id >= 0 && id < 256 && __tc_bp_disabled[id]; }',
351
+ // Console input: poll the UART console device. DEVICE_DT_GET(DT_CHOSEN(zephyr_console))
352
+ // resolves to the board's console (UART0 USB-CDC on the XIAO nRF52840).
353
+ 'static inline char __tc_debug_wait_for_continue(int id) {',
354
+ ' const struct device* __con = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));',
355
+ ' unsigned char __c = 0;',
356
+ " while (uart_poll_in(__con, &__c) != 0) {",
357
+ ' k_msleep(10);',
358
+ ' }',
359
+ " // Drain the rest of the typed line so the next breakpoint waits fresh.",
360
+ " unsigned char __peek = 0;",
361
+ " while (uart_poll_in(__con, &__peek) == 0 && __peek != '\\n') { (void)0; }",
362
+ " if ((__c == 's') || (__c == 'S')) { if (id >= 0 && id < 256) __tc_bp_disabled[id] = true; }",
363
+ ' return static_cast<char>(__c);',
364
+ '}',
365
+ '#endif // __TC_BP_DISABLED_DEFINED',
366
+ '',
367
+ );
368
+ }
369
+
370
+ // --- Zephyr entrypoint: main() runs setup()/loop() directly ---
371
+ // The cuttlefish synthesizer emits setup() and loop() (it keys off
372
+ // entrypointFunctionName()="setup" and requiresLoopFunction()=true). Zephyr
373
+ // is a standard C main()-based RTOS, so main() bridges the two: it calls
374
+ // setup() once, then loops loop() forever, yielding to the scheduler with
375
+ // k_msleep(1) each iteration (cheap cooperative yield — matches the esp32
376
+ // app_main pattern). Declared extern here because setup/loop live in a
377
+ // separate translation unit when generateHeaderFile() splits them into the
378
+ // header.
379
+ lines.push(
380
+ 'extern void setup(void);',
381
+ 'extern void loop(void);',
382
+ '',
383
+ 'int main(void) {',
384
+ ' setup();',
385
+ ' for (;;) {',
386
+ ' loop();',
387
+ ' k_msleep(1);',
388
+ ' }',
389
+ ' return 0;',
390
+ '}',
391
+ );
392
+
393
+ // Safety shims: when the program uses @typecad/safety, provide __tc_gpio_read
394
+ // / __tc_gpio_write backed by the raw controller (a best-effort read that
395
+ // does not depend on a pin having a DT spec). __tc_delay_us uses k_busy_wait.
396
+ //
397
+ // The pin is a RUNTIME value here (safety's voter passes whatever pin it
398
+ // was handed), so the controller cannot be baked in as a single DT_NODELABEL
399
+ // on a multi-controller SoC (ESP32-S3: pins 0–31 → gpio0, 32–48 → gpio1).
400
+ // Emit a tiny __tc_gpio_dev(pin) dispatcher that resolves the owning
401
+ // controller's device per pin; single-controller SoCs collapse it to a
402
+ // one-liner. Each DT_NODELABEL is still compile-time-resolved per branch, so
403
+ // it is always statically valid.
404
+ if (program && programUsesSafety(program)) {
405
+ lines.push(...emitGpioDevDispatcher(chip));
406
+ lines.push(
407
+ 'inline int __tc_gpio_read(uint32_t pin) { return gpio_pin_get_raw(__tc_gpio_dev(pin), pin); }',
408
+ 'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }',
409
+ '#ifndef __TC_DELAY_US_DEFINED',
410
+ '#define __TC_DELAY_US_DEFINED',
411
+ 'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }',
412
+ '#endif',
413
+ );
414
+ }
415
+
416
+ return lines;
417
+ }
418
+
419
+ profileDiagnostics(program?: ProgramIR, ctx?: PlatformContext): Diagnostic[] {
420
+ if (!program) return [];
421
+ const chip = this.resolveChip(ctx, program);
422
+ const a = (ctx as any)?.analysis ?? {};
423
+ const diags: Diagnostic[] = [];
424
+
425
+ // Collect the pins the program uses for output config, ADC reads, and
426
+ // interrupt attaches — deep-walking the IR the same way framework-esp32
427
+ // does (its profileDiagnostics walks program to find gpio.set_mode /
428
+ // power.deep_sleep_pin / adc.read nodes).
429
+ const outputPins = new Set<number>();
430
+ const adcReadPins = new Set<number>();
431
+ const interruptPins = new Set<number>();
432
+ let usesWifiOps = false;
433
+ let usesHttpOps = false;
434
+ let usesMqttOps = false;
435
+ const visit = (node: any): void => {
436
+ if (node && typeof node === 'object') {
437
+ if (node.operation && typeof node.operation === 'object') {
438
+ const op = node.operation;
439
+ if (op.operation === 'gpio.set_mode'
440
+ && typeof op.mode === 'string'
441
+ && op.mode.toLowerCase() === 'output'
442
+ && typeof op.pin === 'number') {
443
+ outputPins.add(op.pin);
444
+ }
445
+ if ((op.operation === 'adc.read' || op.operation === 'adc.read_voltage')
446
+ && typeof op.pin === 'number') {
447
+ adcReadPins.add(op.pin);
448
+ }
449
+ if (op.operation === 'interrupt.attach' && typeof op.pin === 'number') {
450
+ interruptPins.add(op.pin);
451
+ }
452
+ if (typeof op.operation === 'string' && op.operation.startsWith('wifi.')) {
453
+ usesWifiOps = true;
454
+ }
455
+ if (typeof op.operation === 'string' && op.operation.startsWith('http.')) {
456
+ usesHttpOps = true;
457
+ }
458
+ if (typeof op.operation === 'string' && op.operation.startsWith('mqtt.')) {
459
+ usesMqttOps = true;
460
+ }
461
+ }
462
+ for (const k of Object.keys(node)) {
463
+ const v = node[k];
464
+ if (Array.isArray(v)) v.forEach(visit);
465
+ else if (typeof v === 'object' && v !== null) visit(v);
466
+ }
467
+ }
468
+ };
469
+ visit(program);
470
+
471
+ // ── ADC pin validity ────────────────────────────────────────────────────
472
+ // The SAADC lowering resolves a HAL pin to a channel via the chip
473
+ // descriptor's adc.channels map. A pin not in that map resolves to -1,
474
+ // which emits __tc_adc-1_setup() — an undefined symbol → link error. Flag
475
+ // it at compile time with a clear message instead of an opaque link failure.
476
+ const adcPins = new Set((chip.adc?.channels ?? []).map((c) => c.pin));
477
+ for (const pin of adcReadPins) {
478
+ if (!adcPins.has(pin)) {
479
+ const valid = [...adcPins].sort((x, y) => x - y).join(', ');
480
+ diags.push({
481
+ severity: 'error',
482
+ code: 'zephyr-adc-pin-unavailable',
483
+ message: `GPIO ${pin} is not a SAADC channel on ${chip.id} and cannot be read with adc.read.`,
484
+ hint: valid
485
+ ? `Use an analog-capable pin. On ${chip.id} (SAADC): ${valid}.`
486
+ : `This target has no ADC channels mapped in its chip descriptor.`,
487
+ source: program.fileName,
488
+ });
489
+ }
490
+ }
491
+
492
+ // ── Interrupt pin validity ──────────────────────────────────────────────
493
+ // interrupt.attach only wires a real callback for pins listed in the chip
494
+ // descriptor's gpio.interruptPins (the lowering needs a DT spec to build
495
+ // the gpio_callback struct at init). An attach on an unlisted pin emits
496
+ // only a comment — silent no-op. Flag it so the user knows the handler
497
+ // will never fire.
498
+ const intPins = new Set((chip.gpio.interruptPins ?? []).map((p) => p.pin));
499
+ for (const pin of interruptPins) {
500
+ if (!intPins.has(pin)) {
501
+ diags.push({
502
+ severity: 'error',
503
+ code: 'zephyr-interrupt-pin-unavailable',
504
+ message: `GPIO ${pin} has no interrupt DT spec on ${chip.id}; interrupt.attach is a no-op.`,
505
+ hint: intPins.size > 0
506
+ ? `Add the pin to the chip descriptor's gpio.interruptPins, or use an interrupt-capable pin: ${[...intPins].sort((x, y) => x - y).join(', ')}.`
507
+ : `This target declares no interrupt pins in its chip descriptor; interrupts are not available.`,
508
+ source: program.fileName,
509
+ });
510
+ }
511
+ }
512
+
513
+ // ── WiFi target validity ────────────────────────────────────────────────
514
+ // WiFi ops require a chip with a WiFi radio. The ESP32-S3 descriptor sets
515
+ // wifi.supported; the XIAO nRF52840 omits it (no radio). Flag wifi usage on
516
+ // a radioless chip so the user gets a clear "use an ESP32 target" message
517
+ // instead of an opaque link/DT failure.
518
+ if (usesWifiOps && !chip.wifi?.supported) {
519
+ diags.push({
520
+ severity: 'error',
521
+ code: 'zephyr-wifi-unavailable-on-target',
522
+ message: `WiFi ops are used but ${chip.id} has no WiFi radio.`,
523
+ hint: `Use an esp32s3_devkitc or esp32_devkitc target (Espressif ESP32 variants have a 2.4GHz WiFi radio).`,
524
+ source: program.fileName,
525
+ });
526
+ }
527
+
528
+ // ── HTTP target validity ──────────────────────────────────────────────
529
+ // HTTP needs a network transport. On Zephyr the only networked target is
530
+ // the ESP32 (WiFi) — the nRF52840 has neither WiFi nor Ethernet wired in
531
+ // its chip descriptor, so the shim's socket/connect calls would fail at
532
+ // runtime. Flag http usage on a chip without a network radio so the user
533
+ // gets a clear "use an ESP32 target" message instead of an opaque link or
534
+ // runtime failure. (HTTP rides over WiFi here; an Ethernet target would
535
+ // set wifi.supported via a different transport flag if/when added.)
536
+ if (usesHttpOps && !chip.wifi?.supported) {
537
+ diags.push({
538
+ severity: 'error',
539
+ code: 'zephyr-http-unavailable-on-target',
540
+ message: `HTTP ops are used but ${chip.id} has no network stack available.`,
541
+ hint: `Use an esp32s3_devkitc or esp32_devkitc target (HTTP needs a network transport; the ESP32 WiFi radio provides it).`,
542
+ source: program.fileName,
543
+ });
544
+ }
545
+
546
+ // ── MQTT target validity ─────────────────────────────────────────────
547
+ // Same constraint as HTTP: MQTT needs a network transport to reach a broker.
548
+ // Flag mqtt usage on a radioless chip so the user picks a networked target.
549
+ if (usesMqttOps && !chip.wifi?.supported) {
550
+ diags.push({
551
+ severity: 'error',
552
+ code: 'zephyr-mqtt-unavailable-on-target',
553
+ message: `MQTT ops are used but ${chip.id} has no network stack available.`,
554
+ hint: `Use an esp32s3_devkitc or esp32_devkitc target (MQTT needs a network transport; the ESP32 WiFi radio provides it).`,
555
+ source: program.fileName,
556
+ });
557
+ }
558
+
559
+ // ── Unused-analysis: surface a hint that this is a no-analysis probe ────
560
+ // (intentionally minimal — esp32 has richer rules around strapping/RTC pins;
561
+ // nRF52840 has fewer silicon foot-guns, so the rules above are the material
562
+ // ones. Extend as constraints are identified.)
563
+
564
+ return diags;
565
+ }
566
+
567
+ /**
568
+ * `loop` is forward-declared `extern` by the main() bridge shim
569
+ * (extern void loop(void); → int main(void) {...}), so emitting a
570
+ * `static void loop()` definition redeclares it with conflicting linkage,
571
+ * which GCC rejects. Exclude it from the static forward-declaration path
572
+ * — mirrors how ArduinoStrategy excludes `loop` (the Arduino core forward-
573
+ * declares it extern). `setup` is already handled because it equals
574
+ * entrypointFunctionName().
575
+ */
576
+ forwardDeclarationExclusions(): string[] {
577
+ return ['loop'];
578
+ }
579
+
580
+ // ── File shape ──────────────────────────────────────────────────────────
581
+
582
+ sourceExtension(): string {
583
+ return 'cpp';
584
+ }
585
+
586
+ entrypointFunctionName(): string {
587
+ return 'setup';
588
+ }
589
+
590
+ requiresLoopFunction(): boolean {
591
+ return true;
592
+ }
593
+
594
+ overrideBaseName(
595
+ originalBaseName: string,
596
+ outDirBaseName: string,
597
+ isEntryFile: boolean,
598
+ isNpmPackage: boolean,
599
+ ): string {
600
+ // npm packages are library-style — don't rename. Entry files (non-npm) take
601
+ // the out-dir name (mirrors Arduino's .ino-must-match-dir rule). Everything
602
+ // else passes through. (The manifest's entrypoint.overrideBaseName field is
603
+ // dead — never read in src/ — so this method is the sole name source.)
604
+ if (isNpmPackage) return originalBaseName;
605
+ if (isEntryFile) return outDirBaseName;
606
+ return originalBaseName;
607
+ }
608
+
609
+ effectiveEmitMode(requestedMode: string, _isNpmPackage: boolean): string {
610
+ // Zephyr always emits .cpp (no .ino equivalent to force away from), so this
611
+ // is passthrough regardless of npm/app. The 2-param shape matches the
612
+ // interface and Arduino; behavior is identical across branches.
613
+ return requestedMode;
614
+ }
615
+
616
+ // ── Type normalisation ──────────────────────────────────────────────────
617
+
618
+ normalizeCppType(typeName: string): string {
619
+ if (typeName === 'auto') return 'auto';
620
+ if (typeName === 'std::string') return 'const char*';
621
+ return typeName;
622
+ }
623
+
624
+ defaultNumericType(compliance?: { isBanned(ruleId: string): boolean }): string {
625
+ // Zephyr favors fixed-width types. Under AUTOSAR compliance (A3-9-1) emit
626
+ // int32_t; otherwise the standard 'int'.
627
+ return compliance?.isBanned('A3-9-1') ? 'int32_t' : 'int';
628
+ }
629
+
630
+ mapReturnType(functionName: string, returnType: string): string {
631
+ if (functionName === 'main') return 'int';
632
+ if (functionName === 'setup' || functionName === 'loop') return 'void';
633
+ return this.normalizeCppType(returnType);
634
+ }
635
+
636
+ isStringLikeType(cppType: string): boolean {
637
+ return cppType === 'std::string' || cppType === 'const char*' || cppType === 'char*';
638
+ }
639
+
640
+ isPointerType(cppType: string): boolean {
641
+ return cppType.endsWith('*');
642
+ }
643
+
644
+ mapFunctionName(originalName: string): string {
645
+ if (originalName === '__cuttlefish_entrypoint__') return 'main';
646
+ return originalName;
647
+ }
648
+
649
+ // ── Expression rendering ────────────────────────────────────────────────
650
+
651
+ normalizeRawExpression(value: string): string {
652
+ let prev = '';
653
+ let v = value;
654
+ while (prev !== v) {
655
+ prev = v;
656
+ v = v.replace(/\bundefined\b/g, 'CUTTLEFISH_UNDEFINED');
657
+ v = v.replace(/\bnull\b/g, 'CUTTLEFISH_UNDEFINED');
658
+ }
659
+ return v;
660
+ }
661
+
662
+ nullValue(): string {
663
+ return 'CUTTLEFISH_UNDEFINED';
664
+ }
665
+
666
+ wrapStringConcat(): string | undefined {
667
+ return undefined;
668
+ }
669
+
670
+ wrapStringObject(value: string): string {
671
+ return `std::to_string(${value})`;
672
+ }
673
+
674
+ useSnprintfForStrings(): boolean {
675
+ return true;
676
+ }
677
+
678
+ renameEnumMember(_enumName: string, memberName: string): string {
679
+ return memberName;
680
+ }
681
+
682
+ private _largeEnumNames = new Set<string>();
683
+
684
+ setLargeEnumNames(names: ReadonlySet<string>): void {
685
+ this._largeEnumNames = new Set(names);
686
+ }
687
+
688
+ enumCastType(_enumName: string): string | undefined {
689
+ return undefined;
690
+ }
691
+
692
+ renderBoardDefinitionAccess(
693
+ chain: string[],
694
+ boardConstants?: BoardConstants,
695
+ ): string | undefined {
696
+ // Fold Board.definition.<path> / Pins.definition.<path> into the literal
697
+ // board-constant value, mirroring framework-arduino. The renderer calls
698
+ // this (expression-renderer.ts) with the populated boardConstants from the
699
+ // loaded board/MCU package, so a known path resolves to its scalar value.
700
+ // `board.resolve` HAL ops, by contrast, are constant-folded earlier at
701
+ // IR-build time and never reach here; see lowering/board.ts.
702
+ if (chain.length < 3) return undefined;
703
+ if (chain[0] !== 'Board' && chain[0] !== 'Pins') return undefined;
704
+ if (chain[1] !== 'definition') return undefined;
705
+ if (!boardConstants) return undefined;
706
+ const path = chain.slice(2).join('.');
707
+ const val = boardConstants.get(path);
708
+ return val !== undefined ? String(val) : undefined;
709
+ }
710
+
711
+ // ── Statement rendering ─────────────────────────────────────────────────
712
+
713
+ promotesArrayLiteralsToStaticArray(): boolean {
714
+ // No std::vector in the minimal C++ lib — promote array literals to the
715
+ // StaticArray wrapper (the embedded/generic default).
716
+ return true;
717
+ }
718
+
719
+ renderThrow(_valueExpr: string): string {
720
+ // Zephyr minimal config disables C++ exceptions (CONFIG_CPP_EXCEPTIONS=n).
721
+ // A throw becomes an infinite halt loop instead of a real throw.
722
+ return 'for (;;) { k_msleep(1000); }';
723
+ }
724
+
725
+ isConsoleCall(callee: string): boolean {
726
+ return callee.startsWith('console.');
727
+ }
728
+
729
+ transformConsoleCall(method: string, renderedArgs: string, forHeader: boolean): string {
730
+ const semi = forHeader ? '' : ';';
731
+ const empty = !renderedArgs || renderedArgs.trim() === '';
732
+ const tag = method === 'error' ? '[ERROR] ' : method === 'warn' ? '[WARN] ' : '';
733
+ if (empty) return `printk("%s\\n", "${tag}")${semi}`;
734
+ const parts = renderedArgs.split(' << ');
735
+ if (parts.length === 1) {
736
+ return `printk("%s%s\\n", "${tag}", (${renderedArgs}))${semi}`;
737
+ }
738
+ const fmt = '%s' + '%s'.repeat(parts.length) + '\\n';
739
+ const args = [`"${tag}"`, ...parts].join(', ');
740
+ return `printk("${fmt}", ${args})${semi}`;
741
+ }
742
+
743
+ transformConsoleExpression(_method: string, _renderedArgs: string): string | undefined {
744
+ return undefined;
745
+ }
746
+
747
+ objectFieldInitializer(): string | undefined {
748
+ return undefined;
749
+ }
750
+
751
+ overrideClassFieldType(_fieldName: string, normalizedType: string): string {
752
+ return normalizedType;
753
+ }
754
+
755
+ // ── Name guards ─────────────────────────────────────────────────────────
756
+
757
+ reservedNames(): ReadonlySet<string> {
758
+ return new Set<string>();
759
+ }
760
+
761
+ passthroughMacroNames(): ReadonlySet<string> {
762
+ return new Set<string>();
763
+ }
764
+
765
+ apiReservedEnumNames(): ReadonlySet<string> {
766
+ return new Set<string>();
767
+ }
768
+
769
+ apiReservedEnumGuard(): string {
770
+ return '';
771
+ }
772
+
773
+ ambientTypeDeclarations(): string[] {
774
+ // Preferences is the only HAL surface the framework lowers that is used as
775
+ // a bare global (the HAL Preferences class is exported, but the canonical
776
+ // usage — and the hal/tests/14-preferences hardware suite — references it
777
+ // as an unqualified `Preferences.*`). Declaring it ambient lets those
778
+ // programs type-check and resolve to the preferences.* ops the lowering
779
+ // in src/lowering/preferences.ts handles (ZMS-backed settings). Mirrors
780
+ // framework-arduino's ambient Preferences declaration.
781
+ return [
782
+ "",
783
+ " // Persistent key/value store (ZMS-backed Zephyr settings — see",
784
+ " // src/lowering/preferences.ts). begin/end carry the namespace prefix;",
785
+ " // typed put/get round-trip through an in-RAM cache + settings_save_one.",
786
+ " const Preferences: {",
787
+ " begin(name: string, readOnly?: boolean): void;",
788
+ " end(): void;",
789
+ " clear(): void;",
790
+ " putInt(key: string, value: number): void;",
791
+ " getInt(key: string, defaultValue: number): number;",
792
+ " putUInt(key: string, value: number): void;",
793
+ " getUInt(key: string, defaultValue: number): number;",
794
+ " putBool(key: string, value: boolean): void;",
795
+ " getBool(key: string, defaultValue: boolean): boolean;",
796
+ " putFloat(key: string, value: number): void;",
797
+ " getFloat(key: string, defaultValue: number): number;",
798
+ " putString(key: string, value: string): void;",
799
+ " getString(key: string, defaultValue: string): string;",
800
+ " remove(key: string): void;",
801
+ " };",
802
+ ];
803
+ }
804
+
805
+ // ── Includes ────────────────────────────────────────────────────────────
806
+
807
+ needsIostream(): boolean {
808
+ return false;
809
+ }
810
+
811
+ needsStdString(): boolean {
812
+ // Zephyr's minimal C++ lib has no <string>. Set false so the transpiler
813
+ // does not emit std::string-dependent code (e.g. the string-method
814
+ // polyfills). A program that needs it must enable a full STL.
815
+ return false;
816
+ }
817
+
818
+ needsStdVector(): boolean {
819
+ // No <vector> in the minimal C++ lib.
820
+ return false;
821
+ }
822
+
823
+ needsStdExcept(): boolean {
824
+ return false;
825
+ }
826
+
827
+ needsStdFunction(): boolean {
828
+ // No <functional> in the minimal C++ lib.
829
+ return false;
830
+ }
831
+
832
+ mathHeader(): string {
833
+ // <math.h> is the schema-permitted value (the manifest enum allows none |
834
+ // <math.h> | <Arduino.h>). Zephyr's toolchain provides it; the C++ <cmath>
835
+ // names are available via it as well.
836
+ return '<math.h>';
837
+ }
838
+
839
+ cstringHeader(): string {
840
+ return '<cstring>';
841
+ }
842
+
843
+ needsVectorOverload(): boolean {
844
+ return true;
845
+ }
846
+
847
+ needsLargeEnumUnderlying(): boolean {
848
+ return false;
849
+ }
850
+
851
+ // ── Struct field handling ───────────────────────────────────────────────
852
+
853
+ renameStructField(fieldName: string): string {
854
+ return fieldName;
855
+ }
856
+
857
+ structFieldInitializer(): string | undefined {
858
+ return undefined;
859
+ }
860
+
861
+ // ── Async ───────────────────────────────────────────────────────────────
862
+ // Hybrid: timers are native (k_timer + k_work, see src/async/timer-polyfill.ts);
863
+ // Promises use the heap-free static runtime (generateStaticAsyncRuntime), pumped
864
+ // cooperatively in loop() via cuttlefish_pump_microtasks(). There is no
865
+ // __tc_timer_runtime.run() poll — native timers fire from their own expiry path.
866
+
867
+ getAsyncRuntimeConfig(): AsyncRuntimeConfig {
868
+ return {
869
+ queueCapacity: 64,
870
+ scheduler: 'microtask',
871
+ waitForPinEdge: 'stub',
872
+ hasPromiseRuntime: true,
873
+ hasTimers: true,
874
+ // Static (heap-free) runtime — no STL headers required.
875
+ requiredIncludes: [],
876
+ };
877
+ }
878
+
879
+ asyncLoopInjection(taskVarNames: string[], config: AsyncRuntimeConfig): string[];
880
+ asyncLoopInjection(taskVarNames: string[], hasPromiseRuntime: boolean, hasTimers: boolean): string[];
881
+ asyncLoopInjection(
882
+ taskVarNames: string[],
883
+ configOrBool: AsyncRuntimeConfig | boolean,
884
+ _hasTimers?: boolean,
885
+ ): string[] {
886
+ const cfg =
887
+ typeof configOrBool === 'boolean'
888
+ ? { hasPromiseRuntime: configOrBool, hasTimers: _hasTimers ?? false }
889
+ : configOrBool;
890
+ // Drive every async state-machine task once per loop() iteration. The task
891
+ // globals auto-start on their first .run() (constructor sets STATE_0, which
892
+ // runs unconditionally), so this is both the start and the per-frame advance.
893
+ // The state machine no-ops in its terminal/cyclic state, so unconditional
894
+ // .run() is correct (mirrors framework-arduino). No isComplete() gating.
895
+ const lines: string[] = [];
896
+ if (cfg.hasPromiseRuntime) {
897
+ lines.push('cuttlefish_pump_microtasks();');
898
+ }
899
+ for (const n of taskVarNames) {
900
+ lines.push(`${n}.run();`);
901
+ }
902
+ // NOTE: no __tc_timer_runtime.run() — Zephyr timers are native k_timer
903
+ // (timer-polyfill.ts), not a cooperative poll.
904
+ return lines;
905
+ }
906
+
907
+ asyncDriverFunctionName(): string {
908
+ return 'loop';
909
+ }
910
+
911
+ // ── Type aliases ────────────────────────────────────────────────────────
912
+
913
+ shouldSkipTypeAlias(): boolean {
914
+ return false;
915
+ }
916
+
917
+ // ── Diagnostics ─────────────────────────────────────────────────────────
918
+
919
+ emitDiagnostics(): Diagnostic[] {
920
+ return [];
921
+ }
922
+
923
+ currentTimeMillis(): string {
924
+ return 'millis()';
925
+ }
926
+
927
+ // ── Build configuration ──────────────────────────────────────────────────
928
+
929
+ asyncQueueCapacity(): number {
930
+ return 64;
931
+ }
932
+
933
+ outputSubdirectory(_baseName: string): string {
934
+ // Zephyr convention: application sources live under src/.
935
+ return 'src';
936
+ }
937
+
938
+ generateHeaderFile(): boolean {
939
+ return true;
940
+ }
941
+
942
+ enumApiGuard(_enumName: string): { open: string; close: string } | undefined {
943
+ return undefined;
944
+ }
945
+
946
+ getStdLibSupport(_architecture?: string): StdLibSupport {
947
+ // Zephyr's minimal C++ support (lib/cpp/minimal) provides only <cstddef>,
948
+ // <cstdint>, <new>. No <vector>, <string>, <iostream>, <functional>, no
949
+ // exceptions, no RTTI. The blink MVP uses only GPIO + kernel timing, so
950
+ // none of those are needed. Array/string literals are not promoted to the
951
+ // STL containers; a future program needing them must enable a full STL
952
+ // and update these flags.
953
+ return {
954
+ hasVector: false,
955
+ hasString: false,
956
+ hasIostream: false,
957
+ hasExceptions: false,
958
+ hasRTTI: false,
959
+ recommendedArrayImpl: 'static_array',
960
+ recommendedStringImpl: 'static_string',
961
+ };
962
+ }
963
+
964
+ // ── Polyfills ───────────────────────────────────────────────────────────
965
+ // No native polyfills for the MVP — the shared runtime's string/array
966
+ // polyfills are pulled in when a program uses them. cuttlefish_halt is the
967
+ // one symbol the runtime header may reference; supply it as a halt loop.
968
+
969
+ nativePolyfills(): Set<string> {
970
+ // cuttlefish_halt: always (the runtime header may reference it).
971
+ // timer_methods: k_timer/k_work pool for setInterval/setTimeout (gated on
972
+ // timerCallCount at emit time in generateNativePolyfills).
973
+ // async_runtime: heap-free static Promise/microtask runtime (no STL needed).
974
+ return new Set<string>(['cuttlefish_halt', 'wiring_compat', 'timer_methods', 'async_runtime']);
975
+ }
976
+
977
+ generateNativePolyfills(program?: ProgramIR, ctx?: PlatformContext): RuntimePolyfillIR[] {
978
+ const polyfills: RuntimePolyfillIR[] = [
979
+ {
980
+ kind: 'polyfill',
981
+ id: 'cuttlefish_halt',
982
+ domain: 'standard' as const,
983
+ requiredIncludes: [],
984
+ forwardDeclarations: [],
985
+ helperStructs: [],
986
+ helperFunctions: [
987
+ '[[noreturn]] inline void cuttlefish_halt() { for (;;) { k_msleep(1000); } }',
988
+ ],
989
+ shimMacros: [],
990
+ dependencies: [],
991
+ },
992
+ {
993
+ // Wiring-compatibility shims for symbols the UI runtime header
994
+ // references unconditionally (e.g. init-press-input.ts polls pin
995
+ // watchers via digitalRead/HIGH/LOW even when none are configured —
996
+ // the loop body is dead but must compile). Zephyr lowers GPIO through
997
+ // its __tc_gpio_* helpers (defined in shimLines); these macros route
998
+ // the Wiring tokens to them.
999
+ kind: 'polyfill',
1000
+ id: 'wiring_compat',
1001
+ domain: 'standard' as const,
1002
+ requiredIncludes: [],
1003
+ forwardDeclarations: [
1004
+ // Forward-declared so the digitalRead macro (below) can reference it
1005
+ // before the shim block defines the body. The shim emits the full
1006
+ // definition via gpio_pin_get_raw.
1007
+ 'int __tc_gpio_read(int pin);',
1008
+ ],
1009
+ helperStructs: [],
1010
+ helperFunctions: [],
1011
+ shimMacros: [
1012
+ '#ifndef HIGH',
1013
+ '#define HIGH 1',
1014
+ '#endif',
1015
+ '#ifndef LOW',
1016
+ '#define LOW 0',
1017
+ '#endif',
1018
+ '#ifndef digitalRead',
1019
+ '#define digitalRead(pin) __tc_gpio_read(pin)',
1020
+ '#endif',
1021
+ ],
1022
+ dependencies: [],
1023
+ },
1024
+ ];
1025
+
1026
+ // Worker-offload runtime (Phase 1). Emitted only when the program uses
1027
+ // worker.* ops, backed by the Zephyr primitives in worker-backing.ts
1028
+ // (k_work system workqueue + k_sem for the completion barrier).
1029
+ const usesWorker = !!((ctx as any)?.analysis?.usesWorker);
1030
+ if (program && usesWorker) {
1031
+ const workerPoly = buildWorkerRuntimePolyfill(program, this, buildZephyrWorkerBacking(), { poolSize: 4 });
1032
+ if (workerPoly) polyfills.push(workerPoly);
1033
+ }
1034
+
1035
+ // timer_methods — k_timer/k_work pool. Gated on observed timer call count;
1036
+ // a program with no setInterval/setTimeout emits nothing.
1037
+ const analysis = (ctx as { analysis?: { timerCallCount?: number } } | undefined)?.analysis;
1038
+ const timerCallCount = analysis?.timerCallCount ?? 0;
1039
+ if (timerCallCount > 0) {
1040
+ polyfills.push(buildTimerPolyfill(timerCallCount));
1041
+ }
1042
+
1043
+ // async_runtime — heap-free static Promise/microtask runtime. Emitted when
1044
+ // the program declares an async function OR references an async-runtime
1045
+ // symbol (Async.sleep/.then from a non-async fn). The static path requires
1046
+ // no STL headers, so it is safe under Zephyr's minimal C++ lib.
1047
+ const usesAsync = !!program
1048
+ && (program.functions.some((fn: any) => fn && fn.isAsync) || this.programUsesAsyncRuntime(program));
1049
+ if (usesAsync) {
1050
+ polyfills.push({
1051
+ kind: 'polyfill',
1052
+ id: 'async_runtime',
1053
+ domain: 'embedded',
1054
+ requiredIncludes: [],
1055
+ forwardDeclarations: [],
1056
+ helperStructs: [generateStaticAsyncRuntime(8, this.getAsyncRuntimeConfig().waitForPinEdge)],
1057
+ helperFunctions: [],
1058
+ shimMacros: [],
1059
+ dependencies: [],
1060
+ hasPromiseRuntime: true,
1061
+ } as RuntimePolyfillIR);
1062
+ }
1063
+ return polyfills;
1064
+ }
1065
+
1066
+ // ── HAL ──────────────────────────────────────────────────────────────────
1067
+
1068
+ resolveHALOperation(op: HALOpIR): { code?: string; expression?: string } | undefined {
1069
+ return lowerHalOp(op);
1070
+ }
1071
+
1072
+ modelsGpio(): boolean {
1073
+ return true;
1074
+ }
1075
+
1076
+ // ── Atomic HAL primitives ─────────────────────────────────────────────────
1077
+ // Zephyr lowers GPIO through devicetree specs and its own __tc_gpio_* helpers
1078
+ // (defined in shimLines via gpio_pin_get_raw / gpio_pin_set_raw). Cuttlefish
1079
+ // asks these instead of emitting Wiring tokens by name. The async polling
1080
+ // path is gated to 'stub' on Zephyr (waitForPinEdge), so delayMs is unlikely
1081
+ // to be called here, but a busy-wait form is provided for completeness.
1082
+ readDigitalPin(pin: string): string {
1083
+ return `__tc_gpio_read(${pin})`;
1084
+ }
1085
+ readAnalogPin(pin: string): string {
1086
+ // Zephyr ADC is lowered through its own shim; this stub keeps cuttlefish
1087
+ // from emitting a Wiring analogRead token. Update if a __tc_adc_read helper
1088
+ // is introduced.
1089
+ return `/* adc lowering via zephyr shim */ 0`;
1090
+ }
1091
+ writeDigitalPin(pin: string, val: string): string {
1092
+ return `__tc_gpio_write(${pin}, ${val})`;
1093
+ }
1094
+ setPinMode(_pin: string, _mode: string): string {
1095
+ // Zephyr configures pin direction via devicetree, not a runtime pinMode.
1096
+ return `/* pin mode configured via devicetree */`;
1097
+ }
1098
+ delayMs(ms: string): string {
1099
+ return `k_msleep(${ms})`;
1100
+ }
1101
+ delayMicroseconds(us: string): string {
1102
+ return `__tc_delay_us(${us})`;
1103
+ }
1104
+ halCallNames(): ReadonlySet<string> {
1105
+ // Zephyr's HAL surface uses __tc_ prefixed helpers + the Zephyr API.
1106
+ return new Set<string>([
1107
+ "__tc_gpio_read", "__tc_gpio_write", "__tc_delay_us",
1108
+ "gpio_pin_get_raw", "gpio_pin_set_raw", "k_msleep", "k_busy_wait",
1109
+ ]);
1110
+ }
1111
+ isHalCall(name: string): boolean {
1112
+ return this.halCallNames().has(name);
1113
+ }
1114
+ analogReadCallNames(): ReadonlySet<string> {
1115
+ return new Set<string>();
1116
+ }
1117
+
1118
+ // ── RTOS ─────────────────────────────────────────────────────────────────
1119
+
1120
+ isRtosTarget(): boolean {
1121
+ // Zephyr is a preemptive RTOS — delay()/k_msleep inside loop() is the
1122
+ // expected cooperative yield, not an anti-pattern to warn about.
1123
+ return true;
1124
+ }
1125
+
1126
+ // ── Worker offload backing (Phase 1) ─────────────────────────────────────
1127
+ // Delegates to the Zephyr backing (worker-backing.ts): k_work system
1128
+ // workqueue + k_sem for completion. k_sem provides the kernel memory barrier
1129
+ // the dual-core contract requires (the worker runs on a workqueue thread).
1130
+ private _workerBacking = buildZephyrWorkerBacking();
1131
+
1132
+ // Display state (mirrors Arduino's _displayCtx). Seeded on display.init; the
1133
+ // validator-probe path seeds the default profile lazily.
1134
+ private _displayState: DisplayState = newDisplayState();
1135
+
1136
+ workerSpawnLines(handleId: number, trampolineName: string, waiterExpr: string): string[] | undefined {
1137
+ return this._workerBacking.spawnLines(handleId, trampolineName, waiterExpr);
1138
+ }
1139
+ workerSignalDoneExpr(handleId: number): string | undefined {
1140
+ return this._workerBacking.signalDoneExpr(handleId);
1141
+ }
1142
+ workerIsDoneExpr(handleId: number): string | undefined {
1143
+ return this._workerBacking.isDoneExpr(handleId);
1144
+ }
1145
+
1146
+ // ── Graphics ──────────────────────────────────────────────────────────────
1147
+ // Generic <zephyr/drivers/display.h> + ported GFX primitives (see src/display/).
1148
+ // resolveDisplayOp delegates to resolveZephyrDisplayOp with the per-build
1149
+ // DisplayState; the GFX runtime (device handle + line buffer + helpers) is
1150
+ // emitted into shimLines when usesDisplay.
1151
+
1152
+ resolveDisplayOp(op: DisplayHALOp): { code?: string; expression?: string } | undefined {
1153
+ return resolveZephyrDisplayOp(op, this._displayState);
1154
+ }
1155
+
1156
+ supportedDisplayDrivers(): ReadonlySet<string> {
1157
+ return new Set<string>(Object.keys(ZEPHYR_DISPLAY_PROFILES));
1158
+ }
1159
+
1160
+ // ── Strategy-owned display/touch adapter seam ────────────────────────────
1161
+ // Zephyr owns its display + touch adapters: the UI display adapter bridges
1162
+ // the in-tree CuttlefishGFX class to Zephyr's display_write() API (see
1163
+ // src/display/ui-adapter.ts), and the FT6336U touch adapter drives the I2C
1164
+ // controller via Zephyr's i2c API (src/display/touch-adapter.ts). Both live
1165
+ // in this package so cuttlefish carries no Zephyr/Wiring-specific display or
1166
+ // touch knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1167
+
1168
+ providesDisplayAdapter(): boolean { return true; }
1169
+
1170
+ resolveDisplayAdapter(display: ResolvedDisplay): DisplayAdapterCode | undefined {
1171
+ const code = zephyrDisplayAdapterGenerator(display);
1172
+ return code ?? undefined;
1173
+ }
1174
+
1175
+ providesTouchAdapter(): boolean { return true; }
1176
+
1177
+ resolveTouchAdapter(touch: TouchProfile): TouchAdapterCodegen | undefined {
1178
+ return zephyrTouchAdapter(touch);
1179
+ }
1180
+
1181
+ // Named display-profile registry: maps config `profile` values (e.g.
1182
+ // "st7796-zephyr") to the shared DisplayProfile shape so transpile.ts can
1183
+ // resolve them per-framework. The Zephyr profiles are DT-binding descriptors;
1184
+ // they're mapped to the shared shape (driver/width/height/colorFormat/
1185
+ // rotation) the profile resolver expects.
1186
+ getProfileRegistry(): Map<string, DisplayProfile> {
1187
+ const m = new Map<string, DisplayProfile>();
1188
+ for (const [name, p] of Object.entries(ZEPHYR_DISPLAY_PROFILES)) {
1189
+ m.set(name, {
1190
+ driver: p.driver,
1191
+ width: p.width,
1192
+ height: p.height,
1193
+ nativeWidth: p.nativeWidth,
1194
+ nativeHeight: p.nativeHeight,
1195
+ colorFormat: p.colorFormat,
1196
+ rotation: p.rotation ?? 1,
1197
+ });
1198
+ }
1199
+ return m;
1200
+ }
1201
+
1202
+ colorFormat(): 'rgb565' | 'rgb666' | 'rgb888' | 'mono' {
1203
+ return 'rgb565';
1204
+ }
1205
+
1206
+ graphicsCapacity(): GraphicsCapacity {
1207
+ return {
1208
+ maxNodes: 256,
1209
+ maxBindings: 64,
1210
+ maxActiveTransitions: 32,
1211
+ nodeStorage: 'flash',
1212
+ };
1213
+ }
1214
+
1215
+ // ── Debug ─────────────────────────────────────────────────────────────────
1216
+ // Zephyr's minimal C++ config has no <iostream>, so the GenericStrategy
1217
+ // std::cout fallback the debug preprocessor uses by default would NOT
1218
+ // compile. Override the debug surface to route through printk (always
1219
+ // available, no CONFIG_CONSOLE dependency) and the __tc_debug_wait_for_continue
1220
+ // halt emitted in shimLines. See src/debug-codegen.ts.
1221
+ //
1222
+ // Target-selective: targets with a debug probe get native GDB source-level
1223
+ // debugging (core emits #line markers + skips printf instrumentation); the
1224
+ // rest fall back to the printk instrumentation path. The ESP32-S3 has a
1225
+ // built-in USB-JTAG (single-cable GDB via OpenOCD) so it selects 'gdb'.
1226
+ // The XIAO nRF52840 needs its J-Link wired up; its GDB path is a follow-on,
1227
+ // so it stays on printf for now.
1228
+
1229
+ debugMode(target?: string): 'gdb' | 'printf' {
1230
+ // `target` is the Zephyr board id (optionally with a /qualifier suffix,
1231
+ // e.g. 'esp32s3_devkitc/esp32s3/procpu'). Match on the bare board id.
1232
+ const boardId = (target ?? '').split('/')[0];
1233
+ if (boardId === 'esp32s3_devkitc' || boardId.startsWith('esp32s3')) {
1234
+ return 'gdb';
1235
+ }
1236
+ // The plain ESP32 (esp32_devkitc) intentionally stays on 'printf': unlike
1237
+ // the S3 it has NO built-in USB-JTAG, so gdb needs an external ESP-PROG
1238
+ // probe + a different OpenOCD cfg/toolchain dir (deferred). Falls through.
1239
+ return 'printf';
1240
+ }
1241
+
1242
+ generateDebugInitCode(): string[] {
1243
+ return generateZephyrInitCode();
1244
+ }
1245
+
1246
+ generateDebugBreakpointCode(params: {
1247
+ fileName: string; lineNum: number; originalLine: string;
1248
+ variables: Array<{ name: string; isFunction?: boolean; cppType?: 'bool'|'int'|'long'|'float'|'string'|'unknown' }>;
1249
+ normalizedCondition?: string;
1250
+ breakpointId?: number;
1251
+ }): string[] {
1252
+ return generateZephyrBreakpointCode(
1253
+ params.fileName, params.lineNum, params.originalLine,
1254
+ params.variables, params.normalizedCondition, params.breakpointId,
1255
+ );
1256
+ }
1257
+
1258
+ generateDebugLogpointCode(params: {
1259
+ fileName: string; lineNum: number;
1260
+ parts: Array<{ type: 'text' | 'variable'; value: string }>;
1261
+ variables: Array<{ name: string; isFunction?: boolean }>;
1262
+ }): string[] {
1263
+ return generateZephyrLogpointCode(params.fileName, params.lineNum, params.parts, params.variables);
1264
+ }
1265
+ }