From c47608c30d97dc9f9534adfb44e63697dc6e0183 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Jul 2026 03:07:33 -0400 Subject: [PATCH] fix: per-panel BWRY code table + 4-gray table additions (M3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BWRY nibbles were hardcoded to the u8Colors_4clr_v2 order (black/white/ yellow=2/red=3). Panels 0x001D/0x001E (EP29YR) use u8Colors_4clr, where the native codes are 2=red, 3=yellow, and the firmware direct-write path streams the nibble raw — so yellow and red rendered swapped on those panels. Add a per-panel BWRY code table (get_bwry_codes) analogous to the 4-gray table, and apply it when encoding BWRY in prepare_image via encode_2bpp(codes=...). Also add the EP368_792x528_4GRAY (0x0048) V2 code entry (its mid-grays were swapped) and the missing 4-gray panel ids 0x0043/0x0044/0x0046/0x004C to PANELS_4GRAY. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JRrm95f1qNZzDM9r2SB6KW --- src/opendisplay/device.py | 8 +++++- src/opendisplay/display_palettes.py | 27 ++++++++++++++++++ src/opendisplay/encoding/images.py | 7 ++++- tests/unit/test_bwry_panel_palette.py | 40 +++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_bwry_panel_palette.py diff --git a/src/opendisplay/device.py b/src/opendisplay/device.py index b7ee997..6ee6503 100644 --- a/src/opendisplay/device.py +++ b/src/opendisplay/device.py @@ -19,11 +19,12 @@ encrypt_command, generate_client_nonce, ) -from .display_palettes import PANELS_4GRAY, get_gray4_codes, get_palette_for_display +from .display_palettes import PANELS_4GRAY, get_bwry_codes, get_gray4_codes, get_palette_for_display from .encoding import ( DEFAULT_ZLIB_WINDOW_BITS, ZIPXL_ZLIB_WINDOW_BITS, compress_image_data, + encode_2bpp, encode_bitplanes, encode_gray4_bitplanes, encode_image, @@ -250,6 +251,11 @@ def prepare_image( elif color_scheme == ColorScheme.GRAYSCALE_4: # Two pre-split 1-bit planes concatenated; firmware streams the halves to PLANE_0/PLANE_1. image_data = b"".join(encode_gray4_bitplanes(dithered, get_gray4_codes(panel_ic_type))) + elif color_scheme == ColorScheme.BWRY: + # Some YR panels (0x001D/0x001E) use a native 4-color code order with + # yellow/red swapped relative to the dither palette; apply the per-panel + # code table so the firmware's raw-nibble direct write shows the right color. + image_data = encode_2bpp(dithered, codes=get_bwry_codes(panel_ic_type)) else: image_data = encode_image(dithered, color_scheme) diff --git a/src/opendisplay/display_palettes.py b/src/opendisplay/display_palettes.py index e687803..670d61a 100644 --- a/src/opendisplay/display_palettes.py +++ b/src/opendisplay/display_palettes.py @@ -20,6 +20,11 @@ 0x002F, # EP29Z_128x296_4GRAY 0x0031, # EP213Z_122x250_4GRAY 0x003C, # EP75_800x480_4GRAY_GEN2 + 0x0043, # newer 4-gray panel + 0x0044, # newer 4-gray panel + 0x0046, # newer 4-gray panel + 0x0048, # EP368_792x528_4GRAY + 0x004C, # newer 4-gray panel } ) @@ -31,6 +36,7 @@ _GRAY4_CODES_V2: tuple[int, int, int, int] = (3, 2, 1, 0) # u8Colors_4gray_v2 _GRAY4_CODES_BY_PANEL: dict[int, tuple[int, int, int, int]] = { 0x0028: _GRAY4_CODES_V2, # EP426_800x480_4GRAY + 0x0048: _GRAY4_CODES_V2, # EP368_792x528_4GRAY } @@ -41,6 +47,27 @@ def get_gray4_codes(panel_ic_type: int | None) -> tuple[int, int, int, int]: return _GRAY4_CODES_BY_PANEL.get(panel_ic_type, _GRAY4_CODES_BASE) +# Per-panel BWRY palette-index -> stored-nibble tables, mirroring bb_epaper's +# 4-color swatch tables (bb_ep.inl). The dither palette orders indices as +# black=0, white=1, yellow=2, red=3. Most YR panels use u8Colors_4clr_v2, whose +# native codes match that order. Panels 0x001D/0x001E use u8Colors_4clr, where +# native code 2=red and 3=yellow, so yellow/red must be swapped on the wire +# (the firmware direct-write path streams the nibble raw). +_BWRY_CODES_DEFAULT: tuple[int, int, int, int] = (0, 1, 2, 3) # u8Colors_4clr_v2 +_BWRY_CODES_SWAPPED: tuple[int, int, int, int] = (0, 1, 3, 2) # u8Colors_4clr +_BWRY_CODES_BY_PANEL: dict[int, tuple[int, int, int, int]] = { + 0x001D: _BWRY_CODES_SWAPPED, # EP29YR_128x296 + 0x001E: _BWRY_CODES_SWAPPED, # EP29YR_168x384 +} + + +def get_bwry_codes(panel_ic_type: int | None) -> tuple[int, int, int, int]: + """Return the BWRY palette-index->stored-nibble table for a panel.""" + if panel_ic_type is None: + return _BWRY_CODES_DEFAULT + return _BWRY_CODES_BY_PANEL.get(panel_ic_type, _BWRY_CODES_DEFAULT) + + # Map: (panel_ic_type, color_scheme) -> measured ColorPalette # panel_ic_type identifies the e-paper panel model # color_scheme identifies the color mode (MONO, BWR, BWGBRY, etc.) diff --git a/src/opendisplay/encoding/images.py b/src/opendisplay/encoding/images.py index bbf6b4e..34925fd 100644 --- a/src/opendisplay/encoding/images.py +++ b/src/opendisplay/encoding/images.py @@ -128,7 +128,7 @@ def encode_1bpp(image: Image.Image) -> bytes: return bytes(output) -def encode_2bpp(image: Image.Image) -> bytes: +def encode_2bpp(image: Image.Image, codes: tuple[int, int, int, int] | None = None) -> bytes: """Encode image to 2-bits-per-pixel format (4 colors). Format: 4 pixels per byte, MSB first @@ -136,6 +136,9 @@ def encode_2bpp(image: Image.Image) -> bytes: Args: image: Palette image (mode 'P') + codes: Optional palette-index -> stored-nibble table. Used by BWRY panels + whose native 4-color code order differs from the dither palette order + (e.g. yellow/red swapped). Defaults to identity. Returns: Encoded bytes @@ -157,6 +160,8 @@ def encode_2bpp(image: Image.Image) -> bytes: bit_shift = (3 - pixel_in_byte) * 2 # MSB first palette_idx = pixels[y, x] & 0x03 # 2-bit value + if codes is not None: + palette_idx = codes[palette_idx] output[byte_idx] |= palette_idx << bit_shift return bytes(output) diff --git a/tests/unit/test_bwry_panel_palette.py b/tests/unit/test_bwry_panel_palette.py new file mode 100644 index 0000000..20bd107 --- /dev/null +++ b/tests/unit/test_bwry_panel_palette.py @@ -0,0 +1,40 @@ +"""Tests for per-panel BWRY code tables and 4-gray table additions (M3).""" + +from __future__ import annotations + +from PIL import Image + +from opendisplay.display_palettes import ( + PANELS_4GRAY, + get_bwry_codes, + get_gray4_codes, +) +from opendisplay.encoding.images import encode_2bpp + + +def test_get_bwry_codes_swaps_yellow_red_on_u8colors_4clr_panels() -> None: + assert get_bwry_codes(0x001D) == (0, 1, 3, 2) + assert get_bwry_codes(0x001E) == (0, 1, 3, 2) + + +def test_get_bwry_codes_default_is_identity() -> None: + assert get_bwry_codes(55) == (0, 1, 2, 3) + assert get_bwry_codes(None) == (0, 1, 2, 3) + + +def test_encode_2bpp_applies_code_table() -> None: + img = Image.new("P", (4, 1)) + img.putdata([0, 1, 2, 3]) # black, white, yellow, red + + # identity: 0b00_01_10_11 + assert encode_2bpp(img) == bytes([0b00011011]) + # yellow/red swapped: 0b00_01_11_10 + assert encode_2bpp(img, codes=(0, 1, 3, 2)) == bytes([0b00011110]) + + +def test_gray4_v2_table_includes_ep368() -> None: + assert get_gray4_codes(0x0048) == (3, 2, 1, 0) + + +def test_panels_4gray_includes_new_ids() -> None: + assert {0x0043, 0x0044, 0x0046, 0x0048, 0x004C} <= PANELS_4GRAY