diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json
index 6be612bb..0f0ad0fe 100644
--- a/frontend/public/components-metadata.json
+++ b/frontend/public/components-metadata.json
@@ -4451,6 +4451,34 @@
"motion",
"sensor"
]
+ },
+ {
+ "id": "ssd1306-i2c-4pin",
+ "tagName": "velxio-ssd1306-i2c-4pin",
+ "name": "SSD1306 OLED (I2C, 4-pin)",
+ "category": "displays",
+ "pinCount": 4,
+ "tags": [
+ "ssd1306",
+ "oled",
+ "display",
+ "i2c",
+ "4-pin",
+ "0x3c"
+ ],
+ "thumbnail": "",
+ "properties": [
+ {
+ "name": "i2cAddress",
+ "type": "string",
+ "defaultValue": "0x3c",
+ "control": "text",
+ "description": "I2C address"
+ }
+ ],
+ "defaultValues": {
+ "i2cAddress": "0x3c"
+ }
}
]
}
\ No newline at end of file
diff --git a/frontend/src/components/ComponentPickerModal.tsx b/frontend/src/components/ComponentPickerModal.tsx
index 3b14b6dd..83250b6a 100644
--- a/frontend/src/components/ComponentPickerModal.tsx
+++ b/frontend/src/components/ComponentPickerModal.tsx
@@ -21,6 +21,7 @@ import { Attiny85 } from './velxio-components/Attiny85';
import './velxio-components/Esp32Element'; // registers velxio-esp32
import './velxio-components/PiPicoWElement'; // registers velxio-pi-pico-w
import './velxio-components/Stm32BluePillElement'; // registers velxio-stm32-bluepill
+import './velxio-components/Ssd1306I2cElement'; // registers velxio-ssd1306-i2c-4pin
// Register every wokwi tag that the picker might try to instantiate as a
// thumbnail. The picker calls `document.createElement(tagName)`, so any tag
// that isn't already a registered custom element renders as an empty
diff --git a/frontend/src/components/DynamicComponent.tsx b/frontend/src/components/DynamicComponent.tsx
index 615a1eaf..b6b114cd 100644
--- a/frontend/src/components/DynamicComponent.tsx
+++ b/frontend/src/components/DynamicComponent.tsx
@@ -38,6 +38,7 @@ import { getBoardLogicFamily } from '../simulation/LogicFamilies';
// ) that don't exist upstream.
import '@wokwi/elements';
import '../velxio-elements';
+import './velxio-components/Ssd1306I2cElement'; // registers velxio-ssd1306-i2c-4pin (4-pin I2C OLED)
// Map metadataId → [pinA, pinB] for 2-terminal passives.
// "Tracing through" means: if the caller arrived on pinA, continue from pinB
diff --git a/frontend/src/components/velxio-components/Ssd1306I2cElement.ts b/frontend/src/components/velxio-components/Ssd1306I2cElement.ts
new file mode 100644
index 00000000..03eac953
--- /dev/null
+++ b/frontend/src/components/velxio-components/Ssd1306I2cElement.ts
@@ -0,0 +1,160 @@
+/**
+ * Ssd1306I2cElement — `` Web Component.
+ *
+ * The common cheap 0.96" SSD1306 OLED **I2C module with only 4 pins**
+ * (GND / VCC / SCL / SDA) — the counterpart to the 8-pin `wokwi-ssd1306`
+ * breakout, and the layout most beginners actually have on their desk
+ * (matches Wokwi's `board-ssd1306`). A velxio-local element because
+ * `@wokwi/elements` only ships the 8-pin variant. See issue #215.
+ *
+ * Same rendering surface as `wokwi-ssd1306` so `SSD1306Core.syncElement`
+ * (simulation/parts/ProtocolParts.ts) drives it unchanged:
+ * - `element.imageData` — a 128×64 ImageData (RGBA)
+ * - `element.redraw()` — flushes imageData to the internal canvas
+ *
+ * Per CLAUDE.md §6a this MUST be a real Web Component — the wire system reads
+ * `pinInfo` from the DOM node to place wire endpoints on the pin tips.
+ */
+
+const SCREEN_W = 128;
+const SCREEN_H = 64;
+
+// Body geometry (CSS px). Kept visually in the same family as wokwi-ssd1306
+// (blue PCB, dark screen, corner mounting holes, star) but narrower — a 4-pin
+// header instead of 8. Screen sits at (SCREEN_X, SCREEN_Y).
+const BODY_W = 150;
+const BODY_H = 108;
+const SCREEN_X = 11;
+const SCREEN_Y = 30;
+
+// 4-pin 0.1" header, centred along the top edge. `x`/`y` are the pin tips the
+// wire system snaps to.
+const PIN_Y = 11;
+const PIN_SPACING = 15;
+const PIN_NAMES = ['GND', 'VCC', 'SCL', 'SDA'] as const;
+const PIN_X0 = BODY_W / 2 - ((PIN_NAMES.length - 1) * PIN_SPACING) / 2;
+// Ring colours echo the wokwi-ssd1306 pin styling.
+const PIN_RING = ['#B4AEAB', '#C08540', '#009E9B', '#007ADB'];
+
+function signalsFor(name: string) {
+ if (name === 'GND') return [{ type: 'power', signal: 'GND' }];
+ if (name === 'VCC') return [{ type: 'power', signal: 'VCC' }];
+ if (name === 'SCL') return [{ type: 'i2c', signal: 'SCL' }];
+ if (name === 'SDA') return [{ type: 'i2c', signal: 'SDA' }];
+ return [];
+}
+
+class Ssd1306I2cElement extends HTMLElement {
+ private _imageData: ImageData = new ImageData(SCREEN_W, SCREEN_H);
+ private ctx: CanvasRenderingContext2D | null = null;
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ }
+
+ connectedCallback(): void {
+ this.render();
+ this.initContext();
+ }
+
+ /** Wire system reads this from the DOM — do NOT memoize. */
+ get pinInfo() {
+ return PIN_NAMES.map((name, i) => ({
+ name,
+ x: PIN_X0 + i * PIN_SPACING,
+ y: PIN_Y,
+ number: i + 1,
+ signals: signalsFor(name),
+ }));
+ }
+
+ get canvas(): HTMLCanvasElement | null {
+ return this.shadowRoot?.querySelector('canvas') ?? null;
+ }
+
+ /** Accepts the ImageData pushed by SSD1306Core; ignores anything else (e.g. a
+ * stray string property assignment from the loader). */
+ set imageData(v: ImageData) {
+ if (v instanceof ImageData) this._imageData = v;
+ }
+ get imageData(): ImageData {
+ return this._imageData;
+ }
+
+ /** Flush the current imageData to the canvas (called by SSD1306Core). */
+ redraw(): void {
+ if (!this.ctx) this.initContext();
+ try {
+ this.ctx?.putImageData(this._imageData, 0, 0);
+ } catch {
+ /* canvas not ready yet */
+ }
+ }
+
+ private initContext(): void {
+ const c = this.canvas;
+ this.ctx = c?.getContext('2d') ?? null;
+ this.ctx?.putImageData(this._imageData, 0, 0);
+ }
+
+ private render(): void {
+ if (!this.shadowRoot) return;
+ const holes = [
+ [8, 8],
+ [BODY_W - 8, 8],
+ [8, BODY_H - 8],
+ [BODY_W - 8, BODY_H - 8],
+ ];
+ this.shadowRoot.innerHTML = `
+
+
+
+ `;
+ }
+}
+
+if (!customElements.get('velxio-ssd1306-i2c-4pin')) {
+ customElements.define('velxio-ssd1306-i2c-4pin', Ssd1306I2cElement);
+}
+
+export type { Ssd1306I2cElement };
diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts
index 2fe328e2..cec4ce55 100644
--- a/frontend/src/data/examples.ts
+++ b/frontend/src/data/examples.ts
@@ -91,6 +91,249 @@ export interface ExampleProject {
}
const legacyExamples: ExampleProject[] = [
+ // ── SSD1306 OLED — 4-pin I2C module (velxio-ssd1306-i2c-4pin) across boards.
+ // The cheap GND/VCC/SCL/SDA module beginners actually own (issue #215). ──
+ {
+ id: 'uno-oled-4pin-i2c',
+ title: 'Arduino Uno: SSD1306 OLED (4-pin I2C)',
+ description:
+ 'Drive the cheap 4-pin SSD1306 OLED module (GND/VCC/SCL/SDA) over I2C from an Arduino Uno (SDA=A4, SCL=A5). Shows "Hello Velxio!" with a live counter.',
+ libraries: ['Adafruit SSD1306', 'Adafruit GFX Library'],
+ category: 'displays',
+ difficulty: 'intermediate',
+ boardFilter: 'arduino-uno',
+ boards: [
+ {
+ boardKind: 'arduino-uno',
+ x: 60,
+ y: 60,
+ code: `// Arduino Uno — SSD1306 128x64 OLED (4-pin I2C module)
+// Wiring: SDA -> A4 | SCL -> A5 | VCC -> 5V | GND -> GND
+#include
+#include
+#include
+
+Adafruit_SSD1306 display(128, 64, &Wire, -1);
+int n = 0;
+
+void setup() {
+ Serial.begin(115200);
+ Wire.begin();
+ if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
+ Serial.println("SSD1306 not found!");
+ while (true) delay(10);
+ }
+ display.clearDisplay();
+ Serial.println("OLED ready");
+}
+
+void loop() {
+ n++;
+ display.clearDisplay();
+ display.setTextSize(2);
+ display.setTextColor(SSD1306_WHITE);
+ display.setCursor(0, 0);
+ display.println("Hello");
+ display.println("Velxio!");
+ display.setTextSize(1);
+ display.setCursor(0, 48);
+ display.print("Count: ");
+ display.print(n);
+ display.display();
+ delay(500);
+}`,
+ },
+ ],
+ code: '',
+ components: [{ type: 'velxio-ssd1306-i2c-4pin', id: 'oled', x: 470, y: 100, properties: {} }],
+ wires: [
+ { id: 'w-vcc', start: { componentId: 'arduino-uno', pinName: '5V' }, end: { componentId: 'oled', pinName: 'VCC' }, color: '#ff4444' },
+ { id: 'w-gnd', start: { componentId: 'arduino-uno', pinName: 'GND.2' }, end: { componentId: 'oled', pinName: 'GND' }, color: '#000000' },
+ { id: 'w-sda', start: { componentId: 'arduino-uno', pinName: 'A4' }, end: { componentId: 'oled', pinName: 'SDA' }, color: '#22aaff' },
+ { id: 'w-scl', start: { componentId: 'arduino-uno', pinName: 'A5' }, end: { componentId: 'oled', pinName: 'SCL' }, color: '#ff8800' },
+ ],
+ },
+ {
+ id: 'esp32-oled-4pin-i2c',
+ title: 'ESP32: SSD1306 OLED (4-pin I2C)',
+ description:
+ 'Drive the cheap 4-pin SSD1306 OLED module (GND/VCC/SCL/SDA) over I2C from an ESP32 (SDA=21, SCL=22).',
+ libraries: ['Adafruit SSD1306', 'Adafruit GFX Library'],
+ category: 'displays',
+ difficulty: 'intermediate',
+ boardFilter: 'esp32',
+ boards: [
+ {
+ boardKind: 'esp32',
+ x: 60,
+ y: 60,
+ code: `// ESP32 — SSD1306 128x64 OLED (4-pin I2C module)
+// Wiring: SDA -> 21 | SCL -> 22 | VCC -> 3V3 | GND -> GND
+#include
+#include
+#include
+
+Adafruit_SSD1306 display(128, 64, &Wire, -1);
+int n = 0;
+
+void setup() {
+ Serial.begin(115200);
+ Wire.begin(21, 22);
+ if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
+ Serial.println("SSD1306 not found!");
+ while (true) delay(10);
+ }
+ display.clearDisplay();
+ Serial.println("OLED ready");
+}
+
+void loop() {
+ n++;
+ display.clearDisplay();
+ display.setTextSize(2);
+ display.setTextColor(SSD1306_WHITE);
+ display.setCursor(0, 0);
+ display.println("Hello");
+ display.println("Velxio!");
+ display.setTextSize(1);
+ display.setCursor(0, 48);
+ display.printf("Count: %d", n);
+ display.display();
+ delay(500);
+}`,
+ },
+ ],
+ code: '',
+ components: [{ type: 'velxio-ssd1306-i2c-4pin', id: 'oled', x: 470, y: 100, properties: {} }],
+ wires: [
+ { id: 'w-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'oled', pinName: 'VCC' }, color: '#ff4444' },
+ { id: 'w-gnd', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'oled', pinName: 'GND' }, color: '#000000' },
+ { id: 'w-sda', start: { componentId: 'esp32', pinName: '21' }, end: { componentId: 'oled', pinName: 'SDA' }, color: '#22aaff' },
+ { id: 'w-scl', start: { componentId: 'esp32', pinName: '22' }, end: { componentId: 'oled', pinName: 'SCL' }, color: '#ff8800' },
+ ],
+ },
+ {
+ id: 'pico-oled-4pin-i2c',
+ title: 'Raspberry Pi Pico: SSD1306 OLED (4-pin I2C)',
+ description:
+ 'Drive the cheap 4-pin SSD1306 OLED module (GND/VCC/SCL/SDA) over I2C from a Raspberry Pi Pico (SDA=GP4, SCL=GP5).',
+ libraries: ['Adafruit SSD1306', 'Adafruit GFX Library'],
+ category: 'displays',
+ difficulty: 'intermediate',
+ boardFilter: 'raspberry-pi-pico',
+ boards: [
+ {
+ boardKind: 'raspberry-pi-pico',
+ x: 60,
+ y: 60,
+ code: `// Raspberry Pi Pico — SSD1306 128x64 OLED (4-pin I2C module)
+// Wiring: SDA -> GP4 | SCL -> GP5 | VCC -> 3V3 | GND -> GND
+#include
+#include
+#include
+
+Adafruit_SSD1306 display(128, 64, &Wire, -1);
+int n = 0;
+
+void setup() {
+ Serial.begin(115200);
+ Wire.setSDA(4);
+ Wire.setSCL(5);
+ Wire.begin();
+ if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
+ Serial.println("SSD1306 not found!");
+ while (true) delay(10);
+ }
+ display.clearDisplay();
+ Serial.println("OLED ready");
+}
+
+void loop() {
+ n++;
+ display.clearDisplay();
+ display.setTextSize(2);
+ display.setTextColor(SSD1306_WHITE);
+ display.setCursor(0, 0);
+ display.println("Hello");
+ display.println("Velxio!");
+ display.setTextSize(1);
+ display.setCursor(0, 48);
+ display.print("Count: ");
+ display.print(n);
+ display.display();
+ delay(500);
+}`,
+ },
+ ],
+ code: '',
+ components: [{ type: 'velxio-ssd1306-i2c-4pin', id: 'oled', x: 470, y: 100, properties: {} }],
+ wires: [
+ { id: 'w-vcc', start: { componentId: 'raspberry-pi-pico', pinName: '3V3' }, end: { componentId: 'oled', pinName: 'VCC' }, color: '#ff4444' },
+ { id: 'w-gnd', start: { componentId: 'raspberry-pi-pico', pinName: 'GND.3' }, end: { componentId: 'oled', pinName: 'GND' }, color: '#000000' },
+ { id: 'w-sda', start: { componentId: 'raspberry-pi-pico', pinName: 'GP4' }, end: { componentId: 'oled', pinName: 'SDA' }, color: '#22aaff' },
+ { id: 'w-scl', start: { componentId: 'raspberry-pi-pico', pinName: 'GP5' }, end: { componentId: 'oled', pinName: 'SCL' }, color: '#ff8800' },
+ ],
+ },
+ {
+ id: 'stm32-oled-4pin-i2c',
+ title: 'STM32 Blue Pill: SSD1306 OLED (4-pin I2C)',
+ description:
+ 'Drive the cheap 4-pin SSD1306 OLED module (GND/VCC/SCL/SDA) over I2C1 from an STM32 Blue Pill (SDA=PB7, SCL=PB6).',
+ libraries: ['Adafruit SSD1306', 'Adafruit GFX Library'],
+ category: 'displays',
+ difficulty: 'intermediate',
+ boardFilter: 'stm32-bluepill',
+ boards: [
+ {
+ boardKind: 'stm32-bluepill',
+ x: 60,
+ y: 60,
+ code: `// STM32 Blue Pill (F103) — SSD1306 128x64 OLED (4-pin I2C module)
+// Wiring: SDA -> PB7 | SCL -> PB6 | VCC -> 3V3 | GND -> GND
+#include
+#include
+#include
+
+Adafruit_SSD1306 display(128, 64, &Wire, -1);
+int n = 0;
+
+void setup() {
+ Serial.begin(115200);
+ Wire.begin();
+ if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
+ Serial.println("SSD1306 not found!");
+ while (true) delay(10);
+ }
+ display.clearDisplay();
+ Serial.println("OLED ready");
+}
+
+void loop() {
+ n++;
+ display.clearDisplay();
+ display.setTextSize(2);
+ display.setTextColor(SSD1306_WHITE);
+ display.setCursor(0, 0);
+ display.println("Hello");
+ display.println("Velxio!");
+ display.setTextSize(1);
+ display.setCursor(0, 48);
+ display.print("Count: ");
+ display.print(n);
+ display.display();
+ delay(500);
+}`,
+ },
+ ],
+ code: '',
+ components: [{ type: 'velxio-ssd1306-i2c-4pin', id: 'oled', x: 470, y: 100, properties: {} }],
+ wires: [
+ { id: 'w-vcc', start: { componentId: 'stm32-bluepill', pinName: '3V3' }, end: { componentId: 'oled', pinName: 'VCC' }, color: '#ff4444' },
+ { id: 'w-gnd', start: { componentId: 'stm32-bluepill', pinName: 'GND' }, end: { componentId: 'oled', pinName: 'GND' }, color: '#000000' },
+ { id: 'w-sda', start: { componentId: 'stm32-bluepill', pinName: 'PB7' }, end: { componentId: 'oled', pinName: 'SDA' }, color: '#22aaff' },
+ { id: 'w-scl', start: { componentId: 'stm32-bluepill', pinName: 'PB6' }, end: { componentId: 'oled', pinName: 'SCL' }, color: '#ff8800' },
+ ],
+ },
{
id: 'ky-040-rotary-encoder',
title: 'KY-040 Rotary Encoder',
diff --git a/frontend/src/simulation/parts/ProtocolParts.ts b/frontend/src/simulation/parts/ProtocolParts.ts
index 17835fc6..889606c3 100644
--- a/frontend/src/simulation/parts/ProtocolParts.ts
+++ b/frontend/src/simulation/parts/ProtocolParts.ts
@@ -413,6 +413,20 @@ PartSimulationRegistry.register('ssd1306', {
},
});
+/**
+ * SSD1306 OLED (I2C, 4-pin) — the cheap `velxio-ssd1306-i2c` module (GND/VCC/
+ * SCL/SDA). Same display core as the 8-pin part but I2C-only by construction,
+ * so there's no protocol to detect. Issue #215.
+ */
+PartSimulationRegistry.register('ssd1306-i2c-4pin', {
+ attachEvents: (element, simulator, getPin, componentId) => {
+ const { components } = useSimulatorStore.getState();
+ const comp = components.find((c) => c.id === componentId);
+ const i2cAddr = parseI2cAddress(comp?.properties?.i2cAddress, 0x3c);
+ return attachSSD1306(element, simulator, getPin, 'i2c', i2cAddr);
+ },
+});
+
// ─── DS1307 RTC ──────────────────────────────────────────────────────────────
/**
diff --git a/scripts/component-overrides.json b/scripts/component-overrides.json
index 960369ef..b69ea800 100644
--- a/scripts/component-overrides.json
+++ b/scripts/component-overrides.json
@@ -2455,6 +2455,35 @@
"2004",
"liquidcrystal"
]
+ },
+ {
+ "$comment": "4-pin I2C-only SSD1306 module (GND/VCC/SCL/SDA) — the cheap 0.96\" OLED most beginners have; velxio-ssd1306-i2c native element. Counterpart to the 8-pin wokwi-ssd1306. Issue #215.",
+ "id": "ssd1306-i2c-4pin",
+ "tagName": "velxio-ssd1306-i2c-4pin",
+ "name": "SSD1306 OLED (I2C, 4-pin)",
+ "category": "displays",
+ "pinCount": 4,
+ "tags": [
+ "ssd1306",
+ "oled",
+ "display",
+ "i2c",
+ "4-pin",
+ "0x3c"
+ ],
+ "thumbnail": "",
+ "properties": [
+ {
+ "name": "i2cAddress",
+ "type": "string",
+ "defaultValue": "0x3c",
+ "control": "text",
+ "description": "I2C address"
+ }
+ ],
+ "defaultValues": {
+ "i2cAddress": "0x3c"
+ }
}
],
"led": {