Skip to content

Commit 8a52204

Browse files
committed
Replace the queued communications with deferred delays.
The queued communications was, I believe, intended to ensure that different parts of the system could issues the commands to the display asynchronously and not have them trample on one another. However, it was not effective with my flagship device, and almost never produced an uncorrupted display. The queue seems unnecessary, and complicates the logic of the display communications. It has been removed, so that the operations all happen when they are requested by clients, rather than being delayed by an arbitrary amount of time by being queued. Instead, all the operations have been protected by a mutex, similar to the prior code, but surrounding the entire operation that must be sequenced in order. My own investigation implied that there needed to be a delay between the bitmap data and the next command sent. It's not clear why, but with this delay in place, the results were never corrupted. Inserting the delay immediately after the bitmap data send would work to allow the data to be sent uncorrupted, but it would force there to always be a delay even when there would be time between the bitmap and the next command. Instead the delay is deferred until we try to send the next command - at which point we will sleep. This means that the delay is amortised into subsequent work. The results are solid on my flagship model with a 0.02s delay. It is possible that this is high, and it could be reduced. It is possible that this does not work on model A devices, but I cannot test this at this time. The necessary changes have been made to the code, but I just have to hope they work.
1 parent f374841 commit 8a52204

File tree

4 files changed

+88
-69
lines changed

4 files changed

+88
-69
lines changed

library/display.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,11 @@ def __init__(self):
3535
if CONFIG_DATA["display"]["REVISION"] == "A":
3636
self.lcd = LcdCommRevA(com_port=CONFIG_DATA['config']['COM_PORT'],
3737
display_width=CONFIG_DATA["display"]["DISPLAY_WIDTH"],
38-
display_height=CONFIG_DATA["display"]["DISPLAY_HEIGHT"],
39-
update_queue=config.update_queue)
38+
display_height=CONFIG_DATA["display"]["DISPLAY_HEIGHT"])
4039
elif CONFIG_DATA["display"]["REVISION"] == "B":
4140
self.lcd = LcdCommRevB(com_port=CONFIG_DATA['config']['COM_PORT'],
4241
display_width=CONFIG_DATA["display"]["DISPLAY_WIDTH"],
43-
display_height=CONFIG_DATA["display"]["DISPLAY_HEIGHT"],
44-
update_queue=config.update_queue)
42+
display_height=CONFIG_DATA["display"]["DISPLAY_HEIGHT"])
4543
else:
4644
logger.error("Unknown display revision '", CONFIG_DATA["display"]["REVISION"], "'")
4745

library/lcd_comm.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ class Orientation(IntEnum):
1717

1818

1919
class LcdComm(ABC):
20-
def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_height: int = 480,
21-
update_queue: queue.Queue = None):
20+
# Amount of time that we should delay between sending bitmap data and sending the next
21+
# command. If commands are issued too quickly after a bitmap, corruption is seen.
22+
inter_bitmap_delay = 0.02
23+
24+
def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_height: int = 480):
2225
self.lcd_serial = None
2326

2427
# String containing absolute path to serial port e.g. "COM3", "/dev/ttyACM1" or "AUTO" for auto-discovery
@@ -31,13 +34,11 @@ def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_hei
3134
# Display height in default orientation (portrait)
3235
self.display_height = display_height
3336

34-
# Queue containing the serial requests to send to the screen. An external thread should run to process requests
35-
# on the queue. If you want serial requests to be done in sequence, set it to None
36-
self.update_queue = update_queue
37+
# Last time we sent bitmap data
38+
self.last_bitmap_time = 0
3739

38-
# Mutex to protect the queue in case a thread want to add multiple requests (e.g. image data) that should not be
39-
# mixed with other requests in-between
40-
self.update_queue_mutex = threading.Lock()
40+
# Mutex that must be held over any requests that must be kept together.
41+
self.com_mutex = threading.Lock()
4142

4243
def get_width(self) -> int:
4344
if self.orientation == Orientation.PORTRAIT or self.orientation == Orientation.REVERSE_PORTRAIT:

library/lcd_comm_rev_a.py

+39-27
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ class Command(IntEnum):
1919

2020

2121
class LcdCommRevA(LcdComm):
22-
def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_height: int = 480,
23-
update_queue: queue.Queue = None):
24-
LcdComm.__init__(self, com_port, display_width, display_height, update_queue)
22+
def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_height: int = 480):
23+
super().__init__(com_port, display_width, display_height)
2524
self.openSerial()
2625

2726
def __del__(self):
@@ -38,7 +37,13 @@ def auto_detect_com_port():
3837

3938
return auto_com_port
4039

41-
def SendCommand(self, cmd: Command, x: int, y: int, ex: int, ey: int, bypass_queue: bool = False):
40+
def SendCommand(self, cmd: Command, x: int, y: int, ex: int, ey: int):
41+
42+
# Commands must be sent at least 'inter_bitmap_delay' after the bitmap data.
43+
delay = (self.last_bitmap_time + self.inter_bitmap_delay) - time.time()
44+
if delay > 0:
45+
time.sleep(delay)
46+
4247
byteBuffer = bytearray(6)
4348
byteBuffer[0] = (x >> 2)
4449
byteBuffer[1] = (((x & 3) << 6) + (y >> 4))
@@ -48,35 +53,37 @@ def SendCommand(self, cmd: Command, x: int, y: int, ex: int, ey: int, bypass_que
4853
byteBuffer[5] = cmd
4954

5055
# If no queue for async requests, or if asked explicitly to do the request sequentially: do request now
51-
if not self.update_queue or bypass_queue:
52-
self.WriteData(byteBuffer)
53-
else:
54-
# Lock queue mutex then queue the request
55-
with self.update_queue_mutex:
56-
self.update_queue.put((self.WriteData, [byteBuffer]))
56+
self.WriteData(byteBuffer)
5757

5858
def InitializeComm(self):
5959
# HW revision A does not need init commands
6060
pass
6161

6262
def Reset(self):
6363
logger.info("Display reset (COM port may change)...")
64-
# Reset command bypasses queue because it is run when queue threads are not yet started
65-
self.SendCommand(Command.RESET, 0, 0, 0, 0, bypass_queue=True)
66-
# Wait for display reset then reconnect
67-
time.sleep(1)
68-
self.openSerial()
64+
65+
with self.com_mutex:
66+
self.SendCommand(Command.RESET, 0, 0, 0, 0)
67+
68+
# NOTE: Shouldn't we close serial before we try to open it again ?
69+
70+
# Wait for display reset then reconnect
71+
time.sleep(1)
72+
self.openSerial()
6973

7074
def Clear(self):
7175
self.SetOrientation(Orientation.PORTRAIT) # Bug: orientation needs to be PORTRAIT before clearing
72-
self.SendCommand(Command.CLEAR, 0, 0, 0, 0)
76+
with self.com_mutex:
77+
self.SendCommand(Command.CLEAR, 0, 0, 0, 0)
7378
self.SetOrientation() # Restore default orientation
7479

7580
def ScreenOff(self):
76-
self.SendCommand(Command.SCREEN_OFF, 0, 0, 0, 0)
81+
with self.com_mutex:
82+
self.SendCommand(Command.SCREEN_OFF, 0, 0, 0, 0)
7783

7884
def ScreenOn(self):
79-
self.SendCommand(Command.SCREEN_ON, 0, 0, 0, 0)
85+
with self.com_mutex:
86+
self.SendCommand(Command.SCREEN_ON, 0, 0, 0, 0)
8087

8188
def SetBrightness(self, level: int = 25):
8289
assert 0 <= level <= 100, 'Brightness level must be [0-100]'
@@ -86,7 +93,8 @@ def SetBrightness(self, level: int = 25):
8693
level_absolute = int(255 - ((level / 100) * 255))
8794

8895
# Level : 0 (brightest) - 255 (darkest)
89-
self.SendCommand(Command.SET_BRIGHTNESS, level_absolute, 0, 0, 0)
96+
with self.com_mutex:
97+
self.SendCommand(Command.SET_BRIGHTNESS, level_absolute, 0, 0, 0)
9098

9199
def SetBackplateLedColor(self, led_color: tuple[int, int, int] = (255, 255, 255)):
92100
logger.info("HW revision A does not support backplate LED color setting")
@@ -112,7 +120,8 @@ def SetOrientation(self, orientation: Orientation = Orientation.PORTRAIT):
112120
byteBuffer[8] = (width & 255)
113121
byteBuffer[9] = (height >> 8)
114122
byteBuffer[10] = (height & 255)
115-
self.lcd_serial.write(bytes(byteBuffer))
123+
with self.com_mutex:
124+
self.lcd_serial.write(bytes(byteBuffer))
116125

117126
def DisplayPILImage(
118127
self,
@@ -141,13 +150,12 @@ def DisplayPILImage(
141150
(x0, y0) = (x, y)
142151
(x1, y1) = (x + image_width - 1, y + image_height - 1)
143152

144-
self.SendCommand(Command.DISPLAY_BITMAP, x0, y0, x1, y1)
153+
with self.com_mutex:
154+
self.SendCommand(Command.DISPLAY_BITMAP, x0, y0, x1, y1)
145155

146-
pix = image.load()
147-
line = bytes()
156+
pix = image.load()
157+
line = bytes()
148158

149-
# Lock queue mutex then queue all the requests for the image data
150-
with self.update_queue_mutex:
151159
for h in range(image_height):
152160
for w in range(image_width):
153161
R = pix[w, h][0] >> 3
@@ -159,9 +167,13 @@ def DisplayPILImage(
159167

160168
# Send image data by multiple of DISPLAY_WIDTH bytes
161169
if len(line) >= self.get_width() * 8:
162-
self.SendLine(line)
170+
self.WriteLine(line)
163171
line = bytes()
164172

165173
# Write last line if needed
166174
if len(line) > 0:
167-
self.SendLine(line)
175+
self.WriteLine(line)
176+
177+
# There must be a short period between the last write of the bitmap data and the next
178+
# command. This seems to be around 0.02s on the flagship device.
179+
self.last_bitmap_time = time.time()

library/lcd_comm_rev_b.py

+38-30
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import struct
2+
import time
23

34
from serial.tools.list_ports import comports
45

@@ -30,9 +31,8 @@ class SubRevision(IntEnum):
3031

3132

3233
class LcdCommRevB(LcdComm):
33-
def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_height: int = 480,
34-
update_queue: queue.Queue = None):
35-
LcdComm.__init__(self, com_port, display_width, display_height, update_queue)
34+
def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_height: int = 480):
35+
super().__init__(com_port, display_width, display_height)
3636
self.openSerial()
3737
self.sub_revision = SubRevision.A01 # Run a Hello command to detect correct sub-rev.
3838

@@ -56,7 +56,13 @@ def auto_detect_com_port():
5656

5757
return auto_com_port
5858

59-
def SendCommand(self, cmd: Command, payload=None, bypass_queue: bool = False):
59+
def SendCommand(self, cmd: Command, payload=None):
60+
61+
# Commands must be sent at least 'inter_bitmap_delay' after the bitmap data.
62+
delay = (self.last_bitmap_time + self.inter_bitmap_delay) - time.time()
63+
if delay > 0:
64+
time.sleep(delay)
65+
6066
# New protocol (10 byte packets, framed with the command, 8 data bytes inside)
6167
if payload is None:
6268
payload = [0] * 8
@@ -75,20 +81,15 @@ def SendCommand(self, cmd: Command, payload=None, bypass_queue: bool = False):
7581
byteBuffer[8] = payload[7]
7682
byteBuffer[9] = cmd
7783

78-
# If no queue for async requests, or if asked explicitly to do the request sequentially: do request now
79-
if not self.update_queue or bypass_queue:
80-
self.WriteData(byteBuffer)
81-
else:
82-
# Lock queue mutex then queue the request
83-
with self.update_queue_mutex:
84-
self.update_queue.put((self.WriteData, [byteBuffer]))
84+
self.WriteData(byteBuffer)
8585

8686
def Hello(self):
8787
hello = [ord('H'), ord('E'), ord('L'), ord('L'), ord('O')]
8888

8989
# This command reads LCD answer on serial link, so it bypasses the queue
90-
self.SendCommand(Command.HELLO, payload=hello, bypass_queue=True)
91-
response = self.lcd_serial.read(10)
90+
with self.com_mutex:
91+
self.SendCommand(Command.HELLO, payload=hello)
92+
response = self.lcd_serial.read(10)
9293

9394
if len(response) != 10:
9495
logger.warning("Device not recognised (short response to HELLO)")
@@ -138,29 +139,31 @@ def SetBrightness(self, level_user: int = 25):
138139

139140
if self.is_brightness_range():
140141
# Brightness scales from 0 to 255, with 255 being the brightest and 0 being the darkest.
141-
# Convert our brightness % to an absolute value.
142142
level = int((level_user / 100) * 255)
143143
else:
144144
# Brightness is 1 (off) or 0 (full brightness)
145145
logger.info("Your display does not support custom brightness level")
146146
level = 1 if level_user == 0 else 0
147147

148-
self.SendCommand(Command.SET_BRIGHTNESS, payload=[level])
148+
with self.com_mutex:
149+
self.SendCommand(Command.SET_BRIGHTNESS, payload=[level])
149150

150151
def SetBackplateLedColor(self, led_color: tuple[int, int, int] = (255, 255, 255)):
151152
if self.is_flagship():
152-
self.SendCommand(Command.SET_LIGHTING, payload=led_color)
153+
with self.com_mutex:
154+
self.SendCommand(Command.SET_LIGHTING, payload=led_color)
153155
else:
154156
logger.info("Only HW revision 'flagship' supports backplate LED color setting")
155157

156158
def SetOrientation(self, orientation: Orientation = Orientation.PORTRAIT, new_width: int = 320, new_height: int = 480):
157159
# In revision B, basic orientations (portrait / landscape) are managed by the display
158160
# The reverse orientations (reverse portrait / reverse landscape) are software-managed
159161
self.orientation = orientation
160-
if self.orientation == Orientation.PORTRAIT or self.orientation == Orientation.REVERSE_PORTRAIT:
161-
self.SendCommand(Command.SET_ORIENTATION, payload=[OrientationValueRevB.ORIENTATION_PORTRAIT])
162-
else:
163-
self.SendCommand(Command.SET_ORIENTATION, payload=[OrientationValueRevB.ORIENTATION_LANDSCAPE])
162+
with self.com_mutex:
163+
if self.orientation == Orientation.PORTRAIT or self.orientation == Orientation.REVERSE_PORTRAIT:
164+
self.SendCommand(Command.SET_ORIENTATION, payload=[OrientationValueRevB.ORIENTATION_PORTRAIT])
165+
else:
166+
self.SendCommand(Command.SET_ORIENTATION, payload=[OrientationValueRevB.ORIENTATION_LANDSCAPE])
164167

165168
def DisplayPILImage(
166169
self,
@@ -193,16 +196,17 @@ def DisplayPILImage(
193196
(x0, y0) = (self.get_width() - x - image_width, self.get_height() - y - image_height)
194197
(x1, y1) = (self.get_width() - x - 1, self.get_height() - y - 1)
195198

196-
self.SendCommand(Command.DISPLAY_BITMAP,
197-
payload=[(x0 >> 8) & 255, x0 & 255,
198-
(y0 >> 8) & 255, y0 & 255,
199-
(x1 >> 8) & 255, x1 & 255,
200-
(y1 >> 8) & 255, y1 & 255])
199+
# Do the image load outside the mutex in case it takes a long time
201200
pix = image.load()
202-
line = bytes()
203201

204-
# Lock queue mutex then queue all the requests for the image data
205-
with self.update_queue_mutex:
202+
with self.com_mutex:
203+
self.SendCommand(Command.DISPLAY_BITMAP,
204+
payload=[(x0 >> 8) & 255, x0 & 255,
205+
(y0 >> 8) & 255, y0 & 255,
206+
(x1 >> 8) & 255, x1 & 255,
207+
(y1 >> 8) & 255, y1 & 255])
208+
line = bytes()
209+
206210
for h in range(image_height):
207211
for w in range(image_width):
208212
if self.orientation == Orientation.PORTRAIT or self.orientation == Orientation.LANDSCAPE:
@@ -227,9 +231,13 @@ def DisplayPILImage(
227231

228232
# Send image data by multiple of DISPLAY_WIDTH bytes
229233
if len(line) >= self.get_width() * 8:
230-
self.SendLine(line)
234+
self.WriteLine(line)
231235
line = bytes()
232236

233237
# Write last line if needed
234238
if len(line) > 0:
235-
self.SendLine(line)
239+
self.WriteLine(line)
240+
241+
# There must be a short period between the last write of the bitmap data and the next
242+
# command. This seems to be around 0.02s on the flagship device.
243+
self.last_bitmap_time = time.time()

0 commit comments

Comments
 (0)