Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/opendisplay/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
27 changes: 27 additions & 0 deletions src/opendisplay/display_palettes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
)

Expand All @@ -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
}


Expand All @@ -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.)
Expand Down
7 changes: 6 additions & 1 deletion src/opendisplay/encoding/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,17 @@ 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
Each 2-bit value maps to palette index (0-3)

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
Expand All @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions tests/unit/test_bwry_panel_palette.py
Original file line number Diff line number Diff line change
@@ -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