Skip to content

Commit 150e56a

Browse files
committed
ili9341: add color_order kwarg, keep bgr compat, update tests/docs
1 parent c49a3cb commit 150e56a

File tree

4 files changed

+214
-96
lines changed

4 files changed

+214
-96
lines changed

README.rst

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,14 @@ Usage Example
6464
while True:
6565
pass
6666
67-
Custom MADCTL Example
68-
---------------------
67+
Color Order Example
68+
-------------------
6969

70-
The driver accepts an optional ``madctl`` keyword argument. If provided, the driver writes
71-
this value directly to the MADCTL register (0x36) during initialization. If omitted, the
72-
driver leaves MADCTL unchanged.
73-
74-
This allows advanced users to apply hardware-level rotations or control BGR/RGB order
75-
without relying on displayio’s software rotation. Using MADCTL can improve performance
76-
when displaying large bitmaps (e.g. with ``OnDiskBitmap``).
70+
The driver accepts a ``color_order`` keyword argument (default ``"RGB"``).
71+
Set it to ``"BGR"`` if your panel uses BGR pixel order instead of RGB.
7772

73+
For backward compatibility, a ``bgr`` boolean argument is still supported
74+
but is deprecated; prefer ``color_order="BGR"``.
7875

7976
.. code-block:: python
8077
@@ -90,9 +87,11 @@ when displaying large bitmaps (e.g. with ``OnDiskBitmap``).
9087
displayio.release_displays()
9188
display_bus = fourwire.FourWire(spi, command=tft_dc, chip_select=tft_cs)
9289
93-
# Pass a custom MADCTL value (example: 0b01011000)
94-
# Note: if you don't pass madctl, the driver does not modify the register.
95-
display = adafruit_ili9341.ILI9341(display_bus, width=320, height=240, madctl=0b01011000)
90+
# Use BGR color order
91+
display = adafruit_ili9341.ILI9341(display_bus, width=320, height=240, color_order="BGR")
92+
93+
Note: Display rotation continues to be handled by displayio. The driver does not
94+
expose controller registers such as MADCTL directly.
9695

9796
Documentation
9897
=============

adafruit_ili9341.py

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@
6060
__version__ = "0.0.0+auto.0"
6161
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ILI9341.git"
6262

63+
_MADCTL = 0x36
64+
_MADCTL_MY = 0x80
65+
_MADCTL_MX = 0x40
66+
_MADCTL_MV = 0x20
67+
_MADCTL_ML = 0x10
68+
_MADCTL_BGR = 0x08
69+
_MADCTL_MH = 0x04
70+
6371
_INIT_SEQUENCE = (
6472
b"\x01\x80\x80" # Software reset then delay 0x80 (128ms)
6573
b"\xef\x03\x03\x80\x02"
@@ -91,42 +99,42 @@ class ILI9341(BusDisplay):
9199
"""
92100
ILI9341 display driver
93101
94-
:param FourWire bus: bus that the display is connected to
95-
:param bool bgr: Use BGR color order instead of RGB
102+
:param str color_order: "RGB" (default) or "BGR"
103+
:param bool bgr: (deprecated) legacy option for color order
96104
:param bool invert: Invert the display
97-
:keyword int madctl: (optional) Raw value to write to the MADCTL register (0x36).
98-
If provided, the driver emits a MADCTL write during init.
99-
If omitted, the driver leaves MADCTL unchanged.
100-
101105
102106
"""
103107

104-
def __init__(
105-
self,
106-
bus: FourWire,
107-
*,
108-
bgr: bool = False,
109-
invert: bool = False,
110-
**kwargs: Any,
111-
):
112-
# Accept optional raw MADCTL via kwargs (requested by maintainers)
113-
madctl = kwargs.pop("madctl", None)
114-
115-
# Start with base init sequence
116-
init_sequence = bytearray(_INIT_SEQUENCE)
117-
118-
# Only touch MADCTL if caller explicitly provided it via kwargs
119-
if madctl is not None:
120-
try:
121-
idx = init_sequence.index(0x36) # MADCTL command
122-
init_sequence[idx + 1] = 0x01 # data length
123-
init_sequence[idx + 2] = madctl & 0xFF
124-
except ValueError:
125-
# Not present — append a single-byte MADCTL write
126-
init_sequence += bytes((0x36, 0x01, madctl & 0xFF))
127-
128-
# Inversion
129-
if invert:
130-
init_sequence += b"\x21\x00"
131-
132-
super().__init__(bus, init_sequence, **kwargs)
108+
109+
# ruff: noqa: PLR0913
110+
def __init__(
111+
self,
112+
bus,
113+
*,
114+
width=240,
115+
height=320,
116+
rotation=0,
117+
color_order="RGB",
118+
bgr=None,
119+
invert=False,
120+
**kwargs,
121+
):
122+
# Start with base init sequence
123+
init_sequence = bytearray(_INIT_SEQUENCE)
124+
125+
# Back-compat shim for old 'bgr' kwarg
126+
if bgr is not None:
127+
color_order = "BGR" if bgr else "RGB"
128+
129+
madctl = 0x00
130+
if str(color_order).upper() == "BGR":
131+
madctl |= _MADCTL_BGR
132+
133+
init_sequence += bytes((_MADCTL, 0x01, madctl & 0xFF))
134+
135+
# Inversion
136+
if invert:
137+
init_sequence += b"\x21\x00"
138+
139+
# IMPORTANT: pass the sequence into BusDisplay so tests can capture it
140+
super().__init__(bus, init_sequence, width=width, height=height, rotation=rotation, **kwargs)

test_madctl.py

Lines changed: 0 additions & 49 deletions
This file was deleted.

tests/test_color_order.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# SPDX-FileCopyrightText: 2025 Ritesh
2+
# SPDX-License-Identifier: MIT
3+
4+
import pathlib
5+
import sys
6+
import types
7+
8+
# 1) Ensure project root (where adafruit_ili9341.py lives) is importable
9+
ROOT = pathlib.Path(__file__).resolve().parents[1]
10+
if str(ROOT) not in sys.path:
11+
sys.path.insert(0, str(ROOT))
12+
13+
# 2) Stub 'busdisplay' so the driver can import BusDisplay without hardware deps
14+
busdisplay_stub = types.ModuleType("busdisplay")
15+
16+
17+
def _to_bytes(obj):
18+
if obj is None:
19+
return b""
20+
if isinstance(obj, (bytes, bytearray, memoryview)):
21+
return bytes(obj)
22+
if isinstance(obj, (list, tuple)):
23+
try:
24+
return bytes(obj)
25+
except Exception:
26+
return b""
27+
return b""
28+
29+
30+
def _pick_bytes_from_args_kwargs(args, kwargs):
31+
# Prefer explicit kwarg value that looks bytes-like
32+
for v in kwargs.values():
33+
b = _to_bytes(v)
34+
if b:
35+
return b
36+
# Common names: init_sequence, init, sequence
37+
for key in ("init_sequence", "init", "sequence"):
38+
if key in kwargs:
39+
b = _to_bytes(kwargs[key])
40+
if b:
41+
return b
42+
# Otherwise, search positional args after the bus (args[0] usually 'bus')
43+
best = b""
44+
for v in args[1:]:
45+
b = _to_bytes(v)
46+
if len(b) > len(best):
47+
best = b
48+
return best
49+
50+
51+
class BusDisplay:
52+
# Accept any signature; extract a bytes-like init sequence robustly
53+
def __init__(self, *args, **kwargs):
54+
init_seq = _pick_bytes_from_args_kwargs(args, kwargs)
55+
self.init_sequence = init_seq
56+
self._busdisplay_debug = {
57+
"arg_types": [type(a).__name__ for a in args],
58+
"kw_keys": list(kwargs.keys()),
59+
"init_seq_len": len(init_seq),
60+
}
61+
62+
63+
busdisplay_stub.BusDisplay = BusDisplay
64+
sys.modules["busdisplay"] = busdisplay_stub
65+
66+
import adafruit_ili9341 as ili
67+
68+
69+
def _last_madctl(seq: bytes) -> int:
70+
"""
71+
Return the last MADCTL data byte written in the init sequence.
72+
73+
Init sequence encoding per Adafruit style:
74+
[CMD][LEN|0x80 if delay][<LEN data bytes>][<1 delay byte if delay flag set>]
75+
"""
76+
cmd = ili._MADCTL
77+
i = 0
78+
last = None
79+
L = len(seq)
80+
while i < L:
81+
if i >= L:
82+
break
83+
c = seq[i]
84+
i += 1
85+
if i >= L:
86+
break
87+
length_byte = seq[i]
88+
i += 1
89+
90+
delay_flag = (length_byte & 0x80) != 0
91+
n = length_byte & 0x7F # actual data length
92+
93+
# If this is MADCTL, expect exactly 1 data byte
94+
if c == cmd:
95+
assert n == 1, f"Expected MADCTL length 1, got {n}"
96+
assert i + n <= L, "MADCTL payload truncated"
97+
last = seq[i] # the one data byte
98+
# advance over data
99+
i += n
100+
# consume delay byte if present
101+
if delay_flag:
102+
i += 1
103+
assert last is not None, f"No MADCTL write found. seq={seq.hex(' ')}"
104+
return last
105+
106+
107+
def test_color_order_defaults_rgb():
108+
d = ili.ILI9341(bus=object())
109+
assert len(getattr(d, "init_sequence", b"")) > 0, (
110+
f"Driver did not pass an init sequence to BusDisplay. "
111+
f"debug={getattr(d, '_busdisplay_debug', {})}"
112+
)
113+
madctl = _last_madctl(d.init_sequence)
114+
# Default is RGB => BGR bit NOT set
115+
assert (madctl & ili._MADCTL_BGR) == 0
116+
117+
118+
def test_color_order_bgr_sets_bit():
119+
d = ili.ILI9341(bus=object(), color_order="BGR")
120+
assert len(getattr(d, "init_sequence", b"")) > 0, (
121+
f"Driver did not pass an init sequence to BusDisplay. "
122+
f"debug={getattr(d, '_busdisplay_debug', {})}"
123+
)
124+
madctl = _last_madctl(d.init_sequence)
125+
# BGR => BGR bit set
126+
assert (madctl & ili._MADCTL_BGR) == ili._MADCTL_BGR
127+
128+
129+
def test_legacy_bgr_true_sets_bit():
130+
d = ili.ILI9341(bus=object(), bgr=True)
131+
assert len(getattr(d, "init_sequence", b"")) > 0, (
132+
f"Driver did not pass an init sequence to BusDisplay. "
133+
f"debug={getattr(d, '_busdisplay_debug', {})}"
134+
)
135+
madctl = _last_madctl(d.init_sequence)
136+
# legacy bgr=True still sets the bit
137+
assert (madctl & ili._MADCTL_BGR) == ili._MADCTL_BGR
138+
139+
140+
def _has_invon(seq: bytes) -> bool:
141+
# Scan TLV-style sequence; check command 0x21 is present
142+
i, L = 0, len(seq)
143+
while i + 2 <= L:
144+
cmd = seq[i]
145+
i += 1
146+
lb = seq[i]
147+
i += 1
148+
delay = (lb & 0x80) != 0
149+
n = lb & 0x7F
150+
i += n
151+
if delay:
152+
i += 1
153+
if cmd == 0x21:
154+
return True
155+
return False
156+
157+
158+
def test_invert_true_appends_invon():
159+
d = ili.ILI9341(bus=object(), invert=True)
160+
assert _has_invon(d.init_sequence)

0 commit comments

Comments
 (0)