2525import time
2626from abc import ABC , abstractmethod
2727from enum import IntEnum
28- from typing import Tuple , List
28+ from typing import Tuple , List , Optional , Dict
2929
3030import serial
3131from PIL import Image , ImageDraw , ImageFont
3232
3333from library .log import logger
34+ from library .lcd .color import Color , parse_color
3435
3536
3637class Orientation (IntEnum ):
@@ -42,7 +43,7 @@ class Orientation(IntEnum):
4243
4344class LcdComm (ABC ):
4445 def __init__ (self , com_port : str = "AUTO" , display_width : int = 320 , display_height : int = 480 ,
45- update_queue : queue .Queue = None ):
46+ update_queue : Optional [ queue .Queue ] = None ):
4647 self .lcd_serial = None
4748
4849 # String containing absolute path to serial port e.g. "COM3", "/dev/ttyACM1" or "AUTO" for auto-discovery
@@ -67,7 +68,10 @@ def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_hei
6768 self .image_cache = {} # { key=path, value=PIL.Image }
6869
6970 # Create a cache to store opened fonts, to avoid opening and loading from the filesystem every time
70- self .font_cache = {} # { key=(font, size), value=PIL.ImageFont }
71+ self .font_cache : Dict [
72+ Tuple [str , int ], # key=(font, size)
73+ ImageFont .FreeTypeFont # value= a loaded freetype font
74+ ] = {}
7175
7276 def get_width (self ) -> int :
7377 if self .orientation == Orientation .PORTRAIT or self .orientation == Orientation .REVERSE_PORTRAIT :
@@ -97,7 +101,7 @@ def openSerial(self):
97101 logger .debug (f"Static COM port: { self .com_port } " )
98102
99103 try :
100- self .lcd_serial = serial .Serial (self .com_port , 115200 , timeout = 1 , rtscts = 1 )
104+ self .lcd_serial = serial .Serial (self .com_port , 115200 , timeout = 1 , rtscts = True )
101105 except Exception as e :
102106 logger .error (f"Cannot open COM port { self .com_port } : { e } " )
103107 try :
@@ -106,10 +110,20 @@ def openSerial(self):
106110 os ._exit (0 )
107111
108112 def closeSerial (self ):
109- try :
113+ if self . lcd_serial is not None :
110114 self .lcd_serial .close ()
111- except :
112- pass
115+
116+ def serial_write (self , data : bytes ):
117+ assert self .lcd_serial is not None
118+ self .lcd_serial .write (data )
119+
120+ def serial_read (self , size : int ) -> bytes :
121+ assert self .lcd_serial is not None
122+ return self .lcd_serial .read (size )
123+
124+ def serial_flush_input (self ):
125+ if self .lcd_serial is not None :
126+ self .lcd_serial .reset_input_buffer ()
113127
114128 def WriteData (self , byteBuffer : bytearray ):
115129 self .WriteLine (bytes (byteBuffer ))
@@ -124,39 +138,39 @@ def SendLine(self, line: bytes):
124138
125139 def WriteLine (self , line : bytes ):
126140 try :
127- self .lcd_serial . write (line )
128- except serial .serialutil . SerialTimeoutException :
141+ self .serial_write (line )
142+ except serial .SerialTimeoutException :
129143 # We timed-out trying to write to our device, slow things down.
130144 logger .warning ("(Write line) Too fast! Slow down!" )
131- except serial .serialutil . SerialException :
145+ except serial .SerialException :
132146 # Error writing data to device: close and reopen serial port, try to write again
133147 logger .error (
134148 "SerialException: Failed to send serial data to device. Closing and reopening COM port before retrying once." )
135149 self .closeSerial ()
136150 time .sleep (1 )
137151 self .openSerial ()
138- self .lcd_serial . write (line )
152+ self .serial_write (line )
139153
140154 def ReadData (self , readSize : int ):
141155 try :
142- response = self .lcd_serial . read (readSize )
156+ response = self .serial_read (readSize )
143157 # logger.debug("Received: [{}]".format(str(response, 'utf-8')))
144158 return response
145- except serial .serialutil . SerialTimeoutException :
159+ except serial .SerialTimeoutException :
146160 # We timed-out trying to read from our device, slow things down.
147161 logger .warning ("(Read data) Too fast! Slow down!" )
148- except serial .serialutil . SerialException :
162+ except serial .SerialException :
149163 # Error writing data to device: close and reopen serial port, try to read again
150164 logger .error (
151165 "SerialException: Failed to read serial data from device. Closing and reopening COM port before retrying once." )
152166 self .closeSerial ()
153167 time .sleep (1 )
154168 self .openSerial ()
155- return self .lcd_serial . read (readSize )
169+ return self .serial_read (readSize )
156170
157171 @staticmethod
158172 @abstractmethod
159- def auto_detect_com_port ():
173+ def auto_detect_com_port () -> Optional [ str ] :
160174 pass
161175
162176 @abstractmethod
@@ -193,7 +207,7 @@ def SetOrientation(self, orientation: Orientation):
193207 @abstractmethod
194208 def DisplayPILImage (
195209 self ,
196- image : Image ,
210+ image : Image . Image ,
197211 x : int = 0 , y : int = 0 ,
198212 image_width : int = 0 ,
199213 image_height : int = 0
@@ -213,20 +227,17 @@ def DisplayText(
213227 height : int = 0 ,
214228 font : str = "roboto-mono/RobotoMono-Regular.ttf" ,
215229 font_size : int = 20 ,
216- font_color : Tuple [ int , int , int ] = (0 , 0 , 0 ),
217- background_color : Tuple [ int , int , int ] = (255 , 255 , 255 ),
218- background_image : str = None ,
230+ font_color : Color = (0 , 0 , 0 ),
231+ background_color : Color = (255 , 255 , 255 ),
232+ background_image : Optional [ str ] = None ,
219233 align : str = 'left' ,
220- anchor : str = None ,
234+ anchor : str = 'la' ,
221235 ):
222236 # Convert text to bitmap using PIL and display it
223237 # Provide the background image path to display text with transparent background
224238
225- if isinstance (font_color , str ):
226- font_color = tuple (map (int , font_color .split (', ' )))
227-
228- if isinstance (background_color , str ):
229- background_color = tuple (map (int , background_color .split (', ' )))
239+ font_color = parse_color (font_color )
240+ background_color = parse_color (background_color )
230241
231242 assert x <= self .get_width (), 'Text X coordinate ' + str (x ) + ' must be <= display width ' + str (
232243 self .get_width ())
@@ -247,13 +258,11 @@ def DisplayText(
247258 text_image = self .open_image (background_image )
248259
249260 # Get text bounding box
250- if (font , font_size ) not in self .font_cache :
251- self .font_cache [(font , font_size )] = ImageFont .truetype ("./res/fonts/" + font , font_size )
252- font = self .font_cache [(font , font_size )]
261+ fontdata = self .open_font (font , font_size )
253262 d = ImageDraw .Draw (text_image )
254263
255264 if width == 0 or height == 0 :
256- left , top , right , bottom = d .textbbox ((x , y ), text , font = font , align = align , anchor = anchor )
265+ left , top , right , bottom = d .textbbox ((x , y ), text , font = fontdata , align = align , anchor = anchor )
257266
258267 # textbbox may return float values, which is not good for the bitmap operations below.
259268 # Let's extend the bounding box to the next whole pixel in all directions
@@ -263,21 +272,21 @@ def DisplayText(
263272 left , top , right , bottom = x , y , x + width , y + height
264273
265274 if anchor .startswith ("m" ):
266- x = ( right + left ) / 2
275+ x = int (( right + left ) / 2 )
267276 elif anchor .startswith ("r" ):
268277 x = right
269278 else :
270279 x = left
271280
272281 if anchor .endswith ("m" ):
273- y = ( bottom + top ) / 2
282+ y = int (( bottom + top ) / 2 )
274283 elif anchor .endswith ("b" ):
275284 y = bottom
276285 else :
277286 y = top
278287
279288 # Draw text onto the background image with specified color & font
280- d .text ((x , y ), text , font = font , fill = font_color , align = align , anchor = anchor )
289+ d .text ((x , y ), text , font = fontdata , fill = font_color , align = align , anchor = anchor )
281290
282291 # Restrict the dimensions if they overflow the display size
283292 left = max (left , 0 )
@@ -292,18 +301,15 @@ def DisplayText(
292301
293302 def DisplayProgressBar (self , x : int , y : int , width : int , height : int , min_value : int = 0 , max_value : int = 100 ,
294303 value : int = 50 ,
295- bar_color : Tuple [ int , int , int ] = (0 , 0 , 0 ),
304+ bar_color : Color = (0 , 0 , 0 ),
296305 bar_outline : bool = True ,
297- background_color : Tuple [ int , int , int ] = (255 , 255 , 255 ),
298- background_image : str = None ):
306+ background_color : Color = (255 , 255 , 255 ),
307+ background_image : Optional [ str ] = None ):
299308 # Generate a progress bar and display it
300309 # Provide the background image path to display progress bar with transparent background
301310
302- if isinstance (bar_color , str ):
303- bar_color = tuple (map (int , bar_color .split (', ' )))
304-
305- if isinstance (background_color , str ):
306- background_color = tuple (map (int , background_color .split (', ' )))
311+ bar_color = parse_color (bar_color )
312+ background_color = parse_color (background_color )
307313
308314 assert x <= self .get_width (), 'Progress bar X coordinate must be <= display width'
309315 assert y <= self .get_height (), 'Progress bar Y coordinate must be <= display height'
@@ -343,26 +349,21 @@ def DisplayProgressBar(self, x: int, y: int, width: int, height: int, min_value:
343349
344350 def DisplayLineGraph (self , x : int , y : int , width : int , height : int ,
345351 values : List [float ],
346- min_value : int = 0 ,
347- max_value : int = 100 ,
352+ min_value : float = 0 ,
353+ max_value : float = 100 ,
348354 autoscale : bool = False ,
349- line_color : Tuple [ int , int , int ] = (0 , 0 , 0 ),
355+ line_color : Color = (0 , 0 , 0 ),
350356 line_width : int = 2 ,
351357 graph_axis : bool = True ,
352- axis_color : Tuple [ int , int , int ] = (0 , 0 , 0 ),
353- background_color : Tuple [ int , int , int ] = (255 , 255 , 255 ),
354- background_image : str = None ):
358+ axis_color : Color = (0 , 0 , 0 ),
359+ background_color : Color = (255 , 255 , 255 ),
360+ background_image : Optional [ str ] = None ):
355361 # Generate a plot graph and display it
356362 # Provide the background image path to display plot graph with transparent background
357363
358- if isinstance (line_color , str ):
359- line_color = tuple (map (int , line_color .split (', ' )))
360-
361- if isinstance (axis_color , str ):
362- axis_color = tuple (map (int , axis_color .split (', ' )))
363-
364- if isinstance (background_color , str ):
365- background_color = tuple (map (int , background_color .split (', ' )))
364+ line_color = parse_color (line_color )
365+ axis_color = parse_color (axis_color )
366+ background_color = parse_color (background_color )
366367
367368 assert x <= self .get_width (), 'Progress bar X coordinate must be <= display width'
368369 assert y <= self .get_height (), 'Progress bar Y coordinate must be <= display height'
@@ -428,47 +429,41 @@ def DisplayLineGraph(self, x: int, y: int, width: int, height: int,
428429 # Draw Legend
429430 draw .line ([0 , 0 , 1 , 0 ], fill = axis_color )
430431 text = f"{ int (max_value )} "
431- font = ImageFont . truetype ( "./res/fonts/" + "roboto/Roboto-Black.ttf" , 10 )
432- left , top , right , bottom = font .getbbox (text )
432+ fontdata = self . open_font ( "roboto/Roboto-Black.ttf" , 10 )
433+ _ , top , right , bottom = fontdata .getbbox (text )
433434 draw .text ((2 , 0 - top ), text ,
434- font = font , fill = axis_color )
435+ font = fontdata , fill = axis_color )
435436
436437 text = f"{ int (min_value )} "
437- font = ImageFont .truetype ("./res/fonts/" + "roboto/Roboto-Black.ttf" , 10 )
438- left , top , right , bottom = font .getbbox (text )
438+ _ , top , right , bottom = fontdata .getbbox (text )
439439 draw .text ((width - 1 - right , height - 2 - bottom ), text ,
440- font = font , fill = axis_color )
440+ font = fontdata , fill = axis_color )
441441
442442 self .DisplayPILImage (graph_image , x , y )
443443
444444 def DisplayRadialProgressBar (self , xc : int , yc : int , radius : int , bar_width : int ,
445445 min_value : int = 0 ,
446446 max_value : int = 100 ,
447- angle_start : int = 0 ,
448- angle_end : int = 360 ,
447+ angle_start : float = 0 ,
448+ angle_end : float = 360 ,
449449 angle_sep : int = 5 ,
450450 angle_steps : int = 10 ,
451451 clockwise : bool = True ,
452452 value : int = 50 ,
453- text : str = None ,
453+ text : Optional [ str ] = None ,
454454 with_text : bool = True ,
455455 font : str = "roboto/Roboto-Black.ttf" ,
456456 font_size : int = 20 ,
457- font_color : Tuple [ int , int , int ] = (0 , 0 , 0 ),
458- bar_color : Tuple [ int , int , int ] = (0 , 0 , 0 ),
459- background_color : Tuple [ int , int , int ] = (255 , 255 , 255 ),
460- background_image : str = None ):
457+ font_color : Color = (0 , 0 , 0 ),
458+ bar_color : Color = (0 , 0 , 0 ),
459+ background_color : Color = (255 , 255 , 255 ),
460+ background_image : Optional [ str ] = None ):
461461 # Generate a radial progress bar and display it
462462 # Provide the background image path to display progress bar with transparent background
463463
464- if isinstance (bar_color , str ):
465- bar_color = tuple (map (int , bar_color .split (', ' )))
466-
467- if isinstance (background_color , str ):
468- background_color = tuple (map (int , background_color .split (', ' )))
469-
470- if isinstance (font_color , str ):
471- font_color = tuple (map (int , font_color .split (', ' )))
464+ bar_color = parse_color (bar_color )
465+ background_color = parse_color (background_color )
466+ font_color = parse_color (font_color )
472467
473468 if angle_start % 361 == angle_end % 361 :
474469 if clockwise :
@@ -586,17 +581,22 @@ def DisplayRadialProgressBar(self, xc: int, yc: int, radius: int, bar_width: int
586581 if with_text :
587582 if text is None :
588583 text = f"{ int (pct * 100 + .5 )} %"
589- font = ImageFont . truetype ( "./res/fonts/" + font , font_size )
590- left , top , right , bottom = font .getbbox (text )
584+ fontdata = self . open_font ( font , font_size )
585+ left , top , right , bottom = fontdata .getbbox (text )
591586 w , h = right - left , bottom - top
592587 draw .text ((radius - w / 2 , radius - top - h / 2 ), text ,
593- font = font , fill = font_color )
588+ font = fontdata , fill = font_color )
594589
595590 self .DisplayPILImage (bar_image , xc - radius , yc - radius )
596591
597592 # Load image from the filesystem, or get from the cache if it has already been loaded previously
598- def open_image (self , bitmap_path : str ) -> Image :
593+ def open_image (self , bitmap_path : str ) -> Image . Image :
599594 if bitmap_path not in self .image_cache :
600595 logger .debug ("Bitmap " + bitmap_path + " is now loaded in the cache" )
601596 self .image_cache [bitmap_path ] = Image .open (bitmap_path )
602597 return copy .copy (self .image_cache [bitmap_path ])
598+
599+ def open_font (self , name : str , size : int ) -> ImageFont .FreeTypeFont :
600+ if (name , size ) not in self .font_cache :
601+ self .font_cache [(name , size )] = ImageFont .truetype ("./res/fonts/" + name , size )
602+ return self .font_cache [(name , size )]
0 commit comments