import machine import st7789py as st7789 from config import CURRENT_CONFIG import font class Display: def __init__(self): self.tft = None self.width = 240 self.height = 240 self._init_display() self.font = font.Font() def _init_display(self): print(">>> Initializing Display...") try: pins = CURRENT_CONFIG.pins spi = machine.SPI(2, baudrate=40000000, polarity=1, phase=1, sck=machine.Pin(pins['sck']), mosi=machine.Pin(pins['mosi'])) cs_pin = pins.get('cs') cs = machine.Pin(cs_pin, machine.Pin.OUT) if cs_pin is not None else None rst_pin = pins.get('rst') dc_pin = pins.get('dc') self.tft = st7789.ST7789(spi, self.width, self.height, reset=machine.Pin(rst_pin, machine.Pin.OUT) if rst_pin else None, dc=machine.Pin(dc_pin, machine.Pin.OUT) if dc_pin else None, cs=cs, backlight=None) self.tft.init() self.tft.fill(st7789.BLUE) except Exception as e: print(f"Display error: {e}") self.tft = None def fill(self, color): if self.tft: self.tft.fill(color) def fill_rect(self, x, y, w, h, color): if self.tft: self.tft.fill_rect(x, y, w, h, color) def set_ws(self, ws): if self.font: self.font.set_ws(ws) def text(self, text, x, y, color, wait=True): if self.tft: self.font.text(self.tft, text, x, y, color, wait=wait) def init_ui(self): """初始化 UI 背景""" if self.tft: self.tft.fill(st7789.BLACK) self.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) def update_audio_bar(self, bar_height, last_bar_height): """更新音频可视化的柱状图""" if not self.tft: return last_bar_height # 确定当前颜色 color = st7789.GREEN if bar_height > 50: color = st7789.YELLOW if bar_height > 100: color = st7789.RED # 确定上一次颜色 last_color = st7789.GREEN if last_bar_height > 50: last_color = st7789.YELLOW if last_bar_height > 100: last_color = st7789.RED # 1. 如果变矮了,清除顶部多余部分 if bar_height < last_bar_height: self.tft.fill_rect(100, 240 - last_bar_height, 40, last_bar_height - bar_height, st7789.BLACK) # 2. 如果颜色变了,必须重绘整个条 if color != last_color: self.tft.fill_rect(100, 240 - bar_height, 40, bar_height, color) # 3. 如果颜色没变且变高了,只绘新增部分 elif bar_height > last_bar_height: self.tft.fill_rect(100, 240 - bar_height, 40, bar_height - last_bar_height, color) return bar_height def show_image(self, x, y, width, height, rgb565_data): """在指定位置显示RGB565格式的图片数据""" if not self.tft: return try: # 将字节数据转换为适合blit_buffer的格式 self.tft.blit_buffer(rgb565_data, x, y, width, height) except Exception as e: print(f"Show image error: {e}") def show_image_chunk(self, x, y, width, height, data, offset): """流式显示图片数据块""" if not self.tft: return # ST7789 blit_buffer expects a complete buffer for the window # But we can calculate which pixels this chunk corresponds to # This is tricky because blit_buffer sets a window and then writes data. # If we want to stream, we should probably set the window once and then write chunks. # But st7789py library might not expose raw write easily without window set. # Alternative: Calculate the sub-window for this chunk. # Data is a linear sequence of pixels (2 bytes per pixel) # We assume data length is even. try: # Simple approach: If offset is 0, we set the window for the whole image # And then write data. But st7789py's blit_buffer does both. # Let's look at st7789py implementation. # fill_rect sets window then writes. # blit_buffer sets window then writes. # We can use a modified approach: # If it's the first chunk, set window. # Then write data. # But we can't easily modify the library state from here. # So we calculate the rect for this chunk. # Total pixels total_pixels = width * height # Current pixel offset pixel_offset = offset // 2 num_pixels = len(data) // 2 # This only works if chunks align with rows, or if we can write partial rows. # ST7789 supports writing continuous memory. # Let's try to determine the x, y, w, h for this chunk. # This is complex if it wraps around lines. # Easier approach for ESP32 memory constrained environment: # We just need to use the raw write method of the display driver if available. if offset == 0: # Set window for the whole image self.tft.set_window(x, y, x + width - 1, y + height - 1) # Write raw data self.tft.write(None, data) except Exception as e: print(f"Show chunk error: {e}") def render_home_screen(self): """渲染首页""" if not self.tft: return self.tft.fill(st7789.BLACK) # 顶部标题栏 self.tft.fill_rect(0, 0, 240, 40, 0x2124) # Dark Grey self.text("量迹AI贴纸生成", 45, 12, st7789.WHITE) # 中间Logo区域(简单绘制一个框) self.tft.fill_rect(80, 80, 80, 80, st7789.BLUE) self.text("AI", 108, 110, st7789.WHITE) # 底部提示 self.text("正在启动...", 80, 200, st7789.CYAN) def render_wifi_connecting(self): """渲染WiFi连接中界面""" if not self.tft: return self.tft.fill(st7789.BLACK) self.text("WiFi连接中...", 60, 110, st7789.WHITE) # 加载动画会在主循环中绘制 def render_wifi_status(self, success): """渲染WiFi连接结果""" if not self.tft: return self.tft.fill(st7789.BLACK) if success: self.text("WiFi连接成功!", 60, 100, st7789.GREEN) self.draw_check_icon(110, 130) else: self.text("WiFi连接失败", 60, 100, st7789.RED) self.text("请重试", 95, 130, st7789.WHITE) def draw_top_tip(self, text): """在右上角显示提示文字""" if not self.tft: return # 清除区域 (假设背景是白色的,因为顶部栏通常是白色) # x=170, w=70, h=30 self.tft.fill_rect(170, 0, 70, 30, st7789.WHITE) if text: # 使用红色显示提示,醒目 self.text(text, 170, 8, st7789.RED, wait=False) def draw_check_icon(self, x, y): """绘制勾选图标""" if not self.tft: return self.tft.line(x, y + 5, x + 3, y + 8, st7789.GREEN) self.tft.line(x + 3, y + 8, x + 10, y, st7789.GREEN) def measure_text(self, text): """计算文本宽度""" width = 0 for char in text: if ord(char) > 127: width += 16 else: width += 8 return width def draw_centered_text(self, text, x, y, w, h, color, bg=None): """在指定区域居中显示文本""" if not self.tft: return text_width = self.measure_text(text) start_x = x + (w - text_width) // 2 start_y = y + (h - 16) // 2 # 确保不超出边界 start_x = max(x, start_x) if bg is not None: self.tft.fill_rect(x, y, w, h, bg) self.text(text, start_x, start_y, color) def draw_button(self, text, x, y, w, h, bg_color, text_color=st7789.WHITE): """绘制带居中文字的按钮""" if not self.tft: return self.tft.fill_rect(x, y, w, h, bg_color) self.draw_centered_text(text, x, y, w, h, text_color) def render_confirm_screen(self, asr_text="", waiting=False): """渲染确认界面""" if not self.tft: return self.tft.fill(st7789.BLACK) # Header self.tft.fill_rect(0, 0, 240, 30, st7789.CYAN) self.draw_centered_text("说完了吗?", 0, 0, 240, 30, st7789.BLACK) # Content box self.tft.fill_rect(10, 50, 220, 90, 0x4208) # DARKGREY if waiting: self.draw_centered_text("正在识别...", 10, 50, 220, 90, st7789.YELLOW) elif asr_text: # 自动换行逻辑 max_width = 200 lines = [] current_line = "" current_width = 0 for char in asr_text: char_width = 16 if ord(char) > 127 else 8 if current_width + char_width > max_width: lines.append(current_line) current_line = char current_width = char_width else: current_line += char current_width += char_width if current_line: lines.append(current_line) # 限制显示行数,避免溢出 lines = lines[:4] # 计算起始Y坐标以垂直居中 total_height = len(lines) * 20 start_y = 50 + (90 - total_height) // 2 for i, line in enumerate(lines): # 计算水平居中 line_width = self.measure_text(line) center_x = 20 + (200 - line_width) // 2 self.text(line, center_x, start_y + i * 20, st7789.WHITE, wait=False) else: self.draw_centered_text("未识别到文字", 10, 50, 220, 90, st7789.WHITE) # Buttons self.draw_button("短按确认", 20, 160, 90, 30, st7789.GREEN, st7789.BLACK) self.draw_button("长按重录", 130, 160, 90, 30, st7789.RED, st7789.WHITE) def render_recording_screen(self, asr_text="", audio_level=0, is_recording=False): """渲染录音界面""" if not self.tft: return self.tft.fill(st7789.BLACK) self.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) self.draw_centered_text("语音识别", 0, 0, 240, 30, st7789.BLACK) self.draw_mic_icon(105, 50) if audio_level > 0: bar_width = min(int(audio_level * 2), 200) self.tft.fill_rect(20, 100, bar_width, 10, st7789.GREEN) if asr_text: self.text(asr_text[:20], 20, 130, st7789.WHITE, wait=False) if is_recording: self.draw_button("松开停止", 60, 200, 120, 25, st7789.RED, st7789.WHITE) else: self.draw_button("长按录音", 60, 200, 120, 25, st7789.BLUE, st7789.WHITE) def render_result_screen(self, status="", prompt="", image_received=False): """渲染结果界面""" if not self.tft: return if status == "OPTIMIZING": self.tft.fill(st7789.BLACK) self.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) self.draw_centered_text("AI 生成中", 0, 0, 240, 30, st7789.BLACK) self.draw_centered_text("正在思考...", 0, 60, 240, 20, st7789.CYAN) self.draw_centered_text("优化提示词中", 0, 80, 240, 20, st7789.CYAN) self.draw_progress_bar(40, 110, 160, 6, 0.3, st7789.CYAN) elif status == "RENDERING": self.tft.fill(st7789.BLACK) self.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) self.draw_centered_text("AI 生成中", 0, 0, 240, 30, st7789.BLACK) self.draw_centered_text("正在绘画...", 0, 60, 240, 20, st7789.YELLOW) self.draw_centered_text("AI作画中", 0, 80, 240, 20, st7789.YELLOW) self.draw_progress_bar(40, 110, 160, 6, 0.7, st7789.YELLOW) elif status == "COMPLETE" or image_received: # Don't clear screen, image is already there self.tft.fill_rect(230, 230, 10, 10, st7789.GREEN) elif status == "ERROR": self.tft.fill(st7789.BLACK) self.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) self.draw_centered_text("AI 生成中", 0, 0, 240, 30, st7789.BLACK) self.draw_centered_text("生成失败", 0, 50, 240, 20, st7789.RED) if prompt and not image_received: self.tft.fill_rect(10, 140, 220, 50, 0x2124) # Dark Grey self.text("提示词:", 15, 145, st7789.CYAN) self.text(prompt[:25] + "..." if len(prompt) > 25 else prompt, 15, 165, st7789.WHITE) if not image_received: self.draw_button("长按返回", 60, 210, 120, 25, st7789.BLUE, st7789.WHITE) def draw_loading_spinner(self, x, y, angle, color=st7789.WHITE): """绘制旋转加载图标""" if not self.tft: return import math rad = math.radians(angle) center_x = x + 10 center_y = y + 10 radius = 8 for i in range(8): theta = math.radians(i * 45) + rad px = int(center_x + radius * math.cos(theta)) py = int(center_y + radius * math.sin(theta)) self.tft.pixel(px, py, color) def draw_progress_bar(self, x, y, width, height, progress, color=st7789.CYAN): """绘制进度条""" if not self.tft: return self.tft.fill_rect(x, y, width, height, 0x4208) # DARKGREY if progress > 0: bar_width = int(width * min(progress, 1.0)) self.tft.fill_rect(x, y, bar_width, height, color) def draw_mic_icon(self, x, y, active=True): """绘制麦克风图标""" if not self.tft: return color = st7789.GREEN if active else 0x4208 # DARKGREY self.tft.fill_rect(x + 5, y, 10, 5, color) self.tft.fill_rect(x + 3, y + 5, 14, 10, color) self.tft.fill_rect(x + 8, y + 15, 4, 8, color) self.tft.fill_rect(x + 6, y + 23, 8, 2, color) self.tft.fill_rect(x + 8, y + 25, 4, 3, color)