This commit is contained in:
jeremygan2021
2026-03-02 22:58:02 +08:00
parent 4c51f52654
commit 124b185b8a
7 changed files with 297 additions and 191 deletions

273
font.py
View File

@@ -19,8 +19,47 @@ class Font:
color_bytes = struct.pack(">H", color)
bg_bytes = struct.pack(">H", bg)
# Create LUT for current color/bg
lut = [bytearray(16) for _ in range(256)]
for i in range(256):
for bit in range(8):
# bit 7 is first pixel (leftmost)
# target index: (7-bit)*2
val = (i >> bit) & 1
idx = (7 - bit) * 2
if val:
lut[i][idx] = color_bytes[0]
lut[i][idx+1] = color_bytes[1]
else:
lut[i][idx] = bg_bytes[0]
lut[i][idx+1] = bg_bytes[1]
initial_x = x
# 1. Identify missing fonts
missing_codes = set()
for char in text:
if ord(char) > 127:
code = ord(char)
if code not in self.cache:
missing_codes.add(code)
# 2. Batch request missing fonts
if missing_codes and self.ws:
# Convert to list for consistent order/string
missing_list = list(missing_codes)
# Limit batch size? Maybe 20 chars at a time?
# For short ASR result, usually < 20 chars.
req_str = ",".join([str(c) for c in missing_list])
print(f"Batch requesting fonts: {req_str}")
try:
self.ws.send(f"GET_FONTS_BATCH:{req_str}")
self._wait_for_fonts(missing_codes)
except Exception as e:
print(f"Batch font request failed: {e}")
# 3. Draw text
for char in text:
# Handle newlines
if char == '\n':
@@ -38,56 +77,20 @@ class Font:
is_chinese = False
buf_data = None
# Check if it's Chinese
# Check if it's Chinese (or non-ASCII)
if ord(char) > 127:
try:
gb = char.encode('gb2312')
if len(gb) == 2:
code = struct.unpack('>H', gb)[0]
# Try to get from cache
if code in self.cache:
buf_data = self.cache[code]
is_chinese = True
else:
# Need to fetch from server
# Since we can't block easily here (unless we use a blocking socket recv or a callback mechanism),
# we have to rely on the main loop to handle responses.
# But we want to draw *now*.
#
# Solution:
# 1. Send request
# 2. Wait for response with timeout (blocking wait)
# This is slow for long text but works for small amounts.
if self.ws:
# Send request: GET_FONT:0xA1A1
hex_code = "0x{:04X}".format(code)
print(f"Requesting font for {hex_code} ({char})")
self.ws.send(f"GET_FONT:{hex_code}")
# Wait for response
# We need to peek/read from WS until we get FONT_DATA
buf_data = self._wait_for_font(hex_code)
if buf_data:
self.cache[code] = buf_data
is_chinese = True
print(f"Font loaded for {hex_code}")
else:
print(f"Font fetch timeout for {hex_code}")
# Fallback: draw question mark or box
self._draw_ascii(tft, '?', x, y, color, bg)
x += 8
continue # Skip drawing bitmap logic
else:
print("WS not available for font fetch")
except Exception as e:
print(f"Font error: {e}")
code = ord(char)
if code in self.cache:
buf_data = self.cache[code]
is_chinese = True
else:
# Still missing after batch request?
# Could be timeout or invalid char.
pass
if is_chinese and buf_data:
# Draw Chinese character (16x16)
self._draw_bitmap(tft, buf_data, x, y, 16, 16, color_bytes, bg_bytes)
self._draw_bitmap(tft, buf_data, x, y, 16, 16, lut)
x += 16
else:
# Draw ASCII (8x16) using built-in framebuf font (8x8 actually)
@@ -97,78 +100,148 @@ class Font:
self._draw_ascii(tft, char, x, y, color, bg)
x += 8
def _wait_for_font(self, target_hex_code):
def _wait_for_fonts(self, target_codes):
"""
Blocking wait for specific font data from WebSocket.
Timeout 1s.
WARNING: This might consume other messages (like audio playback commands)!
We need to handle them or put them back?
WebSocketClient doesn't support peeking easily.
This is a limitation. If we receive other messages, we should probably print them or ignore them.
But for ASR result display, usually we are in a state where we just received ASR result and are waiting for TTS.
Blocking wait for a set of font codes.
Buffers other messages to self.ws.unread_messages.
"""
if not self.ws:
return None
if not self.ws or not target_codes:
return
start = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), start) < 1000:
# We use a non-blocking poll if possible, but here we want to block until data arrives
# ws.recv() is blocking.
# But we might block forever if server doesn't reply.
# So we should use poll with timeout.
self.local_deferred = []
# 2 seconds timeout for batch
while time.ticks_diff(time.ticks_ms(), start) < 2000 and target_codes:
# Using uselect in main.py, but here we don't have easy access to it unless passed in.
# Let's try a simple approach: set socket timeout temporarily?
# Or use select.poll()
# Check unread_messages first?
# Actually ws.recv() in our modified client already checks unread_messages.
# But wait, if we put something BACK into unread_messages, we need to be careful not to read it again immediately if we are looping?
# No, we only put NON-FONT messages back. We are looking for FONT messages.
# So if we pop a non-font message, we put it back?
# If we put it back at head, we will read it again next loop! Infinite loop!
#
# Solution: We should NOT use ws.recv() which pops from unread.
# We should assume unread_messages might contain what we need?
#
# Actually, `ws.recv()` pops from `unread_messages`.
# If we get a message that is NOT what we want, we should store it in a temporary list, and push them all back at the end?
# Or append to `unread_messages` (if it's a queue).
# But `unread_messages` is used as a LIFO or FIFO?
# pop(0) -> FIFO.
# If we append, it goes to end.
# So:
# 1. recv() -> gets msg.
# 2. Is it font?
# Yes -> process.
# No -> append to `temp_buffer`.
# 3. After function finishes (or timeout), extend `unread_messages` with `temp_buffer`?
# Wait, `unread_messages` should be preserved order.
# If we had [A, B] in unread.
# recv() gets A. Not font. Temp=[A].
# recv() gets B. Not font. Temp=[A, B].
# recv() gets network C (Font). Process.
# End.
# Restore: unread = Temp + unread? (unread is empty now).
# So unread becomes [A, B]. Correct.
import uselect
poller = uselect.poll()
poller.register(self.ws.sock, uselect.POLLIN)
events = poller.poll(200) # 200ms timeout
if events:
try:
msg = self.ws.recv()
# Fast check if we can read
# But we want to block until SOMETHING arrives.
# If unread_messages is not empty, we should process them first.
# But we can't peak easily without modifying recv again.
# Let's just use recv() and handle the buffering logic here.
try:
# Use a poller for the socket part to implement timeout
# But recv() handles logic.
# If unread_messages is empty, we poll socket.
can_read = False
if self.ws.unread_messages:
can_read = True
else:
poller = uselect.poll()
poller.register(self.ws.sock, uselect.POLLIN)
events = poller.poll(100) # 100ms
if events:
can_read = True
if can_read:
msg = self.ws.recv() # This will pop from unread or read from sock
if msg is None:
# Socket closed or error?
# Or just timeout in recv (but we polled).
continue
if isinstance(msg, str):
if msg.startswith(f"FONT_DATA:{target_hex_code}:"):
# Found it!
hex_data = msg.split(":")[2]
return binascii.unhexlify(hex_data)
if msg == "FONT_BATCH_END":
# Batch complete. Mark remaining as failed.
# We need to iterate over a copy because we are modifying target_codes?
# Actually we just clear it.
# But wait, target_codes is passed by reference (set).
# If we clear it, loop breaks.
# But we also want to mark cache as None for missing ones.
temp_missing = list(target_codes)
for c in temp_missing:
print(f"Batch missing/failed: {c}")
self.cache[c] = None # Cache failure
target_codes.clear()
elif msg.startswith("FONT_DATA:"):
# Wrong font data? Ignore or cache it?
# General font data handler
parts = msg.split(":")
if len(parts) >= 3:
c = int(parts[1], 16)
d = binascii.unhexlify(parts[2])
self.cache[c] = d
try:
key_str = parts[1]
if key_str.startswith("0x"):
c = int(key_str, 16)
else:
c = int(key_str)
d = binascii.unhexlify(parts[2])
self.cache[c] = d
if c in target_codes:
target_codes.remove(c)
# print(f"Batch loaded: {c}")
except:
pass
else:
# Other message, e.g. START_PLAYBACK
# We can't put it back easily.
# For now, just print it and ignore (it will be lost!)
# ideally we should have a message queue.
print(f"Ignored msg during font fetch: {msg}")
except:
pass
return None
self.local_deferred.append(msg)
elif msg is not None:
# Binary message? Buffer it too.
self.local_deferred.append(msg)
except Exception as e:
print(f"Wait font error: {e}")
# End of wait. Restore deferred messages.
if self.local_deferred:
# We want new_list = local_deferred + old_list
self.ws.unread_messages = self.local_deferred + self.ws.unread_messages
self.local_deferred = []
def _draw_bitmap(self, tft, bitmap, x, y, w, h, color_bytes, bg_bytes):
# Convert 1bpp bitmap to RGB565 buffer
# bitmap length is w * h / 8 = 32 bytes for 16x16
def _wait_for_font(self, target_code_str):
# Compatibility wrapper or deprecated?
# The new logic uses batch wait.
pass
def _draw_bitmap(self, tft, bitmap, x, y, w, h, lut):
# Convert 1bpp bitmap to RGB565 buffer using LUT
# Optimize buffer allocation
rgb_buf = bytearray(w * h * 2)
idx = 0
for byte in bitmap:
for i in range(7, -1, -1):
if (byte >> i) & 1:
rgb_buf[idx] = color_bytes[0]
rgb_buf[idx+1] = color_bytes[1]
else:
rgb_buf[idx] = bg_bytes[0]
rgb_buf[idx+1] = bg_bytes[1]
idx += 2
# bitmap length is w * h / 8 = 32 bytes for 16x16
# Create list of chunks
chunks = [lut[b] for b in bitmap]
# Join chunks into one buffer
rgb_buf = b''.join(chunks)
tft.blit_buffer(rgb_buf, x, y, w, h)
def _draw_ascii(self, tft, char, x, y, color, bg):