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": "GND VCC SCL SDASSD1306I2C 4-pin", + "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 = ` + + + + + + + + ${holes.map(([cx, cy]) => ``).join('')} + + + + + + + + + + + ${PIN_NAMES.map( + (name, i) => `${name}`, + ).join('')} + + + ${PIN_NAMES.map( + (_, i) => + ``, + ).join('')} + + + + `; + } +} + +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": "GND VCC SCL SDASSD1306I2C 4-pin", + "properties": [ + { + "name": "i2cAddress", + "type": "string", + "defaultValue": "0x3c", + "control": "text", + "description": "I2C address" + } + ], + "defaultValues": { + "i2cAddress": "0x3c" + } } ], "led": {