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": "",
"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": "",
+ "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": "",
- "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": "",
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": "",
- "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": "",
- "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": {