diff --git a/frontend/src/__tests__/ssd1306-render.test.ts b/frontend/src/__tests__/ssd1306-render.test.ts index d245e4d9..6fae5a9e 100644 --- a/frontend/src/__tests__/ssd1306-render.test.ts +++ b/frontend/src/__tests__/ssd1306-render.test.ts @@ -169,6 +169,36 @@ describe('SSD1306 — ImageData rendering (syncElement fix)', () => { expect(px[idxOff + 2]).toBe(0); }); + it('page addressing (Tiny4kOLED): 0xB0+page / 0x00-0x1F col, no 0x20, cursor persists across data streams', () => { + const el = makeOLEDElement(); + const sim = makeSim(); + PartSimulationRegistry.get('ssd1306')!.attachEvents!(el, sim as any, () => null); + const device = sim._devices[0]; + + // Page-addressing setCursor: page 1, column 8 (col high nibble = 0x10, + // col low nibble = 0x08). No 0x20 — relies on the power-on page-mode + // default that Tiny4kOLED / U8g2-page / classic SSD1306 drivers assume. + sendCommandStream(device, [0xb1, 0x10, 0x08]); + // TinyWireM flushes its small buffer as distinct 16-byte I2C transactions, + // so the column pointer MUST persist across separate data streams. + sendDataStream(device, [0xff, 0x00]); // col 8, 9 + sendDataStream(device, [0x00, 0xff]); // col 10, 11 — cursor continued + + const px = el.imageData.data; + const lit = (row: number, col: number) => { + const i = (row * 128 + col) * 4; + return px[i] + px[i + 1] + px[i + 2] > 0; + }; + // page 1 → rows 8..15; 0xff lights the whole 8-pixel column. + expect(lit(8, 8)).toBe(true); + expect(lit(15, 8)).toBe(true); + expect(lit(8, 9)).toBe(false); // 0x00 + expect(lit(8, 10)).toBe(false); // 0x00 (start of 2nd stream) + // col 11 lit proves the cursor advanced across the STOP/new transaction. + expect(lit(8, 11)).toBe(true); + expect(lit(15, 11)).toBe(true); + }); + it('fills all 1024 GDDRAM bytes via horizontal addressing', () => { const el = makeOLEDElement(); const sim = makeSim(); diff --git a/frontend/src/simulation/parts/ProtocolParts.ts b/frontend/src/simulation/parts/ProtocolParts.ts index 9fccc052..0fbbb023 100644 --- a/frontend/src/simulation/parts/ProtocolParts.ts +++ b/frontend/src/simulation/parts/ProtocolParts.ts @@ -64,7 +64,12 @@ class SSD1306Core { private colEnd = 127; private pageStart = 0; private pageEnd = 7; - private memMode = 0; // 0=horizontal, 1=vertical, 2=page + // 0=horizontal, 1=vertical, 2=page. SSD1306 power-on default is PAGE + // addressing (datasheet 10b). Adafruit_SSD1306 overrides it to horizontal + // via 0x20,0x00; page-mode drivers (Tiny4kOLED, U8g2 page buffer) rely on + // this default and never send 0x20 — so the default MUST be 2 or their + // setCursor (0xB0-0xB7 + 0x00-0x1F) renders garbled. + private memMode = 2; // Multi-byte command accumulation private cmdBuf: number[] = []; @@ -125,7 +130,19 @@ class SSD1306Core { this.page = this.pageStart; break; default: - if (cmd >= 0x40 && cmd <= 0x7f) { + // Page-addressing-mode cursor commands (single-byte). Used by + // Tiny4kOLED / U8g2 page buffer / classic SSD1306 drivers whose + // setCursor() does NOT use the 0x21/0x22 column/page-range commands. + if (cmd >= 0xb0 && cmd <= 0xb7) { + // set page start address (B0..B7 → page 0..7) + this.page = cmd & 0x07; + } else if (cmd <= 0x0f) { + // set lower column nibble (0x00..0x0F) + this.col = (this.col & 0xf0) | (cmd & 0x0f); + } else if (cmd >= 0x10 && cmd <= 0x1f) { + // set higher column nibble (0x10..0x1F) + this.col = (this.col & 0x0f) | ((cmd & 0x0f) << 4); + } else if (cmd >= 0x40 && cmd <= 0x7f) { /* display start line — visual, skip */ } break;