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

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 (185) hide show
  1. package/README.md +22 -47
  2. package/dist/as-built.d.ts +33 -0
  3. package/dist/as-built.js +188 -0
  4. package/dist/async/timer-polyfill.js +1 -1
  5. package/dist/boardgen.d.ts +128 -0
  6. package/dist/boardgen.js +1366 -0
  7. package/dist/chips/board-overrides.d.ts +7 -0
  8. package/dist/chips/board-overrides.js +11 -0
  9. package/dist/chips/esp32s3.js +49 -0
  10. package/dist/chips/index.d.ts +6 -12
  11. package/dist/chips/index.js +18 -34
  12. package/dist/chips/resolve.d.ts +16 -2
  13. package/dist/chips/resolve.js +267 -10
  14. package/dist/chips/soc/.d.ts +2 -0
  15. package/dist/chips/soc/.js +129 -0
  16. package/dist/chips/soc/esp32.d.ts +2 -0
  17. package/dist/chips/soc/esp32.js +120 -0
  18. package/dist/chips/soc/esp32c3.d.ts +2 -0
  19. package/dist/chips/soc/esp32c3.js +90 -0
  20. package/dist/chips/soc/esp32c6.d.ts +2 -0
  21. package/dist/chips/soc/esp32c6.js +109 -0
  22. package/dist/chips/soc/esp32s3.d.ts +2 -0
  23. package/dist/chips/soc/esp32s3.js +189 -0
  24. package/dist/chips/soc/index.d.ts +2 -0
  25. package/dist/chips/soc/index.js +23 -0
  26. package/dist/chips/soc/nrf52840.d.ts +2 -0
  27. package/dist/chips/soc/nrf52840.js +130 -0
  28. package/dist/chips/soc/rp2040.d.ts +2 -0
  29. package/dist/chips/soc/rp2040.js +141 -0
  30. package/dist/chips/soc/rp2350a.d.ts +2 -0
  31. package/dist/chips/soc/rp2350a.js +145 -0
  32. package/dist/chips/soc/samd21g18a.d.ts +2 -0
  33. package/dist/chips/soc/samd21g18a.js +143 -0
  34. package/dist/chips/soc/stm32f411xe.d.ts +2 -0
  35. package/dist/chips/soc/stm32f411xe.js +251 -0
  36. package/dist/chips/types.d.ts +319 -0
  37. package/dist/chips/xiao-ble.js +21 -0
  38. package/dist/display/profiles.js +1 -1
  39. package/dist/doctor.js +38 -4
  40. package/dist/dt-config/custom-board.d.ts +23 -0
  41. package/dist/dt-config/custom-board.js +227 -0
  42. package/dist/dt-config/kconfig.d.ts +72 -1
  43. package/dist/dt-config/kconfig.js +115 -26
  44. package/dist/dt-config/overlay.js +548 -28
  45. package/dist/framework.manifest.d.ts +3 -3
  46. package/dist/framework.manifest.js +122 -185
  47. package/dist/index.d.ts +4 -1
  48. package/dist/index.js +8 -2
  49. package/dist/lowering/adc.d.ts +20 -2
  50. package/dist/lowering/adc.js +121 -29
  51. package/dist/lowering/ble.js +31 -28
  52. package/dist/lowering/dac.js +9 -9
  53. package/dist/lowering/fs.js +18 -10
  54. package/dist/lowering/gpio.d.ts +2 -0
  55. package/dist/lowering/gpio.js +82 -5
  56. package/dist/lowering/http.js +39 -14
  57. package/dist/lowering/hwtimer.d.ts +4 -0
  58. package/dist/lowering/hwtimer.js +35 -10
  59. package/dist/lowering/i2c.js +46 -54
  60. package/dist/lowering/index.d.ts +5 -5
  61. package/dist/lowering/index.js +15 -16
  62. package/dist/lowering/interrupts.d.ts +22 -5
  63. package/dist/lowering/interrupts.js +104 -22
  64. package/dist/lowering/mqtt.js +3 -5
  65. package/dist/lowering/power.js +0 -4
  66. package/dist/lowering/preferences.js +49 -77
  67. package/dist/lowering/pulse.js +0 -31
  68. package/dist/lowering/pwm.d.ts +12 -1
  69. package/dist/lowering/pwm.js +78 -14
  70. package/dist/lowering/random.js +3 -3
  71. package/dist/lowering/sensor.d.ts +37 -0
  72. package/dist/lowering/sensor.js +119 -0
  73. package/dist/lowering/spi.d.ts +12 -0
  74. package/dist/lowering/spi.js +65 -40
  75. package/dist/lowering/thread.d.ts +14 -0
  76. package/dist/lowering/thread.js +55 -0
  77. package/dist/lowering/timing.js +20 -46
  78. package/dist/lowering/tone.js +16 -4
  79. package/dist/lowering/uart.d.ts +13 -0
  80. package/dist/lowering/uart.js +72 -51
  81. package/dist/lowering/usb.d.ts +34 -0
  82. package/dist/lowering/usb.js +206 -0
  83. package/dist/lowering/wdt.d.ts +1 -1
  84. package/dist/lowering/wdt.js +17 -7
  85. package/dist/lowering/wifi.d.ts +3 -2
  86. package/dist/lowering/wifi.js +97 -111
  87. package/dist/sdk/board-catalog-sync.d.ts +16 -0
  88. package/dist/sdk/board-catalog-sync.js +41 -0
  89. package/dist/sdk/board-data.generated.d.ts +2 -0
  90. package/dist/sdk/board-data.generated.js +4 -0
  91. package/dist/sdk/catalog-walker.d.ts +90 -0
  92. package/dist/sdk/catalog-walker.js +682 -0
  93. package/dist/sdk/dts-reader.d.ts +83 -0
  94. package/dist/sdk/dts-reader.js +596 -0
  95. package/dist/strategy.d.ts +136 -17
  96. package/dist/strategy.js +839 -222
  97. package/dist/toolchain/bossac-touch.d.ts +18 -0
  98. package/dist/toolchain/bossac-touch.js +88 -0
  99. package/dist/toolchain/compat.d.ts +2 -2
  100. package/dist/toolchain/compat.js +8 -3
  101. package/dist/toolchain/debug-config.d.ts +21 -26
  102. package/dist/toolchain/debug-config.js +226 -76
  103. package/dist/toolchain/env-check.d.ts +4 -4
  104. package/dist/toolchain/env-check.js +5 -5
  105. package/dist/toolchain/index.d.ts +115 -1
  106. package/dist/toolchain/index.js +680 -71
  107. package/dist/toolchain/scaffold.js +39 -7
  108. package/dist/toolchain/west-discover.d.ts +4 -0
  109. package/dist/toolchain/west-discover.js +17 -6
  110. package/installer/CHANGELOG.md +473 -0
  111. package/installer/README.md +328 -0
  112. package/installer/environment.yml +46 -0
  113. package/installer/etc/conda/activate.d/zephyr.bat +7 -0
  114. package/installer/etc/conda/activate.d/zephyr.ps1 +12 -0
  115. package/installer/etc/conda/activate.d/zephyr.sh +20 -0
  116. package/installer/etc/conda/deactivate.d/zephyr.bat +6 -0
  117. package/installer/etc/conda/deactivate.d/zephyr.ps1 +6 -0
  118. package/installer/etc/conda/deactivate.d/zephyr.sh +4 -0
  119. package/installer/install.mjs +580 -0
  120. package/installer/install.ps1 +619 -0
  121. package/installer/install.sh +310 -0
  122. package/installer/lib/detect-platform.sh +62 -0
  123. package/installer/lib/fetch-sdk.sh +304 -0
  124. package/installer/lib/init-workspace.sh +153 -0
  125. package/installer/lib/write-activation.sh +76 -0
  126. package/installer/patches/tfm-c-include-separators.patch +19 -0
  127. package/installer/patches/zephyr-tfm-nordic-zephyr-include.patch +24 -0
  128. package/installer/templates/project/.typecad/activate-zephyr.ps1 +31 -0
  129. package/installer/templates/project/.typecad/activate-zephyr.sh +31 -0
  130. package/installer/templates/project/.vscode/settings.json +30 -0
  131. package/installer/templates/project/README.md +58 -0
  132. package/installer/versions.env +110 -0
  133. package/package.json +15 -15
  134. package/src/as-built.ts +206 -0
  135. package/src/boardgen.ts +1428 -0
  136. package/src/chips/index.ts +35 -54
  137. package/src/chips/resolve.ts +285 -10
  138. package/src/chips/types.ts +325 -1
  139. package/src/display/profiles.ts +1 -1
  140. package/src/doctor.ts +110 -77
  141. package/src/dt-config/custom-board.ts +325 -0
  142. package/src/dt-config/kconfig.ts +165 -18
  143. package/src/dt-config/overlay.ts +537 -25
  144. package/src/framework.manifest.ts +123 -185
  145. package/src/index.ts +57 -41
  146. package/src/lowering/adc.ts +222 -118
  147. package/src/lowering/ble.ts +32 -28
  148. package/src/lowering/dac.ts +82 -82
  149. package/src/lowering/fs.ts +18 -10
  150. package/src/lowering/gpio.ts +242 -155
  151. package/src/lowering/http.ts +512 -488
  152. package/src/lowering/hwtimer.ts +139 -101
  153. package/src/lowering/i2c.ts +116 -126
  154. package/src/lowering/index.ts +91 -93
  155. package/src/lowering/interrupts.ts +113 -21
  156. package/src/lowering/mqtt.ts +3 -5
  157. package/src/lowering/preferences.ts +327 -354
  158. package/src/lowering/pwm.ts +167 -99
  159. package/src/lowering/random.ts +3 -3
  160. package/src/lowering/sensor.ts +155 -0
  161. package/src/lowering/spi.ts +164 -124
  162. package/src/lowering/thread.ts +63 -0
  163. package/src/lowering/timing.ts +46 -72
  164. package/src/lowering/uart.ts +73 -51
  165. package/src/lowering/usb.ts +221 -0
  166. package/src/lowering/wdt.ts +95 -84
  167. package/src/lowering/wifi.ts +97 -112
  168. package/src/sdk/board-catalog-sync.ts +62 -0
  169. package/src/strategy.ts +848 -237
  170. package/src/toolchain/bossac-touch.ts +112 -0
  171. package/src/toolchain/compat.ts +8 -3
  172. package/src/toolchain/debug-config.ts +676 -522
  173. package/src/toolchain/env-check.ts +9 -9
  174. package/src/toolchain/index.ts +1359 -645
  175. package/src/toolchain/scaffold.ts +38 -7
  176. package/src/toolchain/west-discover.ts +19 -6
  177. package/src/async/timer-polyfill.ts +0 -107
  178. package/src/chips/esp32.ts +0 -74
  179. package/src/chips/esp32s3.ts +0 -57
  180. package/src/chips/xiao-ble.ts +0 -82
  181. package/src/lowering/power.ts +0 -70
  182. package/src/lowering/pulse.ts +0 -93
  183. package/src/lowering/tone.ts +0 -60
  184. package/src/lowering/worker-backing.ts +0 -94
  185. package/src/lowering/worker.ts +0 -17
@@ -11,10 +11,66 @@
11
11
  // then just read. A static `adc_channel_cfg` + a per-channel "configured" flag
12
12
  // guards the setup, so repeated reads don't reconfigure.
13
13
  // ---------------------------------------------------------------------------
14
- /** Resolve the SAADC channel index for a HAL pin number; -1 if unmapped. */
14
+ import { ZEPHYR_ADC_GAINS, ZEPHYR_ADC_REFERENCES } from '@typecad/hal';
15
+ /**
16
+ * Resolve the SAADC channel index for an adc.read argument; -1 if unmapped.
17
+ *
18
+ * The numeric argument serves two HAL forms: `InputPin.readAnalog()` passes
19
+ * a GPIO number, while the Arduino-compat `ADC.read(channel)` passes an ADC
20
+ * channel number. Pin-first resolution keeps readAnalog behavior unchanged;
21
+ * the channel fallback makes `ADC.read(n)` resolve on targets where channel
22
+ * n does not alias GPIO n (e.g. the XIAO's AIN2 = P0.28). Where a number
23
+ * matches both a mapped pin and another channel's index, the pin wins.
24
+ */
15
25
  export function adcChannelForPin(chip, pin) {
16
- const ch = chip.adc?.channels.find((c) => c.pin === pin);
17
- return ch ? ch.channel : -1;
26
+ const channels = chip.adc?.channels ?? [];
27
+ const byPin = channels.find((c) => c.pin === pin);
28
+ if (byPin)
29
+ return byPin.channel;
30
+ const byChannel = channels.find((c) => c.channel === pin);
31
+ return byChannel ? byChannel.channel : -1;
32
+ }
33
+ /**
34
+ * Resolve the owning ADC controller's node label for an adc.read argument.
35
+ * Defaults to the descriptor's primary (`chip.adc.nodeLabel`) — channels on
36
+ * additional controllers (STM32 adc3-only routes, ESP32 adc2 pads) carry
37
+ * their controller explicitly, and their device handle is
38
+ * `__tc_adc_<label>_dev` (the primary keeps the unsuffixed `__tc_adc_dev`
39
+ * so single-controller output is unchanged).
40
+ */
41
+ export function adcControllerForPin(chip, pin) {
42
+ const channels = chip.adc?.channels ?? [];
43
+ const byPin = channels.find((c) => c.pin === pin);
44
+ if (byPin)
45
+ return byPin.controller ?? chip.adc?.nodeLabel ?? 'adc';
46
+ const byChannel = channels.find((c) => c.channel === pin);
47
+ return byChannel?.controller ?? chip.adc?.nodeLabel ?? 'adc';
48
+ }
49
+ /** The C++ device-handle symbol for an ADC controller nodelabel. The primary
50
+ * controller maps to the historical unsuffixed `__tc_adc_dev`. */
51
+ function adcDevSymbol(controller, primary) {
52
+ return controller === primary ? '__tc_adc_dev' : `__tc_adc_${controller}_dev`;
53
+ }
54
+ // ── Thin ADC (hal/adc-pin.ts) — gain/reference tokens ──────────────────────
55
+ //
56
+ // "ADC.GAIN_1_4" / "ADC.REF_INTERNAL" token text maps name-for-
57
+ // name onto the enum adc_gain / adc_reference macros. The name sets come from
58
+ // the GENERATED Zephyr token tables (scripts/gen-zephyr-hal-tokens.mjs —
59
+ // parsed from the pinned tree's headers, so the set cannot drift from
60
+ // upstream); empty/absent tokens mean "use the chip descriptor's pair".
61
+ const ADC_GAIN_TOKENS = Object.fromEntries(ZEPHYR_ADC_GAINS.map((g) => [`ADC.GAIN_${g}`, `ADC_GAIN_${g}`]));
62
+ const ADC_REF_TOKENS = Object.fromEntries(ZEPHYR_ADC_REFERENCES.map((r) => [`ADC.REF_${r}`, `ADC_REF_${r}`]));
63
+ /** Resolve one thin-ADC token to its macro; undefined when absent/empty.
64
+ * Unknown tokens are build errors naming the valid spellings. */
65
+ function adcThinToken(token, map, kind) {
66
+ const t = String(token ?? '').trim();
67
+ if (!t || t === '0' || t === "''")
68
+ return undefined;
69
+ const m = map[t];
70
+ if (!m) {
71
+ throw new Error(`ADC ${kind} token '${t}' is not a known ADC.* token — valid ${kind}s: ${Object.keys(map).join(', ')}.`);
72
+ }
73
+ return m;
18
74
  }
19
75
  /**
20
76
  * Emit the per-channel ADC setup state. One block per channel in the chip
@@ -25,8 +81,8 @@ export function adcChannelForPin(chip, pin) {
25
81
  * `static bool __tc_adc<N>_ready` so the first read configures it and
26
82
  * subsequent reads skip. Called from shimLines when the program uses ADC.
27
83
  */
28
- export function adcInitLines(chip, usedPins) {
29
- const dev = `DEVICE_DT_GET(DT_NODELABEL(${chip.adc?.nodeLabel ?? 'adc'}))`;
84
+ export function adcInitLines(chip, usedPins, overrideDevices) {
85
+ const primary = chip.adc?.nodeLabel ?? 'adc';
30
86
  const res = chip.adc?.resolution ?? 12;
31
87
  const vref = chip.adc?.vrefMv ?? 3000;
32
88
  // Gain/reference are SoC-specific: the nRF SAADC scheme (gain 1/4 against
@@ -36,12 +92,34 @@ export function adcInitLines(chip, usedPins) {
36
92
  // the SoC's pair so the emitted channel setup validates in the driver.
37
93
  const gain = chip.adc?.gain ?? 'ADC_GAIN_1_4';
38
94
  const reference = chip.adc?.reference ?? 'ADC_REF_INTERNAL';
39
- const channels = (chip.adc?.channels ?? []).filter((c) => !usedPins || usedPins.has(c.pin));
95
+ const owns = (c, label) => (c.controller ?? primary) === label;
96
+ const channels = (chip.adc?.channels ?? []).filter((c) => !usedPins ||
97
+ // Pin-form usage (the ADC/readAnalog path): the exact channel.
98
+ [...usedPins].some((n) => n === c.pin) ||
99
+ // Channel-number fallback (the Arduino-compat ADC.read(n) form) — must
100
+ // match the CONTROLLER too, or colliding indices across controllers
101
+ // (both ESP32 SARADC units expose channel 0) over-include.
102
+ [...usedPins].some((n) => adcChannelForPin(chip, n) === c.channel && owns(c, adcControllerForPin(chip, n))));
40
103
  const lines = ['// CUTTLEFISH_ADC_BEGIN'];
41
- lines.push(`static const struct device* __tc_adc_dev = ${dev};`);
104
+ lines.push(`static const struct device* __tc_adc_dev = DEVICE_DT_GET(DT_NODELABEL(${primary}));`);
105
+ // Additional controllers referenced by used channels get their own device
106
+ // handle (sorted for deterministic output). Channel indices collide across
107
+ // controllers, so non-primary setup symbols carry the controller label.
108
+ const extraControllers = [...new Set(channels.map((c) => c.controller).filter((x) => !!x && x !== primary))].sort();
109
+ // Inline-override devices (escape hatch): handles for the DT labels the
110
+ // construction opts name — they may not appear in the manifest at all.
111
+ for (const label of (overrideDevices ?? [])) {
112
+ if (label !== primary && !extraControllers.includes(label))
113
+ extraControllers.push(label);
114
+ }
115
+ for (const label of extraControllers) {
116
+ lines.push(`static const struct device* __tc_adc_${label}_dev = DEVICE_DT_GET(DT_NODELABEL(${label}));`);
117
+ }
42
118
  for (const c of channels) {
43
119
  const n = c.channel;
44
- lines.push(`static bool __tc_adc${n}_ready = false;`, `static void __tc_adc${n}_setup(void) {`, ` if (__tc_adc${n}_ready) return;`, ` const struct adc_channel_cfg cfg = {`, ` .gain = ${gain},`, ` .reference = ${reference},`, ` .acquisition_time = ADC_ACQ_TIME_DEFAULT,`, ` .channel_id = ${n},`, ` .differential = 0,`, ` };`, ` adc_channel_setup(__tc_adc_dev, &cfg);`, ` __tc_adc${n}_ready = true;`, `}`);
120
+ const dev = adcDevSymbol(c.controller ?? primary, primary);
121
+ const sym = c.controller && c.controller !== primary ? `__tc_adc_${c.controller}_${n}` : `__tc_adc${n}`;
122
+ lines.push(`static bool ${sym}_ready = false;`, `static void ${sym}_setup(void) {`, ` if (${sym}_ready) return;`, ` const struct adc_channel_cfg cfg = {`, ` .gain = ${gain},`, ` .reference = ${reference},`, ` .acquisition_time = ADC_ACQ_TIME_DEFAULT,`, ` .channel_id = ${n},`, ` .differential = 0,`, ` };`, ` adc_channel_setup(${dev}, &cfg);`, ` ${sym}_ready = true;`, `}`);
45
123
  }
46
124
  lines.push(`#define __TC_ADC_VREF_MV ${vref}`);
47
125
  lines.push(`#define __TC_ADC_RESOLUTION ${res}`);
@@ -59,30 +137,44 @@ export function lowerAdc(op, chip) {
59
137
  const gain = chip.adc?.gain ?? 'ADC_GAIN_1_4';
60
138
  const reference = chip.adc?.reference ?? 'ADC_REF_INTERNAL';
61
139
  switch (op.operation) {
62
- case 'adc.read': {
63
- const ch = adcChannelForPin(chip, o.pin);
64
- // GCC statement-expression: configures-on-first-call, reads, returns raw.
65
- return {
66
- expression: `({ __tc_adc${ch}_setup(); int16_t __b = 0; struct adc_sequence __s = { .channels = BIT(${ch}), .buffer = &__b, .buffer_size = sizeof(__b), .resolution = ${res} }; adc_read(__tc_adc_dev, &__s); __b; })`,
67
- };
68
- }
69
- case 'adc.read_voltage': {
70
- const ch = adcChannelForPin(chip, o.pin);
71
- // Read raw, convert to millivolts via adc_raw_to_millivolts with the
72
- // descriptor's gain (raw_to_millivolts divides out the gain the channel
73
- // was set up with). Returns mV as int.
140
+ case 'adc.read_raw':
141
+ case 'adc.read_mv': {
142
+ // Construction-time overrides (hal/adc-pin.ts opts: channel/device/
143
+ // pinctrl): the user vouches for the routing on a pin the facts layer
144
+ // does not cover. The channel and device symbol come from the op; the
145
+ // marker comment carries device/pinctrl to the overlay regen (the
146
+ // transpiler cannot synthesize DT nodes) and suppresses the
147
+ // unavailable-pin diagnostic.
148
+ const hasOverride = (typeof o.channelOverride === 'number' && o.channelOverride >= 0)
149
+ || (typeof o.deviceOverride === 'string' && o.deviceOverride !== '')
150
+ || (typeof o.pinctrlOverride === 'string' && o.pinctrlOverride !== '');
151
+ const ch = hasOverride && typeof o.channelOverride === 'number' && o.channelOverride >= 0
152
+ ? o.channelOverride
153
+ : adcChannelForPin(chip, o.pin);
154
+ const primary = chip.adc?.nodeLabel ?? 'adc';
155
+ const devCtrl = (typeof o.deviceOverride === 'string' && o.deviceOverride !== '')
156
+ ? o.deviceOverride
157
+ : adcControllerForPin(chip, o.pin);
158
+ const dev = adcDevSymbol(devCtrl, primary);
159
+ const gainMacro = adcThinToken(o.gain, ADC_GAIN_TOKENS, 'gain');
160
+ const refMacro = adcThinToken(o.reference, ADC_REF_TOKENS, 'reference');
161
+ const g = gainMacro ?? gain;
162
+ const r = refMacro ?? reference;
163
+ const marker = hasOverride
164
+ ? `/* cuttlefish-user-facts: adc pin=${o.pin}`
165
+ + `${typeof o.deviceOverride === 'string' && o.deviceOverride !== '' ? ` device=${o.deviceOverride}` : ''}`
166
+ + `${typeof o.pinctrlOverride === 'string' && o.pinctrlOverride !== '' ? ` pinctrl=${o.pinctrlOverride}` : ''}`
167
+ + ` channel=${ch} */ `
168
+ : '';
169
+ const setup = `static bool __tc_adct${o.pin}_done = false; if (!__tc_adct${o.pin}_done) { const struct adc_channel_cfg __tc_adct${o.pin}_cfg = { .gain = ${g}, .reference = ${r}, .acquisition_time = ADC_ACQ_TIME_DEFAULT, .channel_id = ${ch}, .differential = 0 }; adc_channel_setup(${dev}, &__tc_adct${o.pin}_cfg); __tc_adct${o.pin}_done = true; }`;
170
+ const read = `int16_t __b = 0; struct adc_sequence __s = { .channels = BIT(${ch}), .buffer = &__b, .buffer_size = sizeof(__b), .resolution = ${res} }; adc_read(${dev}, &__s);`;
171
+ if (op.operation === 'adc.read_raw') {
172
+ return { expression: `({ ${marker}${setup} ${read} __b; })` };
173
+ }
74
174
  return {
75
- expression: `({ __tc_adc${ch}_setup(); int16_t __b = 0; struct adc_sequence __s = { .channels = BIT(${ch}), .buffer = &__b, .buffer_size = sizeof(__b), .resolution = ${res} }; adc_read(__tc_adc_dev, &__s); int32_t __v = __b; adc_raw_to_millivolts(${vref}, ${gain}, ${res}, &__v); __v; })`,
175
+ expression: `({ ${marker}${setup} ${read} int32_t __v = __b; adc_raw_to_millivolts(${vref}, ${g}, ${res}, &__v); __v; })`,
76
176
  };
77
177
  }
78
- case 'adc.get_resolution':
79
- return { expression: String(res) };
80
- case 'adc.set_reference':
81
- // Zephyr configures the reference at channel-setup time; runtime switching
82
- // would require re-setup. Record the intent as a no-op statement.
83
- return { code: `/* adc.set_reference(${o.reference}): configured at channel setup (${reference}) */` };
84
- case 'adc.get_reference':
85
- return { expression: `0 /* DEFAULT (${reference}) */` };
86
178
  default:
87
179
  throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
88
180
  `Open an issue or use rawCpp() to emit it manually.`);
@@ -88,6 +88,9 @@ export function bleInitLines() {
88
88
  `// bt_gatt_chrc metadata for each characteristic declaration attr.`,
89
89
  `static struct bt_gatt_chrc __tc_ble_chrc_meta[__TC_BLE_MAX_CHARS];`,
90
90
  `// The flat attribute array + its wrapping service. Plus per-char CCC cfg.`,
91
+ `static struct bt_gatt_ccc_managed_user_data __tc_ble_ccc[__TC_BLE_MAX_CHARS];`,
92
+ `// write_ccc dereferences cfg_changed — every slot needs the no-op.`,
93
+ `static void __tc_ble_ccc_changed(const struct bt_gatt_attr* a, uint16_t v) { (void)a; (void)v; }`,
91
94
  `static struct bt_gatt_attr __tc_ble_attrs[__TC_BLE_MAX_ATTRS];`,
92
95
  `static size_t __tc_ble_attr_count = 0;`,
93
96
  `static struct bt_gatt_service __tc_ble_svc;`,
@@ -142,8 +145,10 @@ export function bleInitLines() {
142
145
  ` if (idx < 0 || idx >= __TC_BLE_MAX_CHARS || !__tc_ble.on_read[idx]) return 0;`,
143
146
  ` const char* t = __tc_ble_char_defs[idx].type;`,
144
147
  ` if (t && strcmp(t, "utf8") == 0) {`,
145
- ` const char* s_c = reinterpret_cast<const char*(*)(void)>(__tc_ble.on_read[idx])();`,
146
- ` return bt_gatt_attr_read(conn, attr, buf, len, offset, s_c, strlen(s_c));`,
148
+ ` // Hoisted string callbacks return std::string BY VALUE — call through`,
149
+ ` // the true signature and borrow c_str() for the copy below.`,
150
+ ` std::string s_v = reinterpret_cast<std::string(*)(void)>(__tc_ble.on_read[idx])();`,
151
+ ` return bt_gatt_attr_read(conn, attr, buf, len, offset, s_v.c_str(), s_v.size());`,
147
152
  ` } else if (t && strcmp(t, "float32") == 0) {`,
148
153
  ` double d = reinterpret_cast<double(*)(void)>(__tc_ble.on_read[idx])();`,
149
154
  ` float f = static_cast<float>(d);`,
@@ -263,6 +268,7 @@ export function bleInitLines() {
263
268
  `// (declaration + value); the value attr carries the char index in user_data.`,
264
269
  `static void __tc_ble_build_svc_table(void) {`,
265
270
  ` size_t a = 0;`,
271
+ ` for (int c = 0; c < __TC_BLE_MAX_CHARS; c++) { __tc_ble_ccc[c].cfg_changed = __tc_ble_ccc_changed; }`,
266
272
  ` for (int s = 0; s < static_cast<int>(__tc_ble.svc_count); s++) {`,
267
273
  ` // Primary service declaration.`,
268
274
  ` __tc_ble_attrs[a].uuid = BT_UUID_GATT_PRIMARY;`,
@@ -298,6 +304,17 @@ export function bleInitLines() {
298
304
  ` __tc_ble_attrs[a].perm = BT_GATT_PERM_READ | BT_GATT_PERM_WRITE;`,
299
305
  ` __tc_ble.val_attr_idx[c] = static_cast<int>(a);`,
300
306
  ` a++;`,
307
+ ` if (__tc_ble_char_defs[c].perms & 4) {`,
308
+ ` // CCC descriptor — without it a central cannot subscribe`,
309
+ ` // and notifications never reach anyone.`,
310
+ ` __tc_ble_attrs[a].uuid = BT_UUID_GATT_CCC;`,
311
+ ` __tc_ble_attrs[a].read = bt_gatt_attr_read_ccc;`,
312
+ ` __tc_ble_attrs[a].write = bt_gatt_attr_write_ccc;`,
313
+ ` __tc_ble_attrs[a].user_data = &__tc_ble_ccc[c];`,
314
+ ` __tc_ble_attrs[a].handle = 0;`,
315
+ ` __tc_ble_attrs[a].perm = BT_GATT_PERM_READ | BT_GATT_PERM_WRITE;`,
316
+ ` a++;`,
317
+ ` }`,
301
318
  ` }`,
302
319
  ` }`,
303
320
  ` __tc_ble_attr_count = a;`,
@@ -320,24 +337,28 @@ export function bleInitLines() {
320
337
  ` bt_gatt_service_register(&__tc_ble_svc);`,
321
338
  ` __tc_ble.status = 1; // Initializing`,
322
339
  ` bt_enable(NULL); // synchronous`,
340
+ ` bt_enable(NULL); // synchronous`,
323
341
  ` __tc_ble.inited = true;`,
324
342
  ` __tc_ble_advertise_start();`,
325
343
  `}`,
326
344
  ``,
327
345
  `// ── Notify ──`,
346
+ `// Explicit-conn: the NULL (broadcast) form hit an ESP32 controller assert`,
347
+ `// (lld_con.c) on the first push after subscription on the S3.`,
328
348
  `static void __tc_ble_notify(int idx, int16_t value) {`,
329
349
  ` if (idx < 0 || idx >= __TC_BLE_MAX_CHARS) return;`,
350
+ ` if (__tc_ble.conn == NULL) return; // no link: a NULL-conn notify asserted the controller`,
330
351
  ` int ai = __tc_ble.val_attr_idx[idx];`,
331
352
  ` if (ai == 0) return;`,
332
353
  ` const char* t = __tc_ble_char_defs[idx].type;`,
333
354
  ` if (t && strcmp(t, "uint32") == 0) {`,
334
- ` uint32_t u = static_cast<uint32_t>(value); bt_gatt_notify(NULL, &__tc_ble_attrs[ai], &u, sizeof(u));`,
355
+ ` uint32_t u = static_cast<uint32_t>(value); bt_gatt_notify(__tc_ble.conn, &__tc_ble_attrs[ai], &u, sizeof(u));`,
335
356
  ` } else if (t && strcmp(t, "float32") == 0) {`,
336
- ` float f = static_cast<float>(value); bt_gatt_notify(NULL, &__tc_ble_attrs[ai], &f, sizeof(f));`,
357
+ ` float f = static_cast<float>(value); bt_gatt_notify(__tc_ble.conn, &__tc_ble_attrs[ai], &f, sizeof(f));`,
337
358
  ` } else if (t && strcmp(t, "boolean") == 0) {`,
338
- ` uint8_t b = value ? 1 : 0; bt_gatt_notify(NULL, &__tc_ble_attrs[ai], &b, sizeof(b));`,
359
+ ` uint8_t b = value ? 1 : 0; bt_gatt_notify(__tc_ble.conn, &__tc_ble_attrs[ai], &b, sizeof(b));`,
339
360
  ` } else {`,
340
- ` int16_t s16 = value; bt_gatt_notify(NULL, &__tc_ble_attrs[ai], &s16, sizeof(s16));`,
361
+ ` int16_t s16 = value; bt_gatt_notify(__tc_ble.conn, &__tc_ble_attrs[ai], &s16, sizeof(s16));`,
341
362
  ` }`,
342
363
  `}`,
343
364
  ``,
@@ -345,18 +366,7 @@ export function bleInitLines() {
345
366
  ` return __tc_ble.status == 3 && __tc_ble.connected_clients > 0;`,
346
367
  `}`,
347
368
  `static inline int __tc_ble_client_count(void) { return __tc_ble.connected_clients; }`,
348
- `static inline void __tc_ble_set_name(const char* name) { strncpy(__tc_ble.name, name, sizeof(__tc_ble.name) - 1); __tc_ble.name[sizeof(__tc_ble.name) - 1] = 0; }`,
349
- `static inline void __tc_ble_set_tx_power(int dbm) { (void)dbm; /* nRF TX power via bt_le_set_tx_power — deferred */ }`,
350
369
  ``,
351
- `static inline bool __tc_ble_until_connected(uint32_t timeout_ms) {`,
352
- ` int64_t deadline = static_cast<int64_t>(k_uptime_get()) + static_cast<int64_t>(timeout_ms);`,
353
- ` while (!__tc_ble_is_connected()) {`,
354
- ` if (timeout_ms > 0 && k_uptime_get() >= deadline) return false;`,
355
- ` k_msleep(50);`,
356
- ` }`,
357
- ` return true;`,
358
- `}`,
359
- `static inline void __tc_ble_until_connected_start(void) { /* __tc_ble_server_begin already started advertising */ }`,
360
370
  `// CUTTLEFISH_BLE_END`,
361
371
  ``,
362
372
  ];
@@ -399,21 +409,14 @@ export function lowerBle(op) {
399
409
  case 'ble.on_disconnect':
400
410
  return { code: `__tc_ble.on_disconnect = (${s(o.handler)});` };
401
411
  case 'ble.notify':
402
- return { expression: `__tc_ble_notify(__tc_ble.current_char, ${s(o.value)})` };
412
+ // Index-based: val_attr_idx[idx] resolves the registered attr (the
413
+ // declaration-order slot). current_char would be whatever add_char ran
414
+ // last — wrong for a standalone notify after later declarations.
415
+ return { expression: `__tc_ble_notify(${s(o.index ?? 0)}, ${s(o.value)})` };
403
416
  case 'ble.is_connected':
404
417
  return { expression: `__tc_ble_is_connected()` };
405
418
  case 'ble.client_count':
406
419
  return { expression: `__tc_ble_client_count()` };
407
- case 'ble.status':
408
- return { expression: `__tc_ble.status` };
409
- case 'ble.set_name':
410
- return { code: `__tc_ble_set_name(${s(o.name)});` };
411
- case 'ble.set_tx_power':
412
- return { code: `__tc_ble_set_tx_power(${s(o.dbm)});` };
413
- case 'ble.until_connected':
414
- return { expression: `__tc_ble_until_connected(${s(o.timeoutMs)})` };
415
- case 'ble.until_connected_start':
416
- return { code: `__tc_ble_until_connected_start();` };
417
420
  default:
418
421
  throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
419
422
  `Open an issue or use rawCpp() to emit it manually.`);
@@ -46,17 +46,17 @@ export function lowerDac(op, chip) {
46
46
  };
47
47
  }
48
48
  switch (op.operation) {
49
- case 'dac.write': {
50
- // Lazy one-time channel setup on first write (the HAL DAC surface has no
51
- // begin()), then output the value. Arduino analogWrite is 0–255 against
52
- // the channel's resolution; dac_write_value takes the raw code.
49
+ case 'dac.write_value': {
50
+ // Thin DAC: raw code with the construction resolution (0 = the
51
+ // descriptor channel's), lazy setup per pin.
52
+ const res = Number(o.resolution) > 0 ? Number(o.resolution) : ch.resolution;
53
53
  return {
54
54
  code: [
55
- `{ static bool __tc_dac_done = false;`,
56
- ` if (!__tc_dac_done) {`,
57
- ` static const struct dac_channel_cfg __tc_dac_cfg = { .channel_id = ${ch.channel}, .resolution = ${ch.resolution} };`,
58
- ` (void)dac_channel_setup(__tc_dac_dev, &__tc_dac_cfg);`,
59
- ` __tc_dac_done = true;`,
55
+ `{ static bool __tc_dact${o.pin}_done = false;`,
56
+ ` if (!__tc_dact${o.pin}_done) {`,
57
+ ` static const struct dac_channel_cfg __tc_dact${o.pin}_cfg = { .channel_id = ${ch.channel}, .resolution = ${res} };`,
58
+ ` (void)dac_channel_setup(__tc_dac_dev, &__tc_dact${o.pin}_cfg);`,
59
+ ` __tc_dact${o.pin}_done = true;`,
60
60
  ` }`,
61
61
  ` (void)dac_write_value(__tc_dac_dev, ${ch.channel}, ${o.value}); }`,
62
62
  ].join(' '),
@@ -31,21 +31,29 @@ export function fsInitLines() {
31
31
  'FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(__tc_fs_data);',
32
32
  'static struct fs_mount_t __tc_fs_mp_init = {',
33
33
  ' .type = FS_LITTLEFS,',
34
+ ' .mnt_point = "/lfs",',
34
35
  ' .fs_data = &__tc_fs_data,',
35
36
  ' .storage_dev = (void *)FIXED_PARTITION_ID(storage_partition),',
36
- ' .mnt_point = "/lfs",',
37
37
  '};',
38
38
  '',
39
39
  'static void __tc_fs_ensure_mount(void) {',
40
40
  ' if (__tc_fs_mounted) return;',
41
41
  ' __tc_fs_mp = __tc_fs_mp_init;',
42
42
  ' int rc = fs_mount(&__tc_fs_mp);',
43
- ' if (rc == -ENODATA || rc == -EINVAL) {',
44
- ' // Unformatted partition format it for littlefs, then remount.',
43
+ ' if (rc != 0) {',
44
+ ' // Any mount failure with a valid config means the partition is',
45
+ ' // unformatted or corrupt (littlefs reports -EIO on erased flash,',
46
+ ' // not only -ENODATA/-EINVAL) — format it and remount once.',
45
47
  ' (void)fs_mkfs(FS_LITTLEFS, static_cast<uintptr_t>(FIXED_PARTITION_ID(storage_partition)), NULL, 0);',
46
- ' (void)fs_mount(&__tc_fs_mp);',
48
+ ' rc = fs_mount(&__tc_fs_mp);',
49
+ ' }',
50
+ ' if (rc == 0) {',
51
+ ' __tc_fs_mounted = true;',
52
+ ' } else {',
53
+ ' char __tc_fs_err[48];',
54
+ ' (void)snprintf(__tc_fs_err, sizeof(__tc_fs_err), "cuttlefish-fs: mount failed rc=%d", rc);',
55
+ ' __tc_println(__tc_fs_err);',
47
56
  ' }',
48
- ' __tc_fs_mounted = true;',
49
57
  '}',
50
58
  '',
51
59
  '// Join the mount point with the user path (paths are within the FS).',
@@ -66,8 +74,11 @@ export function fsInitLines() {
66
74
  ' struct fs_file_t f;',
67
75
  ' fs_file_t_init(&f);',
68
76
  ' if (fs_open(&f, __tc_fs_path(path), FS_O_READ) == 0) {',
69
- ' (void)fs_read(&f, buf, sizeof(buf) - 1);',
70
- ' buf[sizeof(buf) - 1] = 0;',
77
+ ' // Terminate at the READ length: the buffer is static and shared',
78
+ ' // across reads, so bytes from a longer previous file survive a',
79
+ ' // short read otherwise.',
80
+ ' ssize_t n = fs_read(&f, buf, sizeof(buf) - 1);',
81
+ ' buf[(n > 0) ? n : 0] = 0;',
71
82
  ' (void)fs_close(&f);',
72
83
  ' }',
73
84
  ' return buf;',
@@ -103,9 +114,6 @@ export function fsInitLines() {
103
114
  export function lowerFs(op) {
104
115
  const o = op;
105
116
  switch (op.operation) {
106
- case 'fs.begin':
107
- // No payload (the op carries only its discriminator).
108
- return { code: 'return __tc_fs_begin();' };
109
117
  case 'fs.read_text':
110
118
  return { code: `return __tc_fs_read_text(${o.path});` };
111
119
  case 'fs.write_text':
@@ -2,6 +2,8 @@ import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
2
2
  import type { ZephyrChipDescriptor } from '../chips/types.js';
3
3
  /** The C identifier emitted for a pin's gpio_dt_spec variable. */
4
4
  export declare function dtSpecVarName(dtSpec: string): string;
5
+ /** Map thin-GPIO flag token text to the GPIO_* macro expression. */
6
+ export declare function gpioFlagsToMacros(flags: string): string;
5
7
  /**
6
8
  * Resolve a HAL gpio.* op to Zephyr C++.
7
9
  * Returns `{ code }` for statement ops, `{ expression }` for value-returning ops.
@@ -15,6 +15,7 @@
15
15
  // port) hits this path, so it must return a lowered result, not undefined.
16
16
  // ---------------------------------------------------------------------------
17
17
  import { controllerNodelabelForPin, controllerRawPinForPin } from '../chips/controllers.js';
18
+ import { ZEPHYR_GPIO_FLAGS } from '@typecad/hal';
18
19
  /** The C identifier emitted for a pin's gpio_dt_spec variable. */
19
20
  export function dtSpecVarName(dtSpec) {
20
21
  return `__tc_dt_${dtSpec}`;
@@ -55,6 +56,50 @@ function flagsForMode(mode) {
55
56
  const base = MODE_MAP[mode] ?? 'GPIO_INPUT';
56
57
  return base + dtFlagsForMode(mode);
57
58
  }
59
+ // ── Thin GPIO (hal/gpio-pin.ts) — flag tokens ─────────────────────────────
60
+ //
61
+ // "GPIO.OUTPUT | GPIO.PULL_UP" token text maps name-for-name onto the GPIO_*
62
+ // macros. The name set comes from the GENERATED Zephyr token table (parsed
63
+ // from the pinned tree's headers); unknown tokens are build errors listing
64
+ // the valid spellings — the sensor-catalog discipline.
65
+ const GPIO_FLAG_TOKENS = Object.fromEntries(ZEPHYR_GPIO_FLAGS.map((f) => [`GPIO.${f}`, `GPIO_${f}`]));
66
+ /** Map thin-GPIO flag token text to the GPIO_* macro expression. */
67
+ export function gpioFlagsToMacros(flags) {
68
+ const tokens = String(flags ?? '')
69
+ .split('|')
70
+ .map((t) => t.trim())
71
+ .filter(Boolean);
72
+ if (tokens.length === 0)
73
+ return 'GPIO_INPUT';
74
+ const macros = tokens.map((t) => {
75
+ const m = GPIO_FLAG_TOKENS[t];
76
+ if (!m) {
77
+ throw new Error(`GPIO flag '${t}' is not a known GPIO.* token — valid: ${Object.keys(GPIO_FLAG_TOKENS).join(', ')}.`);
78
+ }
79
+ return m;
80
+ });
81
+ return macros.join(' | ');
82
+ }
83
+ /** Guarded per-pin configure statement for the dt-spec path: construction
84
+ * flags apply exactly once, ahead of the first use. */
85
+ function dtSpecConfigure(varName, dtSpec, flags) {
86
+ return `{ ${dtSpecGuardConfigure(varName, dtSpec, flags)} }`;
87
+ }
88
+ /** The guard body only (no wrapping braces) — for fusing into
89
+ * statement-expressions. */
90
+ function dtSpecGuardConfigure(varName, dtSpec, flags) {
91
+ const done = `__tc_gpio_cfg_${dtSpec.replace(/-/g, '_')}_done`;
92
+ return `static bool ${done} = false; if (!${done}) { gpio_pin_configure_dt(&${varName}, ${gpioFlagsToMacros(flags)}); ${done} = true; }`;
93
+ }
94
+ /** Guarded per-pin configure statement for the raw-controller path. */
95
+ function rawConfigure(controller, rawPin, pin, flags) {
96
+ return `{ ${rawGuardConfigure(controller, rawPin, pin, flags)} }`;
97
+ }
98
+ /** The raw-path guard body only — for fusing into statement-expressions. */
99
+ function rawGuardConfigure(controller, rawPin, pin, flags) {
100
+ const done = `__tc_gpio_cfg_raw${pin}_done`;
101
+ return `static bool ${done} = false; if (!${done}) { gpio_pin_configure(${controller}, ${rawPin}, ${gpioFlagsToMacros(flags)}); ${done} = true; }`;
102
+ }
58
103
  /**
59
104
  * Resolve a HAL gpio.* op to Zephyr C++.
60
105
  * Returns `{ code }` for statement ops, `{ expression }` for value-returning ops.
@@ -73,9 +118,17 @@ function lowerGpioDtSpec(op, dtSpec) {
73
118
  const o = op;
74
119
  const varName = dtSpecVarName(dtSpec);
75
120
  switch (op.operation) {
76
- case 'gpio.set_mode': {
77
- return { code: `gpio_pin_configure_dt(&${varName}, ${flagsForMode(o.mode)});` };
78
- }
121
+ case 'gpio.configure':
122
+ // Thin GPIO: construction flags, applied once per pin (guard).
123
+ return { code: dtSpecConfigure(varName, dtSpec, o.flags) };
124
+ case 'gpio.read_cfg':
125
+ // Thin GPIO get(): the guarded configure FUSED into the read — one
126
+ // statement-expression, correct in any expression position (a method's
127
+ // leading side-effect ops are dropped when the call sits in a pure
128
+ // expression, e.g. an if-condition).
129
+ return {
130
+ expression: `({ ${dtSpecGuardConfigure(varName, dtSpec, o.flags)} gpio_pin_get_dt(&${varName}); })`,
131
+ };
79
132
  case 'gpio.write': {
80
133
  // Literal 0/1 stays as-is; runtime expression coerced to int via ternary.
81
134
  const v = o.value;
@@ -105,8 +158,32 @@ function lowerGpioRaw(op, chip) {
105
158
  const controller = `DEVICE_DT_GET(DT_NODELABEL(${controllerNodelabelForPin(chip, pin)}))`;
106
159
  const rawPin = controllerRawPinForPin(chip, pin);
107
160
  switch (op.operation) {
108
- case 'gpio.set_mode': {
109
- return { code: `gpio_pin_configure(${controller}, ${rawPin}, ${flagsForMode(o.mode)});` };
161
+ case 'gpio.configure':
162
+ // Thin GPIO: construction flags, applied once per pin (guard).
163
+ return { code: rawConfigure(controller, rawPin, pin, o.flags) };
164
+ case 'gpio.read_cfg':
165
+ // Fused guarded configure + raw read (see the dt-spec path above).
166
+ return {
167
+ expression: `({ ${rawGuardConfigure(controller, rawPin, pin, o.flags)} gpio_pin_get_raw(${controller}, ${rawPin}); })`,
168
+ };
169
+ case 'gpio.shift_out':
170
+ case 'gpio.shift_in': {
171
+ // Bit-bang over the raw-controller path (shift pairs are arbitrary
172
+ // pins): configure both once (guarded), then clock the bits. Zephyr
173
+ // verbs only — gpio_pin_set_raw / gpio_pin_get_raw / k_busy_wait.
174
+ const dataCtrl = `DEVICE_DT_GET(DT_NODELABEL(${controllerNodelabelForPin(chip, o.dataPin)}))`;
175
+ const dataRaw = controllerRawPinForPin(chip, o.dataPin);
176
+ const clkCtrl = `DEVICE_DT_GET(DT_NODELABEL(${controllerNodelabelForPin(chip, o.clockPin)}))`;
177
+ const clkRaw = controllerRawPinForPin(chip, o.clockPin);
178
+ const cfg = `{ static bool __tc_shf${o.dataPin}_${o.clockPin}_done = false; if (!__tc_shf${o.dataPin}_${o.clockPin}_done) { gpio_pin_configure(${dataCtrl}, ${dataRaw}, GPIO_INPUT); gpio_pin_configure(${clkCtrl}, ${clkRaw}, GPIO_OUTPUT); __tc_shf${o.dataPin}_${o.clockPin}_done = true; } }`;
179
+ if (op.operation === 'gpio.shift_out') {
180
+ return {
181
+ code: `${cfg} for (int __i = ${(o.msbFirst ?? true) ? '7' : '0'}; ${(o.msbFirst ?? true) ? '__i >= 0' : '__i < 8'}; ${(o.msbFirst ?? true) ? '__i--' : '__i++'}) { gpio_pin_set_raw(${dataCtrl}, ${dataRaw}, ((static_cast<uint8_t>(${o.value})) >> __i) & 1); gpio_pin_set_raw(${clkCtrl}, ${clkRaw}, 1); k_busy_wait(1); gpio_pin_set_raw(${clkCtrl}, ${clkRaw}, 0); }`,
182
+ };
183
+ }
184
+ return {
185
+ expression: `({ ${cfg} uint8_t __b = 0; for (int __i = ${(o.msbFirst ?? true) ? '7' : '0'}; ${(o.msbFirst ?? true) ? '__i >= 0' : '__i < 8'}; ${(o.msbFirst ?? true) ? '__i--' : '__i++'}) { gpio_pin_set_raw(${clkCtrl}, ${clkRaw}, 1); k_busy_wait(1); __b = static_cast<uint8_t>((__b << 1) | (gpio_pin_get_raw(${dataCtrl}, ${dataRaw}) & 1)); gpio_pin_set_raw(${clkCtrl}, ${clkRaw}, 0); } __b; })`,
186
+ };
110
187
  }
111
188
  case 'gpio.write': {
112
189
  const v = o.value;
@@ -62,7 +62,8 @@ export function httpInitLines() {
62
62
  ` const char* body; // request body (user-owned)`,
63
63
  ` int32_t body_len;`,
64
64
  ` bool insecure;`,
65
- ` const char* ca_cert; // PEM, user-owned; nullptr = no pinned CA`,
65
+ ` const uint8_t* ca_der; // DER, user-owned; nullptr = no pinned CA`,
66
+ ` size_t ca_der_len;`,
66
67
  ` const char* hdr_name[__TC_HTTP_MAX_HEADERS];`,
67
68
  ` const char* hdr_value[__TC_HTTP_MAX_HEADERS];`,
68
69
  ` int32_t hdr_count;`,
@@ -199,17 +200,23 @@ export function httpInitLines() {
199
200
  ` // Register the pinned CA as the active sec tag. tls_credential_add`,
200
201
  ` // is idempotent-enough for the single-slot model: a duplicate tag`,
201
202
  ` // is an error we ignore, and the most recently added cert wins.`,
202
- ` if (__tc_http.ca_cert != nullptr) {`,
203
- ` (void)tls_credential_add(__TC_HTTP_SEC_TAG,`,
203
+ ` if (__tc_http.ca_der != nullptr) {`,
204
+ ` int cred_rc = tls_credential_add(__TC_HTTP_SEC_TAG,`,
204
205
  ` TLS_CREDENTIAL_CA_CERTIFICATE,`,
205
- ` __tc_http.ca_cert, strlen(__tc_http.ca_cert) + 1U);`,
206
+ ` __tc_http.ca_der, __tc_http.ca_der_len);`,
206
207
  ` sec_tag_t tags[1] = { __TC_HTTP_SEC_TAG };`,
207
208
  ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_SEC_TAG_LIST, tags, sizeof(tags));`,
209
+ ` (void)cred_rc;`,
208
210
  ` }`,
209
211
  ` // Hostname SNI + verification. insecure() disables verification;`,
210
212
  ` // the default (no caCert, no insecure) leaves verification required`,
211
213
  ` // with no sec tag, so the handshake fails safely until a CA is set.`,
212
- ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_HOSTNAME, __tc_http.host, strlen(__tc_http.host) + 1U);`,
214
+ ` // mbedtls does not match IP literals against SAN entries —`,
215
+ ` // skip the hostname check for IP hosts (chain-only verify).`,
216
+ ` bool host_is_ip = true; for (const char* p3 = __tc_http.host; *p3 != 0; p3++) { if (!((*p3 >= '0' && *p3 <= '9') || *p3 == '.')) { host_is_ip = false; break; } }`,
217
+ ` if (!host_is_ip) {`,
218
+ ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_HOSTNAME, __tc_http.host, strlen(__tc_http.host) + 1U);`,
219
+ ` }`,
213
220
  ` int32_t verify = __tc_http.insecure ? TLS_PEER_VERIFY_NONE : TLS_PEER_VERIFY_REQUIRED;`,
214
221
  ` (void)zsock_setsockopt(sock_tls, SOL_TLS, TLS_PEER_VERIFY, &verify, sizeof(verify));`,
215
222
  ` }`,
@@ -245,7 +252,8 @@ export function httpInitLines() {
245
252
  ` __tc_http.body = nullptr;`,
246
253
  ` __tc_http.body_len = 0;`,
247
254
  ` __tc_http.insecure = false;`,
248
- ` __tc_http.ca_cert = nullptr;`,
255
+ ` __tc_http.ca_der = nullptr;`,
256
+ ` __tc_http.ca_der_len = 0;`,
249
257
  ` __tc_http.hdr_count = 0;`,
250
258
  ` __tc_http.status = 0;`,
251
259
  ` __tc_http.done = false;`,
@@ -293,7 +301,7 @@ export function httpInitLines() {
293
301
  ``,
294
302
  `static inline void __tc_http_set_insecure(void) { __tc_http.insecure = true; }`,
295
303
  ``,
296
- `static inline void __tc_http_set_ca_cert(const char* pem) { __tc_http.ca_cert = pem; }`,
304
+ `static inline void __tc_http_set_ca_cert_der(const uint8_t* der, size_t len) { __tc_http.ca_der = der; __tc_http.ca_der_len = len; }`,
297
305
  ``,
298
306
  `// ── perform (the shared blocking core) ──────────────────────────────────`,
299
307
  `// Parse url → open socket → build http_request → http_client_req → close.`,
@@ -441,12 +449,12 @@ const METHOD_MAP = {
441
449
  export function lowerHttp(op) {
442
450
  const o = op;
443
451
  switch (op.operation) {
444
- case 'http.reset':
445
- return { code: `__tc_http_reset();` };
446
452
  case 'http.begin': {
447
453
  const methodName = String(o.method).replace(/^["']|["']$/g, '').toUpperCase();
448
454
  const method = METHOD_MAP[methodName] ?? 'HTTP_GET';
449
- return { code: `__tc_http_begin(${method}, ${s(o.url)});` };
455
+ // Fresh shim state per request — the reset used to ride the deleted
456
+ // factory op (http.reset); construction now owns it.
457
+ return { code: `__tc_http_reset(); __tc_http_begin(${method}, ${s(o.url)});` };
450
458
  }
451
459
  case 'http.set_header':
452
460
  return { code: `__tc_http_set_header(${s(o.name)}, ${s(o.value)});` };
@@ -455,11 +463,28 @@ export function lowerHttp(op) {
455
463
  case 'http.set_max_body':
456
464
  return { code: `__tc_http_set_max_body(${s(o.bytes)});` };
457
465
  case 'http.set_body':
458
- return { code: `__tc_http_set_body(${s(o.data)}, ${o.json ? 'true' : 'false'});` };
466
+ // '' is the no-body sentinel the op always emits from send(); elide.
467
+ return { code: o.data === '""' || o.data === '' ? '' : `__tc_http_set_body(${s(o.data)}, ${o.json ? 'true' : 'false'});` };
459
468
  case 'http.set_insecure':
460
- return { code: `__tc_http_set_insecure();` };
461
- case 'http.set_ca_cert':
462
- return { code: `__tc_http_set_ca_cert(${s(o.pem)});` };
469
+ return { code: o.insecure ? `__tc_http_set_insecure();` : '' };
470
+ case 'http.set_ca_cert': {
471
+ // This tree's tf-psa-crypto mbedTLS has no PEM parser (MBEDTLS_PEM_C is
472
+ // not settable) — decode the PEM literal to DER here and emit a static
473
+ // byte array. tls_credential_add takes the DER directly (crt_is_pem is
474
+ // false → mbedtls_x509_crt_parse_der_nocopy on the verify path).
475
+ const pem = String(o.pem).replace(/^"|"$/g, '').replace(/\\n/g, '\n');
476
+ if (pem.replace(/\s+/g, '') === '') {
477
+ // No-CA sentinel (Request always emits the op; opts.caCert defaults
478
+ // to '') — elide silently so plain-HTTP requests carry no comment.
479
+ return { code: '' };
480
+ }
481
+ const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\s+/g, '');
482
+ const der = Buffer.from(b64, 'base64');
483
+ if (der.length < 100)
484
+ return { code: `// tc-http: caCert PEM failed to decode` };
485
+ const hex = [...der].map((b) => `0x${b.toString(16).padStart(2, '0')}`).join(', ');
486
+ return { code: `{ static const uint8_t __tc_ca_der[] = { ${hex} }; __tc_http_set_ca_cert_der(__tc_ca_der, sizeof(__tc_ca_der)); }` };
487
+ }
463
488
  case 'http.send':
464
489
  return { expression: `__tc_http_send()` };
465
490
  case 'http.send_start':