diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json index 64c75a13..6be612bb 100644 --- a/frontend/public/components-metadata.json +++ b/frontend/public/components-metadata.json @@ -1430,7 +1430,7 @@ { "id": "ssd1306", "tagName": "wokwi-ssd1306", - "name": "SSD1306", + "name": "SSD1306 OLED", "category": "displays", "thumbnail": "\n \n \n SSD1306\n \n ", "properties": [ @@ -1440,75 +1440,25 @@ "control": "text" }, { - "name": "protocol", + "name": "i2cAddress", "type": "string", - "defaultValue": "i2c", - "control": "select", - "description": "Communication protocol", - "options": [ - "i2c", - "spi" - ] + "defaultValue": "0x3c", + "control": "text", + "description": "I2C address (I2C mode)" } ], "defaultValues": { - "protocol": "i2c" + "i2cAddress": "0x3c" }, - "pinCount": 0, - "tags": [ - "ssd1306" - ] - }, - { - "thumbnail": "SSD1306I2C 0x3CSDA SCL", + "pinCount": 8, "tags": [ "ssd1306", "oled", "display", "i2c", + "spi", "0x3c" - ], - "properties": [ - { - "name": "imageData", - "type": "string", - "control": "text" - } - ], - "defaultValues": { - "protocol": "i2c" - }, - "pinCount": 4, - "$comment": "Picker shortcut for SSD1306 in I2C mode — same wokwi-ssd1306 element, but the user finds it by name without having to discover the protocol selector on the generic ssd1306 entry. Issue #101.", - "id": "ssd1306-i2c", - "tagName": "wokwi-ssd1306", - "name": "SSD1306 OLED (I2C)", - "category": "displays" - }, - { - "thumbnail": "SSD1306SPI modeMOSI/SCK/DC", - "tags": [ - "ssd1306", - "oled", - "display", - "spi" - ], - "properties": [ - { - "name": "imageData", - "type": "string", - "control": "text" - } - ], - "defaultValues": { - "protocol": "spi" - }, - "pinCount": 7, - "$comment": "Picker shortcut for SSD1306 in SPI mode — counterpart to ssd1306-i2c.", - "id": "ssd1306-spi", - "tagName": "wokwi-ssd1306", - "name": "SSD1306 OLED (SPI)", - "category": "displays" + ] }, { "thumbnail": "\n \n \n MOTOR-DRIVER-L293D\n \n ", diff --git a/frontend/src/__tests__/protocol-parts.test.ts b/frontend/src/__tests__/protocol-parts.test.ts index 160339bd..55de1a24 100644 --- a/frontend/src/__tests__/protocol-parts.test.ts +++ b/frontend/src/__tests__/protocol-parts.test.ts @@ -206,6 +206,56 @@ describe('ssd1306 — I2C device', () => { }); }); +// ─── ssd1306 — protocol auto-detect (one component, wired like real life) ───── + +describe('ssd1306 — protocol auto-detect', () => { + it('runs I2C when neither CS nor DC is wired', () => { + const sim = makeI2CSim(); + PartSimulationRegistry.get('ssd1306')!.attachEvents!(makeElement(), sim as any, noPins); + expect(sim.addI2CDevice).toHaveBeenCalledOnce(); + expect(sim.spi).toBeNull(); + }); + + it('runs SPI when CS is wired to a GPIO', () => { + const sim = makeSPISim(); + PartSimulationRegistry.get('ssd1306')!.attachEvents!( + makeElement(), + sim as any, + pinMap({ CS: 5 }), + ); + // SPI decoder hooks spi.onByte; the I2C path would call addI2CDevice instead. + expect(typeof sim.spi.onByte).toBe('function'); + expect(sim.addI2CDevice).not.toHaveBeenCalled(); + }); + + it('runs SPI when DC is wired to a GPIO', () => { + const sim = makeSPISim(); + 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'); + }); +}); + // ─── ds1307 ─────────────────────────────────────────────────────────────────── describe('ds1307 — I2C RTC', () => { diff --git a/frontend/src/simulation/parts/ProtocolParts.ts b/frontend/src/simulation/parts/ProtocolParts.ts index 0fbbb023..fd998356 100644 --- a/frontend/src/simulation/parts/ProtocolParts.ts +++ b/frontend/src/simulation/parts/ProtocolParts.ts @@ -337,20 +337,22 @@ function attachSSD1306( simulator: unknown, getPin: (n: string) => number | null, protocol: 'i2c' | 'spi', + i2cAddr = 0x3c, ): () => void { if (protocol === 'spi') { return attachSSD1306SPI(element, simulator, getPin); } const sim = simulator as any; - const i2cAddr = 0x3c; const device = new VirtualSSD1306(i2cAddr, element); - // Check ESP32 first — its shim exposes BOTH registerSensor (for the - // backend QEMU slave) AND addI2CDevice (for the frontend bus used by - // the Interconnect cross-board bridge). AVR / RP2040 only expose - // addI2CDevice, so registerSensor is the unambiguous ESP32 marker. + // The ESP32/STM32 bridge shims expose registerSensor (backend QEMU slave) + + // addI2CTransactionListener (framebuffer bytes streamed back) AND addI2CDevice + // (frontend bus for the cross-board Interconnect). AVR / RP2040 also carry a + // registerSensor() stub that returns false, so they enter this branch too — + // harmlessly: registerSensor no-ops, the absent addI2CTransactionListener is + // skipped, and the real attach happens via the addI2CDevice mirror below. if (typeof sim.registerSensor === 'function') { - // ── ESP32 path ───────────────────────────────────────────────────────── + // ── ESP32 / STM32 (and AVR/RP2040 via the addI2CDevice mirror) ────────── const virtualPin = 200 + i2cAddr; sim.registerSensor('ssd1306', virtualPin, { addr: i2cAddr }); sim.addI2CTransactionListener?.(i2cAddr, (data: number[]) => { @@ -374,31 +376,47 @@ function attachSSD1306( } /** - * Generic `ssd1306` entry — reads the user-selectable `protocol` - * property (control: select, options: i2c | spi). Keeps backward - * compatibility for existing projects whose components carry this id. + * 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. + * + * 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'; +} + +/** + * 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). */ PartSimulationRegistry.register('ssd1306', { attachEvents: (element, simulator, getPin, componentId) => { const { components } = useSimulatorStore.getState(); const comp = components.find((c) => c.id === componentId); - const protocol = ((comp?.properties?.protocol as string) ?? 'i2c') as 'i2c' | 'spi'; - return attachSSD1306(element, simulator, getPin, protocol); + const i2cAddr = parseI2cAddress(comp?.properties?.i2cAddress, 0x3c); + const protocol = detectSSD1306Protocol(getPin); + return attachSSD1306(element, simulator, getPin, protocol, i2cAddr); }, }); /** - * Picker shortcut: "SSD1306 OLED (I2C)" — same web component, but the - * metadata defaults protocol to 'i2c' and the part skips the property - * lookup. Lets users find the I2C variant by name without having to - * discover the protocol property on the generic ssd1306 entry. + * 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'), }); - -/** Picker shortcut: "SSD1306 OLED (SPI)" — counterpart to ssd1306-i2c. */ PartSimulationRegistry.register('ssd1306-spi', { attachEvents: (element, simulator, getPin) => attachSSD1306(element, simulator, getPin, 'spi'), diff --git a/scripts/component-overrides.json b/scripts/component-overrides.json index f5202828..960369ef 100644 --- a/scripts/component-overrides.json +++ b/scripts/component-overrides.json @@ -2455,57 +2455,6 @@ "2004", "liquidcrystal" ] - }, - { - "$comment": "Picker shortcut for SSD1306 in I2C mode — same wokwi-ssd1306 element, but the user finds it by name without having to discover the protocol selector on the generic ssd1306 entry. Issue #101.", - "id": "ssd1306-i2c", - "tagName": "wokwi-ssd1306", - "name": "SSD1306 OLED (I2C)", - "category": "displays", - "thumbnail": "SSD1306I2C 0x3CSDA SCL", - "properties": [ - { - "name": "imageData", - "type": "string", - "control": "text" - } - ], - "defaultValues": { - "protocol": "i2c" - }, - "pinCount": 4, - "tags": [ - "ssd1306", - "oled", - "display", - "i2c", - "0x3c" - ] - }, - { - "$comment": "Picker shortcut for SSD1306 in SPI mode — counterpart to ssd1306-i2c.", - "id": "ssd1306-spi", - "tagName": "wokwi-ssd1306", - "name": "SSD1306 OLED (SPI)", - "category": "displays", - "thumbnail": "SSD1306SPI modeMOSI/SCK/DC", - "properties": [ - { - "name": "imageData", - "type": "string", - "control": "text" - } - ], - "defaultValues": { - "protocol": "spi" - }, - "pinCount": 7, - "tags": [ - "ssd1306", - "oled", - "display", - "spi" - ] } ], "led": { @@ -2525,21 +2474,28 @@ } }, "ssd1306": { + "$comment": "Single SSD1306 OLED entry. One physical breakout; the sim auto-detects I2C vs SPI from the wiring (see ProtocolParts.detectSSD1306Protocol). Replaces the old ssd1306-i2c/ssd1306-spi picker shortcuts (issues #101/#215).", + "name": "SSD1306 OLED", + "pinCount": 8, + "tags": [ + "ssd1306", + "oled", + "display", + "i2c", + "spi", + "0x3c" + ], "properties": { - "protocol": { - "name": "protocol", + "i2cAddress": { + "name": "i2cAddress", "type": "string", - "defaultValue": "i2c", - "control": "select", - "description": "Communication protocol", - "options": [ - "i2c", - "spi" - ] + "defaultValue": "0x3c", + "control": "text", + "description": "I2C address (I2C mode)" } }, "defaultValues": { - "protocol": "i2c" + "i2cAddress": "0x3c" } }, "resistor": {