@@ -92,17 +92,11 @@ class Command(Enum):
92
92
NO_FLIP = bytearray ((0x00 ,))
93
93
SEND_PAYLOAD = bytearray ((0xFF ,))
94
94
95
- def __init__ (self , command ):
96
- self .command = command
97
-
98
95
99
96
class Padding (Enum ):
100
97
NULL = bytearray ([0x00 ])
101
98
START_DISPLAY_BITMAP = bytearray ([0x2c ])
102
99
103
- def __init__ (self , command ):
104
- self .command = command
105
-
106
100
107
101
class SleepInterval (Enum ):
108
102
OFF = bytearray ((0x00 ,))
@@ -117,19 +111,13 @@ class SleepInterval(Enum):
117
111
NINE = bytearray ((0x09 ,))
118
112
TEN = bytearray ((0x0a ,))
119
113
120
- def __init__ (self , command ):
121
- self .command = command
122
-
123
114
124
115
class SubRevision (Enum ):
125
116
UNKNOWN = ""
126
117
REV_2INCH = "chs_21inch"
127
118
REV_5INCH = "chs_5inch"
128
119
REV_8INCH = "chs_88inch"
129
120
130
- def __init__ (self , command ):
131
- self .command = command
132
-
133
121
134
122
# This class is for Turing Smart Screen 2.1" / 5" / 8" screens
135
123
class LcdCommRevC (LcdComm ):
@@ -146,7 +134,20 @@ def __del__(self):
146
134
def auto_detect_com_port () -> Optional [str ]:
147
135
com_ports = comports ()
148
136
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
150
151
for com_port in com_ports :
151
152
if com_port .serial_number == '20080411' :
152
153
return com_port .device
@@ -155,25 +156,24 @@ def auto_detect_com_port() -> Optional[str]:
155
156
if com_port .vid == 0x1d6b and (com_port .pid == 0x0121 or com_port .pid == 0x0106 ):
156
157
return com_port .device
157
158
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
-
166
159
return None
167
160
168
161
@staticmethod
169
- def _connect_to_reset_device_name (com_port ):
162
+ def _wake_up_device (com_port ):
170
163
# 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 )
177
177
178
178
def _send_command (self , cmd : Command , payload : Optional [bytearray ] = None , padding : Optional [Padding ] = None ,
179
179
bypass_queue : bool = False , readsize : Optional [int ] = None ):
@@ -210,29 +210,23 @@ def _send_command(self, cmd: Command, payload: Optional[bytearray] = None, paddi
210
210
def _hello (self ):
211
211
# This command reads LCD answer on serial link, so it bypasses the queue
212
212
self .sub_revision = SubRevision .UNKNOWN
213
+ self .serial_flush_input ()
213
214
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" ))))
215
217
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
236
230
if self .display_width == 480 and self .display_height == 480 :
237
231
self .sub_revision = SubRevision .REV_2INCH
238
232
elif self .display_width == 480 and self .display_height == 800 :
@@ -242,6 +236,18 @@ def _hello(self):
242
236
else :
243
237
logger .error (f"Unsupported resolution { self .display_width } x{ self .display_height } for revision C" )
244
238
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
+
245
251
def InitializeComm (self ):
246
252
self ._hello ()
247
253
@@ -250,8 +256,15 @@ def Reset(self):
250
256
# Reset command bypasses queue because it is run when queue threads are not yet started
251
257
self ._send_command (Command .RESTART , bypass_queue = True )
252
258
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
255
268
self .openSerial ()
256
269
257
270
def Clear (self ):
@@ -267,13 +280,13 @@ def Clear(self):
267
280
self .SetOrientation (orientation = backup_orientation )
268
281
269
282
def ScreenOff (self ):
270
- logger .info ("Calling ScreenOff" )
283
+ # logger.info("Calling ScreenOff")
271
284
self ._send_command (Command .STOP_VIDEO )
272
285
self ._send_command (Command .STOP_MEDIA , readsize = 1024 )
273
286
self ._send_command (Command .TURNOFF )
274
287
275
288
def ScreenOn (self ):
276
- logger .info ("Calling ScreenOn" )
289
+ # logger.info("Calling ScreenOn")
277
290
self ._send_command (Command .STOP_VIDEO )
278
291
self ._send_command (Command .STOP_MEDIA , readsize = 1024 )
279
292
# self._send_command(Command.SET_BRIGHTNESS, payload=bytearray([255]))
@@ -293,8 +306,8 @@ def SetOrientation(self, orientation: Orientation = Orientation.PORTRAIT):
293
306
# logger.info(f"Call SetOrientation to: {self.orientation.name}")
294
307
295
308
# 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)
298
311
# else:
299
312
b = Command .STARTMODE_DEFAULT .value + Padding .NULL .value + Command .NO_FLIP .value + SleepInterval .OFF .value
300
313
self ._send_command (Command .OPTIONS , payload = b )
@@ -339,7 +352,8 @@ def DisplayPILImage(
339
352
display_bmp_cmd = Command .DISPLAY_BITMAP_8INCH
340
353
341
354
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" )))
343
357
self ._send_command (Command .SEND_PAYLOAD ,
344
358
payload = bytearray (self ._generate_full_image (image )),
345
359
readsize = 1024 )
@@ -354,6 +368,7 @@ def DisplayPILImage(
354
368
355
369
def _generate_full_image (self , image : Image .Image ) -> bytes :
356
370
if self .sub_revision == SubRevision .REV_8INCH :
371
+ # Switch landscape/portrait mode for 8"
357
372
if self .orientation == Orientation .LANDSCAPE :
358
373
image = image .rotate (270 , expand = True )
359
374
elif self .orientation == Orientation .REVERSE_LANDSCAPE :
@@ -370,7 +385,7 @@ def _generate_full_image(self, image: Image.Image) -> bytes:
370
385
elif self .orientation == Orientation .REVERSE_LANDSCAPE :
371
386
image = image .rotate (180 )
372
387
373
- bgra_data = image_to_BGRA (image )
388
+ bgra_data , pixel_size = image_to_BGRA (image )
374
389
375
390
return b'\x00 ' .join (chunked (bgra_data , 249 ))
376
391
@@ -379,6 +394,7 @@ def _generate_update_image(
379
394
) -> Tuple [bytearray , bytearray ]:
380
395
x0 , y0 = x , y
381
396
if self .sub_revision == SubRevision .REV_8INCH :
397
+ # Switch landscape/portrait mode for 8"
382
398
if self .orientation == Orientation .LANDSCAPE :
383
399
image = image .rotate (270 , expand = True )
384
400
y0 = self .get_height () - y - image .width
@@ -408,9 +424,20 @@ def _generate_update_image(
408
424
y0 = x
409
425
410
426
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 )):
413
439
if self .sub_revision == SubRevision .REV_8INCH :
440
+ # Switch landscape/portrait mode for 8"
414
441
img_raw_data += int (((x0 + h ) * self .display_width ) + y0 ).to_bytes (3 , "big" )
415
442
else :
416
443
img_raw_data += int (((x0 + h ) * self .display_height ) + y0 ).to_bytes (3 , "big" )
0 commit comments