Skip to content

Add 4-level greyscale support for ThinkInk_420_Grayscale4_MFGN#2

Draft
tyeth wants to merge 4 commits into
adafruit:mainfrom
tyeth:grayscale4-mfgn-support
Draft

Add 4-level greyscale support for ThinkInk_420_Grayscale4_MFGN#2
tyeth wants to merge 4 commits into
adafruit:mainfrom
tyeth:grayscale4-mfgn-support

Conversation

@tyeth

@tyeth tyeth commented May 6, 2026

Copy link
Copy Markdown
Member

Summary

Drives 4 distinct grey levels on the 4.2" 400×300 ThinkInk_420_Grayscale4_MFGN https://www.adafruit.com/product/6381 . The chip-default OTP LUT is mono and cannot resolve 4 levels — so even with grayscale=True the existing driver collapsed to two levels in practice. This adds the missing pieces:

  • Panel-specific 227-byte waveform LUT (THINKINK_420_GRAYSCALE4_MFGN_LUT) — verbatim port of ti_420mfgn_gray4_lut_code from Adafruit_EPD/src/panels/ThinkInk_420_Grayscale4_MFGN.h.
  • Extra init bytes (THINKINK_420_GRAYSCALE4_MFGN_INIT) the chip default omits — boost-softstart (0x0C), end-option override (0x18), gate voltage (0x03), source voltage (0x04), VCOM (0x2C), plus the greyscale border value on 0x3C. Threaded through a new extra_init kwarg that is byte-packed and spliced between the chip-default _START_SEQUENCE and the LUT load.
  • Display Update Control 2 fix: when custom_lut is provided, set command 0x22 to 0xCF (Display Mode 2, bit 3 set) instead of 0xC7. 0xC7 selects Display Mode 1 — the chip's mono waveform path — which silently ignores any LUT loaded via 0x32. With 0xCF the LUT actually drives the panel.
  • Conditional write_black_ram_commandwrite_color_ram_command swap when grayscale=True. The Adafruit_EPD waveform LUT encodes the lighter mid-tone as (black_RAM=0, color_RAM=1) and the darker mid-tone as (1, 0), but displayio's greyscale palette quantiser puts luma bit 7 on pass 0 (the black-RAM command) and luma bit 6 on pass 1 (the color-RAM command), giving the opposite mid-tone bits. Swapping the two 0x24/0x26 command IDs aligns displayio's encoding with the LUT; the white (1,1) and black (0,0) corners are unaffected.

Adds examples/4_2_inch_400x300_grayscale.py wiring the new constants together for the MFGN panel.

Test plan

  • Run new example on a Pi e-Ink Bonnet driving a ThinkInk_420_Grayscale4_MFGN — confirmed four monotonic, visibly distinct grey levels (white → light grey → dark grey → black) where the previous behaviour collapsed everything except 0xAAAAAA to black.
  • Existing ssd1683_simpletest.py (mono path) and ssd1683_ThinkInk_420_Tricolor_MFGNR.py continue to work unchanged — extra_init defaults to b"", the RAM-command swap only fires when grayscale=True, and the 0xC7→0xCF change only fires when custom_lut is provided.

🤖 Generated with Claude Code

The chip-default OTP LUT is mono. Driving 4 levels needs three pieces
working together that the existing driver was missing:

- A panel-specific 227-byte waveform LUT loaded via 0x32. This is
  ported verbatim from Adafruit_EPD's
  src/panels/ThinkInk_420_Grayscale4_MFGN.h
  (ti_420mfgn_gray4_lut_code) and exposed as
  THINKINK_420_GRAYSCALE4_MFGN_LUT.
- Extra init bytes the chip default omits — boost-softstart (0x0C),
  end-option override (0x18), gate voltage (0x03), source voltage
  (0x04), VCOM (0x2C), plus the greyscale border value on 0x3C —
  exposed as THINKINK_420_GRAYSCALE4_MFGN_INIT and spliced via a new
  extra_init kwarg.
- Display Update Control 2 (0x22) set to 0xCF instead of 0xC7 when
  custom_lut is provided, so the chip runs the Display-Mode-2 refresh
  path (bit 3) and actually uses the LUT we just pushed via 0x32.
  Previously 0xC7 selected Display-Mode-1 (the mono waveform), which
  silently ignored any custom LUT and prevented 4-level rendering.

Plus a conditional swap of write_black_ram_command and
write_color_ram_command when grayscale=True: the LUT encodes the
lighter mid-tone as (black_RAM=0, color_RAM=1) and the darker mid-tone
as (1, 0), but displayio's grayscale palette quantiser puts luma bit 7
on pass 0 (the black-RAM command) and luma bit 6 on pass 1 (the
color-RAM command), giving the opposite mid-tone bits. Swapping the
commands aligns displayio's encoding with the LUT; white (1,1) and
black (0,0) are unaffected.

Verified end-to-end on a Pi e-Ink Bonnet driving the
ThinkInk_420_Grayscale4_MFGN: a 4-band swatch (0xFFFFFF / 0xAAAAAA /
0x555555 / 0x000000) renders as four visibly distinct, monotonic grey
levels.

Adds examples/4_2_inch_400x300_grayscale.py wiring the new constants
together for the MFGN panel.
CI:
- Collapse the aligned-comment whitespace padding in
  THINKINK_420_GRAYSCALE4_MFGN_INIT.
- Wrap the 227-byte LUT bytes block in `# fmt: off` / `# fmt: on` so
  ruff format doesn't expand each byte to its own line.
- Collapse the start_sequence assembly back to one line.

The grayscale4-lut-followup doc records the rationale for the
RAM-command swap and an alternative fix path that modifies the LUT
data itself, including what we tried and what would still need to be
done to make that path land. The conditional command swap stays in
place as the working fix.
@tyeth

tyeth commented May 6, 2026

Copy link
Copy Markdown
Member Author

Drafting this while I investigate if the black_ram_command/white_ram_command need swapping or if the LUT can be adjusted for displayio compatibility instead.

@tyeth tyeth marked this pull request as draft May 6, 2026 17:03
@tyeth

tyeth commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

ZJY400300-042CAAMFGN_revA is the panel id to add in comments and in adafruit/Wippersnapper_Components#304

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant