/** * Board protocol pin classifier. * * Given a (boardKind, pinName) pair, return the protocol role of that * pin (UART TX/RX, I2C SDA/SCL, SPI MISO/MOSI/SCK, raw digital, or * power). Used by the Interconnect router as a hint for enabling * byte-level UART shortcut on cross-process boards. * * Pin-level propagation works regardless of classification. The * classifier is purely an optimization — when a wire connects two * UART pins on at least one cross-process simulator (ESP32 / Pi3B), * we additionally route per-byte serial data so that high-baud links * don't drop bytes when the WebSocket round-trip would be too slow. * * Pin naming is normalized: "D7" → "7", "GP10" → "10". This module * uses the board's preferred naming convention as the lookup key, * but accepts numeric / D-prefix / GP-prefix interchangeably where * the board does. */ import type { BoardKind } from '../types/board'; export type PinRole = | { kind: 'uart-tx'; uart: number } | { kind: 'uart-rx'; uart: number } | { kind: 'i2c-sda'; bus: number } | { kind: 'i2c-scl'; bus: number } | { kind: 'spi-mosi'; bus: number } | { kind: 'spi-miso'; bus: number } | { kind: 'spi-sck'; bus: number } | { kind: 'spi-cs'; bus: number } | { kind: 'digital' } | { kind: 'power' }; // ── Per-board protocol pin tables ──────────────────────────────────────────── /** * Each entry maps a NORMALIZED pin name (the most canonical form for * that board) to a PinRole. The classifier accepts aliases by trying * several normalization forms. */ type RoleTable = Record; const ARDUINO_UNO: RoleTable = { '0': { kind: 'uart-rx', uart: 0 }, '1': { kind: 'uart-tx', uart: 0 }, '11': { kind: 'spi-mosi', bus: 0 }, '12': { kind: 'spi-miso', bus: 0 }, '13': { kind: 'spi-sck', bus: 0 }, '10': { kind: 'spi-cs', bus: 0 }, // Uno I2C is on A4 (= D18) / A5 (= D19) '18': { kind: 'i2c-sda', bus: 0 }, '19': { kind: 'i2c-scl', bus: 0 }, }; const ARDUINO_NANO = ARDUINO_UNO; // same pinout const ARDUINO_MEGA: RoleTable = { '0': { kind: 'uart-rx', uart: 0 }, '1': { kind: 'uart-tx', uart: 0 }, '19': { kind: 'uart-rx', uart: 1 }, '18': { kind: 'uart-tx', uart: 1 }, '17': { kind: 'uart-rx', uart: 2 }, '16': { kind: 'uart-tx', uart: 2 }, '15': { kind: 'uart-rx', uart: 3 }, '14': { kind: 'uart-tx', uart: 3 }, '20': { kind: 'i2c-sda', bus: 0 }, '21': { kind: 'i2c-scl', bus: 0 }, '50': { kind: 'spi-miso', bus: 0 }, '51': { kind: 'spi-mosi', bus: 0 }, '52': { kind: 'spi-sck', bus: 0 }, '53': { kind: 'spi-cs', bus: 0 }, }; const RP2040_DEFAULT: RoleTable = { // Default (Earle Philhower core) Serial1 = UART0 on GP0/GP1 '0': { kind: 'uart-tx', uart: 0 }, '1': { kind: 'uart-rx', uart: 0 }, // Default Serial2 = UART1 on GP4/GP5 (also default I2C0 — ambiguous; // we classify these as I2C since that's the most common Wokwi config) '4': { kind: 'i2c-sda', bus: 0 }, '5': { kind: 'i2c-scl', bus: 0 }, // I2C1 (alt) '6': { kind: 'i2c-sda', bus: 1 }, '7': { kind: 'i2c-scl', bus: 1 }, // SPI0 '16': { kind: 'spi-miso', bus: 0 }, '17': { kind: 'spi-cs', bus: 0 }, '18': { kind: 'spi-sck', bus: 0 }, '19': { kind: 'spi-mosi', bus: 0 }, // UART1 (alt — only if user explicitly wires there, not as I2C0) // Note: same pins as I2C0 → I2C wins by default classification above. }; const ESP32_DEFAULT: RoleTable = { '1': { kind: 'uart-tx', uart: 0 }, '3': { kind: 'uart-rx', uart: 0 }, // UART2 default '17': { kind: 'uart-tx', uart: 2 }, '16': { kind: 'uart-rx', uart: 2 }, // I2C0 '21': { kind: 'i2c-sda', bus: 0 }, '22': { kind: 'i2c-scl', bus: 0 }, // VSPI '23': { kind: 'spi-mosi', bus: 0 }, '19': { kind: 'spi-miso', bus: 0 }, '18': { kind: 'spi-sck', bus: 0 }, '5': { kind: 'spi-cs', bus: 0 }, }; const ESP32_C3_DEFAULT: RoleTable = { '21': { kind: 'uart-tx', uart: 0 }, '20': { kind: 'uart-rx', uart: 0 }, '5': { kind: 'i2c-sda', bus: 0 }, '6': { kind: 'i2c-scl', bus: 0 }, }; // Pi3B uses BCM numbers internally (after physical→BCM translation) const PI3_BCM: RoleTable = { '14': { kind: 'uart-tx', uart: 0 }, '15': { kind: 'uart-rx', uart: 0 }, '2': { kind: 'i2c-sda', bus: 1 }, '3': { kind: 'i2c-scl', bus: 1 }, '10': { kind: 'spi-mosi', bus: 0 }, '9': { kind: 'spi-miso', bus: 0 }, '11': { kind: 'spi-sck', bus: 0 }, '8': { kind: 'spi-cs', bus: 0 }, }; // ── Pi3B physical → BCM mirror (matches frontend/src/utils/boardPinMapping.ts) ─ const PI3_PHYSICAL_TO_BCM: Record = { 3: 2, 5: 3, 7: 4, 8: 14, 10: 15, 11: 17, 12: 18, 13: 27, 15: 22, 16: 23, 18: 24, 19: 10, 21: 9, 22: 25, 23: 11, 24: 8, 26: 7, 29: 5, 31: 6, 32: 12, 33: 13, 35: 19, 36: 16, 37: 26, 38: 20, 40: 21, }; // ── Master table ───────────────────────────────────────────────────────────── function tableFor(boardKind: BoardKind | string): RoleTable | null { if (boardKind === 'arduino-uno' || boardKind === 'arduino-nano') return ARDUINO_UNO; if (boardKind === 'arduino-mega') return ARDUINO_MEGA; if (boardKind === 'raspberry-pi-pico' || boardKind === 'pi-pico-w') return RP2040_DEFAULT; if (boardKind === 'esp32-c3' || (boardKind as string).startsWith('esp32-c3')) return ESP32_C3_DEFAULT; if (boardKind === 'esp32' || (boardKind as string).startsWith('esp32')) return ESP32_DEFAULT; if (boardKind === 'raspberry-pi-3' || (boardKind as string).startsWith('raspberry-pi-3')) return PI3_BCM; return ARDUINO_NANO; // default fallback: treat unknown as arduino-uno-like } // ── Public API ─────────────────────────────────────────────────────────────── /** * Normalize a pin name to a numeric string, suitable for table lookup. * Returns null for power pins (GND/VCC/3V3/5V/VBUS/VSYS). */ function normalizePinName(boardKind: string, pinName: string): string | null { const trimmed = pinName.trim().toUpperCase(); if ( trimmed === 'GND' || trimmed === 'VCC' || trimmed === '3V3' || trimmed.startsWith('3.3') || trimmed === '5V' || trimmed === 'VBUS' || trimmed === 'VSYS' || trimmed === 'AREF' ) { return null; } // Pi3B accepts physical pin numbers (1..40) which map to BCM if (boardKind === 'raspberry-pi-3' || boardKind.startsWith('raspberry-pi-3')) { const phys = parseInt(trimmed, 10); if (!isNaN(phys)) { const bcm = PI3_PHYSICAL_TO_BCM[phys]; return bcm !== undefined ? String(bcm) : null; } return null; } // GP-prefix: "GP10" → "10" if (trimmed.startsWith('GP')) { const n = parseInt(trimmed.substring(2), 10); return isNaN(n) ? null : String(n); } // D-prefix: "D7" → "7" if (trimmed.startsWith('D')) { const n = parseInt(trimmed.substring(1), 10); if (!isNaN(n)) return String(n); } // A-prefix on Uno/Nano: A0..A5 → 14..19; on Mega: A0..A15 → 54..69 if (trimmed.startsWith('A')) { const n = parseInt(trimmed.substring(1), 10); if (!isNaN(n)) { if (boardKind === 'arduino-mega') return String(54 + n); return String(14 + n); // Uno/Nano } } // GPIO-prefix (ESP32): "GPIO5" → "5" if (trimmed.startsWith('GPIO')) { const n = parseInt(trimmed.substring(4), 10); return isNaN(n) ? null : String(n); } // TX/RX aliases — board-specific if (trimmed === 'TX') { 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 (trimmed === 'RX') { 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 (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 (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'; } // Bare numeric const n = parseInt(trimmed, 10); return isNaN(n) ? null : String(n); } export function classifyPin(boardKind: string, pinName: string): PinRole { const trimmed = pinName.trim().toUpperCase(); if ( trimmed === 'GND' || trimmed === 'VCC' || trimmed === '3V3' || trimmed.startsWith('3.3') || trimmed === '5V' || trimmed === 'VBUS' || trimmed === 'VSYS' ) { return { kind: 'power' }; } const normalized = normalizePinName(boardKind, pinName); if (normalized === null) return { kind: 'digital' }; // unknown alias: treat as raw digital const table = tableFor(boardKind); const role = table?.[normalized]; return role ?? { kind: 'digital' }; } /** * Returns true if both endpoints of a wire are UART pins on the same * UART number (one TX, one RX). Used to enable byte-level shortcut. */ export function isUartWire( boardA: string, pinA: string, boardB: string, pinB: string, ): { uartA: number; uartB: number } | null { const ra = classifyPin(boardA, pinA); const rb = classifyPin(boardB, pinB); if (ra.kind === 'uart-tx' && rb.kind === 'uart-rx') return { uartA: ra.uart, uartB: rb.uart }; if (ra.kind === 'uart-rx' && rb.kind === 'uart-tx') return { uartA: ra.uart, uartB: rb.uart }; return null; }