405 lines
15 KiB
Python
405 lines
15 KiB
Python
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)
|