@typecad/framework-zephyr 1.0.0-alpha.11 → 1.0.0-alpha.12

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.
@@ -10,6 +10,7 @@
10
10
  // that brings external capabilities into a build. (Arduino does it by parsing
11
11
  // library headers into .d.ts; Zephyr does it by enabling DT nodes + Kconfig.)
12
12
  // ---------------------------------------------------------------------------
13
+ import { PANEL_CONTROLLER_DEFAULTS, panelControllerFor } from '../display/profiles.js';
13
14
  export function generateOverlay(chip, usage, display, wiring, touch) {
14
15
  const lines = [
15
16
  '/* Auto-generated by @typecad/framework-zephyr from cuttlefish.config.ts. */',
@@ -53,14 +54,18 @@ export function generateOverlay(chip, usage, display, wiring, touch) {
53
54
  // controller with the panel's compatible string + dimensions. The pin
54
55
  // wiring (cs/dc/rst) uses ESP32 GPIO defaults from the demo config; a
55
56
  // real board overlay would carry its own binding.
56
- emitDisplayNode(lines, display, wiring);
57
+ // An XPT2046 on the same bus needs its CS as the second cs-gpios entry,
58
+ // so thread it into the display block (DT assignment replaces the whole
59
+ // property — both entries must be written together).
60
+ emitDisplayNode(lines, display, wiring, touch?.controller === 'xpt2046' ? (touch?.cs ?? DEFAULT_XPT2046_CS) : undefined);
57
61
  }
58
- // FT6336U touch on I2C defined when the program uses touch (the UI touch
59
- // adapter references DT_NODELABEL(ft6336u)). Same rationale: the bare devkit
60
- // has no such node. Gated on usesTouch (not usesI2c/chip.i2c) so it emits
61
- // even when the chip descriptor doesn't declare I2C controllers (ESP32).
62
+ // Touch — FT6336U capacitive on I2C (references DT_NODELABEL(ft6336u)) or
63
+ // XPT2046 resistive on the display's SPI bus (references
64
+ // DT_NODELABEL(xpt2046)). Same rationale as the display node: the bare
65
+ // devkit has no such node. Gated on usesTouch (not usesI2c/chip.i2c) so it
66
+ // emits even when the chip descriptor doesn't declare bus controllers.
62
67
  if (usage.usesTouch) {
63
- emitTouchNode(lines, touch);
68
+ emitTouchNode(lines, touch, display);
64
69
  }
65
70
  // Preferences (ZMS settings backend): point the settings subsystem at the
66
71
  // board's storage_partition. The backend looks for /chosen
@@ -91,11 +96,21 @@ export function generateOverlay(chip, usage, display, wiring, touch) {
91
96
  return lines.join('\n');
92
97
  }
93
98
  /**
94
- * Emit a display DT node definition. The node is attached to spi2 (the ESP32's
95
- * first user SPI controller) via a MIPI DBI SPI bridge. Pin wiring comes from
96
- * the display config (cs/dc/rst); defaults match the demo-st wiring if absent.
99
+ * Emit a display DT node definition. The node is attached to the profile's SPI
100
+ * controller (default spi2, the ESP32's first user SPI controller) via a MIPI
101
+ * DBI SPI bridge. Pin wiring comes from the display config (cs/dc/rst);
102
+ * defaults match the demo-st wiring if absent. The compatible string + node
103
+ * props come from the profile's panel controller (st7796s carries the required
104
+ * pgc/ngc gamma + madctl; ili9341's binding defaults everything else).
105
+ *
106
+ * spiTouchCs: when an XPT2046 SPI touch controller shares the bus, its CS is
107
+ * appended as the second cs-gpios entry (the touch node uses reg = <1>) — DT
108
+ * property assignment replaces, so both entries must be written together.
97
109
  */
98
- function emitDisplayNode(lines, display, wiring) {
110
+ function emitDisplayNode(lines, display, wiring, spiTouchCs) {
111
+ const bus = display.busLabel ?? 'spi2';
112
+ const controller = panelControllerFor(display);
113
+ const compatible = display.dtCompatible ?? PANEL_CONTROLLER_DEFAULTS[controller].dtCompatible;
99
114
  const dc = wiring?.dc ?? 17;
100
115
  const rst = wiring?.rst ?? 16;
101
116
  const cs = wiring?.cs ?? 5;
@@ -116,7 +131,9 @@ function emitDisplayNode(lines, display, wiring) {
116
131
  // macros are used instead of the named SPIM2_*_GPIOxx tokens because the
117
132
  // bindings header omits GPIOs 22-25 from those lists.
118
133
  if (sck !== undefined && mosi !== undefined) {
119
- lines.push('&spim2_default {');
134
+ // 'spi2' → pinctrl group 'spim2_default' (ESP32 SPI-master naming).
135
+ const pinctrlGroup = bus.replace(/^spi(\d)$/, 'spim$1') + '_default';
136
+ lines.push(`&${pinctrlGroup} {`);
120
137
  lines.push(' group1 {');
121
138
  lines.push(` pinmux = <ESP32_PINMUX(${miso ?? 19}, ESP_FSPIQ_IN, ESP_NOSIG)>,`);
122
139
  lines.push(` <ESP32_PINMUX(${sck}, ESP_NOSIG, ESP_FSPICLK_OUT)>,`);
@@ -133,9 +150,9 @@ function emitDisplayNode(lines, display, wiring) {
133
150
  lines.push(' status = "okay";');
134
151
  lines.push('};');
135
152
  lines.push('');
136
- lines.push('&spi2 {');
153
+ lines.push(`&${bus} {`);
137
154
  lines.push(' status = "okay";');
138
- lines.push(` cs-gpios = <&${gpioController(cs)} ${cs} GPIO_ACTIVE_LOW>;`);
155
+ lines.push(` cs-gpios = <&${gpioController(cs)} ${cs} GPIO_ACTIVE_LOW>${spiTouchCs !== undefined ? `, <&${gpioController(spiTouchCs)} ${spiTouchCs} GPIO_ACTIVE_LOW>` : ''};`);
139
156
  // Enable GDMA for the SPI2 host. The ESP32 SPI driver uses DMA only when
140
157
  // dma-enabled is set AND dmas wires tx/rx channels to the GDMA controller;
141
158
  // without it, transfers run PIO through the 64-byte FIFO (~4MHz effective at
@@ -151,25 +168,37 @@ function emitDisplayNode(lines, display, wiring) {
151
168
  lines.push('/ {');
152
169
  lines.push(' mipi_dbi: mipi-dbi {');
153
170
  lines.push(' compatible = "zephyr,mipi-dbi-spi";');
154
- lines.push(' spi-dev = <&spi2>;');
171
+ lines.push(` spi-dev = <&${bus}>;`);
155
172
  lines.push(` dc-gpios = <&${gpioController(dc)} ${dc} GPIO_ACTIVE_HIGH>;`);
156
173
  lines.push(` reset-gpios = <&${gpioController(rst)} ${rst} GPIO_ACTIVE_LOW>;`);
157
174
  lines.push(' write-only;');
158
175
  lines.push(' #address-cells = <1>;');
159
176
  lines.push(' #size-cells = <0>;');
160
177
  lines.push(` ${display.dtLabel}: display@0 {`);
161
- lines.push(' compatible = "sitronix,st7796s";');
178
+ lines.push(` compatible = "${compatible}";`);
162
179
  lines.push(' reg = <0>;');
163
180
  lines.push(` mipi-max-frequency = <${freq}>;`);
164
181
  lines.push(' mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";');
182
+ // Required by the lcd-controller binding (Zephyr 4.x): 0 = RGB565,
183
+ // matching upstream ILI9341 boards (esp_wrover_kit) and the C++
184
+ // runtime, which drives these SPI TFTs as RGB565.
185
+ lines.push(' pixel-format = <0>;');
165
186
  lines.push(` width = <${nativeW}>;`);
166
187
  lines.push(` height = <${nativeH}>;`);
167
- // MADCTL: rotation 1 (landscape, MV=1) + BGR bit, matching the adapter's
168
- // direct-drive init (0x28). The DT copy keeps the stock driver's init
169
- // consistent if it is ever exercised.
170
- lines.push(' madctl = <0x28>;');
171
- lines.push(' pgc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
172
- lines.push(' ngc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
188
+ if (controller === 'st7796s') {
189
+ // MADCTL: rotation 1 (landscape, MV=1) + BGR bit, matching the adapter's
190
+ // direct-drive init (0x28). The DT copy keeps the stock driver's init
191
+ // consistent if it is ever exercised.
192
+ lines.push(' madctl = <0x28>;');
193
+ lines.push(' pgc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
194
+ lines.push(' ngc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
195
+ }
196
+ else {
197
+ // ILI9341: the ilitek,ili9341 binding carries defaults for every register
198
+ // (gamma, power, porch) and expresses orientation via `rotation` (degrees)
199
+ // instead of a raw MADCTL — no panel-specific props are required.
200
+ lines.push(` rotation = <${display.rotation ?? 0}>;`);
201
+ }
173
202
  lines.push(' };');
174
203
  lines.push(' };');
175
204
  lines.push('};');
@@ -195,13 +224,33 @@ function emitDisplayNode(lines, display, wiring) {
195
224
  lines.push('');
196
225
  }
197
226
  }
227
+ /** Default XPT2046 CS/IRQ pins (ESP32-S3 GPIOs clear of the demo-st display
228
+ * wiring: 5/17/16/15 and the remuxed SPI pins). Config values override. */
229
+ const DEFAULT_XPT2046_CS = 6;
230
+ const DEFAULT_XPT2046_IRQ = 7;
198
231
  /**
199
- * Emit an FT6336U touch DT node on the first I2C controller. The node address
200
- * is the FT6336U default (0x38). The UI touch adapter references
201
- * DT_NODELABEL(ft6336u). Pin wiring (irq/resetPin/sda/scl) comes from the
202
- * touch config; defaults match the demo-st wiring if absent.
232
+ * Emit the touch DT node for the configured controller.
233
+ *
234
+ * FT6336U (capacitive, I2C): node on i2c0 at the FT6336U default address
235
+ * (0x38); the C++ touch adapter reads it via i2c_write_read_dt.
236
+ *
237
+ * XPT2046 (resistive, SPI): node on the display's SPI bus as CS index 1. The
238
+ * in-tree xptek,xpt2046 binding (drivers/input) is register-matched for the
239
+ * raw SPI access — CONFIG_INPUT stays off, so the in-tree input driver does
240
+ * not build and the adapter owns the chip (same pattern as FT6336U reusing
241
+ * the ft5336 binding). The binding requires int-gpios, touchscreen-size-*,
242
+ * and min/max calibration props, so defaults are filled for anything the
243
+ * config omits.
203
244
  */
204
- function emitTouchNode(lines, touch) {
245
+ function emitTouchNode(lines, touch, display) {
246
+ if (touch?.controller === 'xpt2046') {
247
+ emitXpt2046Node(lines, touch, display);
248
+ return;
249
+ }
250
+ emitFt6336uNode(lines, touch);
251
+ }
252
+ /** FT6336U capacitive touch node on the first I2C controller. */
253
+ function emitFt6336uNode(lines, touch) {
205
254
  const irq = touch?.irq ?? 15;
206
255
  const resetPin = touch?.resetPin;
207
256
  const sda = touch?.sda;
@@ -249,3 +298,47 @@ function emitTouchNode(lines, touch) {
249
298
  lines.push('};');
250
299
  lines.push('');
251
300
  }
301
+ /** XPT2046 resistive touch node on the display's SPI bus (CS index 1). */
302
+ function emitXpt2046Node(lines, touch, display) {
303
+ const bus = display?.busLabel ?? 'spi2';
304
+ const cs = touch.cs ?? DEFAULT_XPT2046_CS;
305
+ const irq = touch.irq ?? DEFAULT_XPT2046_IRQ;
306
+ const cal = touch.calibration;
307
+ // ESP32-S3 GPIOs 0-31 are on gpio0, 32-48 on gpio1.
308
+ const gpioController = (pin) => pin <= 31 ? 'gpio0' : 'gpio1';
309
+ const zThreshold = touch.minPressure ?? 400;
310
+ // touchscreen-size-* describe the panel the touch layer sits on (the display
311
+ // profile's effective size); a touch-only build falls back to the 12-bit
312
+ // full-scale range so the binding's required props still resolve.
313
+ const sizeX = display?.width ?? cal?.xMax ?? 320;
314
+ const sizeY = display?.height ?? cal?.yMax ?? 240;
315
+ // The display block already wrote cs-gpios with both entries (its CS at
316
+ // index 0, the touch CS at index 1). When there is no display block, enable
317
+ // the bus here with the touch CS as the only entry.
318
+ if (!display) {
319
+ lines.push(`&${bus} {`);
320
+ lines.push(' status = "okay";');
321
+ lines.push(` cs-gpios = <&${gpioController(cs)} ${cs} GPIO_ACTIVE_LOW>;`);
322
+ lines.push('};');
323
+ lines.push('');
324
+ }
325
+ lines.push(`&${bus} {`);
326
+ lines.push(' xpt2046: xpt2046@1 {');
327
+ lines.push(' compatible = "xptek,xpt2046";');
328
+ lines.push(' reg = <1>;');
329
+ // The XPT2046 datasheet max SPI clock is 2.5MHz — the panel bus may run at
330
+ // 80MHz, but this node's spi-max-frequency gates only its own transactions
331
+ // (the adapter's SPI_DT_SPEC picks it up).
332
+ lines.push(' spi-max-frequency = <2500000>;');
333
+ lines.push(` int-gpios = <&${gpioController(irq)} ${irq} GPIO_ACTIVE_LOW>;`);
334
+ lines.push(` touchscreen-size-x = <${sizeX}>;`);
335
+ lines.push(` touchscreen-size-y = <${sizeY}>;`);
336
+ lines.push(` min-x = <${cal?.xMin ?? 0}>;`);
337
+ lines.push(` max-x = <${cal?.xMax ?? 4095}>;`);
338
+ lines.push(` min-y = <${cal?.yMin ?? 0}>;`);
339
+ lines.push(` max-y = <${cal?.yMax ?? 4095}>;`);
340
+ lines.push(` z-threshold = <${zThreshold}>;`);
341
+ lines.push(' };');
342
+ lines.push('};');
343
+ lines.push('');
344
+ }
@@ -1,40 +1,40 @@
1
1
  declare const _default: {
2
+ hal: {
3
+ [x: string]: {
4
+ supported: boolean;
5
+ ops: Record<string, "stub" | "polyfill" | "supported" | "unsupported" | "probe-inconclusive">;
6
+ partialCoverage: boolean;
7
+ unsupportedReason?: string | undefined;
8
+ };
9
+ raw?: unknown;
10
+ } & {
11
+ [k: string]: {
12
+ supported: boolean;
13
+ ops: Record<string, "stub" | "polyfill" | "supported" | "unsupported" | "probe-inconclusive">;
14
+ partialCoverage: boolean;
15
+ unsupportedReason?: string | undefined;
16
+ };
17
+ };
18
+ packageName: string;
2
19
  schemaVersion: 1;
3
20
  frameworkId: string;
4
- packageName: string;
5
21
  canonical: boolean;
6
22
  displayName: string;
7
23
  description: string;
8
24
  implementationMode: "from-scratch" | "extends-canonical" | "extends-other";
9
25
  entrypoint: {
26
+ sourceExtension: "cpp" | "ino" | "cc" | "h";
10
27
  entrypointFunctionName: string;
11
28
  requiresLoopFunction: boolean;
12
- sourceExtension: "ino" | "cc" | "cpp" | "h";
13
29
  generateHeaderFile: boolean;
14
30
  overrideBaseName?: string | undefined;
15
31
  outputSubdirectory?: string | undefined;
16
32
  customBridgeShim?: string | undefined;
17
33
  };
18
34
  profile: {
19
- targets: string[];
20
35
  forcedIncludes: string[];
21
36
  symbolAliases: Record<string, string>;
22
- };
23
- hal: {
24
- [x: string]: {
25
- supported: boolean;
26
- ops: Record<string, "supported" | "stub" | "unsupported" | "probe-inconclusive" | "polyfill">;
27
- partialCoverage: boolean;
28
- unsupportedReason?: string | undefined;
29
- };
30
- raw?: unknown;
31
- } & {
32
- [k: string]: {
33
- supported: boolean;
34
- ops: Record<string, "supported" | "stub" | "unsupported" | "probe-inconclusive" | "polyfill">;
35
- partialCoverage: boolean;
36
- unsupportedReason?: string | undefined;
37
- };
37
+ targets: string[];
38
38
  };
39
39
  polyfills: {
40
40
  emitted: {
@@ -54,16 +54,17 @@ declare const _default: {
54
54
  compile: boolean;
55
55
  upload: boolean;
56
56
  monitor: boolean;
57
+ debug?: boolean | undefined;
57
58
  };
58
59
  reexportedFrom?: string | undefined;
59
60
  };
60
61
  typeEmission: {
61
- normalizeCppType: boolean;
62
- mathHeader: "none" | "<math.h>" | "<Arduino.h>";
62
+ needsIostream: boolean;
63
63
  needsStdString: boolean;
64
64
  needsStdVector: boolean;
65
- needsIostream: boolean;
66
65
  needsStdFunction: boolean;
66
+ mathHeader: "none" | "<math.h>" | "<Arduino.h>";
67
+ normalizeCppType: boolean;
67
68
  stdlibSupport: {
68
69
  hasVector: boolean;
69
70
  hasString: boolean;
@@ -79,6 +80,12 @@ declare const _default: {
79
80
  hardwareTestGroups: string[];
80
81
  halResolutionTests: string[];
81
82
  };
83
+ doctor?: {
84
+ available: boolean;
85
+ } | undefined;
86
+ licenses?: {
87
+ available: boolean;
88
+ } | undefined;
82
89
  basedOn?: string | undefined;
83
90
  inheritsStrategyId?: string | undefined;
84
91
  libraryResolution?: {
@@ -88,12 +95,6 @@ declare const _default: {
88
95
  tryGenerateLibDecl: boolean;
89
96
  reexportedFrom?: string | undefined;
90
97
  } | undefined;
91
- doctor?: {
92
- available: boolean;
93
- } | undefined;
94
- licenses?: {
95
- available: boolean;
96
- } | undefined;
97
98
  compat?: {
98
99
  zephyr?: string | undefined;
99
100
  } | undefined;
@@ -280,8 +280,13 @@ export default defineFrameworkManifest({
280
280
  },
281
281
  display: {
282
282
  supported: true,
283
- partialCoverage: false,
284
- unsupportedReason: undefined,
283
+ partialCoverage: true,
284
+ // Partial: mono profiles (ssd1306-zephyr) drive display.* ops via the
285
+ // direct GFX runtime only — no CuttlefishGFX UI rendering path. The
286
+ // ILI9341 UI adapter shares the ST7796S direct-drive transport with a
287
+ // per-controller init table (16-bit RGB565 wire format); hardware-tuned
288
+ // on ST7796S only. E-ink panels are out of scope at this time.
289
+ unsupportedReason: 'Mono panels (ssd1306) are direct-op only (no UI rendering); ili9341 UI path is ported but not yet hardware-verified; e-ink is out of scope at this time.',
285
290
  drivers: ['ili9341-zephyr', 'st7796-zephyr', 'ssd1306-zephyr'],
286
291
  colorFormat: 'rgb565',
287
292
  ops: {
@@ -484,6 +489,7 @@ export default defineFrameworkManifest({
484
489
  polyfills: {
485
490
  emitted: [
486
491
  { id: 'cuttlefish_halt', domain: 'standard', notes: 'Mapped to a k_msleep halt loop (exceptions disabled)' },
492
+ { id: 'wiring_compat', domain: 'standard', notes: 'HIGH/LOW/digitalRead/etc. macros routing Wiring tokens (referenced unconditionally by the UI runtime header) to the __tc_gpio_* helpers' },
487
493
  { id: 'string_methods', domain: 'embedded', notes: 'STL-free __tc_* string helpers (const char*, inline ASCII case conv, <cstring> only)' },
488
494
  { id: 'static_array', domain: 'embedded', notes: 'STL-free __tc_StaticArray<T,N> wrapper for no-<vector> mutated/struct array literals' },
489
495
  { id: 'timer_methods', domain: 'embedded', notes: 'k_timer + k_work pool (system workqueue); callbacks run in thread context' },
@@ -493,7 +499,7 @@ export default defineFrameworkManifest({
493
499
  },
494
500
  toolchain: {
495
501
  backend: 'west',
496
- operations: { prepare: true, compile: true, upload: true, monitor: true },
502
+ operations: { prepare: true, compile: true, upload: true, monitor: true, debug: true },
497
503
  },
498
504
  libraryResolution: {
499
505
  isFrameworkLibraryImport: false,
@@ -388,7 +388,9 @@ export function lowerBle(op) {
388
388
  case 'ble.on_read':
389
389
  // Store the typed read handler as void*; __tc_ble_attr_read casts it back
390
390
  // to the right signature based on the char's type field. reinterpret_cast
391
- // (not a C-style cast) keeps this AUTOSAR-compliant under --autosar=strict.
391
+ // (not a C-style cast) avoids M5-0-7, but M5-0-10 still flags it — the
392
+ // whole type-erased table is covered by a knownPatterns deviation on
393
+ // that rule (see rules.ts, "BLE type-erased callback table").
392
394
  return { code: `__tc_ble.on_read[__tc_ble.current_char] = reinterpret_cast<void*>(${s(o.handler)});` };
393
395
  case 'ble.on_write':
394
396
  return { code: `__tc_ble.on_write[__tc_ble.current_char] = (${s(o.handler)});` };
@@ -78,6 +78,10 @@ export declare class ZephyrStrategy implements PlatformStrategy {
78
78
  passthroughMacroNames(): ReadonlySet<string>;
79
79
  apiReservedEnumNames(): ReadonlySet<string>;
80
80
  apiReservedEnumGuard(): string;
81
+ isrUnsafeOperations(): Map<string, {
82
+ reason: string;
83
+ severity: 'warning' | 'info';
84
+ }>;
81
85
  ambientTypeDeclarations(): string[];
82
86
  needsIostream(): boolean;
83
87
  needsStdString(): boolean;
package/dist/strategy.js CHANGED
@@ -150,10 +150,11 @@ export class ZephyrStrategy {
150
150
  // program-analysis usesStdString detector doesn't see types generated by
151
151
  // the BLE lowering layer — so without forcing <string> here, any BLE server
152
152
  // with a Utf8 characteristic fails to compile ('std::string does not name a
153
- // type'). Uses <string>, not <string.h>: the latter is the C flat-string
154
- // header (already included for the shim's strncpy/strcmp).
153
+ // type'). <cstdlib>/<cstring> (not <stdlib.h>/<string.h>) back the shim's
154
+ // strtol/strcmp/strncpy the same AUTOSAR-compliant spelling the HTTP,
155
+ // MQTT, and Preferences paths below already use.
155
156
  if (uses('usesBle'))
156
- inc.push('<stdlib.h>', '<string.h>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
157
+ inc.push('<cstdlib>', '<cstring>', '<string>', '<zephyr/bluetooth/bluetooth.h>', '<zephyr/bluetooth/conn.h>', '<zephyr/bluetooth/gatt.h>', '<zephyr/bluetooth/uuid.h>');
157
158
  // Display: the analyzer's usesDisplay flag (set by display.* hal-ops) drives
158
159
  // this include. When ctx.analysis is absent (capability query), uses()
159
160
  // defaults to true so a real build never strips it.
@@ -377,20 +378,27 @@ export class ZephyrStrategy {
377
378
  // separate translation unit when generateHeaderFile() splits them into the
378
379
  // header.
379
380
  lines.push('extern void setup(void);', 'extern void loop(void);', '', 'int main(void) {', ' setup();', ' for (;;) {', ' loop();', ' k_msleep(1);', ' }', ' return 0;', '}');
380
- // Safety shims: when the program uses @typecad/safety, provide __tc_gpio_read
381
- // / __tc_gpio_write backed by the raw controller (a best-effort read that
382
- // does not depend on a pin having a DT spec). __tc_delay_us uses k_busy_wait.
381
+ // GPIO read shim: the wiring_compat polyfill routes the UI runtime
382
+ // header's unconditional digitalRead() poll (init-press-input.ts) to
383
+ // __tc_gpio_read, so the definition must NOT be gated on @typecad/safety.
384
+ // The signature is `int` to match wiring_compat's forward declaration —
385
+ // a uint32_t definition alongside it would leave the declared int
386
+ // overload undefined (int wins overload resolution for small integer
387
+ // arguments).
383
388
  //
384
- // The pin is a RUNTIME value here (safety's voter passes whatever pin it
385
- // was handed), so the controller cannot be baked in as a single DT_NODELABEL
386
- // on a multi-controller SoC (ESP32-S3: pins 0–31 → gpio0, 32–48 → gpio1).
387
- // Emit a tiny __tc_gpio_dev(pin) dispatcher that resolves the owning
388
- // controller's device per pin; single-controller SoCs collapse it to a
389
- // one-liner. Each DT_NODELABEL is still compile-time-resolved per branch, so
390
- // it is always statically valid.
389
+ // The pin is a RUNTIME value here (the UI pin-watch table and safety's
390
+ // voter pass whatever pin they were handed), so the controller cannot be
391
+ // baked in as a single DT_NODELABEL on a multi-controller SoC (ESP32-S3:
392
+ // pins 0–31 → gpio0, 32–48 → gpio1). Emit a tiny __tc_gpio_dev(pin)
393
+ // dispatcher that resolves the owning controller's device per pin;
394
+ // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
395
+ // still compile-time-resolved per branch, so it is always statically valid.
396
+ lines.push(...emitGpioDevDispatcher(chip));
397
+ lines.push('inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), static_cast<gpio_pin_t>(pin)); }');
398
+ // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
399
+ // lowering, so they stay gated on it.
391
400
  if (program && programUsesSafety(program)) {
392
- lines.push(...emitGpioDevDispatcher(chip));
393
- lines.push('inline int __tc_gpio_read(uint32_t pin) { return gpio_pin_get_raw(__tc_gpio_dev(pin), pin); }', 'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }', '#ifndef __TC_DELAY_US_DEFINED', '#define __TC_DELAY_US_DEFINED', 'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }', '#endif');
401
+ lines.push('inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }', '#ifndef __TC_DELAY_US_DEFINED', '#define __TC_DELAY_US_DEFINED', 'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }', '#endif');
394
402
  }
395
403
  return lines;
396
404
  }
@@ -780,6 +788,61 @@ export class ZephyrStrategy {
780
788
  apiReservedEnumGuard() {
781
789
  return '';
782
790
  }
791
+ // ── Interrupt safety ─────────────────────────────────────────────────────
792
+ // Zephyr ISRs run above thread context: anything that sleeps (k_msleep),
793
+ // pends, or takes a driver lock is illegal there (asserted by the kernel in
794
+ // debug builds; corrupts scheduler state otherwise). The names below are the
795
+ // IR-level callees cuttlefish's interrupt-analysis pass matches (the same
796
+ // keys ArduinoStrategy uses; timing.delay/delay_microseconds hal-ops are
797
+ // mapped back to the bare names by the analyzer itself).
798
+ isrUnsafeOperations() {
799
+ return new Map([
800
+ ['delay', {
801
+ reason: 'delay() lowers to k_msleep(), which sleeps the calling thread — illegal in Zephyr interrupt context (submit a k_work item or arm a k_timer instead)',
802
+ severity: 'warning',
803
+ }],
804
+ ['delayMicroseconds', {
805
+ reason: 'delayMicroseconds() busy-waits the CPU for the full delay, stalling every lower-priority interrupt and the scheduler for its duration',
806
+ severity: 'warning',
807
+ }],
808
+ ['console.log', {
809
+ reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
810
+ severity: 'info',
811
+ }],
812
+ ['console.error', {
813
+ reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
814
+ severity: 'info',
815
+ }],
816
+ ['console.warn', {
817
+ reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
818
+ severity: 'info',
819
+ }],
820
+ ['I2C0', {
821
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
822
+ severity: 'warning',
823
+ }],
824
+ ['I2C1', {
825
+ reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
826
+ severity: 'warning',
827
+ }],
828
+ ['SPI0', {
829
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
830
+ severity: 'warning',
831
+ }],
832
+ ['SPI1', {
833
+ reason: 'SPI transfers take driver locks and may wait on DMA completion — not safe in Zephyr interrupt context',
834
+ severity: 'warning',
835
+ }],
836
+ ['UART0', {
837
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
838
+ severity: 'info',
839
+ }],
840
+ ['UART1', {
841
+ reason: 'UART output via uart_poll_out blocks until the TX FIFO has room — a full FIFO stalls the ISR',
842
+ severity: 'info',
843
+ }],
844
+ ]);
845
+ }
783
846
  ambientTypeDeclarations() {
784
847
  // Preferences is the only HAL surface the framework lowers that is used as
785
848
  // a bare global (the HAL Preferences class is exported, but the canonical
@@ -1098,7 +1161,11 @@ struct __tc_StaticArray {
1098
1161
  id: 'async_runtime',
1099
1162
  domain: 'embedded',
1100
1163
  requiredIncludes: [],
1101
- forwardDeclarations: [],
1164
+ // Polyfill definitions emit before shimLines, but the runtime's
1165
+ // timer bodies call millis() (defined in shimLines) — declare it
1166
+ // first so the polyfill compiles even for programs whose source
1167
+ // has no explicit timing call.
1168
+ forwardDeclarations: ['unsigned long millis();'],
1102
1169
  helperStructs: [generateStaticAsyncRuntime(8, this.getAsyncRuntimeConfig().waitForPinEdge)],
1103
1170
  helperFunctions: [],
1104
1171
  shimMacros: [],
@@ -1184,11 +1251,12 @@ struct __tc_StaticArray {
1184
1251
  }
1185
1252
  // ── Strategy-owned display/touch adapter seam ────────────────────────────
1186
1253
  // Zephyr owns its display + touch adapters: the UI display adapter bridges
1187
- // the in-tree CuttlefishGFX class to Zephyr's display_write() API (see
1188
- // src/display/ui-adapter.ts), and the FT6336U touch adapter drives the I2C
1189
- // controller via Zephyr's i2c API (src/display/touch-adapter.ts). Both live
1190
- // in this package so cuttlefish carries no Zephyr/Wiring-specific display or
1191
- // touch knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1254
+ // the in-tree CuttlefishGFX class to the panel (per-controller init + wire
1255
+ // format, see src/display/ui-adapter.ts), and the touch adapters drive the
1256
+ // FT6336U (I2C capacitive) and XPT2046 (SPI resistive) controllers via
1257
+ // Zephyr's bus APIs (src/display/touch-adapter.ts). Both live in this
1258
+ // package so cuttlefish carries no Zephyr/Wiring-specific display or touch
1259
+ // knowledge. Mirrors ArduinoStrategy's provides*/resolve* pattern.
1192
1260
  providesDisplayAdapter() { return true; }
1193
1261
  resolveDisplayAdapter(display) {
1194
1262
  const code = zephyrDisplayAdapterGenerator(display);
@@ -185,12 +185,17 @@ export const Toolchain = {
185
185
  // for either driver. Thread a non-default profile here only if a future
186
186
  // board carries a display node under a different nodelabel.
187
187
  const displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
188
+ // Touch controller kind comes from which DT nodelabel the emitted adapter
189
+ // references (FT6336U on I2C, XPT2046 on the display's SPI bus).
190
+ const usesTouch = uses('ft6336u') || uses('touch_');
191
+ const usesXpt = uses('xpt2046');
188
192
  const overlay = generateOverlay(chip, {
189
193
  usesI2c: uses('i2c_'),
190
194
  usesSpi: uses('spi_'),
191
195
  usesUart: uses('uart_'),
192
196
  usesDisplay,
193
- usesTouch: uses('ft6336u') || uses('touch_'),
197
+ usesTouch: usesTouch || usesXpt,
198
+ touchController: usesXpt ? 'xpt2046' : 'ft6336u',
194
199
  }, displayProfile);
195
200
  const overlayDir = join(projectRoot, 'boards');
196
201
  mkdirSync(overlayDir, { recursive: true });
@@ -274,23 +279,48 @@ export const Toolchain = {
274
279
  backlightPin: typeof dispCfg.backlightPin === 'number' ? dispCfg.backlightPin : undefined,
275
280
  }
276
281
  : undefined;
277
- // Extract touch pin wiring (irq/resetPin/sda/scl) from the config
278
- // display.touch section so the DT overlay wires the I2C bus + touch node.
282
+ // Extract touch pin wiring from the config display.touch section so the
283
+ // DT overlay wires the bus + touch node. I2C (FT6336U) carries
284
+ // irq/resetPin/sda/scl; SPI (XPT2046) carries irq/cs + the calibration
285
+ // range the xptek,xpt2046 binding requires.
279
286
  const touchCfg = dispCfg?.touch;
280
- const touchWiring = touchCfg
287
+ const isXpt = touchCfg?.library === 'XPT2046_Touchscreen';
288
+ const touchCal = touchCfg?.calibration;
289
+ const num = (v) => (typeof v === 'number' ? v : undefined);
290
+ let touchWiring = touchCfg
281
291
  ? {
282
- irq: typeof touchCfg.irq === 'number' ? touchCfg.irq : undefined,
283
- resetPin: typeof touchCfg.resetPin === 'number' ? touchCfg.resetPin : undefined,
284
- sda: typeof touchCfg.sda === 'number' ? touchCfg.sda : undefined,
285
- scl: typeof touchCfg.scl === 'number' ? touchCfg.scl : undefined,
292
+ controller: isXpt ? 'xpt2046' : 'ft6336u',
293
+ irq: num(touchCfg.irq),
294
+ resetPin: num(touchCfg.resetPin),
295
+ sda: num(touchCfg.sda),
296
+ scl: num(touchCfg.scl),
297
+ cs: num(touchCfg.cs),
298
+ calibration: touchCal
299
+ ? {
300
+ xMin: num(touchCal.xMin) ?? 0,
301
+ xMax: num(touchCal.xMax) ?? 4095,
302
+ yMin: num(touchCal.yMin) ?? 0,
303
+ yMax: num(touchCal.yMax) ?? 4095,
304
+ }
305
+ : undefined,
306
+ minPressure: num(touchCfg.minPressure),
286
307
  }
287
308
  : undefined;
309
+ // Touch controller kind for Kconfig (bus driver selection) and the DT
310
+ // node shape: from the config when available, else from the DT nodelabel
311
+ // the emitted adapter references. Forced onto touchWiring so a source
312
+ // scan match without a config section still emits the right node.
313
+ const usesXpt = isXpt || uses('xpt2046');
314
+ if (usesXpt) {
315
+ touchWiring = { controller: 'xpt2046', ...(touchWiring ?? {}) };
316
+ }
288
317
  const overlay = generateOverlay(chip, {
289
318
  usesI2c: uses('i2c_'),
290
319
  usesSpi: uses('spi_'),
291
320
  usesUart: uses('uart_'),
292
321
  usesDisplay,
293
- usesTouch: uses('ft6336u') || uses('touch_'),
322
+ usesTouch: uses('ft6336u') || uses('touch_') || usesXpt,
323
+ touchController: usesXpt ? 'xpt2046' : 'ft6336u',
294
324
  psram: o.psram,
295
325
  }, displayProfile, wiring, touchWiring);
296
326
  const overlayDir = join(projectRoot, 'boards');
@@ -128,8 +128,10 @@ export function scaffoldZephyrProject(projectRoot, debug = false, userKconfig, p
128
128
  '',
129
129
  'project(zephyr_app)',
130
130
  '',
131
- '# Collect cuttlefish-emitted sources.',
132
- 'file(GLOB app_sources src/*.cpp src/*.c)',
131
+ '# Collect cuttlefish-emitted sources. CONFIGURE_DEPENDS makes CMake re-',
132
+ '# check the glob when the source set changes (e.g. the transpiler removes',
133
+ '# a stale entry), instead of linking a file list from the last configure.',
134
+ 'file(GLOB app_sources CONFIGURE_DEPENDS src/*.cpp src/*.c)',
133
135
  '',
134
136
  'target_sources(app PRIVATE ${app_sources})',
135
137
  // When PSRAM is configured, define BOARD_HAS_PSRAM so the UI runtime's
@@ -67,9 +67,12 @@ export function isZephyrBase(dir) {
67
67
  }
68
68
  // ── Strategy 1: `west` on PATH ──────────────────────────────────────────────
69
69
  export function discoverFromPath() {
70
+ // shell only on Windows (where.exe resolution through cmd) — an args array
71
+ // with shell: true triggers Node's DEP0190 deprecation warning on Linux/
72
+ // macOS, where `which` is a plain executable that needs no shell.
70
73
  const which = spawnSync(IS_WIN ? 'where' : 'which', ['west'], {
71
74
  encoding: 'utf8',
72
- shell: true,
75
+ shell: IS_WIN,
73
76
  windowsHide: true,
74
77
  });
75
78
  if (which.status !== 0)