diff --git a/frontend/src/__tests__/esp32-uart-classify.test.ts b/frontend/src/__tests__/esp32-uart-classify.test.ts new file mode 100644 index 00000000..90d60b09 --- /dev/null +++ b/frontend/src/__tests__/esp32-uart-classify.test.ts @@ -0,0 +1,44 @@ +/** + * Regression: ESP32 UART pin names must classify as UART so the multi-board + * Interconnect bridges Serial between two ESP32s. + * + * Bug: a user wired two esp32-devkit-c-v4 boards TX2->RX2 (Serial2) and got no + * data on the receiver. classifyPin() returned 'digital' because: + * - the TX/RX aliases only matched boardKind === 'esp32' exactly (not the + * 'esp32-devkit-c-v4' / 'esp32-cam' / 'esp32-s3' variants), and + * - TX2/RX2 were not handled at all. + * So the wires were treated as raw digital and the byte-level UART shortcut was + * never installed. + */ +import { describe, it, expect } from 'vitest'; +import { classifyPin, isUartWire } from '../utils/boardProtocols'; + +describe('ESP32 UART pin classification (multi-board Serial)', () => { + const variants = ['esp32', 'esp32-devkit-c-v4', 'esp32-cam', 'esp32-s3']; + + for (const bk of variants) { + it(`${bk}: TX/RX -> UART0, TX2/RX2 -> UART2`, () => { + expect(classifyPin(bk, 'TX')).toEqual({ kind: 'uart-tx', uart: 0 }); + expect(classifyPin(bk, 'RX')).toEqual({ kind: 'uart-rx', uart: 0 }); + expect(classifyPin(bk, 'TX2')).toEqual({ kind: 'uart-tx', uart: 2 }); + expect(classifyPin(bk, 'RX2')).toEqual({ kind: 'uart-rx', uart: 2 }); + // GPIO-numbered pins keep working too. + expect(classifyPin(bk, 'GPIO17')).toEqual({ kind: 'uart-tx', uart: 2 }); + expect(classifyPin(bk, '16')).toEqual({ kind: 'uart-rx', uart: 2 }); + }); + } + + it('esp32-c3 uses its own UART0 pins (21/20)', () => { + expect(classifyPin('esp32-c3', 'TX')).toEqual({ kind: 'uart-tx', uart: 0 }); + expect(classifyPin('esp32-c3', 'RX')).toEqual({ kind: 'uart-rx', uart: 0 }); + // c3 has no UART2 — TX2 is not a UART pin. + expect(classifyPin('esp32-c3', 'TX2').kind).not.toBe('uart-tx'); + }); + + it('a TX2 -> RX2 wire between two ESP32s is a UART link', () => { + expect(isUartWire('esp32-devkit-c-v4', 'TX2', 'esp32-devkit-c-v4', 'RX2')).toBeTruthy(); + expect(isUartWire('esp32', 'TX', 'esp32', 'RX')).toBeTruthy(); + // GPIO-labelled pins resolve to the same link. + expect(isUartWire('esp32-devkit-c-v4', 'GPIO17', 'esp32-devkit-c-v4', 'GPIO16')).toBeTruthy(); + }); +}); diff --git a/frontend/src/utils/boardProtocols.ts b/frontend/src/utils/boardProtocols.ts index 146f89c1..e1bf91dc 100644 --- a/frontend/src/utils/boardProtocols.ts +++ b/frontend/src/utils/boardProtocols.ts @@ -199,8 +199,11 @@ function normalizePinName(boardKind: string, pinName: string): string | null { return null; } - // GP-prefix: "GP10" → "10" - if (trimmed.startsWith('GP')) { + // GP-prefix: "GP10" → "10" (RP2040). Exclude "GPIO..." (ESP32) — that is + // handled below; without this guard `parseInt("IO17")` = NaN swallowed every + // GPIOnn pin into null, so ESP32 wires drawn on GPIO-labelled pins never + // classified as UART/I2C/SPI. + if (trimmed.startsWith('GP') && !trimmed.startsWith('GPIO')) { const n = parseInt(trimmed.substring(2), 10); return isNaN(n) ? null : String(n); } @@ -226,26 +229,39 @@ function normalizePinName(boardKind: string, pinName: string): string | null { return isNaN(n) ? null : String(n); } - // TX/RX aliases — board-specific - if (trimmed === 'TX') { + // UART/I2C function-name aliases — board-specific. ESP32 silkscreens label + // pins by function (TX/RX = UART0, TX2/RX2 = UART2), so wires drawn against + // those labels must resolve to GPIO numbers. Use startsWith('esp32') (not an + // exact match) so every variant — esp32-devkit-c-v4, esp32-cam, esp32-s3, + // wemos-lolin32-lite — works, mirroring tableFor(). esp32-c3 has its own pins. + const isEsp32 = boardKind.startsWith('esp32'); + const isEsp32C3 = boardKind.startsWith('esp32-c3'); + if (trimmed === 'TX' || trimmed === 'TX0' || trimmed === 'TXD' || trimmed === 'TXD0') { if (boardKind === 'arduino-uno' || boardKind === 'arduino-nano') return '1'; if (boardKind === 'raspberry-pi-pico' || boardKind === 'pi-pico-w') return '0'; - if (boardKind === 'esp32') return '1'; + if (isEsp32C3) return '21'; + if (isEsp32) return '1'; } - if (trimmed === 'RX') { + if (trimmed === 'RX' || trimmed === 'RX0' || trimmed === 'RXD' || trimmed === 'RXD0') { if (boardKind === 'arduino-uno' || boardKind === 'arduino-nano') return '0'; if (boardKind === 'raspberry-pi-pico' || boardKind === 'pi-pico-w') return '1'; - if (boardKind === 'esp32') return '3'; + if (isEsp32C3) return '20'; + if (isEsp32) return '3'; } + // ESP32 UART2 (Serial2) default pins: TX2 = GPIO17, RX2 = GPIO16. c3 has no UART2. + if ((trimmed === 'TX2' || trimmed === 'TXD2') && isEsp32 && !isEsp32C3) return '17'; + if ((trimmed === 'RX2' || trimmed === 'RXD2') && isEsp32 && !isEsp32C3) return '16'; if (trimmed === 'SDA') { if (boardKind === 'arduino-uno' || boardKind === 'arduino-nano') return '18'; if (boardKind === 'raspberry-pi-pico' || boardKind === 'pi-pico-w') return '4'; - if (boardKind === 'esp32') return '21'; + if (isEsp32C3) return '5'; + if (isEsp32) return '21'; } if (trimmed === 'SCL') { if (boardKind === 'arduino-uno' || boardKind === 'arduino-nano') return '19'; if (boardKind === 'raspberry-pi-pico' || boardKind === 'pi-pico-w') return '5'; - if (boardKind === 'esp32') return '22'; + if (isEsp32C3) return '6'; + if (isEsp32) return '22'; } // STM32 port labels (PA9, PB12, PC13…) are used verbatim as table keys.