diff --git a/frontend/src/__tests__/protocol-parts.test.ts b/frontend/src/__tests__/protocol-parts.test.ts index 55de1a24..de94b89b 100644 --- a/frontend/src/__tests__/protocol-parts.test.ts +++ b/frontend/src/__tests__/protocol-parts.test.ts @@ -17,6 +17,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { PartSimulationRegistry } from '../simulation/parts/PartSimulationRegistry'; import { dispatchSensorUpdate } from '../simulation/SensorUpdateRegistry'; +import { useSimulatorStore } from '../store/useSimulatorStore'; import '../simulation/parts/ProtocolParts'; // ─── Globals ────────────────────────────────────────────────────────────────── @@ -228,31 +229,35 @@ describe('ssd1306 — protocol auto-detect', () => { expect(sim.addI2CDevice).not.toHaveBeenCalled(); }); - it('runs SPI when DC is wired to a GPIO', () => { - const sim = makeSPISim(); + it('runs I2C when only DC is wired (DC is the I2C address-select, not SPI)', () => { + const sim = makeI2CSim(); PartSimulationRegistry.get('ssd1306')!.attachEvents!( makeElement(), sim as any, pinMap({ DC: 4 }), ); - expect(typeof sim.spi.onByte).toBe('function'); - expect(sim.addI2CDevice).not.toHaveBeenCalled(); - }); - - it('legacy ssd1306-i2c alias forces I2C even with CS wired', () => { - const sim = makeI2CSim(); - PartSimulationRegistry.get('ssd1306-i2c')!.attachEvents!( - makeElement(), - sim as any, - pinMap({ CS: 5 }), - ); expect(sim.addI2CDevice).toHaveBeenCalledOnce(); }); - it('legacy ssd1306-spi alias forces SPI even with nothing wired', () => { - const sim = makeSPISim(); - PartSimulationRegistry.get('ssd1306-spi')!.attachEvents!(makeElement(), sim as any, noPins); - expect(typeof sim.spi.onByte).toBe('function'); + it('honors an explicit protocol property (migrated legacy projects)', () => { + // A project migrated from the old ssd1306-spi entry carries protocol:'spi'; + // it must run SPI even though nothing SPI-specific is wired (CS absent). + useSimulatorStore.setState({ + components: [{ id: 'oled-legacy', metadataId: 'ssd1306', properties: { protocol: 'spi' } }], + } as any); + try { + const sim = makeSPISim(); + PartSimulationRegistry.get('ssd1306')!.attachEvents!( + makeElement(), + sim as any, + noPins, + 'oled-legacy', + ); + expect(typeof sim.spi.onByte).toBe('function'); + expect(sim.addI2CDevice).not.toHaveBeenCalled(); + } finally { + useSimulatorStore.setState({ components: [] } as any); + } }); }); diff --git a/frontend/src/simulation/parts/ProtocolParts.ts b/frontend/src/simulation/parts/ProtocolParts.ts index fd998356..17835fc6 100644 --- a/frontend/src/simulation/parts/ProtocolParts.ts +++ b/frontend/src/simulation/parts/ProtocolParts.ts @@ -329,8 +329,8 @@ function attachSSD1306SPI( /** * Internal: SSD1306 attach logic, parameterised over the wire protocol. - * Used by the three picker entries (the generic `ssd1306` plus the two - * dedicated `ssd1306-i2c` / `ssd1306-spi` shortcuts). + * Called by the single `ssd1306` entry once the protocol has been resolved + * (auto-detected from the wiring, or read from an explicit `protocol` property). */ function attachSSD1306( element: HTMLElement, @@ -376,52 +376,43 @@ function attachSSD1306( } /** - * Which wire protocol did the user build? A real SSD1306 breakout is ONE - * board that talks either I2C or SPI depending on how it is wired: SPI drives - * the chip-select (CS) and data/command (DC) lines from MCU GPIOs, while I2C - * leaves them tied to power (address select) or unconnected. So if CS or DC is - * wired to a GPIO we decode SPI; otherwise I2C. This mirrors the physical part - * — one component, no protocol switch to set, just wire it up. + * Which wire protocol did the user build? A real SSD1306 breakout is ONE board + * that talks either I2C or SPI depending on how it is wired. The definitive + * SPI-only signal is chip-select (CS): I2C never uses it. (DC deliberately does + * NOT count — on the 8-pin module DC doubles as the I2C address-select/SA0 line, + * so many I2C circuits wire it too.) So CS wired to a GPIO => SPI, otherwise + * I2C. This mirrors the physical part — one component, no protocol switch to + * set, just wire it up. * - * Pure wiring check: it deliberately does NOT read `simulator.spi`, whose - * getter on some boards (RP2040, and the ESP32/STM32 bridge shims) lazily - * re-routes the board SPI bus as a side effect and must not fire in I2C mode. + * Pure wiring check: it deliberately does NOT read `simulator.spi`, whose getter + * on some boards (RP2040, and the ESP32/STM32 bridge shims) lazily re-routes the + * board SPI bus as a side effect and must not fire in I2C mode. */ function detectSSD1306Protocol(getPin: (n: string) => number | null): 'i2c' | 'spi' { - return getPin('CS') !== null || getPin('DC') !== null ? 'spi' : 'i2c'; + return getPin('CS') !== null ? 'spi' : 'i2c'; } /** * SSD1306 OLED — a single component that works on every board with an I2C or - * SPI bus (AVR, RP2040, ESP32, STM32), auto-detecting the protocol from the - * wiring like the physical module. Consolidates the old ssd1306 / ssd1306-i2c - * / ssd1306-spi picker entries into one (issues #101 / #215). + * SPI bus (AVR, RP2040, ESP32, STM32). New projects just wire it up and the + * protocol is auto-detected from the wiring like the physical module; a + * `protocol` property, when present, pins it explicitly (projects migrated from + * the old ssd1306-i2c / ssd1306-spi entries carry it so their behaviour is + * preserved exactly). Consolidates the old three picker entries into one + * (issues #101 / #215). */ PartSimulationRegistry.register('ssd1306', { attachEvents: (element, simulator, getPin, componentId) => { const { components } = useSimulatorStore.getState(); const comp = components.find((c) => c.id === componentId); const i2cAddr = parseI2cAddress(comp?.properties?.i2cAddress, 0x3c); - const protocol = detectSSD1306Protocol(getPin); + const explicit = comp?.properties?.protocol; + const protocol: 'i2c' | 'spi' = + explicit === 'i2c' || explicit === 'spi' ? explicit : detectSSD1306Protocol(getPin); return attachSSD1306(element, simulator, getPin, protocol, i2cAddr); }, }); -/** - * Legacy aliases kept ONLY for projects saved before the picker entries merged - * into the single auto-detecting `ssd1306` above. New projects never carry - * these ids. They force a fixed protocol (no auto-detect) to reproduce the old - * behaviour exactly. - */ -PartSimulationRegistry.register('ssd1306-i2c', { - attachEvents: (element, simulator, getPin) => - attachSSD1306(element, simulator, getPin, 'i2c'), -}); -PartSimulationRegistry.register('ssd1306-spi', { - attachEvents: (element, simulator, getPin) => - attachSSD1306(element, simulator, getPin, 'spi'), -}); - // ─── DS1307 RTC ────────────────────────────────────────────────────────────── /** diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index a60b26a4..33dcef22 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -1374,8 +1374,23 @@ export const useSimulatorStore = create((set, get) => { // (createFileGroup is a no-op for existing ids) — overwrite their files. useEditorStore.getState().replaceFileGroups(payload.fileGroups); - // Components and wires - setComponents(payload.components); + // Components and wires. Normalize the retired ssd1306-i2c / ssd1306-spi + // ids (merged into the single auto-detecting `ssd1306`, issues #101/#215) + // so old .vlx files and pre-migration snapshots still render and simulate; + // the old id's protocol is pinned so behaviour is preserved exactly. + const normalizedComponents = payload.components.map((c) => + c.metadataId === 'ssd1306-i2c' || c.metadataId === 'ssd1306-spi' + ? { + ...c, + metadataId: 'ssd1306', + properties: { + protocol: c.metadataId === 'ssd1306-spi' ? 'spi' : 'i2c', + ...(c.properties ?? {}), + }, + } + : c, + ); + setComponents(normalizedComponents); setWires(payload.wires); // Active board: prefer the saved one, fall back to the first.