feat(ssd1306): add the 4-pin I2C OLED module + examples on Uno/ESP32/Pico/STM32

Adds `velxio-ssd1306-i2c-4pin`, a native 4-pin SSD1306 OLED module
(GND/VCC/SCL/SDA) — the cheap 0.96" I2C board most beginners actually have,
matching Wokwi's board-ssd1306. The 8-pin `wokwi-ssd1306` breakout stays; this
is the distinct 4-pin part (issue #215). Same SSD1306Core render pipeline
(imageData/redraw) so the display paints identically; I2C-only, address via the
i2cAddress property (default 0x3C). Styled after the existing 8-pin element
(blue PCB, dark screen, corner holes, star).

Ships four "SSD1306 OLED (4-pin I2C)" gallery examples wiring it over I2C on
Arduino Uno (A4/A5), ESP32 (21/22), Raspberry Pi Pico (GP4/GP5) and STM32 Blue
Pill (PB7/PB6).
This commit is contained in:
David Montero 2026-07-09 23:13:50 +02:00
parent 95e9fe9716
commit d7f4e8e966
7 changed files with 476 additions and 0 deletions

View File

@ -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": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#025caf\" rx=\"6\"/><rect x=\"9\" y=\"24\" width=\"46\" height=\"24\" fill=\"#141414\"/><text x=\"32\" y=\"14\" text-anchor=\"middle\" font-size=\"5\" font-family=\"monospace\" fill=\"#fff\">GND VCC SCL SDA</text><circle cx=\"18\" cy=\"19\" r=\"1.6\" fill=\"#b4aeab\"/><circle cx=\"27\" cy=\"19\" r=\"1.6\" fill=\"#c08540\"/><circle cx=\"37\" cy=\"19\" r=\"1.6\" fill=\"#009e9b\"/><circle cx=\"46\" cy=\"19\" r=\"1.6\" fill=\"#007adb\"/><text x=\"32\" y=\"39\" text-anchor=\"middle\" font-size=\"7\" font-family=\"monospace\" fill=\"#9be1ff\" font-weight=\"bold\">SSD1306</text><text x=\"32\" y=\"46\" text-anchor=\"middle\" font-size=\"4.5\" font-family=\"monospace\" fill=\"#9be1ff\">I2C 4-pin</text></svg>",
"properties": [
{
"name": "i2cAddress",
"type": "string",
"defaultValue": "0x3c",
"control": "text",
"description": "I2C address"
}
],
"defaultValues": {
"i2cAddress": "0x3c"
}
}
]
}

View File

@ -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

View File

@ -38,6 +38,7 @@ import { getBoardLogicFamily } from '../simulation/LogicFamilies';
// <velxio-instr-voltmeter>) 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

View File

@ -0,0 +1,160 @@
/**
* Ssd1306I2cElement `<velxio-ssd1306-i2c-4pin>` 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 = `
<style>
:host { display: inline-block; line-height: 0; position: relative; }
svg { display: block; user-select: none; -webkit-user-select: none; }
canvas {
position: absolute;
left: ${SCREEN_X}px;
top: ${SCREEN_Y}px;
image-rendering: pixelated;
pointer-events: none;
}
</style>
<svg width="${BODY_W}" height="${BODY_H}" xmlns="http://www.w3.org/2000/svg">
<!-- Blue PCB -->
<rect stroke="#BE9B72" fill="#025CAF" x=".5" y=".5"
width="${BODY_W - 1}" height="${BODY_H - 1}" rx="10" />
<!-- Corner mounting holes -->
<g fill="#59340A" stroke="#BE9B72" stroke-width="0.6">
${holes.map(([cx, cy]) => `<circle cx="${cx}" cy="${cy}" r="4.5" />`).join('')}
</g>
<!-- 128 x 64 screen (the <canvas> paints on top of this) -->
<rect x="${SCREEN_X}" y="${SCREEN_Y}" width="${SCREEN_W}" height="${SCREEN_H}" fill="#1A1A1A" />
<!-- Star decoration, echoing the 8-pin part -->
<path fill="#FFF" stroke="#FFF"
d="M133 6.5l-1.4 2.6-3 .4 2.2 2-.53 2.83 2.75-1.34 2.75 1.34-.53-2.83 2.2-2-3-.4-1.4-2.6z" />
<!-- 4-pin header + labels -->
<g font-family="monospace" font-size="6" font-weight="300" fill="#FFF" text-anchor="middle">
${PIN_NAMES.map(
(name, i) => `<text x="${PIN_X0 + i * PIN_SPACING}" y="${PIN_Y + 12}">${name}</text>`,
).join('')}
</g>
<g fill="#9D9D9A" stroke-width="0.4">
${PIN_NAMES.map(
(_, i) =>
`<circle stroke="${PIN_RING[i]}" cx="${PIN_X0 + i * PIN_SPACING}" cy="${PIN_Y}" r="3.5" />`,
).join('')}
</g>
</svg>
<canvas width="${SCREEN_W}" height="${SCREEN_H}"></canvas>
`;
}
}
if (!customElements.get('velxio-ssd1306-i2c-4pin')) {
customElements.define('velxio-ssd1306-i2c-4pin', Ssd1306I2cElement);
}
export type { Ssd1306I2cElement };

View File

@ -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 <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
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 <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
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 <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
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 <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
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',

View File

@ -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 ──────────────────────────────────────────────────────────────
/**

View File

@ -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": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#025caf\" rx=\"6\"/><rect x=\"9\" y=\"24\" width=\"46\" height=\"24\" fill=\"#141414\"/><text x=\"32\" y=\"14\" text-anchor=\"middle\" font-size=\"5\" font-family=\"monospace\" fill=\"#fff\">GND VCC SCL SDA</text><circle cx=\"18\" cy=\"19\" r=\"1.6\" fill=\"#b4aeab\"/><circle cx=\"27\" cy=\"19\" r=\"1.6\" fill=\"#c08540\"/><circle cx=\"37\" cy=\"19\" r=\"1.6\" fill=\"#009e9b\"/><circle cx=\"46\" cy=\"19\" r=\"1.6\" fill=\"#007adb\"/><text x=\"32\" y=\"39\" text-anchor=\"middle\" font-size=\"7\" font-family=\"monospace\" fill=\"#9be1ff\" font-weight=\"bold\">SSD1306</text><text x=\"32\" y=\"46\" text-anchor=\"middle\" font-size=\"4.5\" font-family=\"monospace\" fill=\"#9be1ff\">I2C 4-pin</text></svg>",
"properties": [
{
"name": "i2cAddress",
"type": "string",
"defaultValue": "0x3c",
"control": "text",
"description": "I2C address"
}
],
"defaultValues": {
"i2cAddress": "0x3c"
}
}
],
"led": {