/** * piSlaveScanner — turn canvas wires into pi_attach_slave commands. * * When the user wires a virtual I2C/SPI/UART component (e.g. BMP280) * to a Raspberry Pi's protocol pins on the canvas, this helper detects * the connection and emits the backend WebSocket frame that tells the * pro overlay to instantiate the corresponding slave model in the * PiSlaveRegistry. * * The mapping from a wokwi component metadata ID to a backend * model_id lives in COMPONENT_TO_MODEL. New devices are added there * plus a new file under pro/backend/app/pro/services/pi_slaves/. */ import type { RaspberryPi3Bridge } from './RaspberryPi3Bridge'; type CanvasWire = { start: { componentId: string; pinName: string }; end: { componentId: string; pinName: string }; }; /** Shape we depend on from useSimulatorStore.Component. */ type CanvasComponent = { id: string; metadataId: string; properties?: Record; }; // Raspberry Pi 40-pin header → bus assignment. Keys are physical pin // numbers as strings (matching the wire endpoint pinName format). // - SDA1/SCL1 → I2C bus 1 // - MOSI/MISO/SCLK → SPI bus 0 (CE0/CE1 distinguish slaves) // - TXD/RXD → primary UART (port 0) const I2C_PINS = new Set(['3', '5']); const SPI_DATA_PINS = new Set(['19', '21', '23']); const SPI_CE0_PIN = '24'; const SPI_CE1_PIN = '26'; const UART_PINS = new Set(['8', '10']); // Map wokwi component metadata IDs / element types → backend model_id. // Lowercase. Components not in this table get skipped silently. const COMPONENT_TO_MODEL: Record = { 'wokwi-bmp280': 'bme280', // same chip family, treat as superset 'velxio-bmp280': 'bme280', 'wokwi-bme280': 'bme280', }; const DEFAULT_I2C_ADDRESS: Record = { bme280: 0x76, }; function modelIdFor(component: CanvasComponent): string | null { const t = component.metadataId?.toLowerCase(); return t ? COMPONENT_TO_MODEL[t] ?? null : null; } function i2cAddressFor(component: CanvasComponent, modelId: string): number { const propAddr = component.properties?.address; if (typeof propAddr === 'number') return propAddr; if (typeof propAddr === 'string' && propAddr.length > 0) { const parsed = Number(propAddr); if (!Number.isNaN(parsed)) return parsed; } return DEFAULT_I2C_ADDRESS[modelId] ?? 0x76; } function configFor(component: CanvasComponent): Record { // Initial pass: only forward known keys so backend ctor surface is // narrow. Each model declares its own kwargs. const out: Record = {}; const p = component.properties ?? {}; for (const key of ['temperature_c', 'humidity_pct', 'pressure_pa']) { if (typeof p[key] === 'number') out[key] = p[key]; } return out; } /** * Returns the (bus_kind, identifying_pin) describing how `pinName` * on the Pi is being used, or null if it isn't a protocol pin. */ function classifyPiPin(pinName: string): { bus_kind: 'i2c' | 'spi' | 'uart'; bus_num: number; } | null { if (I2C_PINS.has(pinName)) return { bus_kind: 'i2c', bus_num: 1 }; if (SPI_DATA_PINS.has(pinName) || pinName === SPI_CE0_PIN || pinName === SPI_CE1_PIN) return { bus_kind: 'spi', bus_num: 0 }; if (UART_PINS.has(pinName)) return { bus_kind: 'uart', bus_num: 0 }; return null; } /** * Iterate every wire; if one side is a Pi protocol pin and the other * side is a known I2C/SPI/UART component, emit pi_attach_slave. * * Returns the list of attach specs emitted (for tests). */ export function attachSlavesFromCanvas( piBoardId: string, bridge: Pick, components: CanvasComponent[], wires: CanvasWire[], ): Array[0]> { const componentsById = new Map(components.map((c) => [c.id, c])); // dedupe attaches by (bus_kind, bus_num, address_or_cs) const seen = new Set(); const emitted: Array[0]> = []; for (const wire of wires) { let piEndpoint: { pinName: string } | null = null; let otherEndpoint: { componentId: string } | null = null; if (wire.start.componentId === piBoardId) { piEndpoint = wire.start; otherEndpoint = wire.end; } else if (wire.end.componentId === piBoardId) { piEndpoint = wire.end; otherEndpoint = wire.start; } else { continue; } const bus = classifyPiPin(piEndpoint.pinName); if (!bus) continue; const peer = componentsById.get(otherEndpoint.componentId); if (!peer) continue; const modelId = modelIdFor(peer); if (!modelId) continue; let spec: Parameters[0]; if (bus.bus_kind === 'i2c') { const address = i2cAddressFor(peer, modelId); spec = { bus_kind: 'i2c', bus_num: bus.bus_num, address, model_id: modelId, config: configFor(peer), }; } else if (bus.bus_kind === 'spi') { // CS line determines logical slave index. Treat MOSI/MISO/SCLK // wires as informational only — the CE wire is the one that // pins down which slave gets attached. let cs: number; if (piEndpoint.pinName === SPI_CE0_PIN) cs = 0; else if (piEndpoint.pinName === SPI_CE1_PIN) cs = 1; else continue; spec = { bus_kind: 'spi', bus_num: bus.bus_num, cs, model_id: modelId, config: configFor(peer), }; } else { spec = { bus_kind: 'uart', bus_num: bus.bus_num, model_id: modelId, config: configFor(peer), }; } const key = spec.bus_kind === 'i2c' ? `i2c:${spec.bus_num}:${spec.address}` : spec.bus_kind === 'spi' ? `spi:${spec.bus_num}:${spec.cs}` : `uart:${spec.bus_num}`; if (seen.has(key)) continue; seen.add(key); bridge.attachSlave(spec); emitted.push(spec); } return emitted; }