209 lines
8.5 KiB
Python
209 lines
8.5 KiB
Python
import framebuf
|
|
import struct
|
|
import time
|
|
import binascii
|
|
|
|
class Font:
|
|
def __init__(self, ws=None):
|
|
self.ws = ws
|
|
self.cache = {} # Simple cache for font bitmaps: {code: bytes}
|
|
|
|
def set_ws(self, ws):
|
|
self.ws = ws
|
|
|
|
def text(self, tft, text, x, y, color, bg=0x0000):
|
|
"""
|
|
Draw text on ST7789 display using WebSocket to fetch fonts
|
|
"""
|
|
# Pre-calculate color bytes
|
|
color_bytes = struct.pack(">H", color)
|
|
bg_bytes = struct.pack(">H", bg)
|
|
|
|
initial_x = x
|
|
|
|
for char in text:
|
|
# Handle newlines
|
|
if char == '\n':
|
|
x = initial_x
|
|
y += 16
|
|
continue
|
|
|
|
# Boundary check
|
|
if x + 16 > tft.width:
|
|
x = initial_x
|
|
y += 16
|
|
if y + 16 > tft.height:
|
|
break
|
|
|
|
is_chinese = False
|
|
buf_data = None
|
|
|
|
# Check if it's Chinese
|
|
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}")
|
|
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)
|
|
x += 16
|
|
else:
|
|
# Draw ASCII (8x16) using built-in framebuf font (8x8 actually)
|
|
# If char is not ASCII, replace with '?' to avoid framebuf errors
|
|
if ord(char) > 127:
|
|
char = '?'
|
|
self._draw_ascii(tft, char, x, y, color, bg)
|
|
x += 8
|
|
|
|
def _wait_for_font(self, target_hex_code):
|
|
"""
|
|
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.
|
|
"""
|
|
if not self.ws:
|
|
return None
|
|
|
|
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.
|
|
|
|
# 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()
|
|
|
|
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()
|
|
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)
|
|
elif msg.startswith("FONT_DATA:"):
|
|
# Wrong font data? Ignore or cache it?
|
|
parts = msg.split(":")
|
|
if len(parts) >= 3:
|
|
c = int(parts[1], 16)
|
|
d = binascii.unhexlify(parts[2])
|
|
self.cache[c] = d
|
|
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
|
|
|
|
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
|
|
|
|
# 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
|
|
tft.blit_buffer(rgb_buf, x, y, w, h)
|
|
|
|
def _draw_ascii(self, tft, char, x, y, color, bg):
|
|
# Use framebuf for ASCII
|
|
w, h = 8, 8
|
|
buf = bytearray(w * h // 8)
|
|
fb = framebuf.FrameBuffer(buf, w, h, framebuf.MONO_VLSB)
|
|
fb.fill(0)
|
|
fb.text(char, 0, 0, 1)
|
|
|
|
# Since framebuf.text is 8x8, we center it vertically in 16px height
|
|
# Drawing pixel by pixel is slow but compatible
|
|
# To optimize, we can build a small buffer
|
|
|
|
# Create a 8x16 RGB565 buffer
|
|
rgb_buf = bytearray(8 * 16 * 2)
|
|
# Fill with background
|
|
bg_high, bg_low = bg >> 8, bg & 0xFF
|
|
color_high, color_low = color >> 8, color & 0xFF
|
|
|
|
for i in range(0, len(rgb_buf), 2):
|
|
rgb_buf[i] = bg_high
|
|
rgb_buf[i+1] = bg_low
|
|
|
|
# Draw the 8x8 character into the buffer (centered)
|
|
# MONO_VLSB: each byte is a column of 8 pixels
|
|
for col in range(8): # 0..7
|
|
byte = buf[col]
|
|
for row in range(8): # 0..7
|
|
if (byte >> row) & 1:
|
|
# Calculate position in rgb_buf
|
|
# Target: x=col, y=row+4
|
|
pos = ((row + 4) * 8 + col) * 2
|
|
rgb_buf[pos] = color_high
|
|
rgb_buf[pos+1] = color_low
|
|
|
|
tft.blit_buffer(rgb_buf, x, y, 8, 16)
|