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)