@typecad/framework-zephyr 1.0.0-alpha.18 → 1.0.0-alpha.20

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 (114) hide show
  1. package/dist/audit.d.ts +111 -0
  2. package/dist/audit.js +416 -0
  3. package/dist/boardgen.js +63 -5
  4. package/dist/chips/resolve.js +20 -0
  5. package/dist/chips/types.d.ts +21 -0
  6. package/dist/display/bindings.d.ts +55 -0
  7. package/dist/display/bindings.js +316 -0
  8. package/dist/display/gfx.d.ts +2 -3
  9. package/dist/display/gfx.js +166 -154
  10. package/dist/display/index.js +20 -1
  11. package/dist/display/mipi-dbi-host.d.ts +9 -0
  12. package/dist/display/mipi-dbi-host.js +174 -0
  13. package/dist/display/profiles.d.ts +109 -4
  14. package/dist/display/profiles.js +270 -7
  15. package/dist/display/touch-adapter.js +118 -48
  16. package/dist/display/ui-adapter-eink.d.ts +2 -0
  17. package/dist/display/ui-adapter-eink.js +4 -0
  18. package/dist/display/ui-adapter-gray.d.ts +8 -0
  19. package/dist/display/ui-adapter-gray.js +170 -0
  20. package/dist/display/ui-adapter-mono.d.ts +13 -0
  21. package/dist/display/ui-adapter-mono.js +230 -0
  22. package/dist/display/ui-adapter-native.d.ts +10 -0
  23. package/dist/display/ui-adapter-native.js +295 -0
  24. package/dist/display/ui-adapter-shared.d.ts +11 -0
  25. package/dist/display/ui-adapter-shared.js +122 -0
  26. package/dist/display/ui-adapter.js +53 -101
  27. package/dist/dt-config/kconfig.d.ts +62 -1
  28. package/dist/dt-config/kconfig.js +123 -26
  29. package/dist/dt-config/overlay.d.ts +13 -0
  30. package/dist/dt-config/overlay.js +431 -16
  31. package/dist/framework.manifest.d.ts +9 -3
  32. package/dist/framework.manifest.js +123 -10
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +9 -1
  35. package/dist/licenses.js +5 -84
  36. package/dist/lowering/can.d.ts +25 -0
  37. package/dist/lowering/can.js +97 -0
  38. package/dist/lowering/clock.d.ts +17 -0
  39. package/dist/lowering/clock.js +58 -0
  40. package/dist/lowering/hid.d.ts +27 -0
  41. package/dist/lowering/hid.js +244 -0
  42. package/dist/lowering/i2c.d.ts +8 -0
  43. package/dist/lowering/i2c.js +140 -0
  44. package/dist/lowering/i2s.d.ts +27 -0
  45. package/dist/lowering/i2s.js +98 -0
  46. package/dist/lowering/index.d.ts +9 -1
  47. package/dist/lowering/index.js +25 -1
  48. package/dist/lowering/matrix.d.ts +15 -0
  49. package/dist/lowering/matrix.js +63 -0
  50. package/dist/lowering/power.d.ts +10 -0
  51. package/dist/lowering/power.js +35 -0
  52. package/dist/lowering/pwm.js +25 -0
  53. package/dist/lowering/sensor.d.ts +2 -2
  54. package/dist/lowering/sensor.js +8 -4
  55. package/dist/lowering/strip.d.ts +16 -0
  56. package/dist/lowering/strip.js +70 -0
  57. package/dist/lowering/thread.js +5 -1
  58. package/dist/lowering/trace.d.ts +44 -0
  59. package/dist/lowering/trace.js +239 -0
  60. package/dist/lowering/uart.js +6 -1
  61. package/dist/lowering/usb.d.ts +3 -1
  62. package/dist/lowering/usb.js +6 -3
  63. package/dist/sbom.d.ts +181 -0
  64. package/dist/sbom.js +901 -0
  65. package/dist/strategy.d.ts +60 -2
  66. package/dist/strategy.js +485 -25
  67. package/dist/toolchain/index.d.ts +12 -1
  68. package/dist/toolchain/index.js +115 -27
  69. package/dist/toolchain/scaffold.d.ts +4 -1
  70. package/dist/toolchain/scaffold.js +67 -3
  71. package/dist/west-inventory.d.ts +25 -0
  72. package/dist/west-inventory.js +97 -0
  73. package/package.json +6 -6
  74. package/src/audit.ts +529 -0
  75. package/src/boardgen.ts +54 -5
  76. package/src/chips/resolve.ts +21 -0
  77. package/src/chips/types.ts +576 -567
  78. package/src/display/bindings.ts +347 -0
  79. package/src/display/gfx.ts +318 -306
  80. package/src/display/index.ts +87 -70
  81. package/src/display/mipi-dbi-host.ts +183 -0
  82. package/src/display/profiles.ts +458 -139
  83. package/src/display/touch-adapter.ts +274 -204
  84. package/src/display/ui-adapter-eink.ts +13 -0
  85. package/src/display/ui-adapter-gray.ts +178 -0
  86. package/src/display/ui-adapter-mono.ts +238 -0
  87. package/src/display/ui-adapter-native.ts +304 -0
  88. package/src/display/ui-adapter-shared.ts +125 -0
  89. package/src/display/ui-adapter.ts +51 -100
  90. package/src/dt-config/kconfig.ts +647 -511
  91. package/src/dt-config/overlay.ts +433 -16
  92. package/src/framework.manifest.ts +131 -10
  93. package/src/index.ts +11 -1
  94. package/src/licenses.ts +5 -84
  95. package/src/lowering/can.ts +140 -0
  96. package/src/lowering/clock.ts +91 -0
  97. package/src/lowering/hid.ts +261 -0
  98. package/src/lowering/i2c.ts +146 -0
  99. package/src/lowering/i2s.ts +143 -0
  100. package/src/lowering/index.ts +18 -1
  101. package/src/lowering/matrix.ts +70 -0
  102. package/src/lowering/power.ts +41 -0
  103. package/src/lowering/pwm.ts +192 -167
  104. package/src/lowering/sensor.ts +159 -155
  105. package/src/lowering/strip.ts +81 -0
  106. package/src/lowering/thread.ts +5 -1
  107. package/src/lowering/trace.ts +270 -0
  108. package/src/lowering/uart.ts +6 -1
  109. package/src/lowering/usb.ts +224 -221
  110. package/src/sbom.ts +1117 -0
  111. package/src/strategy.ts +440 -25
  112. package/src/toolchain/index.ts +117 -28
  113. package/src/toolchain/scaffold.ts +62 -3
  114. package/src/west-inventory.ts +102 -0
@@ -12,11 +12,71 @@
12
12
  // ---------------------------------------------------------------------------
13
13
 
14
14
  import type { ZephyrChipDescriptor } from '../chips/types.js';
15
+ import { controllerNodelabelForPin, controllerRawPinForPin } from '../chips/controllers.js';
15
16
  import type { ZephyrDisplayProfile } from '../display/profiles.js';
16
- import { PANEL_CONTROLLER_DEFAULTS, panelControllerFor } from '../display/profiles.js';
17
+ import { PANEL_CONTROLLER_DEFAULTS, panelControllerFor, transportFor, isDtCompatible } from '../display/profiles.js';
18
+ import { readDisplayBinding, type RequiredProp } from '../display/bindings.js';
19
+ import { isEinkDisplay } from '../display/profiles.js';
20
+
21
+ /** Standard values for the mono-OLED family's default-free required props
22
+ * (segment/page/display offsets, multiplex ratio, precharge period) — the
23
+ * values upstream board overlays ship; tune per panel from here. */
24
+ function monoPropFallback(prop: string, info: RequiredProp | undefined, nativeHeight: number): string | undefined {
25
+ if (prop === 'width' || prop === 'height' || prop === 'reg' || prop === 'compatible') return undefined;
26
+ const def = info?.default;
27
+ if (def?.kind === 'int') return `${prop} = <${def.value}>`;
28
+ if (prop === 'multiplex-ratio') return `multiplex-ratio = <${nativeHeight - 1}>`;
29
+ if (prop === 'prechargep') return 'prechargep = <0x22>';
30
+ if (prop === 'segment-offset' || prop === 'page-offset' || prop === 'display-offset') return `${prop} = <0>`;
31
+ // SSD1327-class (Stage 3 gray): required by solomon,ssd1327_5-common.yaml
32
+ // with no binding default. Panel-tunable — these are the datasheet/common-
33
+ // module starting points (scanline artifacts: raise oscillator-freq toward
34
+ // 0x70; striping: add 0x40 to remap-value per the binding's bit table).
35
+ if (prop === 'oscillator-freq') return 'oscillator-freq = <0x00>';
36
+ if (prop === 'start-line') return 'start-line = <0>';
37
+ if (prop === 'remap-value') return 'remap-value = <0x51>';
38
+ if (prop === 'phase-length') return 'phase-length = <0x1f>';
39
+ return undefined;
40
+ }
41
+
42
+ /** Format one binding-required property with its harvested default. Binding
43
+ * props that are deliberately default-free ("panel specific" — e.g. the
44
+ * sitronix gamma arrays) fall back to the rig-validated tuning the curated
45
+ * profiles ship, so the generated node is always DT-valid. */
46
+ function emitBindingProp(lines: string[], prop: string, info: RequiredProp | undefined): void {
47
+ const def = info?.default;
48
+ if (def?.kind === 'bytes' && def.value.length > 0) {
49
+ lines.push(` ${prop} = [${def.value.map((b) => b.toString(16).padStart(2, '0')).join(' ')}];`);
50
+ return;
51
+ }
52
+ if (def?.kind === 'bool' && def.value) {
53
+ lines.push(` ${prop};`);
54
+ return;
55
+ }
56
+ if (def?.kind === 'string') {
57
+ lines.push(` ${prop} = "${def.value}";`);
58
+ return;
59
+ }
60
+ if (info?.type === 'uint8-array') {
61
+ // No binding default (panel-specific): emit the rig-validated gamma
62
+ // tuning the curated st7796 profile carries. Tune per panel from here.
63
+ lines.push(` ${prop} = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];`);
64
+ return;
65
+ }
66
+ const v = def?.kind === 'int' ? def.value : 0;
67
+ lines.push(` ${prop} = <${v}>;`);
68
+ }
17
69
  import { SENSOR_PART_INFO } from '@typecad/hal';
18
70
  import type { KconfigUsage } from './kconfig.js';
19
71
 
72
+ /** 1-Wire family codes (the high byte of the 64-bit ROM ID) for the parts
73
+ * the catalog carries on the w1 bus — vendor silicon data, like the ESP32
74
+ * DAC pin table. Unknown compatibles default to the DS18B20's 0x28. */
75
+ const W1_FAMILY_CODES: Record<string, number> = {
76
+ 'maxim,ds18b20': 0x28,
77
+ 'maxim,ds18s20': 0x10,
78
+ };
79
+
20
80
  /** Display panel CS default — shared by the display block and the SPI-sensor
21
81
  * cs-gpios merge so both sites write the same entry. */
22
82
  const DEFAULT_DISPLAY_CS = 5;
@@ -33,7 +93,20 @@ export interface DisplayWiring {
33
93
  /** Tearing-effect (TE) GPIO from display.tearingEffectPin — emitted as
34
94
  * te-gpios on the display DT node. Opt-in; most boards don't wire TE. */
35
95
  tearingEffectPin?: number;
96
+ /** E-ink BUSY GPIO from display.busyPin — emitted as busy-gpios on the
97
+ * panel child (required by the ssd16xx/uc81xx bindings, active-high). */
98
+ busyPin?: number;
36
99
  spiFrequency?: number;
100
+ /** I2C address for i2c-family panels (mono OLEDs, ssd1306-class). */
101
+ address?: number;
102
+ /** I2C bus pins for i2c-family panels. When present, the overlay remuxes
103
+ * the I2C controller's pinctrl to these pins — the board's default I2C
104
+ * pins rarely match a breakout's wiring (same rationale as the SPI pins
105
+ * below; the ESP32 pinctrl headers carry the named I2C0_*_GPIOn macros).
106
+ * Shared bus: when touch wiring also remuxes, the last assignment wins —
107
+ * wire both to the same pins. */
108
+ sda?: number;
109
+ scl?: number;
37
110
  /** SPI bus pins. When present, the overlay remuxes the SPI controller's
38
111
  * pinctrl to these pins (the board defaults rarely match a breakout's
39
112
  * wiring — e.g. demo-st's panel is on SCK=18/MOSI=23, not the devkitc
@@ -190,6 +263,35 @@ export function generateOverlay(
190
263
  lines.push('};');
191
264
  lines.push('');
192
265
  }
266
+ // One w1-gpio master + sensor child per constructed 1-Wire sensor. The
267
+ // master bit-bangs the data pin (open-drain, internal pull-up enabled —
268
+ // the binding recommends an external 4.7 kΩ, the internal one is weak);
269
+ // the child's family-code comes from the vendor table below (part of the
270
+ // 64-bit ROM ID), resolution from the construction opts. The nodelabels
271
+ // match lowering/sensor.ts sensorNames — the tc-pwm discipline.
272
+ for (const sp of sensorParts.filter((s) => s.busKind === 'w1')) {
273
+ const info = SENSOR_PART_INFO[sp.part.replace(/^SENSOR\./, '')];
274
+ const family = W1_FAMILY_CODES[info?.compatible ?? ''] ?? 0x28;
275
+ if (!info) continue; // the lowering already threw on unknown parts
276
+ const pinCtrl = controllerNodelabelForPin(chip, sp.port);
277
+ const pinBit = controllerRawPinForPin(chip, sp.port);
278
+ const nodeName = info.compatible.split(',')[1] ?? sp.part;
279
+ const stem = `${sp.part.replace(/^SENSOR\./, '')}_w1_p${sp.port}`;
280
+ lines.push('/ {');
281
+ lines.push(` tc_w1_p${sp.port}: tc-w1-p${sp.port} {`);
282
+ lines.push(' compatible = "zephyr,w1-gpio";');
283
+ lines.push(` gpios = <&${pinCtrl} ${pinBit} (GPIO_OPEN_DRAIN | GPIO_PULL_UP)>;`);
284
+ lines.push(' status = "okay";');
285
+ lines.push(` tc_${stem}: ${nodeName} {`);
286
+ lines.push(` compatible = "${info.compatible}";`);
287
+ lines.push(` family-code = <0x${family.toString(16)}>;`);
288
+ lines.push(` resolution = <${sp.resolution}>;`);
289
+ lines.push(' status = "okay";');
290
+ lines.push(' };');
291
+ lines.push(' };');
292
+ lines.push('};');
293
+ lines.push('');
294
+ }
193
295
  if (usage.usesSpi && chip.spi) {
194
296
  for (const [i, c] of chip.spi.controllers.entries()) {
195
297
  if (usage.spiUsedInstances && !usage.spiUsedInstances.includes(i)) continue;
@@ -285,6 +387,176 @@ export function generateOverlay(
285
387
  }
286
388
  }
287
389
  }
390
+ // LED strips: one ws2812-spi child per driven SPI controller. The frame
391
+ // tuning (6.4 MHz, one=0xf0 / zero=0xc0 — 625 ns bit halves) is the
392
+ // Zephyr led_strip sample's canonical SPI encoding; the color-mapping
393
+ // matches the WS2812's native GRB order (the driver reorders). The
394
+ // controller is enabled here too — a strip-only program emits no spi_*
395
+ // calls, so the bus enable cannot ride the generic SPI usage path.
396
+ // `line-idle-low` keeps MOSI low between frames, the WS2812 reset level.
397
+ if (usage.usesStrip && usage.strips && usage.strips.length > 0) {
398
+ lines.splice(2, 0, '#include <zephyr/dt-bindings/led/led.h>', '');
399
+ for (const s of usage.strips) {
400
+ const ctrl = chip.spi?.controllers[s.index];
401
+ if (!ctrl) continue;
402
+ lines.push(`&${ctrl.nodeLabel} {`);
403
+ lines.push(' status = "okay";');
404
+ lines.push(' line-idle-low;');
405
+ lines.push(` tc_strip${s.index}: ws2812@0 {`);
406
+ lines.push(' compatible = "worldsemi,ws2812-spi";');
407
+ lines.push(' reg = <0>;');
408
+ lines.push(' spi-max-frequency = <6400000>;');
409
+ lines.push(` chain-length = <${s.count}>;`);
410
+ lines.push(' spi-cpha;');
411
+ lines.push(' spi-one-frame = <0xf0>;');
412
+ lines.push(' spi-zero-frame = <0xc0>;');
413
+ lines.push(' color-mapping = <LED_COLOR_ID_GREEN LED_COLOR_ID_RED LED_COLOR_ID_BLUE>;');
414
+ lines.push(' };');
415
+ lines.push('};');
416
+ lines.push('');
417
+ }
418
+ }
419
+ // I2S: enable the board's own i2s@ controllers — their pinctrl groups
420
+ // are board-authored (the devkitC wires i2s0_default/i2s1_default), so
421
+ // status is the only synthesis needed. The GDMA the ESP32 I2S driver
422
+ // DMA-runs through also ships disabled — enable it alongside.
423
+ // Full-duplex: the RX side's I_BCK/I_WS inputs must be routed in the
424
+ // GPIO matrix to the same pads the TX outputs drive (input-enable on
425
+ // the TX clock pads — the same-pad GPIO matrix trick the CAN loopback
426
+ // uses; the devkitC's i2s0 clock pads are WS=36, BCK=37).
427
+ if (usage.usesI2s && chip.i2s) {
428
+ lines.push('&dma {');
429
+ lines.push(' status = "okay";');
430
+ lines.push('};');
431
+ lines.push('');
432
+ for (const c of chip.i2s.controllers) {
433
+ const n = c.nodeLabel.replace(/[^0-9]/g, '') || '0';
434
+ lines.push(`&${c.nodeLabel} {`);
435
+ lines.push(' status = "okay";');
436
+ lines.push('};');
437
+ lines.push('');
438
+ lines.push('&pinctrl {');
439
+ lines.push(' tc_i2s_clock_loop: tc-i2s-clock-loop {');
440
+ lines.push(' group1 {');
441
+ lines.push(` pinmux = <I2S${n}_I_BCK_GPIO37>,`);
442
+ lines.push(` <I2S${n}_I_WS_GPIO36>;`);
443
+ lines.push(' };');
444
+ lines.push(' };');
445
+ lines.push('};');
446
+ lines.push(`&${c.nodeLabel} {`);
447
+ lines.push(` pinctrl-0 = <&${c.nodeLabel}_default>, <&tc_i2s_clock_loop>;`);
448
+ lines.push(' pinctrl-names = "default";');
449
+ lines.push('};');
450
+ lines.push('');
451
+ }
452
+ }
453
+ // CAN: enable the board's own can@ controller (the harvested node —
454
+ // boards that ship pinctrl groups keep them; the bitrate is the begin()
455
+ // default, overridden at runtime by can_set_bitrate). Classic CAN only.
456
+ // LOOPBACK: the SJA1000 self-test transmits on the TX pad and receives
457
+ // on the RX pad — with no transceiver the overlay routes BOTH TWAI
458
+ // functions onto the TX pad (the Zephyr test suite's own
459
+ // twai-enable.overlay shape: output-enable on the RX mux, input-enable
460
+ // on the TX mux, one shared pad).
461
+ if (usage.usesCan && chip.can) {
462
+ for (const c of chip.can.controllers) {
463
+ lines.push(`&${c.nodeLabel} {`);
464
+ lines.push(' status = "okay";');
465
+ lines.push(' bitrate = <500000>;');
466
+ lines.push('};');
467
+ lines.push('');
468
+ if (usage.canLoopback && c.txPad !== undefined) {
469
+ lines.push('&pinctrl {');
470
+ lines.push(' tc_can_loopback: tc-can-loopback {');
471
+ lines.push(' group1 {');
472
+ lines.push(` pinmux = <TWAI_RX_GPIO${c.txPad}>;`);
473
+ lines.push(' output-enable;');
474
+ lines.push(' };');
475
+ lines.push(' group2 {');
476
+ lines.push(` pinmux = <TWAI_TX_GPIO${c.txPad}>;`);
477
+ lines.push(' input-enable;');
478
+ lines.push(' };');
479
+ lines.push(' };');
480
+ lines.push('};');
481
+ lines.push(`&${c.nodeLabel} { pinctrl-0 = <&tc_can_loopback>; pinctrl-names = "default"; };`);
482
+ lines.push('');
483
+ }
484
+ }
485
+ }
486
+ // Wall-clock shim: a zephyr,rtc-counter child on the board's first free
487
+ // counter node + the `rtc` alias (Zephyr's own discovery convention —
488
+ // the api tests filter on dt_alias_exists("rtc")). The shim is a CHILD:
489
+ // the counter driver keeps the parent node, so Counter and Clock
490
+ // coexist. V1 always synthesizes the shim (uniform semantics, no driver
491
+ // conflicts); preferring a board's own hardware-calendar alias — and the
492
+ // backup-domain persistence it buys — is the follow-up.
493
+ if (usage.usesClock && usage.clockShimCounter) {
494
+ const c = usage.clockShimCounter;
495
+ if (c.parent) {
496
+ // Child-form counters (ESP32 timerN children): the shim's parent
497
+ // must be the counter DEVICE, so it nests INSIDE the counter child —
498
+ // DT_INST_PARENT in the shim driver resolves to the counter, not the
499
+ // timer (which carries no device of its own).
500
+ lines.push(`&${c.parent} {`);
501
+ lines.push(' status = "okay";');
502
+ lines.push(` ${c.label}: counter {`);
503
+ lines.push(' status = "okay";');
504
+ lines.push(' tc_rtc: tc-rtc {');
505
+ lines.push(' compatible = "zephyr,rtc-counter";');
506
+ lines.push(' };');
507
+ lines.push(' };');
508
+ lines.push('};');
509
+ } else {
510
+ lines.push(`&${c.label} {`);
511
+ lines.push(' tc_rtc: tc-rtc {');
512
+ lines.push(' compatible = "zephyr,rtc-counter";');
513
+ lines.push(' };');
514
+ lines.push('};');
515
+ }
516
+ lines.push('/ {');
517
+ lines.push(' aliases {');
518
+ lines.push(' rtc = &tc_rtc;');
519
+ lines.push(' };');
520
+ lines.push('};');
521
+ lines.push('');
522
+ }
523
+ // USB HID: one zephyr,hid-device interface per program (the v1 ceiling —
524
+ // Keyboard OR Mouse, not both). protocol-code stays "none": the boot
525
+ // subclass/protocol codes (keyboard/mouse) made Windows bind the
526
+ // collections but silently discard every input report on this stack,
527
+ // while "none" (pure report-protocol device, like Zephyr's own samples)
528
+ // delivers input normally. The report descriptor carries the real
529
+ // keyboard/mouse semantics either way.
530
+ if (usage.usesHid) {
531
+ lines.push('/ {');
532
+ lines.push(' tc_hid: tc-hid {');
533
+ lines.push(' compatible = "zephyr,hid-device";');
534
+ lines.push(' protocol-code = "none";');
535
+ lines.push(' in-report-size = <8>;');
536
+ lines.push(' in-polling-period-us = <1000>;');
537
+ lines.push(' };');
538
+ lines.push('};');
539
+ lines.push('');
540
+ }
541
+ // Key matrix: the gpio-kbd-matrix node from the construction pad lists.
542
+ // Rows are pull-up inputs (active-low); columns are driven one at a time.
543
+ // Pin → controller/bit via the same mapping the gpio lowering uses.
544
+ if (usage.usesMatrix && usage.matrix && usage.matrix.rows.length > 0) {
545
+ // Same mapping the gpio lowering uses — the canonical controller
546
+ // resolution (single-controller fallback included). The port-relative
547
+ // bit comes from controllerRawPinForPin (split-SoC offsets).
548
+ const gpioRef = (pin: number): string => {
549
+ return `&${controllerNodelabelForPin(chip, pin)} ${controllerRawPinForPin(chip, pin)} (GPIO_PULL_UP | GPIO_ACTIVE_LOW)`;
550
+ };
551
+ lines.push('/ {');
552
+ lines.push(' tc_matrix: matrix {');
553
+ lines.push(' compatible = "gpio-kbd-matrix";');
554
+ lines.push(` row-gpios = ${usage.matrix.rows.map((pin) => `<${gpioRef(pin)}>`).join(', ')};`);
555
+ lines.push(` col-gpios = ${usage.matrix.cols.map((pin) => `<${gpioRef(pin)}>`).join(', ')};`);
556
+ lines.push(' };');
557
+ lines.push('};');
558
+ lines.push('');
559
+ }
288
560
  if (display) {
289
561
  // Emit a full display DT node definition. Boards like the ESP32 devkit
290
562
  // have no display node in their base DT, so a bare `&display0 { status }`
@@ -807,11 +1079,86 @@ function emitDisplayNode(
807
1079
  ): void {
808
1080
  const bus = display.busLabel ?? 'spi2';
809
1081
  const controller = panelControllerFor(display);
810
- const compatible = display.dtCompatible ?? PANEL_CONTROLLER_DEFAULTS[controller].dtCompatible;
1082
+ // Board-provided displays (native_sim's built-in sdl_dc): the board's own
1083
+ // devicetree wires the panel — nothing to emit here.
1084
+ if (display.boardProvidesDisplay) {
1085
+ return;
1086
+ }
1087
+ // I2C-family panels (mono OLEDs, ssd1306-class): a plain &i2cN child node —
1088
+ // no mipi-dbi bridge, no SPI bus block, no DMA. Bus pins come from the
1089
+ // touch wiring's remux (shared bus) or the board's default I2C pins.
1090
+ if (readDisplayBinding(display.dtCompatible ?? (isDtCompatible(display.driver) ? display.driver : ''))?.busFamily === 'i2c') {
1091
+ const addr = wiring?.address ?? 0x3c;
1092
+ // Optional I2C pin remux — mirrors the touch path's remux (the board's
1093
+ // default I2C pins rarely match a breakout's wiring). Named macros from
1094
+ // the SoC pinctrl header; the group lives under &pinctrl and overrides
1095
+ // i2c0's default group via pinctrl-0.
1096
+ const sda = wiring?.sda;
1097
+ const scl = wiring?.scl;
1098
+ if (sda !== undefined && scl !== undefined) {
1099
+ lines.push('&pinctrl {');
1100
+ lines.push(' i2c0_display: i2c0_display {');
1101
+ lines.push(' group1 {');
1102
+ lines.push(` pinmux = <I2C0_SDA_GPIO${sda}>, <I2C0_SCL_GPIO${scl}>;`);
1103
+ lines.push(' bias-pull-up;');
1104
+ lines.push(' drive-open-drain;');
1105
+ lines.push(' };');
1106
+ lines.push(' };');
1107
+ lines.push('};');
1108
+ lines.push('');
1109
+ }
1110
+ // Node name from the compatible's panel segment (ssd1306/ssd1309/sh1106);
1111
+ // DT node names are labels, not compatibles — but matching the panel
1112
+ // keeps the built zephyr.dts readable.
1113
+ const panel = (display.dtCompatible ?? display.driver).split(',')[1] ?? 'ssd1306';
1114
+ lines.push('&i2c0 {');
1115
+ lines.push(' status = "okay";');
1116
+ // Fast mode: the full-frame mono push is bandwidth-bound (a 128x64 frame
1117
+ // is 1KB — ~25ms at the 100kHz default, ~7ms at 400kHz), so animations
1118
+ // step without it. OLED modules and the esp32s3/nRF i2c controllers all
1119
+ // handle 400kHz; declare it whenever we own the bus node generation.
1120
+ lines.push(' clock-frequency = <400000>;');
1121
+ if (sda !== undefined && scl !== undefined) {
1122
+ lines.push(' pinctrl-0 = <&i2c0_display>;');
1123
+ lines.push(' pinctrl-names = "default";');
1124
+ }
1125
+ lines.push(` ${display.dtLabel}: ${panel}@${addr.toString(16)} {`);
1126
+ lines.push(` compatible = "${display.dtCompatible ?? display.driver}";`);
1127
+ lines.push(` reg = <0x${addr.toString(16)}>;`);
1128
+ lines.push(` width = <${display.nativeWidth ?? display.width}>;`);
1129
+ lines.push(` height = <${display.nativeHeight ?? display.height}>;`);
1130
+ const binding = readDisplayBinding(display.dtCompatible ?? display.driver);
1131
+ if (binding) {
1132
+ for (const [prop, info] of binding.required) {
1133
+ const v = monoPropFallback(prop, info, display.nativeHeight ?? display.height);
1134
+ if (v !== undefined) lines.push(` ${v};`);
1135
+ }
1136
+ }
1137
+ lines.push(' };');
1138
+ lines.push('};');
1139
+ lines.push('');
1140
+ return;
1141
+ }
1142
+ // Compatible resolution: explicit override → the registry controllers →
1143
+ // the drop-in path (driver IS a compatible string) → a safe default.
1144
+ // Drop-in drivers (DT-compatible shape) ARE the compatible — they win over
1145
+ // the controller-default table (which only serves registry driver ids whose
1146
+ // id isn't itself a compatible; the e-ink family entries exist for the
1147
+ // Kconfig layer, not for node naming).
1148
+ const compatible = display.dtCompatible
1149
+ ?? (isDtCompatible(display.driver)
1150
+ ? display.driver
1151
+ : (controller !== undefined
1152
+ ? PANEL_CONTROLLER_DEFAULTS[controller].dtCompatible
1153
+ : PANEL_CONTROLLER_DEFAULTS.ili9341.dtCompatible));
811
1154
  const dc = wiring?.dc ?? 17;
812
1155
  const rst = wiring?.rst ?? 16;
813
1156
  const cs = wiring?.cs ?? DEFAULT_DISPLAY_CS;
814
- const freq = wiring?.spiFrequency ?? 80000000;
1157
+ // E-ink SPI tops out far below TFTs (the reel board runs ssd1673 at 4MHz;
1158
+ // the controllers' shift registers clock 4-10MHz). Cap the default for the
1159
+ // e-ink families; explicit spiFrequency always wins.
1160
+ const einkFamily = isEinkDisplay({ driver: display.driver, displayClass: display.displayClass });
1161
+ const freq = wiring?.spiFrequency ?? (einkFamily ? 4000000 : 80000000);
815
1162
  // DT node describes the NATIVE panel geometry; the effective (rotated)
816
1163
  // dimensions live in the display profile.
817
1164
  const nativeW = display.nativeWidth ?? display.width;
@@ -874,19 +1221,25 @@ function emitDisplayNode(
874
1221
  lines.push(` ${display.dtLabel}: display@0 {`);
875
1222
  lines.push(` compatible = "${compatible}";`);
876
1223
  lines.push(' reg = <0>;');
877
- if (wiring?.tearingEffectPin !== undefined) {
1224
+ if (wiring?.tearingEffectPin !== undefined && transportFor(display) === 'direct-spi') {
878
1225
  const tePin = wiring.tearingEffectPin!;
879
1226
  // Tearing-effect input on the display node: GPIO_DT_SPEC_GET(
880
- // DT_NODELABEL(display0), te_gpios) in the adapter. Opt-in —
881
- // most modules don't break the TE pad out.
1227
+ // DT_NODELABEL(display0), te_gpios) in the direct-spi adapter.
1228
+ // Opt-in — most modules don't break the TE pad out. NOT emitted
1229
+ // on the zephyr-display transport: te-gpios makes the bound
1230
+ // in-tree driver allocate the TE GPIO interrupt (the ESP32
1231
+ // VECDESC_FL_SHARED conflict path) — the driver's TE handling
1232
+ // needs re-verification before it can be wired there.
882
1233
  lines.push(` te-gpios = <&${gpioController(tePin)} ${tePin} GPIO_ACTIVE_HIGH>;`);
883
1234
  }
884
1235
  lines.push(` mipi-max-frequency = <${freq}>;`);
885
1236
  lines.push(' mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";');
886
- // Required by the lcd-controller binding (Zephyr 4.x): 0 = RGB565,
887
- // matching upstream ILI9341 boards (esp_wrover_kit) and the C++
888
- // runtime, which drives these SPI TFTs as RGB565.
889
- lines.push(' pixel-format = <0>;');
1237
+ if (wiring?.busyPin !== undefined) {
1238
+ // E-ink BUSY line — required by the ssd16xx/uc81xx bindings
1239
+ // (the drivers block on it through the flash cycle). Active
1240
+ // level fixed per family: ssd16xx BUSY is active-high.
1241
+ lines.push(` busy-gpios = <&${gpioController(wiring.busyPin)} ${wiring.busyPin} GPIO_ACTIVE_HIGH>;`);
1242
+ }
890
1243
  lines.push(` width = <${nativeW}>;`);
891
1244
  lines.push(` height = <${nativeH}>;`);
892
1245
  if (controller === 'st7796s') {
@@ -896,11 +1249,70 @@ function emitDisplayNode(
896
1249
  lines.push(' madctl = <0x28>;');
897
1250
  lines.push(' pgc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
898
1251
  lines.push(' ngc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
899
- } else {
1252
+ // rgb-is-inverted flips the format the driver REPORTS (565 <-> 565X).
1253
+ // Zephyr RGB_565 is little-endian storage, but an 8-bit SPI panel clocks
1254
+ // the LOW byte first — each pixel arrives byte-swapped, which mixes the
1255
+ // green channel into red/blue (white text reads purple, dark backgrounds
1256
+ // read green — verified on the rig). With the flag, the driver reports
1257
+ // 565X and the adapter byte-swaps at pack time, restoring wire order.
1258
+ // (The binding documents the prop for "buggy modules that display RGB as
1259
+ // BGR" — same mechanism, byte order.)
1260
+ lines.push(' rgb-is-inverted;');
1261
+ } else if (controller === 'ili9341') {
900
1262
  // ILI9341: the ilitek,ili9341 binding carries defaults for every register
901
1263
  // (gamma, power, porch) and expresses orientation via `rotation` (degrees)
902
1264
  // instead of a raw MADCTL — no panel-specific props are required.
903
1265
  lines.push(` rotation = <${display.rotation ?? 0}>;`);
1266
+ // pixel-format is REQUIRED by the lcd-controller binding the ilitek
1267
+ // ili9xxx family includes: 0 = RGB565 (dt-bindings/display/panel.h),
1268
+ // matching upstream ILI9341 boards (esp_wrover_kit) and the C++ runtime.
1269
+ // The sitronix,st7796s binding does NOT declare pixel-format (its branch
1270
+ // omits the property — emitting it there is a DTC error, caught the hard
1271
+ // way once the overlay stopped carrying the wrong ilitek compatible).
1272
+ lines.push(' pixel-format = <0>;');
1273
+ } else {
1274
+ // Generic (drop-in) path: required properties come from the panel's own
1275
+ // binding in the user's Zephyr tree — the harvest is the single source
1276
+ // of truth, so the node is valid for ANY bound panel by construction.
1277
+ // Geometry/frequency props above are already emitted; pixel-format is
1278
+ // special (required by the lcd-controller family, value fixed to RGB565).
1279
+ const binding = readDisplayBinding(compatible);
1280
+ if (binding?.requiresPixelFormat) {
1281
+ lines.push(' pixel-format = <0>;');
1282
+ }
1283
+ if (binding) {
1284
+ for (const [prop, def] of binding.required) {
1285
+ // Skip structural properties the generator emits itself.
1286
+ if (prop === 'width' || prop === 'height' || prop === 'mipi-max-frequency'
1287
+ || prop === 'pixel-format' || prop === 'reg' || prop === 'compatible'
1288
+ || prop === 'status' || prop === 'mipi-mode'
1289
+ // phandle-array props are structural (GPIO routing) — the
1290
+ // generator emits them from config wiring (busy-gpios above),
1291
+ // never from a numeric fallback.
1292
+ || prop === 'busy-gpios' || prop === 'reset-gpios' || prop === 'dc-gpios') {
1293
+ continue;
1294
+ }
1295
+ emitBindingProp(lines, prop, def);
1296
+ }
1297
+ // Rotation on madctl-style panels (st7796s family): the binding ships
1298
+ // a neutral portrait default and the driver writes MADCTL verbatim, so
1299
+ // the config's rotation MUST reach the node or the UI's landscape
1300
+ // writes fall outside the portrait address window — half the screen
1301
+ // scrambled, half fine (verified on the rig). Table = ST77xx rotation
1302
+ // bits with BGR set (the verified clone convention; an R/B-swapped
1303
+ // panel flips to the config's channelSwapRb flag instead).
1304
+ // Byte-order quirk flag (clone panels): flips the format the driver
1305
+ // reports so the adapter byte-swaps at pack time. Only bindings that
1306
+ // declare the prop accept it — the DTC rejects it elsewhere, honestly.
1307
+ if (display.rgbInverted === true) {
1308
+ lines.push(' rgb-is-inverted;');
1309
+ }
1310
+ if (binding.props.has('madctl') && display.rotation !== undefined) {
1311
+ const MADCTL_ROTATION = [0x08, 0x28, 0x48, 0xE8];
1312
+ const r = ((display.rotation % 4) + 4) % 4;
1313
+ lines.push(` madctl = <0x${MADCTL_ROTATION[r]!.toString(16)}>;`);
1314
+ }
1315
+ }
904
1316
  }
905
1317
  lines.push(' };');
906
1318
  lines.push(' };');
@@ -995,11 +1407,16 @@ function emitFt6336uNode(lines: string[], touch?: TouchWiring): void {
995
1407
  lines.push(' ft6336u: ft6336u@38 {');
996
1408
  lines.push(' compatible = "focaltech,ft5336";');
997
1409
  lines.push(' reg = <0x38>;');
998
- // NOTE: int-gpios is intentionally omitted. The ft5336 Zephyr driver
999
- // registers a GPIO interrupt on int-gpios, which triggers an assertion
1000
- // failure in the ESP32 interrupt controller (VECDESC_FL_SHARED conflict).
1001
- // The cuttlefish touch adapter polls touch_isTouched() via I2C every frame
1002
- // — it never uses the IRQ pin, so the interrupt registration is unnecessary.
1410
+ // NOTE: int-gpios is intentionally omitted. The driver's interrupt mode
1411
+ // registers a GPIO IRQ (the ESP32 VECDESC_FL_SHARED crash); with no
1412
+ // int-gpios the in-tree driver falls back to polling mode
1413
+ // (CONFIG_INPUT_FT5336_PERIOD, default 10ms) — no IRQ is registered and
1414
+ // the touch adapter consumes the driver's input events.
1415
+ // swapped-x-y: the driver reports pos(col, row) — the Y-register value
1416
+ // first. The swap makes its ABS_X/ABS_Y events carry the controller's
1417
+ // X/Y registers verbatim, which is the raw coordinate space the runtime's
1418
+ // calibration + rotation math (ui_poll_touch) expects from touch_readRaw.
1419
+ lines.push(' swapped-x-y;');
1003
1420
  if (resetPin !== undefined) {
1004
1421
  lines.push(` reset-gpios = <&${gpioController(resetPin)} ${resetPin} GPIO_ACTIVE_LOW>;`);
1005
1422
  }