@typecad/framework-zephyr 1.0.0-alpha.13 → 1.0.0-alpha.15

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 (191) hide show
  1. package/README.md +22 -35
  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/controllers.d.ts +28 -8
  10. package/dist/chips/controllers.js +49 -12
  11. package/dist/chips/esp32s3.js +49 -0
  12. package/dist/chips/index.d.ts +6 -12
  13. package/dist/chips/index.js +18 -34
  14. package/dist/chips/resolve.d.ts +16 -2
  15. package/dist/chips/resolve.js +298 -15
  16. package/dist/chips/soc/.d.ts +2 -0
  17. package/dist/chips/soc/.js +129 -0
  18. package/dist/chips/soc/esp32.d.ts +2 -0
  19. package/dist/chips/soc/esp32.js +120 -0
  20. package/dist/chips/soc/esp32c3.d.ts +2 -0
  21. package/dist/chips/soc/esp32c3.js +90 -0
  22. package/dist/chips/soc/esp32c6.d.ts +2 -0
  23. package/dist/chips/soc/esp32c6.js +109 -0
  24. package/dist/chips/soc/esp32s3.d.ts +2 -0
  25. package/dist/chips/soc/esp32s3.js +189 -0
  26. package/dist/chips/soc/index.d.ts +2 -0
  27. package/dist/chips/soc/index.js +23 -0
  28. package/dist/chips/soc/nrf52840.d.ts +2 -0
  29. package/dist/chips/soc/nrf52840.js +130 -0
  30. package/dist/chips/soc/rp2040.d.ts +2 -0
  31. package/dist/chips/soc/rp2040.js +141 -0
  32. package/dist/chips/soc/rp2350a.d.ts +2 -0
  33. package/dist/chips/soc/rp2350a.js +145 -0
  34. package/dist/chips/soc/samd21g18a.d.ts +2 -0
  35. package/dist/chips/soc/samd21g18a.js +143 -0
  36. package/dist/chips/soc/stm32f411xe.d.ts +2 -0
  37. package/dist/chips/soc/stm32f411xe.js +251 -0
  38. package/dist/chips/types.d.ts +382 -12
  39. package/dist/chips/xiao-ble.js +33 -0
  40. package/dist/display/profiles.js +1 -1
  41. package/dist/doctor.js +38 -4
  42. package/dist/dt-config/custom-board.d.ts +23 -0
  43. package/dist/dt-config/custom-board.js +227 -0
  44. package/dist/dt-config/kconfig.d.ts +83 -1
  45. package/dist/dt-config/kconfig.js +116 -27
  46. package/dist/dt-config/overlay.js +623 -18
  47. package/dist/framework.manifest.d.ts +20 -30
  48. package/dist/framework.manifest.js +124 -178
  49. package/dist/index.d.ts +4 -1
  50. package/dist/index.js +8 -2
  51. package/dist/lowering/adc.d.ts +26 -5
  52. package/dist/lowering/adc.js +137 -31
  53. package/dist/lowering/ble.js +31 -28
  54. package/dist/lowering/dac.js +9 -9
  55. package/dist/lowering/fs.js +18 -10
  56. package/dist/lowering/gpio.d.ts +2 -0
  57. package/dist/lowering/gpio.js +90 -10
  58. package/dist/lowering/http.js +39 -14
  59. package/dist/lowering/hwtimer.d.ts +4 -0
  60. package/dist/lowering/hwtimer.js +35 -10
  61. package/dist/lowering/i2c.js +46 -54
  62. package/dist/lowering/index.d.ts +5 -5
  63. package/dist/lowering/index.js +15 -16
  64. package/dist/lowering/interrupts.d.ts +22 -5
  65. package/dist/lowering/interrupts.js +104 -22
  66. package/dist/lowering/mqtt.js +12 -6
  67. package/dist/lowering/power.js +0 -4
  68. package/dist/lowering/preferences.js +49 -77
  69. package/dist/lowering/pulse.js +5 -36
  70. package/dist/lowering/pwm.d.ts +32 -3
  71. package/dist/lowering/pwm.js +105 -17
  72. package/dist/lowering/random.js +3 -3
  73. package/dist/lowering/sensor.d.ts +37 -0
  74. package/dist/lowering/sensor.js +119 -0
  75. package/dist/lowering/spi.d.ts +12 -0
  76. package/dist/lowering/spi.js +65 -40
  77. package/dist/lowering/thread.d.ts +14 -0
  78. package/dist/lowering/thread.js +55 -0
  79. package/dist/lowering/timing.js +20 -46
  80. package/dist/lowering/tone.js +18 -5
  81. package/dist/lowering/uart.d.ts +13 -0
  82. package/dist/lowering/uart.js +72 -51
  83. package/dist/lowering/usb.d.ts +34 -0
  84. package/dist/lowering/usb.js +206 -0
  85. package/dist/lowering/wdt.d.ts +1 -1
  86. package/dist/lowering/wdt.js +17 -7
  87. package/dist/lowering/wifi.d.ts +3 -2
  88. package/dist/lowering/wifi.js +120 -111
  89. package/dist/sdk/board-catalog-sync.d.ts +16 -0
  90. package/dist/sdk/board-catalog-sync.js +41 -0
  91. package/dist/sdk/board-data.generated.d.ts +2 -0
  92. package/dist/sdk/board-data.generated.js +4 -0
  93. package/dist/sdk/catalog-walker.d.ts +90 -0
  94. package/dist/sdk/catalog-walker.js +682 -0
  95. package/dist/sdk/dts-reader.d.ts +83 -0
  96. package/dist/sdk/dts-reader.js +596 -0
  97. package/dist/strategy.d.ts +136 -17
  98. package/dist/strategy.js +881 -212
  99. package/dist/toolchain/bossac-touch.d.ts +18 -0
  100. package/dist/toolchain/bossac-touch.js +88 -0
  101. package/dist/toolchain/compat.d.ts +2 -2
  102. package/dist/toolchain/compat.js +8 -3
  103. package/dist/toolchain/debug-config.d.ts +21 -26
  104. package/dist/toolchain/debug-config.js +212 -72
  105. package/dist/toolchain/env-check.d.ts +4 -4
  106. package/dist/toolchain/env-check.js +5 -5
  107. package/dist/toolchain/index.d.ts +66 -1
  108. package/dist/toolchain/index.js +713 -58
  109. package/dist/toolchain/scaffold.d.ts +9 -0
  110. package/dist/toolchain/scaffold.js +84 -7
  111. package/dist/toolchain/west-discover.d.ts +8 -1
  112. package/dist/toolchain/west-discover.js +19 -6
  113. package/dist/toolchain/west-spawn.js +17 -5
  114. package/installer/CHANGELOG.md +473 -0
  115. package/installer/README.md +328 -0
  116. package/installer/environment.yml +46 -0
  117. package/installer/etc/conda/activate.d/zephyr.bat +7 -0
  118. package/installer/etc/conda/activate.d/zephyr.ps1 +12 -0
  119. package/installer/etc/conda/activate.d/zephyr.sh +20 -0
  120. package/installer/etc/conda/deactivate.d/zephyr.bat +6 -0
  121. package/installer/etc/conda/deactivate.d/zephyr.ps1 +6 -0
  122. package/installer/etc/conda/deactivate.d/zephyr.sh +4 -0
  123. package/installer/install.mjs +580 -0
  124. package/installer/install.ps1 +619 -0
  125. package/installer/install.sh +310 -0
  126. package/installer/lib/detect-platform.sh +62 -0
  127. package/installer/lib/fetch-sdk.sh +304 -0
  128. package/installer/lib/init-workspace.sh +153 -0
  129. package/installer/lib/write-activation.sh +76 -0
  130. package/installer/patches/tfm-c-include-separators.patch +19 -0
  131. package/installer/patches/zephyr-tfm-nordic-zephyr-include.patch +24 -0
  132. package/installer/templates/project/.typecad/activate-zephyr.ps1 +31 -0
  133. package/installer/templates/project/.typecad/activate-zephyr.sh +31 -0
  134. package/installer/templates/project/.vscode/settings.json +30 -0
  135. package/installer/templates/project/README.md +58 -0
  136. package/installer/versions.env +110 -0
  137. package/package.json +16 -16
  138. package/src/as-built.ts +206 -0
  139. package/src/boardgen.ts +1428 -0
  140. package/src/chips/controllers.ts +61 -12
  141. package/src/chips/index.ts +35 -54
  142. package/src/chips/resolve.ts +316 -14
  143. package/src/chips/types.ts +388 -13
  144. package/src/display/profiles.ts +1 -1
  145. package/src/doctor.ts +110 -77
  146. package/src/dt-config/custom-board.ts +325 -0
  147. package/src/dt-config/kconfig.ts +177 -19
  148. package/src/dt-config/overlay.ts +1058 -450
  149. package/src/framework.manifest.ts +125 -178
  150. package/src/index.ts +57 -41
  151. package/src/lowering/adc.ts +222 -102
  152. package/src/lowering/ble.ts +32 -28
  153. package/src/lowering/dac.ts +82 -82
  154. package/src/lowering/fs.ts +18 -10
  155. package/src/lowering/gpio.ts +242 -152
  156. package/src/lowering/http.ts +512 -488
  157. package/src/lowering/hwtimer.ts +139 -101
  158. package/src/lowering/i2c.ts +116 -126
  159. package/src/lowering/index.ts +91 -93
  160. package/src/lowering/interrupts.ts +113 -21
  161. package/src/lowering/mqtt.ts +12 -6
  162. package/src/lowering/preferences.ts +327 -354
  163. package/src/lowering/pwm.ts +167 -74
  164. package/src/lowering/random.ts +3 -3
  165. package/src/lowering/sensor.ts +155 -0
  166. package/src/lowering/spi.ts +164 -124
  167. package/src/lowering/thread.ts +63 -0
  168. package/src/lowering/timing.ts +46 -72
  169. package/src/lowering/uart.ts +73 -51
  170. package/src/lowering/usb.ts +221 -0
  171. package/src/lowering/wdt.ts +95 -84
  172. package/src/lowering/wifi.ts +120 -111
  173. package/src/sdk/board-catalog-sync.ts +62 -0
  174. package/src/strategy.ts +888 -229
  175. package/src/toolchain/bossac-touch.ts +112 -0
  176. package/src/toolchain/compat.ts +8 -3
  177. package/src/toolchain/debug-config.ts +666 -522
  178. package/src/toolchain/env-check.ts +9 -9
  179. package/src/toolchain/index.ts +765 -44
  180. package/src/toolchain/scaffold.ts +81 -7
  181. package/src/toolchain/west-discover.ts +334 -316
  182. package/src/toolchain/west-spawn.ts +17 -5
  183. package/src/async/timer-polyfill.ts +0 -107
  184. package/src/chips/esp32.ts +0 -74
  185. package/src/chips/esp32s3.ts +0 -57
  186. package/src/chips/xiao-ble.ts +0 -70
  187. package/src/lowering/power.ts +0 -70
  188. package/src/lowering/pulse.ts +0 -93
  189. package/src/lowering/tone.ts +0 -60
  190. package/src/lowering/worker-backing.ts +0 -94
  191. package/src/lowering/worker.ts +0 -17
@@ -1,14 +1,35 @@
1
1
  import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
2
2
  import type { ZephyrChipDescriptor } from '../chips/types.js';
3
- /** Resolve the SAADC channel index for a HAL pin number; -1 if unmapped. */
3
+ /**
4
+ * Resolve the SAADC channel index for an adc.read argument; -1 if unmapped.
5
+ *
6
+ * The numeric argument serves two HAL forms: `InputPin.readAnalog()` passes
7
+ * a GPIO number, while the Arduino-compat `ADC.read(channel)` passes an ADC
8
+ * channel number. Pin-first resolution keeps readAnalog behavior unchanged;
9
+ * the channel fallback makes `ADC.read(n)` resolve on targets where channel
10
+ * n does not alias GPIO n (e.g. the XIAO's AIN2 = P0.28). Where a number
11
+ * matches both a mapped pin and another channel's index, the pin wins.
12
+ */
4
13
  export declare function adcChannelForPin(chip: ZephyrChipDescriptor, pin: number): number;
14
+ /**
15
+ * Resolve the owning ADC controller's node label for an adc.read argument.
16
+ * Defaults to the descriptor's primary (`chip.adc.nodeLabel`) — channels on
17
+ * additional controllers (STM32 adc3-only routes, ESP32 adc2 pads) carry
18
+ * their controller explicitly, and their device handle is
19
+ * `__tc_adc_<label>_dev` (the primary keeps the unsuffixed `__tc_adc_dev`
20
+ * so single-controller output is unchanged).
21
+ */
22
+ export declare function adcControllerForPin(chip: ZephyrChipDescriptor, pin: number): string;
5
23
  /**
6
24
  * Emit the per-channel ADC setup state. One block per channel in the chip
7
- * descriptor, each guarded by a `static bool __tc_adc<N>_ready` so the first
8
- * read configures it and subsequent reads skip. Called from shimLines when
9
- * the program uses ADC.
25
+ * descriptor that the PROGRAM ACTUALLY READS (`usedPins`) an unread
26
+ * channel's `static` setup function would trip -Wunused-function in the
27
+ * single generated TU. When `usedPins` is omitted (probe paths with no
28
+ * program), every descriptor channel is emitted. Each block is guarded by a
29
+ * `static bool __tc_adc<N>_ready` so the first read configures it and
30
+ * subsequent reads skip. Called from shimLines when the program uses ADC.
10
31
  */
11
- export declare function adcInitLines(chip: ZephyrChipDescriptor): string[];
32
+ export declare function adcInitLines(chip: ZephyrChipDescriptor, usedPins?: ReadonlySet<number>, overrideDevices?: ReadonlySet<string>): string[];
12
33
  /**
13
34
  * Resolve a HAL adc.* op to Zephyr C++.
14
35
  * Returns `{ code }` for statement ops, `{ expression }` for value-returning ops.
@@ -11,26 +11,115 @@
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
21
- * descriptor, each guarded by a `static bool __tc_adc<N>_ready` so the first
22
- * read configures it and subsequent reads skip. Called from shimLines when
23
- * the program uses ADC.
77
+ * descriptor that the PROGRAM ACTUALLY READS (`usedPins`) an unread
78
+ * channel's `static` setup function would trip -Wunused-function in the
79
+ * single generated TU. When `usedPins` is omitted (probe paths with no
80
+ * program), every descriptor channel is emitted. Each block is guarded by a
81
+ * `static bool __tc_adc<N>_ready` so the first read configures it and
82
+ * subsequent reads skip. Called from shimLines when the program uses ADC.
24
83
  */
25
- export function adcInitLines(chip) {
26
- 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';
27
86
  const res = chip.adc?.resolution ?? 12;
28
87
  const vref = chip.adc?.vrefMv ?? 3000;
88
+ // Gain/reference are SoC-specific: the nRF SAADC scheme (gain 1/4 against
89
+ // the 0.6V internal ref, vref-mv 3000 = VDD) is the default; the STM32
90
+ // driver requires exactly ADC_GAIN_1 + ADC_REF_INTERNAL (Zephyr maps
91
+ // "internal" to the VREF+ pad) with vref-mv = VDDA. The descriptor carries
92
+ // the SoC's pair so the emitted channel setup validates in the driver.
93
+ const gain = chip.adc?.gain ?? 'ADC_GAIN_1_4';
94
+ const reference = chip.adc?.reference ?? 'ADC_REF_INTERNAL';
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))));
29
103
  const lines = ['// CUTTLEFISH_ADC_BEGIN'];
30
- lines.push(`static const struct device* __tc_adc_dev = ${dev};`);
31
- for (const c of chip.adc?.channels ?? []) {
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
+ }
118
+ for (const c of channels) {
32
119
  const n = c.channel;
33
- 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 = ADC_GAIN_1_4,`, ` .reference = ADC_REF_INTERNAL,`, ` .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;`, `}`);
34
123
  }
35
124
  lines.push(`#define __TC_ADC_VREF_MV ${vref}`);
36
125
  lines.push(`#define __TC_ADC_RESOLUTION ${res}`);
@@ -45,30 +134,47 @@ export function lowerAdc(op, chip) {
45
134
  const o = op;
46
135
  const res = chip.adc?.resolution ?? 12;
47
136
  const vref = chip.adc?.vrefMv ?? 3000;
137
+ const gain = chip.adc?.gain ?? 'ADC_GAIN_1_4';
138
+ const reference = chip.adc?.reference ?? 'ADC_REF_INTERNAL';
48
139
  switch (op.operation) {
49
- case 'adc.read': {
50
- const ch = adcChannelForPin(chip, o.pin);
51
- // GCC statement-expression: configures-on-first-call, reads, returns raw.
52
- return {
53
- 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; })`,
54
- };
55
- }
56
- case 'adc.read_voltage': {
57
- const ch = adcChannelForPin(chip, o.pin);
58
- // Read raw, convert to millivolts via adc_raw_to_millivolts (gain 1/4,
59
- // internal ref). 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
+ }
60
174
  return {
61
- 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}, ADC_GAIN_1_4, ${res}, &__v); __v; })`,
175
+ expression: `({ ${marker}${setup} ${read} int32_t __v = __b; adc_raw_to_millivolts(${vref}, ${g}, ${res}, &__v); __v; })`,
62
176
  };
63
177
  }
64
- case 'adc.get_resolution':
65
- return { expression: String(res) };
66
- case 'adc.set_reference':
67
- // Zephyr configures the reference at channel-setup time; runtime switching
68
- // would require re-setup. Record the intent as a no-op statement.
69
- return { code: `/* adc.set_reference(${o.reference}): configured at channel setup (ADC_REF_INTERNAL) */` };
70
- case 'adc.get_reference':
71
- return { expression: `0 /* DEFAULT (ADC_REF_INTERNAL) */` };
72
178
  default:
73
179
  throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
74
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.
@@ -14,7 +14,8 @@
14
14
  // The manifest validator's probe (which sends {operation, pin:0} with no
15
15
  // port) hits this path, so it must return a lowered result, not undefined.
16
16
  // ---------------------------------------------------------------------------
17
- import { controllerNodelabelForPin } from '../chips/controllers.js';
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;
@@ -99,19 +152,46 @@ function lowerGpioRaw(op, chip) {
99
152
  const o = op;
100
153
  const pin = o.pin;
101
154
  // Resolve the owning controller by pin range (ESP32-S3 splits GPIO across
102
- // gpio0/gpio1). For single-controller SoCs this is just chip.gpioController.
155
+ // gpio0/gpio1; STM32 across gpioa/gpiob/gpioc). For single-controller SoCs
156
+ // this is just chip.gpioController. The raw API takes the PORT-RELATIVE
157
+ // index (STM32 gpiob is 0-15), not the global HAL pin number.
103
158
  const controller = `DEVICE_DT_GET(DT_NODELABEL(${controllerNodelabelForPin(chip, pin)}))`;
159
+ const rawPin = controllerRawPinForPin(chip, pin);
104
160
  switch (op.operation) {
105
- case 'gpio.set_mode': {
106
- return { code: `gpio_pin_configure(${controller}, ${pin}, ${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
+ };
107
187
  }
108
188
  case 'gpio.write': {
109
189
  const v = o.value;
110
190
  const rhs = typeof v === 'string' ? `((${v}) ? 1 : 0)` : v ? 1 : 0;
111
- return { code: `gpio_pin_set_raw(${controller}, ${pin}, ${rhs});` };
191
+ return { code: `gpio_pin_set_raw(${controller}, ${rawPin}, ${rhs});` };
112
192
  }
113
193
  case 'gpio.read':
114
- return { expression: `gpio_pin_get_raw(${controller}, ${pin})` };
194
+ return { expression: `gpio_pin_get_raw(${controller}, ${rawPin})` };
115
195
  case 'gpio.toggle':
116
196
  // Native atomic toggle — never read-modify-write. gpio_pin_get_raw on
117
197
  // a direction-only output reads the input latch, which is undefined on
@@ -119,7 +199,7 @@ function lowerGpioRaw(op, chip) {
119
199
  // gpio_pin_toggle is the driver-level atomic toggle, and for pins
120
200
  // configured without GPIO_ACTIVE_LOW the logical level equals the
121
201
  // physical one, so it matches the get_raw/set_raw used elsewhere.
122
- return { code: `gpio_pin_toggle(${controller}, ${pin});` };
202
+ return { code: `gpio_pin_toggle(${controller}, ${rawPin});` };
123
203
  default:
124
204
  throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
125
205
  `Open an issue or use rawCpp() to emit it manually.`);