Display配置绑定显示层失败

Viewed 54

问题描述


Display.init(Display.ST7701, width=800, height=480, osd_num=2,to_ide=True)

Display.config_layer(
    rect=(0, 0, 800, 480),
    pix_format=image.RGB565,
    layer=Display.LAYER_OSD1,
    alpha=255,
    flag=0
)

# LAYER_OSD2 上层:ARGB8888,UI按钮文字,硬件alpha叠加
Display.config_layer(
    rect=(0, 0, 800, 480),
    pix_format=image.ARGB8888,
    layer=Display.LAYER_OSD0,
    alpha=255,
    flag=0
)

报错RuntimeError: vo bind config layer failed.,请问是哪里配置的问题?

硬件板卡


01Studio CanMV K230

软件版本


CanMV_K230_01Studio_micropython_v1.8-46-g8f620e5_nncase_v2.11.0

1 Answers

你好,请发一下完整的脚本。

from mpp.mp4_format import *
from mpp.mp4_format_struct import *
from media.vencoder import *
from media.sensor import *
from media.media import *
from media.display import *
from media.uvc import *
import uctypes
import time
from machine import TOUCH, PWM, FPIOA, Pin, ADC
import image
from libs.YOLO import YOLOv5
from libs.Utils import *
import os, sys, gc
import ulab.numpy as np
import _thread
import socket
import network
import errno
import cv_lite
import cv2
from media.media import *
from mpp.mp4_format import *
from mpp.mp4_format_struct import *
from media.pyaudio import *
import media.g711 as g711
from mpp.payload_struct import *
import media.vdecoder as vdecoder
from media.display import *
import uctypes
import time
import _thread
import os
from libs.Utils import hwc2chw
from libs.PipeLine import PipeLine
from libs.AIBase import AIBase
from libs.AI2D import Ai2d
from libs.Utils import *
import os,sys,ujson,gc,math
from media.media import *
import nncase_runtime as nn
import ulab.numpy as np
import image
import aicube
import os
from media.mjpeg import MJPEGEncoder
import gc
import errno

# ===================== UVC模拟多路快照全局缓存 =====================
uvc_raw_frame = None
uvc_frame_lock = _thread.allocate_lock()

def ALIGN_UP(x, align):
    return ((x + align - 1) // align) * align

def _uvc_fetch_one_frame():
    """真正从UVC取一帧,返回RGB888 image.Image,外部加锁调用"""
    global uvc_raw_frame
    img = UVC.snapshot()
    if img is None:
        return None
    # UVC输出MJPEG,转RGB888
    rgb888_img = img.to_rgb888()
    del img
    return rgb888_img

def uvc_snapshot_sim(chn=0):
    """
    模拟原sensor.snapshot(chn=ID)
    实际UVC只有一路;取1帧,复制返回;chn参数仅做兼容,不做硬件通道区分
    CAM_CHN_ID_0 / 1 /2 全部走这里
    """
    global uvc_raw_frame
    uvc_frame_lock.acquire()
    try:
        # 获取新帧
        new_frame = _uvc_fetch_one_frame()
        if new_frame is not None:
            if uvc_raw_frame is not None:
                del uvc_raw_frame
            uvc_raw_frame = new_frame
        if uvc_raw_frame is None:
            return None
        # copy一份返回,模拟不同通道取出独立image对象
        out = uvc_raw_frame.copy()
        return out
    finally:
        uvc_frame_lock.release()

def socket_send_all(sock, data, timeout_ms=5000):
    """MicroPython兼容sendall,处理EAGAIN/EWOULDBLOCK,超时抛出异常"""
    total = len(data)
    sent = 0
    start = time.ticks_ms()
    while sent < total:
        os.exitpoint()
        elapsed = time.ticks_diff(time.ticks_ms(), start)
        if elapsed > timeout_ms:
            raise OSError("socket_send_all timeout")
        try:
            n = sock.send(data[sent:])
            sent += n
        except OSError as e:
            if e.args[0] in (errno.EAGAIN):
                # 缓冲区满,短暂让出CPU,再重试
                time.sleep_ms(2)
                continue
            else:
                raise e
    return sent

def blend_argbg_to_rgb(rgb_src_img, argb_osd_img):
    """
    rgb_src_img: image.Image RGB888
    argb_osd_img: image.Image ARGB8888
    return: image.Image 融合后的RGB888图像
    """
    out_img = rgb_src_img.copy()
    # ARGB图层叠加到RGB888画布
    out_img.draw_image(argb_osd_img, 0, 0)
    # 【保险】强制转换格式,重置元数据,规避draw_image跨格式叠加元数据损坏bug
    fixed_img = out_img.to_rgb888()
    del out_img
    return fixed_img

def image_to_jpeg_bytes(img_obj, quality=60):
    """
    纯CPU cv2编码;支持ARGB8888 / RGB888
    img_obj: image.Image
    return: buf对象,拥有tobytes()
    """
    try:
        img_np = img_obj.to_numpy_ref()
        fmt = img_obj.format()
        if fmt == image.ARGB8888:
            A,R,G,B = cv2.split(img_np)
            bgr_np = cv2.merge([B, G, R])
        elif fmt == image.RGB888:
            R,G,B = cv2.split(img_np)
            bgr_np = cv2.merge([B, G, R])
        else:
            print(f"[image_to_jpeg_bytes]不支持图像格式:{fmt}")
            return None
        success, buf = cv2.imencode(".jpg", bgr_np, [95, 60])
        if success:
            return buf
        else:
            print("[image_to_jpeg_bytes]cv2 imencode返回失败")
            return None
    except Exception as e:
        print(f"[image_to_jpeg_bytes]cv2编码异常:{e}")
        import sys
        sys.print_exception(e)
        return None

def send_image_over_tcp():
    global client_conn, yolo_running, yolores, ocr_boxes, ocr_texts
    tcp_lock.acquire()
    conn = client_conn
    tcp_lock.release()
    if conn is None:
        print("[send_image_over_tcp] 无客户端连接,跳过发送图片")
        return False
    photo_lock.acquire()
    tmp_taking = taking_photo
    photo_lock.release()
    if tmp_taking:
        print("[send_image_over_tcp] 正在拍照,跳过发送")
        return False
    cam_img = None
    cam_copy = None
    temp_osd = None
    try:
        sensor_lock.acquire()
        cam_img = uvc_snapshot_sim(chn=CAM_CHN_ID_2)
        if cam_img is None:
            sensor_lock.release()
            return False
        cam_copy = cam_img.copy()
        sensor_lock.release()
        temp_osd = image.Image(DISPLAY_WIDTH, DISPLAY_HEIGHT, image.ARGB8888)
        temp_osd.clear()
        yolo_lock.acquire()
        running = yolo_running
        local_yolo_res = yolores
        yolo_lock.release()
        ocr_lock.acquire()
        local_ocr_box = ocr_boxes
        local_ocr_txt = ocr_texts
        ocr_lock.release()
        if running and local_yolo_res:
            yolo.draw_result(local_yolo_res, temp_osd)
        if running and ocr_det_rec is not None:
            ocr_det_rec.draw_result_to_img(temp_osd, local_ocr_box, local_ocr_txt)
        FONT = 24
        rec_color = (255,0,0,255) if recording else (0,255,0,255)
        temp_osd.draw_string_advanced(10,10, FONT, f"录制:{'中' if recording else '空闲'}", rec_color)
        tcp_lock.acquire()
        tcp_str = "客户端:已连接" if client_conn else "客户端:未连接"
        tcp_lock.release()
        temp_osd.draw_string_advanced(10,40, FONT, tcp_str, (0,0,255,255))
        temp_osd.draw_string_advanced(10,70, FONT, f"YOLO+OCR:{'运行中' if running else '已停止'}", (0,255,120,255))
        temp_osd.draw_string_advanced(10,100, FONT, f"ADC:{adcdianya}", (0,255,0,255))
        temp_osd.draw_string_advanced(10,130,FONT, djshow_status, djstatus_color + (255,))
        ip_str = f"IP:{wifi_ip}" if wifi_ip else "IP:未连接"
        temp_osd.draw_string_advanced(600,10, FONT, ip_str, (255,255,0,255))
        yolo_btn_text = "停止检测" if running else "开始检测"
        buttons = [
            [0,   400, 200, 80, "结束录制" if recording else "开始录制", "rec"],
            [400, 400, 200, 80, "图库", "tuku"],
            [600, 400, 200, 80, yolo_btn_text, "yolo_btn"],
            [200, 400, 200, 80, "拍照", "pic"],
        ]
        for x,y,w,h,t,bid in buttons:
            temp_osd.draw_rectangle(x,y,w,h,(220,220,220,255),fill=True)
            temp_osd.draw_rectangle(x,y,w,h,(180,180,180,255),2)
            temp_osd.draw_string_advanced(x+40,y+30,18,t,(0,0,0,255))
        blend_img = blend_argbg_to_rgb(cam_copy, temp_osd)
        blend_np = blend_img.to_numpy_ref().copy()
        blend_bgr = cv2.cvtColor(blend_np, cv2.COLOR_RGB2BGR)
        success, buf = cv2.imencode(".jpg", blend_bgr, [95, 60])
        if not success:
            print("[send_image_over_tcp] cv2 imencode返回失败")
            return False
        jpeg_data = buf
        img_len = len(jpeg_data)
        header = f"IMG_START:{img_len}\n".encode("utf-8")
        tail = b"\nIMG_END\n"
        tcp_lock.acquire()
        try:
            socket_send_all(conn, header)
            socket_send_all(conn, jpeg_data)
            socket_send_all(conn, tail)
        except OSError as e:
            print(f"[send_image_over_tcp]发送异常 E={e}")
            cid = id(conn)
            map_lock.acquire()
            if cid in conn_valid_map:
                conn_valid_map[cid] = False
            map_lock.release()
            tcp_lock.release()
            return False
        finally:
            tcp_lock.release()
        print(f"[send_image_over_tcp] 发送融合画面成功,字节数:{img_len}")
        return True
    except Exception as e:
        print(f"[send_image_over_tcp] 异常: {e}")
        import sys
        sys.print_exception(e)
        return False
    finally:
        if cam_img:
            del cam_img
        if cam_copy:
            del cam_copy
        if temp_osd:
            del temp_osd
        gc.collect()

# 自定义OCR检测类
class OCRDetectionApp(AIBase):
    def __init__(self,kmodel_path,model_input_size,mask_threshold=0.5,box_threshold=0.2,rgb888p_size=[224,224],display_size=[1920,1080],debug_mode=0):
        super().__init__(kmodel_path,model_input_size,rgb888p_size,debug_mode)
        self.kmodel_path=kmodel_path
        self.model_input_size=model_input_size
        self.mask_threshold=mask_threshold
        self.box_threshold=box_threshold
        self.rgb888p_size=[ALIGN_UP(rgb888p_size[0],16),rgb888p_size[1]]
        self.display_size=[ALIGN_UP(display_size[0],16),display_size[1]]
        self.debug_mode=debug_mode
        self.ai2d=Ai2d(debug_mode)
        self.ai2d.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT,nn.ai2d_format.NCHW_FMT,np.uint8, np.uint8)
    def config_preprocess(self,input_image_size=None):
        with ScopedTiming("set preprocess config",self.debug_mode > 0):
            ai2d_input_size=input_image_size if input_image_size else self.rgb888p_size
            top,bottom,left,right,_=letterbox_pad_param(self.rgb888p_size,self.model_input_size)
            self.ai2d.pad([0,0,0,0,top,bottom,left,right], 0, [0,0,0])
            self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
            self.ai2d.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,self.model_input_size[1],self.model_input_size[0]])
    def postprocess(self,results):
        with ScopedTiming("postprocess",self.debug_mode > 0):
            hwc_array=self.chw2hwc(self.cur_img)
            det_boxes = aicube.ocr_post_process(results[0][:,:,:,0].reshape(-1), hwc_array.reshape(-1),self.model_input_size,self.rgb888p_size, self.mask_threshold, self.box_threshold)
            return det_boxes
    def chw2hwc(self,features):
        ori_shape = (features.shape[0], features.shape[1], features.shape[2])
        c_hw_ = features.reshape((ori_shape[0], ori_shape[1] * ori_shape[2]))
        hw_c_ = c_hw_.transpose()
        new_array = hw_c_.copy()
        hwc_array = new_array.reshape((ori_shape[1], ori_shape[2], ori_shape[0]))
        del c_hw_
        del hw_c_
        del new_array
        return hwc_array

# 自定义OCR识别任务类
class OCRRecognitionApp(AIBase):
    def __init__(self,kmodel_path,model_input_size,dict_path,rgb888p_size=[1920,1080],display_size=[1920,1080],debug_mode=0):
        super().__init__(kmodel_path,model_input_size,rgb888p_size,debug_mode)
        self.kmodel_path=kmodel_path
        self.model_input_size=model_input_size
        self.dict_path=dict_path
        self.rgb888p_size=[ALIGN_UP(rgb888p_size[0],16),rgb888p_size[1]]
        self.display_size=[ALIGN_UP(display_size[0],16),display_size[1]]
        self.debug_mode=debug_mode
        self.dict_word=None
        self.read_dict()
        self.ai2d=Ai2d(debug_mode)
        self.ai2d.set_ai2d_dtype(nn.ai2d_format.RGB_packed,nn.ai2d_format.NCHW_FMT,np.uint8, np.uint8)
    def config_preprocess(self,input_image_size=None,input_np=None):
        with ScopedTiming("set preprocess config",self.debug_mode > 0):
            ai2d_input_size=input_image_size if input_image_size else self.rgb888p_size
            top,bottom,left,right,_=letterbox_pad_param(ai2d_input_size,self.model_input_size)
            self.ai2d.pad([0,0,0,0,top,bottom,left,right], 0, [0,0,0])
            self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
            self.ai2d.build([input_np.shape[0],input_np.shape[1],input_np.shape[2],input_np.shape[3]],[1,3,self.model_input_size[1],self.model_input_size[0]])
    def postprocess(self,results):
        with ScopedTiming("postprocess",self.debug_mode > 0):
            preds = np.argmax(results[0], axis=2).reshape((-1))
            output_txt = ""
            for i in range(len(preds)):
                if preds[i] != (len(self.dict_word) - 1) and (not (i > 0 and preds[i - 1] == preds[i])):
                    output_txt = output_txt + self.dict_word[preds[i]]
            return output_txt
    def read_dict(self):
        if self.dict_path!="":
            with open(self.dict_path, 'r') as file:
                line_one = file.read(100000)
                line_list = line_one.split("\r\n")
            self.dict_word = {num: char.replace("\r", "").replace("\n", "") for num, char in enumerate(line_list)}

class OCRDetRec:
    def __init__(self,ocr_det_kmodel,ocr_rec_kmodel,det_input_size,rec_input_size,dict_path,mask_threshold=0.25,box_threshold=0.3,rgb888p_size=[1920,1080],display_size=[1920,1080],debug_mode=0):
        self.ocr_det_kmodel=ocr_det_kmodel
        self.ocr_rec_kmodel=ocr_rec_kmodel
        self.det_input_size=det_input_size
        self.rec_input_size=rec_input_size
        self.dict_path=dict_path
        self.mask_threshold=mask_threshold
        self.box_threshold=box_threshold
        self.rgb888p_size=[ALIGN_UP(rgb888p_size[0],16),rgb888p_size[1]]
        self.display_size=[ALIGN_UP(display_size[0],16),display_size[1]]
        self.debug_mode=debug_mode
        self.ocr_det=OCRDetectionApp(self.ocr_det_kmodel,model_input_size=self.det_input_size,mask_threshold=self.mask_threshold,box_threshold=self.box_threshold,rgb888p_size=self.rgb888p_size,display_size=self.display_size,debug_mode=0)
        self.ocr_rec=OCRRecognitionApp(self.ocr_rec_kmodel,model_input_size=self.rec_input_size,dict_path=self.dict_path,rgb888p_size=self.rgb888p_size,display_size=self.display_size)
        self.ocr_det.config_preprocess()
    def run(self,input_np):
        det_res=self.ocr_det.run(input_np)
        boxes=[]
        ocr_res=[]
        for det in det_res:
            self.ocr_rec.config_preprocess(input_image_size=[det[0].shape[2],det[0].shape[1]],input_np=det[0])
            ocr_str=self.ocr_rec.run(det[0])
            ocr_res.append(ocr_str)
            boxes.append(det[1])
            gc.collect()
        return boxes,ocr_res
    def draw_result_to_img(self, osd_img, det_res, rec_res):
        if not det_res:
            return
        for j in range(len(det_res)):
            pts = det_res[j]
            for i in range(4):
                x1 = pts[i*2] / self.rgb888p_size[0] * self.display_size[0]
                y1 = pts[i*2+1] / self.rgb888p_size[1] * self.display_size[1]
                x2 = pts[((i+1)*2)%8] / self.rgb888p_size[0] * self.display_size[0]
                y2 = pts[((i+1)*2+1)%8] / self.rgb888p_size[1] * self.display_size[1]
                osd_img.draw_line(int(x1),int(y1),int(x2),int(y2),color=(255,0,0,255),thickness=4)
            tx = int(pts[0]/self.rgb888p_size[0]*self.display_size[0])
            ty = int(pts[1]/self.rgb888p_size[1]*self.display_size[1])
            text = rec_res[j]
            osd_img.draw_string_advanced(tx, ty-28, 24, text, (0,0,255))

# ===================== 全局配置区 =====================
WIFI_SSID = "MNV"
WIFI_PASSWORD = "mnv@2021"
SERVER_PORT = 8080
BUFFER_SIZE = 1024
PWM_CHANNEL = 0
PWM_PIN = 42
PWM_FREQ = 1000
PWM_DEFAULT_DUTY = 50
CONSTRUCT_WITH_FPIOA = False
DISPLAY_WIDTH = ALIGN_UP(800,16)
DISPLAY_HEIGHT = 480
CAM_CHN_ID_0 = 0
CAM_CHN_ID_1 = 1
CAM_CHN_ID_2 = 2
VIDEO_WIDTH = 1280
VIDEO_HEIGHT = 720
VENC_PAYLOAD_TYPE = K_PT_H264
SAVE_DIR = "/data/video"

# ===================== OCR DET+REC 全局配置 =====================
OCR_DET_KMODEL="/sdcard/examples/kmodel/ocr_det_int16.kmodel"
OCR_REC_KMODEL="/sdcard/examples/kmodel/ocr_rec_int16.kmodel"
OCR_DICT_PATH="/sdcard/examples/utils/dict.txt"
OCR_MASK_THRESHOLD=0.25
OCR_BOX_THRESHOLD=0.3
OCR_RGB_SIZE=[640,360]
OCR_DET_INPUT=[640,640]
OCR_REC_INPUT=[512,32]
ocr_det_rec = None
ocr_lock = _thread.allocate_lock()
ocr_boxes = []
ocr_texts = []

# ===================== 图库全局变量 =====================
gallery_path = SAVE_DIR
gallery_file_list = []
gallery_page = 0
gallery_page_size = 6
gallery_mode = False
gallery_file_type = ""
gallery_selected_idx = -1
gallery_refresh_flag = True
thumb_cache = {}
image_view_mode = False
video_view_mode = False
current_media_path = ""
current_media_name = ""

wifi_ip = ""
client_conn = None
tcp_lock = _thread.allocate_lock()
yolores = []
yolo = None
yolo_running = False
yolo_lock = _thread.allocate_lock()
osd_buf_lock = _thread.allocate_lock()
draw_buf = None
show_buf = None
last_det_payload = ""
media_buf = None
recording = False
stop_record = False
video_index = 1
record_lock = _thread.allocate_lock()
pwm = None
pwm_running = False
last_click_time = 0
CLICK_COOLDOWN = 1000
dianyanum = 0
adccaiyang = 0
adcdianya = 0
djshow_status = ""
djstatus_color = (0,0,0)
adc = ADC(0)
GALLERY_THUMB_W = 210
GALLERY_THUMB_H = 105
key_short_flag = False
last_key_short_flag = False
key_lock = _thread.allocate_lock()
sensor_lock = _thread.allocate_lock()
taking_photo = False
photo_lock = _thread.allocate_lock()
BEEP_FREQ = 2000
BEEP_DUTY = 50
BEEP_DURATION_MS = 1000
beep_busy = False
person_continuous_frame = 0
PERSON_TRIGGER_FRAME = 10
MAX_CACHE_IMG = 12

def get_mp4_first_frame_rgb565(fpath):
    global thumb_cache
    thumb_jpg_path = fpath.replace(".mp4", ".thumb.jpg")
    cache_key = f"mp4thumb_{fpath}"
    if cache_key in thumb_cache:
        return thumb_cache[cache_key]
    try:
        src_img = image.Image(thumb_jpg_path).to_rgb565()
        if len(thumb_cache) >= MAX_CACHE_IMG:
            del_key = list(thumb_cache.keys())[0]
            del thumb_cache[del_key]
        thumb_cache[cache_key] = src_img
        return src_img
    except Exception as e:
        print(f"读取mp4配套缩略图失败 {thumb_jpg_path}: {e}")
        gc.collect()
        return None

def get_origin_jpg_rgb565(fpath):
    global thumb_cache
    cache_key = f"origin_{fpath}"
    if cache_key in thumb_cache:
        return thumb_cache[cache_key]
    try:
        src_img = image.Image(fpath).to_rgb565()
        if len(thumb_cache) >= MAX_CACHE_IMG:
            del_key = list(thumb_cache.keys())[0]
            del thumb_cache[del_key]
        thumb_cache[cache_key] = src_img
        return src_img
    except Exception as e:
        print(f"读取原图失败 {fpath}: {e}")
        gc.collect()
        return None

def refresh_gallery_files():
    global gallery_file_list, gallery_refresh_flag, thumb_cache
    flist = []
    try:
        os.stat(gallery_path)
    except OSError:
        os.mkdir(gallery_path)
    try:
        all_files = list(os.listdir(gallery_path))
        for fname in all_files:
            if not isinstance(fname, str):
                continue
            full_path = f"{gallery_path}/{fname}"
            if fname[-4:] in [".jpg",".mp4"] and not fname.endswith(".thumb.jpg"):
                stat = os.stat(full_path)
                item = (stat[8], fname, full_path, fname.split(".")[-1].lower())
                flist.append(item)
        flist.sort(key=lambda x: x[0], reverse=True)
        gallery_file_list = flist
        gallery_refresh_flag = False
        thumb_cache.clear()
        print(f"图库刷新完成,共{len(gallery_file_list)}个媒体文件")
    except Exception as e:
        print("刷新图库失败:", e)
        gallery_file_list = []
        gallery_refresh_flag = False

def get_gallery_total_page():
    total = len(gallery_file_list)
    if total == 0:
        return 0
    return (total + gallery_page_size - 1) // gallery_page_size

def get_current_page_files():
    start = gallery_page * gallery_page_size
    end = start + gallery_page_size
    return gallery_file_list[start:end]

def draw_image_view_ui(buf):
    buf.clear()
    src_img = None
    try:
        src_img = image.Image(current_media_path).to_rgb565()
        img_h = src_img.height()
        img_w = src_img.width()
        view_h = DISPLAY_HEIGHT - 100
        view_w = DISPLAY_WIDTH
        scale = min(view_w / img_w, view_h / img_h)
        draw_x = (DISPLAY_WIDTH - int(img_w * scale)) // 2
        draw_y = 50 + (view_h - int(img_h * scale)) // 2
        buf.draw_image(src_img, draw_x, draw_y, x_scale=scale, y_scale=scale)
        show_name = current_media_name[:24] + "..." if len(current_media_name) > 24 else current_media_name
        buf.draw_string_advanced(30, 15, 24, show_name, (255,255,255))
    except Exception as e:
        buf.draw_string_advanced(280, 200, 28, "图片加载失败", (255,0,0))
        print(f"图片查看异常: {e}")
    finally:
        del src_img
        gc.collect()
    btn_h = 60
    btn_y = DISPLAY_HEIGHT - btn_h - 10
    buf.draw_rectangle(320, btn_y, 160, btn_h, (80,80,80), fill=True)
    buf.draw_rectangle(320, btn_y, 160, btn_h, (255,255,255), 2)
    buf.draw_string_advanced(370, btn_y+18, 24, "返回", (255,255,255))

def draw_video_view_ui(buf):
    buf.clear()
    buf.draw_string_advanced(30, 30, 32, "视频详情", (255,255,255))
    buf.draw_line(30, 75, 770, 75, (100,100,100), 2)
    info_y = 110
    buf.draw_string_advanced(30, info_y, 24, f"文件名: {current_media_name}", (255,255,255))
    info_y += 60
    try:
        stat = os.stat(current_media_path)
        size_kb = stat[6] / 1024
        size_str = f"{size_kb:.2f} KB" if size_kb < 1024 else f"{size_kb/1024:.2f} MB"
        buf.draw_string_advanced(30, info_y, 24, f"文件大小: {size_str}", (255,255,255))
        info_y += 60
        t = time.localtime(stat[8])
        time_str = f"{t[0]}-{t[1]:02d}-{t[2]:02d} {t[3]:02d}:{t[4]:02d}:{t[5]:02d}"
        buf.draw_string_advanced(30, info_y, 24, f"创建时间: {time_str}", (255,255,255))
        info_y += 60
    except Exception as e:
        buf.draw_string_advanced(30, info_y, 24, "获取文件信息失败", (255,0,0))
    buf.draw_string_advanced(30, info_y + 40, 22, "提示:设备端暂不支持视频播放,可导出至电脑查看", (255,200,0))
    btn_h = 60
    btn_y = DISPLAY_HEIGHT - btn_h - 10
    buf.draw_rectangle(320, btn_y, 160, btn_h, (80,80,80), fill=True)
    buf.draw_rectangle(320, btn_y, 160, btn_h, (255,255,255), 2)
    buf.draw_string_advanced(370, btn_y+18, 24, "返回", (255,255,255))

def draw_gallery_ui(buf):
    global gallery_page, gallery_refresh_flag, thumb_cache
    buf.clear()
    total_page = get_gallery_total_page()
    title = f"媒体图库 | {gallery_page+1}/{total_page}" if total_page > 0 else "媒体图库 | 无文件"
    buf.draw_string_advanced(30, 10, 28, title, (255,255,255))
    if gallery_refresh_flag:
        refresh_gallery_files()
        thumb_cache.clear()
        gc.collect()
    current_files = get_current_page_files()
    item_w = 230
    item_h = 160
    gap_x = 20
    gap_y = 20
    start_x = 20
    start_y = 60
    thumb_w = item_w - 20
    thumb_h = item_h - 55
    page_item_with_pos = []
    for idx, file_info in enumerate(current_files):
        ctime, fname, fpath, ftype = file_info
        row = idx // 3
        col = idx % 3
        x = start_x + col * (item_w + gap_x)
        y = start_y + row * (item_h + gap_y)
        is_selected = (gallery_page * gallery_page_size + idx) == gallery_selected_idx
        border_color = (0,255,255) if is_selected else (200,200,200)
        buf.draw_rectangle(x, y, item_w, item_h, (60,60,60), fill=True)
        buf.draw_rectangle(x, y, item_w, item_h, border_color, 2)
        thumb_x = x + 10
        thumb_y = y + 10
        src_img = None
        if ftype == "jpg":
            src_img = get_origin_jpg_rgb565(fpath)
        elif ftype == "mp4":
            src_img = get_mp4_first_frame_rgb565(fpath)
        if src_img:
            ori_w = src_img.width()
            ori_h = src_img.height()
            scale = min(thumb_w / ori_w, thumb_h / ori_h)
            buf.draw_image(src_img, thumb_x, thumb_y, x_scale=scale, y_scale=scale)
        else:
            tip = "图片" if ftype == "jpg" else "视频"
            color = (0,255,0) if ftype == "jpg" else (255,100,0)
            buf.draw_rectangle(thumb_x, thumb_y, thumb_w, thumb_h, (80,80,80), fill=True)
            buf.draw_string_advanced(thumb_x + (thumb_w - len(tip)*12)//2, thumb_y + thumb_h//2 - 12, 24, tip, color)
        show_name = fname[:14] + "..." if len(fname) > 14 else fname
        buf.draw_string_advanced(x+10, y + item_h - 35, 18, show_name, (255,255,255))
        page_item_with_pos.append((ctime, fname, fpath, ftype, x, y, item_w, item_h))
    btn_h = 70
    btn_y = DISPLAY_HEIGHT - btn_h - 10
    buf.draw_rectangle(20, btn_y, 160, btn_h, (80,80,80), fill=True)
    buf.draw_rectangle(20, btn_y, 160, btn_h, (255,255,255), 2)
    buf.draw_string_advanced(60, btn_y+20, 24, "返回", (255,255,255))
    buf.draw_rectangle(200, btn_y, 160, btn_h, (80,80,80), fill=True)
    buf.draw_rectangle(200, btn_y, 160, btn_h, (255,255,255), 2)
    buf.draw_string_advanced(240, btn_y+20, 24, "上一页", (255,255,255))
    buf.draw_rectangle(380, btn_y, 160, btn_h, (80,80,80), fill=True)
    buf.draw_rectangle(380, btn_y, 160, btn_h, (255,255,255), 2)
    buf.draw_string_advanced(420, btn_y+20, 24, "下一页", (255,255,255))
    buf.draw_rectangle(560, btn_y, 200, btn_h, (120,20,20), fill=True)
    buf.draw_rectangle(560, btn_y, 200, btn_h, (255,100,100), 2)
    buf.draw_string_advanced(610, btn_y+20, 24, "删除选中", (255,255,255))
    gc.collect()
    return page_item_with_pos

tcp_task_queue = []
queue_lock = _thread.allocate_lock()
conn_valid_map = {}
map_lock = _thread.allocate_lock()

def get_time_str():
    t = time.localtime()
    return f"{t[0]}-{t[1]:02d}-{t[2]:02d}-{t[3]:02d}-{t[4]:02d}-{t[5]:02d}"

def wifi_connect():
    global wifi_ip
    sta = network.WLAN(0)
    sta.active(True)
    if sta.isconnected():
        wifi_ip = sta.ifconfig()[0]
        return wifi_ip
    print(f"连接WiFi: {WIFI_SSID}...")
    sta.connect(WIFI_SSID, WIFI_PASSWORD)
    retry = 0
    while not sta.isconnected() and retry < 50:
        os.exitpoint()
        time.sleep_ms(200)
        retry += 1
    if not sta.isconnected():
        raise Exception("WiFi连接失败")
    wifi_ip = sta.ifconfig()[0]
    print(f"WiFi已连接 | IP:{wifi_ip}")
    return wifi_ip

def key_detect_thread():
    global djshow_status, djstatus_color, key_short_flag, last_key_short_flag
    key = Pin(2, Pin.IN, pull=Pin.PULL_UP)
    print("硬件按键检测线程已启动,实时监控Pin2短接状态")
    while True:
        os.exitpoint()
        lv = key.value()
        key_lock.acquire()
        last_key_short_flag = key_short_flag
        if lv == 0:
            djshow_status = "按键:已短接"
            djstatus_color = (0,255,0)
            key_short_flag = True
            if not last_key_short_flag:
                print("已短接,开始拍照,临时暂停YOLO")
                ts = get_time_str()
                save_path = f"{SAVE_DIR}/pic-{ts}.jpg"
                photo_lock.acquire()
                taking_photo = True
                photo_lock.release()
                sensor_lock.acquire()
                try:
                    img = uvc_snapshot_sim(chn=CAM_CHN_ID_2)
                    if img is not None:
                        img_np = img.to_numpy_ref().copy()
                        resize_img = cv2.resize(img_np.copy(), (640, 480), interpolation=cv2.INTER_LINEAR)
                        resize_img_bgr = cv2.cvtColor(resize_img, cv2.COLOR_RGB2BGR)
                        cv2.imwrite(save_path, resize_img_bgr)
                        print(f"拍照完成:{save_path}")
                        gallery_refresh_flag = True
                except Exception as e:
                    print("拍照失败:", e)
                finally:
                    sensor_lock.release()
                photo_lock.acquire()
                taking_photo = False
                photo_lock.release()
        else:
            djshow_status = "按键:未短接"
            djstatus_color = (255,0,0)
            key_short_flag = False
        key_lock.release()
        time.sleep_ms(50)

def handle_client_message(msg, conn):
    map_lock.acquire()
    conn_valid_map[id(conn)] = True
    map_lock.release()
    queue_lock.acquire()
    tcp_task_queue.append((msg, conn))
    queue_lock.release()
    return f"ACCEPT:{msg}"

def tcp_worker_thread():
    global pwm_running,yolo_running
    while True:
        os.exitpoint()
        task = None
        queue_lock.acquire()
        if len(tcp_task_queue) > 0:
            task = tcp_task_queue.pop(0)
        queue_lock.release()
        if task is None:
            time.sleep_ms(50)
            continue
        msg, conn = task
        cid = id(conn)
        alive = False
        map_lock.acquire()
        if cid in conn_valid_map and conn_valid_map[cid]:
            alive = True
        map_lock.release()
        if not alive:
            print(f"连接已失效,丢弃任务:{msg}")
            continue
        resp = ""
        if msg == "PING":
            resp = "PONG"
        elif msg == "YOLO_ON":
            yolo_lock.acquire()
            yolo_running = True
            yolo_lock.release()
            resp = "OK:YOLO_ON"
        elif msg == "YOLO_OFF":
            yolo_lock.acquire()
            yolo_running = False
            yolo_lock.release()
            resp = "OK:YOLO_OFF"
        elif msg == "WIFI_INFO":
            resp = f"IP:{wifi_ip},PORT:{SERVER_PORT}"
        elif msg == "GET_YOLO":
            if not yolores:
                resp = "YOLO:NO_DETECTION"
            else:
                res_str = ";".join([f"{i[0]}:{i[1]:.2f}" for i in yolores])
                resp = f"YOLO:{res_str}"
        elif msg == "PWM_ON":
            if pwm and not pwm_running:
                pwm.duty(PWM_DEFAULT_DUTY)
                pwm_running = True
                resp = "OK:PWM_ON"
            else:
                resp = "FAIL:PWM"
        elif msg == "PWM_OFF":
            if pwm and pwm_running:
                pwm.duty(0)
                pwm_running = False
                resp = "OK:PWM_OFF"
            else:
                resp = "FAIL:PWM"
        elif msg == "BTN_SHORT":
            key_lock.acquire()
            lv = 1 if key_short_flag else 0
            key_lock.release()
            resp = f"BTN_SHORT:{lv}"
        elif msg == "SEND_IMG":
            if not gallery_mode and not image_view_mode and not video_view_mode:
                send_image_over_tcp()
                resp = "OK:SEND_IMG"
            else:
                resp = "FAIL:SEND_IMG:not in main camera view"
        else:
            resp = f"UNKNOWN:{msg}"
        try:
            send_data = (resp + "\n").encode("utf-8")
            conn.send(send_data)
            print(f"后台回复客户端: {resp}")
        except Exception as e:
            print(f"后台发送失败 {msg}: {e}")
            map_lock.acquire()
            if cid in conn_valid_map:
                conn_valid_map[cid] = False
            map_lock.release()

def tcp_server_thread():
    global client_conn
    _thread.start_new_thread(tcp_worker_thread, ())
    print("TCP后台工作线程已启动")
    try:
        wifi_connect()
        pwm_init()
    except Exception as e:
        print("WiFi/硬件初始化失败:", e)
        return
    server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_sock.settimeout(2.0)
    try:
        server_sock.bind((wifi_ip, SERVER_PORT))
        server_sock.listen(3)
        print(f"TCP服务器启动: {wifi_ip}:{SERVER_PORT}")
    except Exception as e:
        print(f"服务器绑定失败: {e}")
        return
    def heartbeat_send_loop(conn, addr, cid):
        time.sleep_ms(1000)
        while True:
            os.exitpoint()
            alive = False
            map_lock.acquire()
            if cid in conn_valid_map and conn_valid_map[cid]:
                alive = True
            map_lock.release()
            if not alive:
                break
            try:
                ocr_lock.acquire()
                ocr_cnt = len(ocr_boxes)
                ocr_lock.release()
                base_msg = f"PONG-{1 if recording else 0}-{1 if yolo_running else 0}-OCR:{ocr_cnt}"
                yolo_part = ""
                yolo_lock.acquire()
                tmp_yolo = yolores
                yolo_lock.release()
                MIN_CONF = 0.20
                if tmp_yolo and len(tmp_yolo)==3 and len(tmp_yolo[1])>0:
                    boxes_arr = tmp_yolo[0]
                    clsid_list = tmp_yolo[1]
                    conf_list = tmp_yolo[2]
                    yolo_items = []
                    for idx in range(len(clsid_list)):
                        try:
                            cid = int(clsid_list[idx])
                            conf = float(conf_list[idx])
                            if conf < MIN_CONF:
                                continue
                            box_np = boxes_arr[idx]
                            x1 = float(box_np[0])
                            y1 = float(box_np[1])
                            w  = float(box_np[2])
                            h  = float(box_np[3])
                            x2 = x1 + w
                            y2 = y1 + h
                            yolo_items.append(f"[{cid},{conf:.3f},{x1:.1f},{y1:.1f},{x2:.1f},{y2:.1f}]")
                        except Exception as e:
                            print(f"[心跳组装yolo单条异常 idx={idx}, e={e}]")
                            continue
                    yolo_part = ";".join(yolo_items)
                ocr_part = ""
                ocr_lock.acquire()
                tmp_ocr_boxes = ocr_boxes
                tmp_ocr_texts = ocr_texts
                ocr_lock.release()
                ocr_items = []
                for box, txt in zip(tmp_ocr_boxes, tmp_ocr_texts):
                    box_str = ",".join([str(int(v)) for v in box])
                    safe_txt = txt.replace("@","").replace("|","").replace(";","")
                    ocr_items.append(f"[{box_str}@{safe_txt}]")
                if len(ocr_items)>0:
                    ocr_part = ";".join(ocr_items)
                if yolo_part != "":
                    seg_yolo = f"YOLO|{yolo_part}"
                else:
                    seg_yolo = "YOLO|"
                if ocr_part != "":
                    seg_ocr = f"OCR|{ocr_part}"
                else:
                    seg_ocr = "OCR|"
                det_payload = f"DET_RESULT:{seg_yolo}||{seg_ocr}"
                full_message = base_msg + "|" + det_payload
                message = full_message + "\n"
                tcp_lock.acquire()
                try:
                    socket_send_all(conn, message.encode("utf-8"))
                except OSError as e:
                    print(f"心跳发送失败 {addr}: {e}")
                    map_lock.acquire()
                    if cid in conn_valid_map:
                        conn_valid_map[cid] = False
                    map_lock.release()
                    break
                finally:
                    tcp_lock.release()
            except Exception as e:
                print(f"心跳外层异常 {addr}: {e}")
                import sys
                sys.print_exception(e)
                map_lock.acquire()
                if cid in conn_valid_map:
                    conn_valid_map[cid] = False
                map_lock.release()
                break
            time.sleep_ms(1000)
    def session_loop(conn, addr):
        global client_conn
        print(f"客户端已连接: {addr}")
        conn.settimeout(1.0)
        cid = id(conn)
        tcp_lock.acquire()
        client_conn = conn
        tcp_lock.release()
        map_lock.acquire()
        conn_valid_map[cid] = True
        map_lock.release()
        _thread.start_new_thread(heartbeat_send_loop, (conn, addr, cid))
        cache = b""
        session_alive = True
        while session_alive:
            os.exitpoint()
            try:
                data = conn.recv(BUFFER_SIZE)
                cache += data
                while b"\n" in cache:
                    line, cache = cache.split(b"\n", 1)
                    line = line.strip()
                    if not line:
                        continue
                    msg = line.decode("utf-8")
                    print(f"收到[{addr}]:{msg}")
                    if msg == "HELLO":
                        resp = "HELLO_OK\n"
                        tcp_lock.acquire()
                        try:
                            conn.send(resp.encode("utf-8"))
                            print("握手成功,回复HELLO_OK")
                        except Exception as e:
                            print(f"握手回复失败 {e}")
                            session_alive = False
                            break
                        finally:
                            tcp_lock.release()
                        continue
                    elif msg == "BYE":
                        session_alive = False
                        break
                    tmp_resp = handle_client_message(msg, conn)
                    tcp_lock.acquire()
                    try:
                        conn.send((tmp_resp + "\n").encode("utf-8"))
                        print(f"即时ACK回复: {tmp_resp}")
                    except Exception as e:
                        print(f"ACK发送失败 {msg}: {e}")
                        session_alive = False
                        break
                    finally:
                        tcp_lock.release()
            except socket.timeout:
                continue
            except OSError as e:
                if e.args[0] == errno.EAGAIN or e.args[0] == errno.EWOULDBLOCK:
                    continue
                print(f"客户端[{addr}]OS异常断开: {e}")
                session_alive = False
            except Exception as e:
                print(f"客户端[{addr}]会话异常: {e}")
                session_alive = False
        map_lock.acquire()
        if cid in conn_valid_map:
            conn_valid_map[cid] = False
        map_lock.release()
        tcp_lock.acquire()
        if client_conn == conn:
            client_conn = None
        tcp_lock.release()
        try:
            conn.close()
        except:
            pass
        print(f"客户端 {addr} 连接已关闭\n")
    while True:
        os.exitpoint()
        conn = None
        addr = None
        try:
            conn, addr = server_sock.accept()
            _thread.start_new_thread(session_loop, (conn, addr))
        except OSError as e:
            if e.args[0] != errno.EAGAIN and e.args[0] != errno.EWOULDBLOCK:
                print(f"TCP服务端accept异常: {e}")
                time.sleep(1)
        except Exception:
            pass

def pwm_init():
    global pwm
    if pwm:
        return
    try:
        pwm = PWM(PWM_PIN, freq=PWM_FREQ, duty=0)
        print("[PWM] 通道初始化完成,同时支持输出与蜂鸣")
    except Exception as e:
        pwm = None
        print("[WARN] PWM初始化失败:", e)

def beep_alert_thread():
    global beep_busy, pwm, pwm_running
    if not pwm or beep_busy:
        return
    beep_busy = True
    old_state = pwm_running
    pwm.freq(BEEP_FREQ)
    pwm.duty(BEEP_DUTY)
    time.sleep_ms(BEEP_DURATION_MS)
    pwm.freq(PWM_FREQ)
    if old_state:
        pwm.duty(PWM_DEFAULT_DUTY)
    else:
        pwm.duty(0)
    beep_busy = False
    print("连续10帧检测到人,蜂鸣1秒完成,恢复PWM原有状态")

def yolo_detect_thread():
    global yolores, yolo, ocr_det_rec, ocr_boxes, ocr_texts
    kmodel_path="/sdcard/examples/zhadai.kmodel"
    labels = ['cable_tie']
    try:
        model_input_size=[640,640]
        display_size=[800, 480]
        rgb888p_size=[224,224]
        confidence_threshold = 0.1
        nms_threshold=0.1
        yolo=YOLOv5(task_type="detect",mode="video",kmodel_path=kmodel_path,labels=labels,
                    rgb888p_size=rgb888p_size,model_input_size=model_input_size,display_size=display_size,
                    conf_thresh=confidence_threshold,nms_thresh=nms_threshold,max_boxes_num=50,debug_mode=0)
        yolo.config_preprocess()
        print("YOLO模型加载完成(等待按钮启动)")
    except Exception as e:
        print("YOLO模型加载失败:", e)
        return
    try:
        ocr_det_rec=OCRDetRec(OCR_DET_KMODEL,
                               OCR_REC_KMODEL,
                               det_input_size=OCR_DET_INPUT,
                               rec_input_size=OCR_REC_INPUT,
                               dict_path=OCR_DICT_PATH,
                               mask_threshold=OCR_MASK_THRESHOLD,
                               box_threshold=OCR_BOX_THRESHOLD,
                               rgb888p_size=OCR_RGB_SIZE,
                               display_size=[DISPLAY_WIDTH,DISPLAY_HEIGHT],
                               debug_mode=0)
        print("OCR Det+Rec 检测识别模型加载完成")
    except Exception as e:
        print("OCR Det+Rec模型加载失败:", e)
        ocr_det_rec = None
    OCR_RUN_INTERVAL = 3
    ocr_frame_cnt = 0
    while True:
        with ScopedTiming("total",0):
            os.exitpoint()
            photo_lock.acquire()
            tmp_taking = taking_photo
            photo_lock.release()
            if tmp_taking:
                yolores = []
                ocr_lock.acquire()
                ocr_boxes = []
                ocr_texts = []
                ocr_lock.release()
                time.sleep_ms(50)
                continue
            yolo_lock.acquire()
            running = yolo_running
            yolo_lock.release()
            if not running:
                yolores = []
                ocr_lock.acquire()
                ocr_boxes = []
                ocr_texts = []
                ocr_lock.release()
                time.sleep_ms(100)
                continue
            try:
                sensor_lock.acquire()
                img = uvc_snapshot_sim(chn=CAM_CHN_ID_2)
                sensor_lock.release()
                if img is None:
                    time.sleep_ms(20)
                    continue
                img_np = img.to_numpy_ref()
                img_resize = cv2.resize(img_np, (224, 224), interpolation=cv2.INTER_LINEAR)
                img_chw = hwc2chw(img_resize)
                yolores = yolo.run(img_chw)
                res_str = ""
                has_person = False
                tmp_yolo=yolores
                if yolores and len(yolores) == 3 and len(yolores[1]) > 0:
                    boxes_arr = yolores[0]
                    cls_ids = yolores[1]
                    confs = yolores[2]
                    res_list = []
                    has_person = False
                    for i in range(len(cls_ids)):
                        cls_id = int(cls_ids[i])
                        conf = float(confs[i])
                        box_np = boxes_arr[i]
                        x1 = float(box_np[0])
                        y1 = float(box_np[1])
                        w = float(box_np[2])
                        h = float(box_np[3])
                        x2 = x1 + w
                        y2 = y1 + h
                        label = labels[cls_id] if cls_id < len(labels) else "unknown"
                        res_list.append(f"{label}:{conf:.2f},box=[{x1:.1f},{y1:.1f},{x2:.1f},{y2:.1f}]")
                        if label == "cable_tie":
                            has_person = True
                    res_str = ";".join(res_list)
                    tcp_lock.acquire()
                    has_client = client_conn is not None
                    tcp_lock.release()
                    if has_client:
                        try:
                            client_conn.send(f"YOLO_DETECT:{res_str}\n".encode())
                        except Exception:
                            tcp_lock.acquire()
                            client_conn = None
                            tcp_lock.release()
                else:
                    yolores = []
                if ocr_det_rec is not None:
                    ocr_frame_cnt += 1
                    if ocr_frame_cnt >= OCR_RUN_INTERVAL:
                        ocr_frame_cnt = 0
                        ocr_in_img = cv2.resize(img_np.copy(), tuple(OCR_RGB_SIZE), interpolation=cv2.INTER_LINEAR)
                        ocr_img_chw = hwc2chw(ocr_in_img)
                        boxes, texts = ocr_det_rec.run(ocr_img_chw)
                        ocr_lock.acquire()
                        ocr_boxes = boxes
                        ocr_texts = texts
                        ocr_lock.release()
                global person_continuous_frame
                if has_person:
                    person_continuous_frame += 1
                    if person_continuous_frame >= PERSON_TRIGGER_FRAME and not beep_busy:
                        _thread.start_new_thread(beep_alert_thread, ())
                else:
                    person_continuous_frame = 0
                gc.collect()
                time.sleep_ms(5)
            except Exception as e:
                print(f"YOLO/OCR线程异常: {e}")
                time.sleep_ms(100)

def record_mp4_thread(save_path, width, height):
    global recording, stop_record, video_index
    venc_chn = VENC_CHN_ID_2
    width_align = ALIGN_UP(width, 16)
    video_payload = K_MP4_CODEC_ID_H264
    while not stop_record:
        ts = get_time_str()
        mp4_filepath = f"{save_path}/video_{ts}.mp4"
        thumb_filepath = f"{save_path}/video_{ts}.thumb.jpg"
        print(f"开始录制: {mp4_filepath}")
        mp4 = mp4_muxer_init(mp4_filepath, True)
        track = mp4_muxer_create_video_track(mp4, width_align, height, video_payload)
        cnt = 0
        thumb_saved = False
        while cnt < 200 and not stop_record:
            try:
                # UVC模式:软件把UVC取出图像送入编码器
                sensor_lock.acquire()
                snap_img = uvc_snapshot_sim(CAM_CHN_ID_2)
                sensor_lock.release()
                if snap_img is None:
                    time.sleep_ms(10)
                    continue
                encoder.SendFrame(venc_chn, snap_img)
                encoder.GetStream(venc_chn, streamData)
                pts = streamData.pts[0]
                data = streamData.data[0]
                size = streamData.data_size[0]
                fd = k_mp4_frame_data_s()
                fd.data = data
                fd.data_length = size
                fd.time_stamp = pts
                kd_mp4_write_frame(mp4, track, fd)
                encoder.ReleaseStream(venc_chn, streamData)
                if not thumb_saved and cnt == 20:
                    try:
                        snap_np = snap_img.to_numpy_ref().copy()
                        src_h, src_w = snap_np.shape[0], snap_np.shape[1]
                        scale = min(GALLERY_THUMB_W / src_w, GALLERY_THUMB_H / src_h)
                        dst_w = int(src_w * scale)
                        dst_h = int(src_h * scale)
                        resize_img = cv2.resize(snap_np.copy(), (dst_w, dst_h), interpolation=cv2.INTER_LINEAR)
                        resize_img_bgr = cv2.cvtColor(resize_img, cv2.COLOR_RGB2BGR)
                        cv2.imwrite(thumb_filepath, resize_img_bgr)
                        thumb_saved = True
                        print(f"mp4配套缩略图已保存 {thumb_filepath} 尺寸:{dst_w}x{dst_h}")
                    except Exception as e:
                        print("保存mp4缩略图失败:", e)
                cnt +=1
            except Exception as e:
                print(f"record loop except {e}")
                break
        kd_mp4_destroy_tracks(mp4)
        kd_mp4_destroy(mp4)
        video_index +=1
    record_lock.acquire()
    recording = False
    stop_record = False
    record_lock.release()

def mp4_muxer_init(fn, fmp4):
    cfg = k_mp4_config_s()
    cfg.config_type = K_MP4_CONFIG_MUXER
    cfg.muxer_config.file_name[:] = bytes(fn, 'utf-8')
    cfg.muxer_config.fmp4_flag = fmp4
    h = k_u64_ptr()
    ret = kd_mp4_create(h, cfg)
    if ret:
        raise OSError("kd_mp4_create failed.")
    return h.value

def mp4_muxer_create_video_track(h, w, hh, pt):
    info = k_mp4_track_info_s()
    info.track_type = K_MP4_STREAM_VIDEO
    info.time_scale = 1000
    info.video_info.width = w
    info.video_info.height = hh
    info.video_info.codec_id = pt
    th = k_u64_ptr()
    kd_mp4_create_track(h, th, info)
    return th.value

if __name__ == "__main__":
    _thread.start_new_thread(tcp_server_thread, ())
    _thread.start_new_thread(key_detect_thread, ())
    time.sleep_ms(2000)
    Display.init(Display.ST7701, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, osd_num=2,to_ide=True)

    # LAYER_OSD0 底层:RGB565,UVC视频画面
    Display.config_layer(
        rect=(0, 0, 800, 480),
        pix_format=image.RGB565,
        layer=Display.LAYER_OSD1,
        alpha=255,
        flag=0
    )

    # LAYER_OSD2 上层:ARGB8888,UI按钮文字,硬件alpha叠加
    Display.config_layer(
        rect=(0, 0, 800, 480),
        pix_format=image.ARGB8888,
        layer=Display.LAYER_OSD0,
        alpha=255,
        flag=0
    )

    print("方案B:双OSD硬件图层配置完成 OSD0(RGB565)+OSD2(ARGB8888)")
    print("等待UVC摄像头插入...")
    while True:
        plugin, dev = UVC.probe()
        if plugin:
            print(f"detect USB Camera {dev}")
            break
    mode = UVC.video_mode(640, 480, UVC.FOURCC_MJPEG, 30)
    succ, mode = UVC.select_video_mode(mode)
    print(f"select mode success: {succ}, mode: {mode}")
    UVC.start(cvt=False)
    touch = TOUCH(0)
    key = Pin(2, Pin.IN, pull=Pin.PULL_UP)

    # OSD上层 ARGB8888 UI绘制层
    show_buf = image.Image(DISPLAY_WIDTH, DISPLAY_HEIGHT, image.ARGB8888)
    # UVC底层背景画布 RGB565,给到 LAYER_VIDEO0
    uvc_bg_buf = image.Image(DISPLAY_WIDTH, DISPLAY_HEIGHT, image.RGB565)
    # 图库看图专用RGB565画布
    media_buf = image.Image(DISPLAY_WIDTH, DISPLAY_HEIGHT, image.RGB565)

    # ========= 编码器初始化,软件SendFrame送入RGB图像 =========
    encoder = Encoder()
    venc_chn = VENC_CHN_ID_2
    encoder.SetOutBufs(venc_chn,VIDEO_WIDTH,VIDEO_HEIGHT)
    MediaManager.init()
    chnAttr = ChnAttrStr(encoder.PAYLOAD_TYPE_H264, encoder.H264_PROFILE_MAIN, VIDEO_WIDTH, VIDEO_HEIGHT)
    streamData = StreamData()
    encoder.Create(chnAttr)
    encoder.Start()
    _thread.start_new_thread(yolo_detect_thread, ())

    FONT = 24
    last_touch = False
    page_file_items = []
    try:
        while True:
            os.exitpoint()
            # =========渲染分支=========
            if image_view_mode:
                # 图片查看,关闭UI层OSD2
                Display.config_layer((0,0,0,0), image.ARGB8888, Display.LAYER_OSD2, alpha=0)
                draw_image_view_ui(media_buf)
                Display.show_image(media_buf, layer=Display.LAYER_OSD0)

            elif gallery_mode:
                # 图库模式,关闭UI层OSD2
                Display.config_layer((0,0,0,0), image.ARGB8888, Display.LAYER_OSD2, alpha=0)
                page_file_items = draw_gallery_ui(media_buf)
                Display.show_image(media_buf, layer=Display.LAYER_OSD0)

            elif video_view_mode:
                # 视频详情,关闭UI层OSD2
                Display.config_layer((0,0,0,0), image.ARGB8888, Display.LAYER_OSD2, alpha=0)
                draw_video_view_ui(media_buf)
                Display.show_image(media_buf, layer=Display.LAYER_OSD0)
            else:
                # 切回相机,打开上层UI OSD2
                Display.config_layer((0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT), Display.PIX_FMT_ARGB8888, Display.LAYER_OSD2, alpha=255)

                osd_buf_lock.acquire()
                try:
                    show_buf.clear()
                    yolo_lock.acquire()
                    running = yolo_running
                    yolo_lock.release()
                    if running and yolores:
                        yolo.draw_result(yolores, show_buf)
                    if running and ocr_det_rec is not None:
                        ocr_lock.acquire()
                        local_box = ocr_boxes
                        local_txt = ocr_texts
                        ocr_lock.release()
                        ocr_det_rec.draw_result_to_img(show_buf, local_box, local_txt)

                    # 全部颜色补全4元ARGB (R,G,B,A),A=255不透明
                    show_buf.draw_string_advanced(10,10, FONT, f"录制:{'中' if recording else '空闲'}", (255,0,0,255) if recording else (0,255,0,255))
                    tcp_lock.acquire()
                    tcp_str = "客户端:已连接" if client_conn else "客户端:未连接"
                    tcp_lock.release()
                    show_buf.draw_string_advanced(10,40, FONT, tcp_str, (0,0,255,255))
                    show_buf.draw_string_advanced(10,70, FONT, f"YOLO+OCR:{'运行中' if running else '已停止'}", (0,255,120,255) if running else (255,100,0,255))
                    show_buf.draw_string_advanced(10,100, FONT, f"ADC:{adcdianya}", (0,255,0,255))
                    show_buf.draw_string_advanced(10,130,FONT, djshow_status, djstatus_color + (255,))
                    ip_str = f"IP:{wifi_ip}" if wifi_ip else "IP:未连接"
                    show_buf.draw_string_advanced(600,10, FONT, ip_str, (255,255,0,255))

                    dianyanum +=1
                    if dianyanum>10:
                        adccaiyang = adc.read_u16()
                        adcdianya = adc.read_uv()
                        dianyanum=0

                    yolo_btn_text = "停止检测" if running else "开始检测"
                    buttons = [
                        [0,   400, 200, 80, "结束录制" if recording else "开始录制", "rec"],
                        [400, 400, 200, 80, "图库", "tuku"],
                        [600, 400, 200, 80, yolo_btn_text, "yolo_btn"],
                        [200, 400, 200, 80, "拍照", "pic"],
                    ]
                    for x,y,w,h,t,bid in buttons:
                        show_buf.draw_rectangle(x,y,w,h,(220,220,220,255),fill=True)
                        show_buf.draw_rectangle(x,y,w,h,(180,180,180,255),2)
                        show_buf.draw_string_advanced(x+40,y+30,18,t,(0,0,0,255))
                finally:
                    osd_buf_lock.release()

                sensor_lock.acquire()
                cam_frame = uvc_snapshot_sim(CAM_CHN_ID_2)
                sensor_lock.release()

                if cam_frame is not None:
                    cam_frame_rgb565 = cam_frame.to_rgb565()
                    uvc_bg_buf.clear()
                    # 软件缩放UVC 640x480 → 800x480,绘制到底层画布
                    uvc_bg_buf.draw_image(cam_frame_rgb565, 0,0,
                        x_scale=DISPLAY_WIDTH / 640.0,
                        y_scale=DISPLAY_HEIGHT /480.0
                    )
                    del cam_frame
                    del cam_frame_rgb565
                else:
                    uvc_bg_buf.clear((0,0,0))

                # =====【关键】分别输出到两个OSD硬件图层,VO硬件做叠加,没有软件draw_image混合!=====
                Display.show_image(uvc_bg_buf, layer=Display.LAYER_OSD0)
                Display.show_image(show_buf, layer=Display.LAYER_OSD2)
            # =====================触摸检测逻辑完全不变=====================
            tp = touch.read()
            down = False
            tx,ty=0,0
            for p in tp:
                if p.event == TOUCH.EVENT_DOWN:
                    down=True; tx=p.x; ty=p.y
            if down and not last_touch:
                current_time = time.ticks_ms()
                if time.ticks_diff(current_time, last_click_time) > CLICK_COOLDOWN:
                    last_click_time = current_time
                    # 图片查看模式
                    if image_view_mode:
                        btn_h = 60
                        btn_y = DISPLAY_HEIGHT - btn_h - 10
                        if 320 <= tx <= 480 and btn_y <= ty <= btn_y+btn_h:
                            image_view_mode = False
                            gallery_refresh_flag = True
                    # 图库模式
                    elif gallery_mode:
                        total_page = get_gallery_total_page()
                        btn_h = 70
                        btn_y = DISPLAY_HEIGHT - btn_h - 10
                        if 20 <= tx <= 180 and btn_y <= ty <= btn_y+btn_h:
                            gallery_mode = False
                            gallery_page = 0
                            gallery_refresh_flag = True
                            thumb_cache.clear()
                            gc.collect()
                        elif 200 <= tx <= 360 and btn_y <= ty <= btn_y+btn_h:
                            if gallery_page > 0:
                                thumb_cache.clear()
                                gc.collect()
                                gallery_page -= 1
                        elif 380 <= tx <= 540 and btn_y <= ty <= btn_y+btn_h:
                            if gallery_page < total_page -1:
                                thumb_cache.clear()
                                gc.collect()
                                gallery_page += 1
                        elif 560 <= tx <= 760 and btn_y <= ty <= btn_y+btn_h:
                            if gallery_selected_idx != -1 and gallery_selected_idx < len(gallery_file_list):
                                del_path = gallery_file_list[gallery_selected_idx][2]
                                try:
                                    os.remove(del_path)
                                    print(f"已删除文件: {del_path}")
                                    if del_path.endswith(".mp4"):
                                        thumb_del = del_path.replace(".mp4", ".thumb.jpg")
                                        try:
                                            os.remove(thumb_del)
                                        except OSError:
                                            pass
                                except Exception as e:
                                    print("删除失败:", e)
                                gallery_selected_idx = -1
                                gallery_refresh_flag = True
                                thumb_cache.clear()
                                gc.collect()
                        else:
                            for idx, item in enumerate(page_file_items):
                                _, fname, fpath, ftype, x, y, w, h = item
                                if x <= tx <= x+w and y <= ty <= y+h:
                                    gallery_selected_idx = gallery_page * gallery_page_size + idx
                                    current_media_path = fpath
                                    current_media_name = fname
                                    if ftype == "jpg":
                                        image_view_mode = True
                                    else:
                                        video_view_mode = True
                                    break
                    elif video_view_mode:
                        btn_h = 60
                        btn_y = DISPLAY_HEIGHT - btn_h - 10
                        if 320 <= tx <= 480 and btn_y <= ty <= btn_y+btn_h:
                            video_view_mode = False
                            gallery_refresh_flag = True
                    else:
                        for x,y,w,h,t,bid in buttons:
                            if x<=tx<=x+w and y<=ty<=y+h:
                                if bid=="rec":
                                    record_lock.acquire()
                                    if not recording:
                                        recording=True; stop_record=False
                                        _thread.start_new_thread(record_mp4_thread,("/data/video",VIDEO_WIDTH,VIDEO_HEIGHT))
                                    else:
                                        stop_record=True; recording=False
                                    record_lock.release()
                                elif bid=="yolo_btn":
                                    yolo_lock.acquire()
                                    yolo_running = not yolo_running
                                    yolo_lock.release()
                                    print(f"YOLO+OCR: {'启动' if yolo_running else '停止'}")
                                elif bid=="tuku":
                                    gallery_mode = True
                                    gallery_page = 0
                                    gallery_selected_idx = -1
                                    gallery_refresh_flag = True
                                elif bid=="pic":
                                    ts = get_time_str()
                                    save_path = f"{SAVE_DIR}/pic-touch-{ts}.jpg"
                                    photo_lock.acquire()
                                    taking_photo = True
                                    photo_lock.release()
                                    sensor_lock.acquire()
                                    try:
                                        img = uvc_snapshot_sim(chn=CAM_CHN_ID_2)
                                        if img is not None:
                                            img_np = img.to_numpy_ref().copy()
                                            resize_img = cv2.resize(img_np.copy(), (640, 480), interpolation=cv2.INTER_LINEAR)
                                            resize_img_bgr = cv2.cvtColor(resize_img, cv2.COLOR_RGB2BGR)
                                            cv2.imwrite(save_path, resize_img_bgr)
                                            print(f"拍照完成:{save_path}")
                                            gallery_refresh_flag = True
                                    except Exception as e:
                                        print("触摸拍照失败:", e)
                                    finally:
                                        sensor_lock.release()
                                    photo_lock.acquire()
                                    taking_photo = False
                                    photo_lock.release()
            last_touch = down
    except Exception as e:
        print("main loop exception:",e)
        import sys
        sys.print_exception(e)
    finally:
        # 退出先关闭两个OSD图层
        Display.config_layer((0,0,0,0), image.RGB565, Display.LAYER_OSD0, alpha=0)
        Display.config_layer((0,0,0,0), image.ARGB8888, Display.LAYER_OSD2, alpha=0)

        UVC.stop()
        encoder.Stop()
        MediaManager.deinit()
        Display.deinit()

        del uvc_bg_buf
        del show_buf
        del media_buf

        tcp_lock.acquire()
        if client_conn:
            try:
                client_conn.close()
            except:pass
        tcp_lock.release()
        print("系统退出,资源全部释放完成")

一般不需要手动去config_layer的,直接showimage呢?

我遇到的问题是用uvc获取外接摄像头的图片格式torgb565可以showimage,但我需要在上面叠加一个ARGB图层用于显示按钮等信息,直接用drawimage把argb画在rgb565上后showimage没效果,所以准备两张图片显示不同图层才涉及到config_layer,遇到了报错图层绑定失败,是否有什么实现路线?或者把所有argb上的组件也都画在rgb565上可以么