diff --git a/backend/app/services/esp32_spi_slaves.py b/backend/app/services/esp32_spi_slaves.py index ab6edf85..3188697a 100644 --- a/backend/app/services/esp32_spi_slaves.py +++ b/backend/app/services/esp32_spi_slaves.py @@ -301,18 +301,29 @@ class Ssd168xEpaperSlave: if 0 <= self._x_byte < self._ram_bpr and 0 <= self._y < self._ram_rows: plane[self._y * self._ram_bpr + self._x_byte] = byte x_inc = (self._entry_mode & 0x01) == 0x01 + y_inc = (self._entry_mode & 0x02) == 0x02 + end_of_row = False if x_inc: if self._x_byte < self._xrange[1]: self._x_byte += 1 else: self._x_byte = self._xrange[0] - self._y += 1 + end_of_row = True else: if self._x_byte > self._xrange[0]: self._x_byte -= 1 else: self._x_byte = self._xrange[1] - self._y += 1 + end_of_row = True + if end_of_row: + # Advance Y, WRAPPING at the window boundary like the SSD168x RAM + # address counter. Some drivers (e.g. GxEPD2_3C) write the 0x24 then + # the 0x26 plane without re-seeking the counter, relying on this + # wrap so the second plane lands in the window. + if y_inc: + self._y = self._yrange[0] if self._y >= self._yrange[1] else self._y + 1 + else: + self._y = self._yrange[1] if self._y <= self._yrange[0] else self._y - 1 # ── UC8159c (ACeP 7-colour 5.65" GoodDisplay GDEP0565D90) ─────────────────── diff --git a/frontend/src/simulation/displays/SSD168xDecoder.ts b/frontend/src/simulation/displays/SSD168xDecoder.ts index 088fcb75..e66178d4 100644 --- a/frontend/src/simulation/displays/SSD168xDecoder.ts +++ b/frontend/src/simulation/displays/SSD168xDecoder.ts @@ -374,19 +374,32 @@ export class SSD168xDecoder { } // Auto-increment per data_entry_mode (default 0x03: X+, then Y+ at end of row). const xInc = (this.entryMode & 0x01) === 0x01; + const yInc = (this.entryMode & 0x02) === 0x02; + let endOfRow = false; if (xInc) { if (this.xByte < this.xrange[1]) { this.xByte += 1; } else { this.xByte = this.xrange[0]; - this.y += 1; + endOfRow = true; } } else { if (this.xByte > this.xrange[0]) { this.xByte -= 1; } else { this.xByte = this.xrange[1]; - this.y += 1; + endOfRow = true; + } + } + if (endOfRow) { + // Advance Y, WRAPPING at the window boundary like the SSD168x RAM address + // counter. Some drivers (e.g. GxEPD2_3C) write the 0x24 then the 0x26 + // plane without re-seeking the counter, relying on this wrap so the + // second plane lands in the window. + if (yInc) { + this.y = this.y >= this.yrange[1] ? this.yrange[0] : this.y + 1; + } else { + this.y = this.y <= this.yrange[0] ? this.yrange[1] : this.y - 1; } } } diff --git a/test/test_epaper/ssd168x_decoder.py b/test/test_epaper/ssd168x_decoder.py index 9445d377..1f0baf90 100644 --- a/test/test_epaper/ssd168x_decoder.py +++ b/test/test_epaper/ssd168x_decoder.py @@ -312,17 +312,26 @@ class SSD168xDecoder: plane[self._y * self._ram_bpr + self._x_byte] = byte # Auto-increment per data_entry_mode (default x+, then y+ at end of row). x_inc = (self._entry_mode & 0x01) == 0x01 # bit0: 1 = X+ - # entry_mode bit1: Y direction; bit2: which counter advances first. - # For the default 0x03, X advances; once it hits xrange[1], wrap and Y++. + y_inc = (self._entry_mode & 0x02) == 0x02 # bit1: 1 = Y+ + end_of_row = False if x_inc: if self._x_byte < self._xrange[1]: self._x_byte += 1 else: self._x_byte = self._xrange[0] - self._y += 1 + end_of_row = True else: if self._x_byte > self._xrange[0]: self._x_byte -= 1 else: self._x_byte = self._xrange[1] - self._y += 1 + end_of_row = True + if end_of_row: + # Advance Y, WRAPPING at the window boundary like the SSD168x RAM + # address counter. Some drivers (e.g. GxEPD2_3C) write the 0x24 then + # the 0x26 plane without re-seeking the counter, relying on this wrap + # so the second plane lands in the window. + if y_inc: + self._y = self._yrange[0] if self._y >= self._yrange[1] else self._y + 1 + else: + self._y = self._yrange[1] if self._y <= self._yrange[0] else self._y - 1