feat(ssd1306): one auto-detecting OLED part (merge the I2C/SPI picker entries)

The SSD1306 was three picker entries — a generic `ssd1306` with a protocol
selector plus `ssd1306-i2c` / `ssd1306-spi` shortcuts (issue #101) — all the
same 8-pin wokwi-ssd1306 element. That is confusing for one physical module
(issue #215). Wokwi ships a single I2C-only part; this goes one better: a
single part that auto-detects the protocol from the wiring, like a real
breadboard — CS or DC wired to a GPIO means SPI, otherwise I2C. No protocol
switch to set, just wire it up.

Works on every board with an I2C/SPI bus (AVR, RP2040, ESP32 Xtensa, STM32).
The ssd1306-i2c / ssd1306-spi ids stay as backward-compat simulation aliases
for projects saved before the merge, but are removed from the picker. Adds an
i2cAddress property (0x3c/0x3d) matching the real module and Wokwi.

Note: ESP32-C3, Raspberry Pi 3 and the bare RISC-V board do not emulate I2C/SPI
peripherals, so no I2C/SPI device (this or any other) attaches there yet.
This commit is contained in:
David Montero 2026-07-09 21:46:40 +02:00
parent 061bb91da1
commit b6dd2f5201
4 changed files with 111 additions and 137 deletions

View File

@ -1430,7 +1430,7 @@
{
"id": "ssd1306",
"tagName": "wokwi-ssd1306",
"name": "SSD1306",
"name": "SSD1306 OLED",
"category": "displays",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" fill=\"#e0e0e0\" rx=\"4\"/>\n <text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" font-size=\"10\" fill=\"#666\">\n SSD1306\n </text>\n </svg>",
"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": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#0b1226\" rx=\"4\"/><rect x=\"8\" y=\"14\" width=\"48\" height=\"32\" rx=\"2\" fill=\"#000\"/><text x=\"32\" y=\"30\" text-anchor=\"middle\" font-size=\"7\" font-family=\"monospace\" fill=\"#9be1ff\" font-weight=\"bold\">SSD1306</text><text x=\"32\" y=\"40\" text-anchor=\"middle\" font-size=\"6\" font-family=\"monospace\" fill=\"#9be1ff\">I2C 0x3C</text><text x=\"32\" y=\"60\" text-anchor=\"middle\" font-size=\"7\" font-family=\"sans-serif\" fill=\"#e5e7eb\" font-weight=\"bold\">SDA SCL</text></svg>",
"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": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#0b1226\" rx=\"4\"/><rect x=\"8\" y=\"14\" width=\"48\" height=\"32\" rx=\"2\" fill=\"#000\"/><text x=\"32\" y=\"30\" text-anchor=\"middle\" font-size=\"7\" font-family=\"monospace\" fill=\"#9be1ff\" font-weight=\"bold\">SSD1306</text><text x=\"32\" y=\"40\" text-anchor=\"middle\" font-size=\"6\" font-family=\"monospace\" fill=\"#9be1ff\">SPI mode</text><text x=\"32\" y=\"60\" text-anchor=\"middle\" font-size=\"6\" font-family=\"sans-serif\" fill=\"#e5e7eb\" font-weight=\"bold\">MOSI/SCK/DC</text></svg>",
"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": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" fill=\"#e0e0e0\" rx=\"4\"/>\n <text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" font-size=\"10\" fill=\"#666\">\n MOTOR-DRIVER-L293D\n </text>\n </svg>",

View File

@ -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', () => {

View File

@ -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'),

View File

@ -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": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#0b1226\" rx=\"4\"/><rect x=\"8\" y=\"14\" width=\"48\" height=\"32\" rx=\"2\" fill=\"#000\"/><text x=\"32\" y=\"30\" text-anchor=\"middle\" font-size=\"7\" font-family=\"monospace\" fill=\"#9be1ff\" font-weight=\"bold\">SSD1306</text><text x=\"32\" y=\"40\" text-anchor=\"middle\" font-size=\"6\" font-family=\"monospace\" fill=\"#9be1ff\">I2C 0x3C</text><text x=\"32\" y=\"60\" text-anchor=\"middle\" font-size=\"7\" font-family=\"sans-serif\" fill=\"#e5e7eb\" font-weight=\"bold\">SDA SCL</text></svg>",
"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": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#0b1226\" rx=\"4\"/><rect x=\"8\" y=\"14\" width=\"48\" height=\"32\" rx=\"2\" fill=\"#000\"/><text x=\"32\" y=\"30\" text-anchor=\"middle\" font-size=\"7\" font-family=\"monospace\" fill=\"#9be1ff\" font-weight=\"bold\">SSD1306</text><text x=\"32\" y=\"40\" text-anchor=\"middle\" font-size=\"6\" font-family=\"monospace\" fill=\"#9be1ff\">SPI mode</text><text x=\"32\" y=\"60\" text-anchor=\"middle\" font-size=\"6\" font-family=\"sans-serif\" fill=\"#e5e7eb\" font-weight=\"bold\">MOSI/SCK/DC</text></svg>",
"properties": [
{
"name": "imageData",
"type": "string",
"control": "text"
}
],
"defaultValues": {
"protocol": "spi"
},
"pinCount": 7,
"tags": [
"ssd1306",
"oled",
"display",
"spi"
]
}
],
"led": {
@ -2525,21 +2474,28 @@
}
},
"ssd1306": {
"properties": {
"protocol": {
"name": "protocol",
"type": "string",
"defaultValue": "i2c",
"control": "select",
"description": "Communication protocol",
"options": [
"$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"
]
"spi",
"0x3c"
],
"properties": {
"i2cAddress": {
"name": "i2cAddress",
"type": "string",
"defaultValue": "0x3c",
"control": "text",
"description": "I2C address (I2C mode)"
}
},
"defaultValues": {
"protocol": "i2c"
"i2cAddress": "0x3c"
}
},
"resistor": {