@@ -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
9996class Padding (Enum ):
10097 NULL = bytearray ([0x00 ])
10198 START_DISPLAY_BITMAP = bytearray ([0x2c ])
10299
103- def __init__ (self , command ):
104- self .command = command
105-
106100
107101class 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
124115class 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
135123class 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" )
0 commit comments