Skip to content

Commit ebc1f2b

Browse files
authored
Merge pull request #770 from mathoudebine/fix/724-turing-88-no-info-and-scrambled-screen
2 parents 71f2870 + 94ed336 commit ebc1f2b

File tree

2 files changed

+106
-64
lines changed

2 files changed

+106
-64
lines changed

library/lcd/lcd_comm_rev_c.py

Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,11 @@ class Command(Enum):
9292
NO_FLIP = bytearray((0x00,))
9393
SEND_PAYLOAD = bytearray((0xFF,))
9494

95-
def __init__(self, command):
96-
self.command = command
97-
9895

9996
class Padding(Enum):
10097
NULL = bytearray([0x00])
10198
START_DISPLAY_BITMAP = bytearray([0x2c])
10299

103-
def __init__(self, command):
104-
self.command = command
105-
106100

107101
class SleepInterval(Enum):
108102
OFF = bytearray((0x00,))
@@ -117,19 +111,13 @@ class SleepInterval(Enum):
117111
NINE = bytearray((0x09,))
118112
TEN = bytearray((0x0a,))
119113

120-
def __init__(self, command):
121-
self.command = command
122-
123114

124115
class SubRevision(Enum):
125116
UNKNOWN = ""
126117
REV_2INCH = "chs_21inch"
127118
REV_5INCH = "chs_5inch"
128119
REV_8INCH = "chs_88inch"
129120

130-
def __init__(self, command):
131-
self.command = command
132-
133121

134122
# This class is for Turing Smart Screen 2.1" / 5" / 8" screens
135123
class LcdCommRevC(LcdComm):
@@ -146,7 +134,20 @@ def __del__(self):
146134
def auto_detect_com_port() -> Optional[str]:
147135
com_ports = comports()
148136

149-
# Try to find awake device through serial number or vid/pid
137+
# First, try to find sleeping device and wake it up
138+
for com_port in com_ports:
139+
if com_port.serial_number == 'USB7INCH' or com_port.serial_number == 'CT21INCH':
140+
LcdCommRevC._wake_up_device(com_port)
141+
return LcdCommRevC.auto_detect_com_port()
142+
if com_port.vid == 0x1a86 and com_port.pid == 0xca21:
143+
LcdCommRevC._wake_up_device(com_port)
144+
return LcdCommRevC.auto_detect_com_port()
145+
146+
return LcdCommRevC._get_awake_com_port(com_ports)
147+
148+
@staticmethod
149+
def _get_awake_com_port(com_ports) -> Optional[str]:
150+
# Then try to find awake device through serial number or vid/pid
150151
for com_port in com_ports:
151152
if com_port.serial_number == '20080411':
152153
return com_port.device
@@ -155,25 +156,24 @@ def auto_detect_com_port() -> Optional[str]:
155156
if com_port.vid == 0x1d6b and (com_port.pid == 0x0121 or com_port.pid == 0x0106):
156157
return com_port.device
157158

158-
# Try to find sleeping device and wake it up
159-
for com_port in com_ports:
160-
if com_port.serial_number == 'USB7INCH' or com_port.serial_number == 'CT21INCH':
161-
LcdCommRevC._connect_to_reset_device_name(com_port)
162-
return LcdCommRevC.auto_detect_com_port()
163-
if com_port.serial_number == '20080411':
164-
return com_port.device
165-
166159
return None
167160

168161
@staticmethod
169-
def _connect_to_reset_device_name(com_port):
162+
def _wake_up_device(com_port):
170163
# this device enumerates differently when off, we need to connect once to reset it to correct COM device
171-
try:
172-
logger.debug(f"Waiting for device {com_port} to be turned ON...")
173-
serial.Serial(com_port.device, 115200, timeout=1, rtscts=True)
174-
except serial.SerialException:
175-
pass
176-
time.sleep(10)
164+
logger.debug(f"Waiting for device {com_port} to be turned ON...")
165+
166+
for i in range(15):
167+
try:
168+
# Try to connect every second, since it takes sometimes multiple connect to wake up the device
169+
serial.Serial(com_port.device, 115200, timeout=1, rtscts=True)
170+
except serial.SerialException:
171+
pass
172+
173+
if LcdCommRevC._get_awake_com_port(comports()) is not None:
174+
time.sleep(1)
175+
return
176+
time.sleep(1)
177177

178178
def _send_command(self, cmd: Command, payload: Optional[bytearray] = None, padding: Optional[Padding] = None,
179179
bypass_queue: bool = False, readsize: Optional[int] = None):
@@ -210,29 +210,23 @@ def _send_command(self, cmd: Command, payload: Optional[bytearray] = None, paddi
210210
def _hello(self):
211211
# This command reads LCD answer on serial link, so it bypasses the queue
212212
self.sub_revision = SubRevision.UNKNOWN
213+
self.serial_flush_input()
213214
self._send_command(Command.HELLO, bypass_queue=True)
214-
response = str(self.serial_read(23).decode(errors="ignore"))
215+
response = ''.join(
216+
filter(lambda x: x in set(string.printable), str(self.serial_read(23).decode(errors="ignore"))))
215217
self.serial_flush_input()
216-
logger.debug("HW sub-revision returned: %s" % ''.join(filter(lambda x: x in set(string.printable), response)))
217-
218-
# Note: sub-revisions returned by display are not reliable e.g. 2.1" displays return "chs_5inch"
219-
# if response.startswith(SubRevision.REV_5INCH.value):
220-
# self.sub_revision = SubRevision.REV_5INCH
221-
# self.display_width = 480
222-
# self.display_height = 800
223-
# elif response.startswith(SubRevision.REV_2INCH.value):
224-
# self.sub_revision = SubRevision.REV_2INCH
225-
# self.display_width = 480
226-
# self.display_height = 480
227-
# elif response.startswith(SubRevision.REV_8INCH.value):
228-
# self.sub_revision = SubRevision.REV_8INCH
229-
# self.display_width = 480
230-
# self.display_height = 1920
231-
# else:
232-
# logger.warning("Display returned unknown sub-revision on Hello answer (%s)" % str(response))
233-
# logger.debug("HW sub-revision detected: %s" % (str(self.sub_revision)))
234-
235-
# Relay on width/height for sub-revision detection
218+
logger.debug("Display ID returned: %s" % response)
219+
while not response.startswith("chs_"):
220+
logger.warning("Display returned invalid or unsupported ID, try again in 1 second")
221+
time.sleep(1)
222+
self._send_command(Command.HELLO, bypass_queue=True)
223+
response = ''.join(
224+
filter(lambda x: x in set(string.printable), str(self.serial_read(23).decode(errors="ignore"))))
225+
self.serial_flush_input()
226+
logger.debug("Display ID returned: %s" % response)
227+
228+
# Note: ID returned by display are not reliable for some models e.g. 2.1" displays return "chs_5inch"
229+
# Rely on width/height for sub-revision detection
236230
if self.display_width == 480 and self.display_height == 480:
237231
self.sub_revision = SubRevision.REV_2INCH
238232
elif self.display_width == 480 and self.display_height == 800:
@@ -242,6 +236,18 @@ def _hello(self):
242236
else:
243237
logger.error(f"Unsupported resolution {self.display_width}x{self.display_height} for revision C")
244238

239+
# Detect ROM version
240+
try:
241+
self.rom_version = int(response.split(".")[2])
242+
if self.rom_version < 80 or self.rom_version > 100:
243+
logger.warning("ROM version %d may be invalid, use default ROM version 87" % self.rom_version)
244+
self.rom_version = 87
245+
except:
246+
logger.warning("Display returned invalid or unsupported ID, use default ROM version 87")
247+
self.rom_version = 87
248+
249+
logger.debug("HW sub-revision detected: %s, ROM version: %d" % ((str(self.sub_revision)), self.rom_version))
250+
245251
def InitializeComm(self):
246252
self._hello()
247253

@@ -250,8 +256,15 @@ def Reset(self):
250256
# Reset command bypasses queue because it is run when queue threads are not yet started
251257
self._send_command(Command.RESTART, bypass_queue=True)
252258
self.closeSerial()
253-
# Wait for display reset then reconnect
254-
time.sleep(15)
259+
# Wait for disconnection (max. 15 seconds)
260+
for i in range(15):
261+
if LcdCommRevC._get_awake_com_port(comports()) is not None:
262+
time.sleep(1)
263+
# Wait for reconnection (max. 15 seconds)
264+
for i in range(15):
265+
if LcdCommRevC._get_awake_com_port(comports()) is None:
266+
time.sleep(1)
267+
# Reconnect to device
255268
self.openSerial()
256269

257270
def Clear(self):
@@ -267,13 +280,13 @@ def Clear(self):
267280
self.SetOrientation(orientation=backup_orientation)
268281

269282
def ScreenOff(self):
270-
logger.info("Calling ScreenOff")
283+
# logger.info("Calling ScreenOff")
271284
self._send_command(Command.STOP_VIDEO)
272285
self._send_command(Command.STOP_MEDIA, readsize=1024)
273286
self._send_command(Command.TURNOFF)
274287

275288
def ScreenOn(self):
276-
logger.info("Calling ScreenOn")
289+
# logger.info("Calling ScreenOn")
277290
self._send_command(Command.STOP_VIDEO)
278291
self._send_command(Command.STOP_MEDIA, readsize=1024)
279292
# self._send_command(Command.SET_BRIGHTNESS, payload=bytearray([255]))
@@ -293,8 +306,8 @@ def SetOrientation(self, orientation: Orientation = Orientation.PORTRAIT):
293306
# logger.info(f"Call SetOrientation to: {self.orientation.name}")
294307

295308
# if self.orientation == Orientation.REVERSE_LANDSCAPE or self.orientation == Orientation.REVERSE_PORTRAIT:
296-
# b = Command.STARTMODE_DEFAULT.value + Padding.NULL.value + Command.FLIP_180.value + SleepInterval.OFF.value
297-
# self._send_command(Command.OPTIONS, payload=b)
309+
# b = Command.STARTMODE_DEFAULT.value + Padding.NULL.value + Command.FLIP_180.value + SleepInterval.OFF.value
310+
# self._send_command(Command.OPTIONS, payload=b)
298311
# else:
299312
b = Command.STARTMODE_DEFAULT.value + Padding.NULL.value + Command.NO_FLIP.value + SleepInterval.OFF.value
300313
self._send_command(Command.OPTIONS, payload=b)
@@ -339,7 +352,8 @@ def DisplayPILImage(
339352
display_bmp_cmd = Command.DISPLAY_BITMAP_8INCH
340353

341354
self._send_command(display_bmp_cmd,
342-
payload=bytearray(int(self.display_width * self.display_width / 64).to_bytes(2, "big")))
355+
payload=bytearray(
356+
int(self.display_width * self.display_width / 64).to_bytes(2, "big")))
343357
self._send_command(Command.SEND_PAYLOAD,
344358
payload=bytearray(self._generate_full_image(image)),
345359
readsize=1024)
@@ -354,6 +368,7 @@ def DisplayPILImage(
354368

355369
def _generate_full_image(self, image: Image.Image) -> bytes:
356370
if self.sub_revision == SubRevision.REV_8INCH:
371+
# Switch landscape/portrait mode for 8"
357372
if self.orientation == Orientation.LANDSCAPE:
358373
image = image.rotate(270, expand=True)
359374
elif self.orientation == Orientation.REVERSE_LANDSCAPE:
@@ -370,7 +385,7 @@ def _generate_full_image(self, image: Image.Image) -> bytes:
370385
elif self.orientation == Orientation.REVERSE_LANDSCAPE:
371386
image = image.rotate(180)
372387

373-
bgra_data = image_to_BGRA(image)
388+
bgra_data, pixel_size = image_to_BGRA(image)
374389

375390
return b'\x00'.join(chunked(bgra_data, 249))
376391

@@ -379,6 +394,7 @@ def _generate_update_image(
379394
) -> Tuple[bytearray, bytearray]:
380395
x0, y0 = x, y
381396
if self.sub_revision == SubRevision.REV_8INCH:
397+
# Switch landscape/portrait mode for 8"
382398
if self.orientation == Orientation.LANDSCAPE:
383399
image = image.rotate(270, expand=True)
384400
y0 = self.get_height() - y - image.width
@@ -408,9 +424,20 @@ def _generate_update_image(
408424
y0 = x
409425

410426
img_raw_data = bytearray()
411-
bgr_data = image_to_BGR(image)
412-
for h, line in enumerate(chunked(bgr_data, image.width * 3)):
427+
428+
# Some screens require different RGBA encoding
429+
if self.rom_version > 88:
430+
# BGRA mode on 4 bytes : [B, G, R, A]
431+
img_data, pixel_size = image_to_BGRA(image)
432+
else:
433+
# BGRA mode on 3 bytes: [6-bit B + 2-bit A, 6-bit G + 2-bit A, 8-bit R]
434+
#img_data, pixel_size = image_to_compressed_BGRA(image)
435+
# For now use simple BGR that is more optimized, because this program does not support transparent background
436+
img_data, pixel_size = image_to_BGR(image)
437+
438+
for h, line in enumerate(chunked(img_data, image.width * pixel_size)):
413439
if self.sub_revision == SubRevision.REV_8INCH:
440+
# Switch landscape/portrait mode for 8"
414441
img_raw_data += int(((x0 + h) * self.display_width) + y0).to_bytes(3, "big")
415442
else:
416443
img_raw_data += int(((x0 + h) * self.display_height) + y0).to_bytes(3, "big")

library/lcd/serialize.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
def chunked(data: bytes, chunk_size: int) -> Iterator[bytes]:
88
for i in range(0, len(data), chunk_size):
9-
yield data[i : i + chunk_size]
9+
yield data[i: i + chunk_size]
1010

1111

1212
def image_to_RGB565(image: Image.Image, endianness: Literal["big", "little"]) -> bytes:
@@ -39,20 +39,35 @@ def image_to_RGB565(image: Image.Image, endianness: Literal["big", "little"]) ->
3939
return rgb565.astype(typ).tobytes()
4040

4141

42-
def image_to_BGR(image: Image.Image) -> bytes:
42+
def image_to_BGR(image: Image.Image) -> (bytes, int):
4343
if image.mode not in ["RGB", "RGBA"]:
4444
# we need the first 3 channels to be R, G and B
4545
image = image.convert("RGB")
4646
rgb = np.asarray(image)
4747
# same as rgb[:, :, [2, 1, 0]] but faster
4848
bgr = np.take(rgb, (2, 1, 0), axis=-1)
49-
return bgr.tobytes()
49+
return bgr.tobytes(), 3
5050

5151

52-
def image_to_BGRA(image: Image.Image) -> bytes:
52+
def image_to_BGRA(image: Image.Image) -> (bytes, int):
5353
if image.mode != "RGBA":
5454
image = image.convert("RGBA")
5555
rgba = np.asarray(image)
5656
# same as rgba[:, :, [2, 1, 0, 3]] but faster
5757
bgra = np.take(rgba, (2, 1, 0, 3), axis=-1)
58-
return bgra.tobytes()
58+
return bgra.tobytes(), 4
59+
60+
61+
# FIXME: to optimize like other functions above
62+
def image_to_compressed_BGRA(image: Image.Image) -> (bytes, int):
63+
compressed_bgra = bytearray()
64+
image_data = image.convert("RGBA").load()
65+
for h in range(image.height):
66+
for w in range(image.width):
67+
# r = pixel[0], g = pixel[1], b = pixel[2], a = pixel[3]
68+
pixel = image_data[w, h]
69+
a = pixel[3] >> 4
70+
compressed_bgra.append(pixel[2] & 0xFC | a >> 2)
71+
compressed_bgra.append(pixel[1] & 0xFC | a & 2)
72+
compressed_bgra.append(pixel[0])
73+
return bytes(compressed_bgra), 3

0 commit comments

Comments
 (0)