import cv2
import threading
import sys, os, time
import argparse
import numpy as np
import serial
import json
import glob
import shutil
from collections import deque
from datetime import datetime
from PyQt5.QtWidgets import (QApplication, QLabel, QWidget, QVBoxLayout,
                              QHBoxLayout, QMainWindow, QStackedWidget,
                              QFrame, QListWidget, QListWidgetItem,
                              QSizePolicy, QProgressBar, QGridLayout)
from PyQt5.QtGui import QImage, QPixmap, QFont
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject
from PIL import Image, ImageDraw, ImageFont

try:
    from surround_view import FisheyeCameraModel, BirdView
    import surround_view.param_settings as settings
except ImportError as e:
    print("[ERROR] Cannot import surround_view modules: {}".format(e))
    sys.exit(1)

# ==========================================================
# 遥控键码
# ==========================================================
KEY_UP    = 0x01
KEY_DOWN  = 0x09
KEY_LEFT  = 0x04
KEY_RIGHT = 0x06
KEY_BACK  = 0x02
KEY_OK    = 0x05

# ==========================================================
# 录制参数
# ==========================================================
VIDEO_DIR     = "/videos"
SEGMENT_SECS  = 180
RECORD_FPS    = 10
CELL_W        = 640
CELL_H        = 360
GRID_W        = CELL_W * 2
GRID_H        = CELL_H * 2
DISK_WARN_PCT = 90

# ==========================================================
# 显示参数（全屏，运行时从屏幕获取实际尺寸）
# ==========================================================
SCREEN_W   = 1024
SCREEN_H   = 600
BIRD_W     = SCREEN_W // 3
FRONT_W    = SCREEN_W - BIRD_W
DIR_CN     = {"front":"前方","left":"左方","back":"后方","right":"右方"}
WARN_COLOR = (255, 80,  80)
SAFE_COLOR = (100, 255, 100)

MODE_360  = 0
MODE_QUAD = 1

SEND_INTERVAL_MS = 100

# ==========================================================
# 配置
# ==========================================================
CONFIG_PATH = os.path.join(os.getcwd(), "surround_config.json")
DEFAULT_CONFIG = {
    "carousel_interval": 4,
    "brightness":        50,
    "radar_unit":        0,
    "show_radar":        1,
    "radar_alarm_dist":  50,
    "display_mode":      MODE_360,
}

def load_config():
    if os.path.exists(CONFIG_PATH):
        try:
            with open(CONFIG_PATH, "r") as f:
                cfg = json.load(f)
            for k, v in DEFAULT_CONFIG.items():
                cfg.setdefault(k, v)
            return cfg
        except Exception:
            pass
    return dict(DEFAULT_CONFIG)

def save_config(cfg):
    try:
        with open(CONFIG_PATH, "w") as f:
            json.dump(cfg, f, indent=2, ensure_ascii=False)
        print("[CONFIG] 已保存: {}".format(CONFIG_PATH))
    except Exception as e:
        print("[CONFIG ERROR] {}".format(e))

# ==========================================================
# 磁盘管理
# ==========================================================
def get_disk_usage_pct(path):
    try:
        usage = shutil.disk_usage(path)
        return usage.used * 100 // usage.total
    except Exception:
        return 0

def get_disk_info_str(path):
    try:
        usage    = shutil.disk_usage(path)
        used_gb  = usage.used  / 1024**3
        total_gb = usage.total / 1024**3
        pct      = usage.used * 100 // usage.total
        return "磁盘: {:.1f}GB / {:.1f}GB  ({}%)".format(
            used_gb, total_gb, pct), pct
    except Exception:
        return "磁盘: 未知", 0

def cleanup_old_videos():
    files = []
    for pat in ("rec_*.mp4", "rec_*.avi"):
        files.extend(glob.glob(os.path.join(VIDEO_DIR, pat)))
    files = sorted(files)
    while files and get_disk_usage_pct(VIDEO_DIR) >= DISK_WARN_PCT:
        oldest = files.pop(0)
        try:
            os.remove(oldest)
            print("[清理] 已删除: {}".format(oldest))
        except Exception as e:
            print("[清理 ERROR] {}".format(e))
            break

# ==========================================================
# 1. 字体管理
# ==========================================================
class FontManager:
    _fonts = {}
    _lock  = threading.Lock()

    @classmethod
    def get(cls, size=20):
        with cls._lock:
            if size not in cls._fonts:
                candidates = [
                    "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
                    "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
                    "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
                    "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
                    "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
                ]
                loaded = False
                for p in candidates:
                    if os.path.exists(p):
                        try:
                            cls._fonts[size] = ImageFont.truetype(p, size)
                            loaded = True
                            break
                        except Exception:
                            continue
                if not loaded:
                    cls._fonts[size] = ImageFont.load_default()
        return cls._fonts[size]

# ==========================================================
# 2. 中文叠加
# ==========================================================
def add_texts_to_pil(pil_image, text_list):
    draw = ImageDraw.Draw(pil_image)
    for item in text_list:
        if len(item) == 5:
            x, y, text, color, size = item
            font = FontManager.get(size)
        else:
            x, y, text, color = item
            font = FontManager.get(20)
        draw.text((x+1, y+1), text, font=font, fill=(0, 0, 0))
        draw.text((x,   y  ), text, font=font, fill=color)

# ==========================================================
# 3. 串口1
# ==========================================================
class SerialWorker(threading.Thread):
    def __init__(self, port="/dev/ttyS0", baudrate=115200):
        super().__init__(daemon=True)
        self.port     = port
        self.baudrate = baudrate
        self.running  = True

        self._rx_lock     = threading.Lock()
        self._radar_dist  = 0
        self._stopped     = False
        self._active_dirs = []

        self._tx_lock      = threading.Lock()
        self._tx_front     = 0x00
        self._tx_left      = 0x00
        self._tx_back      = 0x00
        self._tx_right     = 0x00
        self._tx_radar_thr = 0x00
        self._rx_enabled   = True

    def set_rx_enabled(self, enabled):
        self._rx_enabled = enabled

    def update_send(self, front, left, back, right, radar_thr):
        with self._tx_lock:
            self._tx_front     = front     & 0xFF
            self._tx_left      = left      & 0xFF
            self._tx_back      = back      & 0xFF
            self._tx_right     = right     & 0xFF
            self._tx_radar_thr = radar_thr & 0xFF

    def _build_tx_frame(self):
        with self._tx_lock:
            b1 = self._tx_front
            b2 = self._tx_left
            b3 = self._tx_back
            b4 = self._tx_right
            b5 = 0x00
            b6 = self._tx_radar_thr
        chk = (b1 + b2 + b3 + b4 + b5 + b6) & 0xFF
        return bytes([0x55, b1, b2, b3, b4, b5, b6, chk, 0xAA])

    def get_state(self):
        with self._rx_lock:
            return list(self._active_dirs), self._radar_dist, self._stopped

    def _parse_rx_frame(self, frame):
        if len(frame) != 9 or frame[0] != 0x55 or frame[8] != 0xAA:
            return
        front = frame[1]; left  = frame[2]
        back  = frame[3]; right = frame[4]
        radar = frame[5]; stop  = frame[6]
        active = []
        for name, val in [("front", front), ("left", left),
                          ("back",  back),  ("right", right)]:
            if val != 0x00:
                active.append(name)
        with self._rx_lock:
            self._active_dirs = active
            self._radar_dist  = radar
            self._stopped     = (stop != 0x00)

    def run(self):
        try:
            ser = serial.Serial(
                port=self.port, baudrate=self.baudrate,
                bytesize=serial.EIGHTBITS,
                stopbits=serial.STOPBITS_ONE,
                parity=serial.PARITY_NONE,
                timeout=0.05)
        except Exception as e:
            print("[串口1 ERROR] 无法打开: {}".format(e))
            return

        def _send_loop():
            interval = SEND_INTERVAL_MS / 1000.0
            while self.running:
                try:
                    frame = self._build_tx_frame()
                    print("[串口1 TX] " + " ".join("{:02X}".format(b) for b in frame))
                    ser.write(frame)
                except Exception as e:
                    if self.running:
                        print("[串口1 TX ERROR] {}".format(e))
                time.sleep(interval)

        tx_thread = threading.Thread(target=_send_loop, daemon=True)
        tx_thread.start()

        buf = bytearray()
        while self.running:
            try:
                if not self._rx_enabled:
                    time.sleep(0.05)
                    buf.clear()
                    continue
                byte = ser.read(1)
                if not byte:
                    continue
                b = byte[0]
                if len(buf) == 0:
                    if b == 0x55:
                        buf.append(b)
                    continue
                buf.append(b)
                if len(buf) == 9:
                    self._parse_rx_frame(bytes(buf))
                    buf.clear()
                elif len(buf) > 9:
                    buf.clear()
            except Exception as e:
                if self.running:
                    print("[串口1 RX ERROR] {}".format(e))
                time.sleep(0.1)
        ser.close()

    def stop(self):
        self.running = False

# ==========================================================
# 4. 串口2：红外遥控
# ==========================================================
REMOTE_HEADER    = bytes([0x01, 0x52, 0xF0, 0xFF, 0x00, 0x00, 0xFF])
REMOTE_FRAME_LEN = 11

class RemoteSignals(QObject):
    key_pressed    = pyqtSignal(int)
    enter_settings = pyqtSignal()
    exit_settings  = pyqtSignal()

class RemoteWorker(threading.Thread):
    def __init__(self, port="/dev/ttyS3", baudrate=115200):
        super().__init__(daemon=True)
        self.port     = port
        self.baudrate = baudrate
        self.running  = True
        self.signals  = RemoteSignals()

    def _parse_frame(self, frame):
        if len(frame) != REMOTE_FRAME_LEN:
            return
        if frame[:7] != REMOTE_HEADER:
            return
        code = frame[7]
        print("[遥控] 0x{:02X}".format(code))
        self.signals.key_pressed.emit(code)
        if code == KEY_OK:
            self.signals.enter_settings.emit()
        elif code == KEY_BACK:
            self.signals.exit_settings.emit()

    def run(self):
        try:
            ser = serial.Serial(port=self.port, baudrate=self.baudrate,
                                bytesize=serial.EIGHTBITS,
                                stopbits=serial.STOPBITS_ONE,
                                parity=serial.PARITY_NONE, timeout=1)
        except Exception as e:
            print("[串口2 ERROR] {}".format(e))
            return
        buf = bytearray()
        while self.running:
            try:
                byte = ser.read(1)
                if not byte:
                    continue
                b = byte[0]
                if len(buf) == 0:
                    if b == 0x01:
                        buf.append(b)
                    continue
                buf.append(b)
                if len(buf) <= 7:
                    if buf[len(buf)-1] != REMOTE_HEADER[len(buf)-1]:
                        buf.clear()
                    continue
                if len(buf) == REMOTE_FRAME_LEN:
                    self._parse_frame(bytes(buf))
                    buf.clear()
                elif len(buf) > REMOTE_FRAME_LEN:
                    buf.clear()
            except Exception as e:
                if self.running:
                    print("[串口2 ERROR] {}".format(e))
                time.sleep(0.1)
        ser.close()

    def stop(self):
        self.running = False

# ==========================================================
# 5. 轮播调度器
# ==========================================================
class CarouselScheduler:
    INTERVAL = 4.0

    def __init__(self):
        self._queue     = []
        self._cur_dir   = None
        self._cur_start = 0.0

    def update(self, active_dirs):
        if not active_dirs:
            self._queue   = []
            self._cur_dir = None
            return None, 0
        for d in active_dirs:
            if d not in self._queue:
                self._queue.append(d)
        self._queue = [d for d in self._queue
                       if d in active_dirs or d == self._cur_dir]
        if self._cur_dir is None or self._cur_dir not in self._queue:
            self._cur_dir   = self._queue[0] if self._queue else None
            self._cur_start = time.time()
            self._skip_gone(active_dirs)
        if time.time() - self._cur_start >= self.INTERVAL:
            self._advance(active_dirs)
        remaining = max(0.0, self.INTERVAL - (time.time() - self._cur_start))
        return self._cur_dir, remaining

    def _advance(self, active_dirs):
        if not self._queue:
            self._cur_dir = None
            return
        try:
            idx = self._queue.index(self._cur_dir)
        except ValueError:
            idx = -1
        self._cur_dir   = self._queue[(idx+1) % len(self._queue)]
        self._cur_start = time.time()
        self._skip_gone(active_dirs)

    def _skip_gone(self, active_dirs):
        for _ in range(len(self._queue)):
            if self._cur_dir in active_dirs:
                break
            self._advance(active_dirs)

# ==========================================================
# 6. 原始帧采集
# ==========================================================
class RawCaptureWorker(threading.Thread):
    def __init__(self, caps, names):
        super().__init__(daemon=True)
        self.caps    = caps
        self.names   = names
        self.running = True
        self.queues  = {name: deque(maxlen=2) for name in names}

    def get_frame(self, name):
        q = self.queues.get(name)
        if q:
            try:    return q[-1]
            except: pass
        return None

    def run(self):
        while self.running:
            for cap, name in zip(self.caps, self.names):
                ret, frame = cap.read()
                if ret and frame is not None:
                    self.queues[name].append(frame)

    def stop(self):
        self.running = False

# ==========================================================
# 7. 鸟瞰处理
# ==========================================================
class BirdViewWorker(threading.Thread):
    def __init__(self, raw_worker, camera_models, birdview, framestep=2):
        super().__init__(daemon=True)
        self.raw_worker = raw_worker
        self.models     = camera_models
        self.names      = raw_worker.names
        self.birdview   = birdview
        self.framestep  = framestep
        self.running    = True
        self.frame_idx  = 0
        self._rq        = deque(maxlen=2)
        self._lock      = threading.Lock()

    def get_bird(self):
        with self._lock:
            try:    return self._rq[-1]
            except: return None

    def run(self):
        while self.running:
            self.frame_idx += 1
            if self.frame_idx % self.framestep != 0:
                time.sleep(0.005)
                continue
            frames = []
            for name, model in zip(self.names, self.models):
                frame = self.raw_worker.get_frame(name)
                if frame is None:
                    break
                try:
                    frames.append(
                        model.flip(model.project(model.undistort(frame))))
                except Exception as e:
                    print("[BIRD ERROR] {}".format(e))
                    break
            if len(frames) != 4:
                time.sleep(0.01)
                continue
            try:
                self.birdview.update_frames(frames)
                self.birdview.stitch_all_parts()
                self.birdview.make_white_balance()
                self.birdview.copy_car_image()
                bird = self.birdview.image.copy()
                with self._lock:
                    self._rq.append(bird)
            except Exception as e:
                print("[BIRDVIEW ERROR] {}".format(e))
                time.sleep(0.01)

    def stop(self):
        self.running = False

# ==========================================================
# 8. 四路录制线程
# ==========================================================
class RecordWorker(threading.Thread):
    ORDER = ["front", "left", "back", "right"]
    CODEC_CANDIDATES = [
        ("mp4v", ".mp4"),
        ("MJPG", ".avi"),
        ("XVID", ".avi"),
    ]

    def __init__(self, raw_worker):
        super().__init__(daemon=True)
        self.raw_worker = raw_worker
        self.running    = True
        self._writer    = None
        self._seg_start = 0.0
        self._cur_path  = ""
        self._ext       = ".mp4"
        self._fourcc    = cv2.VideoWriter_fourcc(*"mp4v")
        os.makedirs(VIDEO_DIR, exist_ok=True)
        self._probe_codec()

    def _probe_codec(self):
        test_base = os.path.join(VIDEO_DIR, "_probe_test")
        for codec, ext in self.CODEC_CANDIDATES:
            test_path = test_base + ext
            try:
                fourcc = cv2.VideoWriter_fourcc(*codec)
                w = cv2.VideoWriter(test_path, fourcc, RECORD_FPS,
                                    (GRID_W, GRID_H))
                if not w.isOpened():
                    w.release()
                    continue
                blank = np.zeros((GRID_H, GRID_W, 3), dtype=np.uint8)
                w.write(blank)
                w.release()
                if os.path.exists(test_path) and \
                        os.path.getsize(test_path) > 500:
                    self._fourcc = fourcc
                    self._ext    = ext
                    print("[录制] 编码器: {}  格式: {}".format(codec, ext))
                    try: os.remove(test_path)
                    except: pass
                    return
                try: os.remove(test_path)
                except: pass
            except Exception as e:
                print("[录制] 编码器 {} 不可用: {}".format(codec, e))
        print("[录制] 使用默认 mp4v")

    def _new_segment(self):
        if self._writer:
            self._writer.release()
            print("[录制] 已保存: {}".format(self._cur_path))
        cleanup_old_videos()
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        self._cur_path = os.path.join(
            VIDEO_DIR, "rec_{}{}".format(ts, self._ext))
        self._writer = cv2.VideoWriter(
            self._cur_path, self._fourcc, RECORD_FPS, (GRID_W, GRID_H))
        if not self._writer.isOpened():
            print("[录制 ERROR] 无法创建: {}".format(self._cur_path))
            self._writer = None
        self._seg_start = time.time()
        print("[录制] 新段落: {}".format(self._cur_path))

    def run(self):
        self._new_segment()
        frame_interval  = 1.0 / RECORD_FPS
        next_frame_time = time.time()
        while self.running:
            now = time.time()
            if now - self._seg_start >= SEGMENT_SECS:
                self._new_segment()
                next_frame_time = time.time()
                continue
            wait = next_frame_time - now
            if wait > 0:
                time.sleep(wait)
            next_frame_time += frame_interval
            if self._writer is None:
                self._new_segment()
                continue
            cells = {}
            for name in self.ORDER:
                f = self.raw_worker.get_frame(name)
                cells[name] = (
                    cv2.resize(f, (CELL_W, CELL_H),
                               interpolation=cv2.INTER_LINEAR)
                    if f is not None
                    else np.zeros((CELL_H, CELL_W, 3), dtype=np.uint8)
                )
            top    = np.hstack([cells["front"], cells["left"]])
            bottom = np.hstack([cells["back"],  cells["right"]])
            grid   = np.vstack([top, bottom])
            for label, x, y in [
                ("前", 10,        10),
                ("左", CELL_W+10, 10),
                ("后", 10,        CELL_H+10),
                ("右", CELL_W+10, CELL_H+10),
            ]:
                cv2.putText(grid, label, (x, y+28),
                            cv2.FONT_HERSHEY_SIMPLEX, 1.0,
                            (0, 255, 100), 2)
            self._writer.write(grid)

    def stop(self):
        self.running = False
        if self._writer:
            self._writer.release()
            print("[录制] 最终段落: {}".format(self._cur_path))

    @staticmethod
    def list_videos():
        files = []
        for pat in ("rec_*.mp4", "rec_*.avi"):
            files.extend(glob.glob(os.path.join(VIDEO_DIR, pat)))
        return sorted(files, reverse=True)

    @staticmethod
    def parse_ts(path):
        base = os.path.basename(path)
        for rep in ("rec_", ".mp4", ".avi"):
            base = base.replace(rep, "")
        try:
            return datetime.strptime(base, "%Y%m%d_%H%M%S").strftime(
                "%Y-%m-%d  %H:%M:%S")
        except:
            return base

    @staticmethod
    def file_size_str(path):
        try:
            sz = os.path.getsize(path)
            if sz >= 1024**3: return " {:.1f}GB".format(sz / 1024**3)
            if sz >= 1024**2: return " {:.1f}MB".format(sz / 1024**2)
            return " {:.0f}KB".format(sz / 1024)
        except:
            return " ??"

# ==========================================================
# 9. 多摄像头初始化
# ==========================================================
class MultiCameraBirdView:
    def __init__(self):
        self.running = True
        self.names   = settings.camera_names
        self.yamls   = [
            os.path.join(os.getcwd(), "yaml", n + ".yaml")
            for n in self.names
        ]
        try:
            self.camera_models = [
                FisheyeCameraModel(f, n)
                for f, n in zip(self.yamls, self.names)
            ]
        except Exception as e:
            print("[ERROR] {}".format(e))
            self.running = False
            return

        self.caps          = []
        self.which_cameras = {"front":0, "left":1, "back":2, "right":3}
        for name in self.names:
            cap_id = self.which_cameras.get(name, 0)
            cap    = cv2.VideoCapture(cap_id, cv2.CAP_V4L2)
            cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"YUYV"))
            cap.set(cv2.CAP_PROP_FRAME_WIDTH,  1280)
            cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
            if not cap.isOpened():
                print("[ERROR] 无法打开摄像头 {} ID:{}".format(name, cap_id))
                self.running = False
                return
            self.caps.append(cap)

        self.birdview = BirdView()
        self._init_weights()

    def _init_weights(self):
        try:
            imgs = [
                os.path.join(os.getcwd(), "images", n + ".png")
                for n in self.names
            ]
            frames = []
            for p, model in zip(imgs, self.camera_models):
                img = cv2.imread(p)
                if img is None:
                    continue
                frames.append(
                    model.flip(model.project(model.undistort(img))))
            if len(frames) == 4:
                self.birdview.get_weights_and_masks(frames)
                print("[INFO] 权重初始化完成")
            else:
                print("[WARNING] 静态图不完整")
        except Exception as e:
            print("[ERROR] 权重初始化失败: {}".format(e))

# ==========================================================
# 10. 360渲染线程
#     ★ 渲染时使用实际屏幕尺寸，避免黑边
# ==========================================================
class RenderWorker(threading.Thread):
    TARGET_FPS  = 30
    FRAME_DELAY = 1.0 / TARGET_FPS

    def __init__(self, bird_worker, raw_worker, serial_worker, carousel,
                 cfg_ref, screen_w, screen_h):
        super().__init__(daemon=True)
        self.bird_worker   = bird_worker
        self.raw_worker    = raw_worker
        self.serial_worker = serial_worker
        self.carousel      = carousel
        self.cfg_ref       = cfg_ref
        self.sw            = screen_w
        self.sh            = screen_h
        self.running       = True
        self._out_q        = deque(maxlen=2)
        self._lock         = threading.Lock()
        self._last_bird    = None
        for sz in (18, 20, 24, 28, 32, 36):
            FontManager.get(sz)

    def get_qimage(self):
        with self._lock:
            try:    return self._out_q[-1]
            except: return None

    def run(self):
        while self.running:
            t0   = time.time()
            bird = self.bird_worker.get_bird()
            if bird is not None:
                self._last_bird = bird
            if self._last_bird is None:
                time.sleep(0.02)
                continue
            try:
                sw = self.sw
                sh = self.sh
                bw = sw // 3
                fw = sw - bw

                active_dirs, radar_dist, is_stopped = \
                    self.serial_worker.get_state()
                display_dir, remaining = self.carousel.update(active_dirs)
                cfg = self.cfg_ref

                right_frame = (self.raw_worker.get_frame(display_dir)
                               if display_dir else None)
                if right_frame is None:
                    right_frame = self.raw_worker.get_frame("front")
                if right_frame is None:
                    time.sleep(0.01)
                    continue

                # ★ 强制拉伸到精确尺寸，无黑边
                bird_part  = cv2.resize(self._last_bird, (bw, sh),
                                        interpolation=cv2.INTER_LINEAR)
                front_part = cv2.resize(right_frame, (fw, sh),
                                        interpolation=cv2.INTER_LINEAR)

                brightness = cfg.get("brightness", 50)
                if brightness != 50:
                    alpha = brightness / 50.0
                    front_part = cv2.convertScaleAbs(
                        front_part, alpha=alpha, beta=0)

                display = np.hstack((bird_part, front_part))
                rgb     = cv2.cvtColor(display, cv2.COLOR_BGR2RGB)
                pil_img = Image.fromarray(rgb)

                texts = [(10, 10, "鸟瞰视图", (0, 255, 100), 24)]

                if display_dir:
                    dir_cn = DIR_CN.get(display_dir, display_dir)
                    texts.append((bw+10, 10,
                                  "{} 检测到人员".format(dir_cn),
                                  WARN_COLOR, 36))
                    if len(active_dirs) > 1:
                        queue_cn = " → ".join(
                            DIR_CN.get(d, d) for d in self.carousel._queue)
                        texts.append((bw+10, 58,
                                      "轮播: {}".format(queue_cn),
                                      (255, 200, 0), 22))
                        texts.append((bw+10, 88,
                                      "当前: {}  倒计时: {:.1f}s".format(
                                          dir_cn, remaining),
                                      (200, 200, 200), 22))
                    else:
                        texts.append((bw+10, 58,
                                      "仅 {} 有人".format(dir_cn),
                                      (200, 200, 200), 22))
                else:
                    texts.append((bw+10, 10,
                                  "前视监控  (周围无人)",
                                  SAFE_COLOR, 28))

                if cfg.get("show_radar", 1):
                    unit_str = "厘米" if cfg.get("radar_unit", 0) else "分米"
                    dist_val = (radar_dist * 10
                                if cfg.get("radar_unit", 0) else radar_dist)
                    alarm_d  = cfg.get("radar_alarm_dist", 50)
                    rc = ((255, 80, 80) if dist_val <= alarm_d
                          else (180, 220, 255))
                    texts.append((bw+10, sh-80,
                                  "雷达: {} {}  阈值: {}{}".format(
                                      dist_val, unit_str, alarm_d, unit_str),
                                  rc, 22))

                stop_text  = "已停机" if is_stopped else "运行中"
                stop_color = ((255, 100, 100) if is_stopped
                              else (100, 255, 100))
                texts.append((bw+10, sh-52,
                               "状态: {}".format(stop_text),
                               stop_color, 22))
                texts.append((bw+10, sh-26,
                               time.strftime("%Y-%m-%d %H:%M:%S"),
                               (160, 160, 160), 20))

                add_texts_to_pil(pil_img, texts)
                arr = np.ascontiguousarray(np.array(pil_img))
                h, w, ch = arr.shape
                qimg = QImage(arr.tobytes(), w, h, w * ch,
                              QImage.Format_RGB888)
                with self._lock:
                    self._out_q.append(qimg)

                # 更新发送内容
                dir_map  = {"front":0, "left":1, "back":2, "right":3}
                presence = [0x00, 0x00, 0x00, 0x00]
                for d in active_dirs:
                    idx = dir_map.get(d)
                    if idx is not None:
                        presence[idx] = 0x01
                radar_thr = cfg.get("radar_alarm_dist", 50)
                self.serial_worker.update_send(
                    presence[0], presence[1], presence[2], presence[3],
                    radar_thr)

            except Exception as e:
                print("[RENDER ERROR] {}".format(e))

            elapsed = time.time() - t0
            sl = self.FRAME_DELAY - elapsed
            if sl > 0:
                time.sleep(sl)

    def stop(self):
        self.running = False

# ==========================================================
# 11. 四分屏渲染线程
# ==========================================================
class QuadRenderWorker(threading.Thread):
    TARGET_FPS  = 30
    FRAME_DELAY = 1.0 / TARGET_FPS
    ORDER       = [("front","前"), ("left","左"),
                   ("back", "后"), ("right","右")]

    def __init__(self, raw_worker, serial_worker, cfg_ref, screen_w, screen_h):
        super().__init__(daemon=True)
        self.raw_worker    = raw_worker
        self.serial_worker = serial_worker
        self.cfg_ref       = cfg_ref
        self.sw            = screen_w
        self.sh            = screen_h
        self.running       = True
        self._out_q        = deque(maxlen=2)
        self._lock         = threading.Lock()
        for sz in (24, 28, 32):
            FontManager.get(sz)

    def get_qimage(self):
        with self._lock:
            try:    return self._out_q[-1]
            except: return None

    def run(self):
        while self.running:
            t0 = time.time()
            try:
                sw = self.sw
                sh = self.sh
                cw = sw // 2
                ch = sh // 2

                cells = []
                labels_info = []
                for (name, label_cn) in self.ORDER:
                    f = self.raw_worker.get_frame(name)
                    if f is not None:
                        # ★ 强制拉伸到格子尺寸，无黑边
                        cell = cv2.resize(f, (cw, ch),
                                          interpolation=cv2.INTER_LINEAR)
                        brightness = self.cfg_ref.get("brightness", 50)
                        if brightness != 50:
                            alpha = brightness / 50.0
                            cell = cv2.convertScaleAbs(
                                cell, alpha=alpha, beta=0)
                    else:
                        cell = np.zeros((ch, cw, 3), dtype=np.uint8)
                    cells.append(cell)
                    labels_info.append(label_cn)

                top    = np.hstack([cells[0], cells[1]])
                bottom = np.hstack([cells[2], cells[3]])
                grid   = np.vstack([top, bottom])
                rgb    = cv2.cvtColor(grid, cv2.COLOR_BGR2RGB)
                pil_img = Image.fromarray(rgb)

                positions = [
                    (10,    10,    labels_info[0]),
                    (cw+10, 10,    labels_info[1]),
                    (10,    ch+10, labels_info[2]),
                    (cw+10, ch+10, labels_info[3]),
                ]
                texts = []
                for (x, y, lbl) in positions:
                    texts.append((x, y, lbl, (0, 255, 100), 32))
                texts.append((10, sh-26,
                              time.strftime("%Y-%m-%d %H:%M:%S"),
                              (160, 160, 160), 20))

                add_texts_to_pil(pil_img, texts)
                arr = np.ascontiguousarray(np.array(pil_img))
                h, w, ch2 = arr.shape
                qimg = QImage(arr.tobytes(), w, h, w * ch2,
                              QImage.Format_RGB888)
                with self._lock:
                    self._out_q.append(qimg)

                radar_thr = self.cfg_ref.get("radar_alarm_dist", 50)
                # self.serial_worker.update_send(0, 0, 0, 0, radar_thr)

            except Exception as e:
                print("[QUAD RENDER ERROR] {}".format(e))

            elapsed = time.time() - t0
            sl = self.FRAME_DELAY - elapsed
            if sl > 0:
                time.sleep(sl)

    def stop(self):
        self.running = False

# ==========================================================
# 12. 回放面板
#  ★ 视频充满右侧区域：强制拉伸到 label 尺寸（不保持宽高比）
#  ★ 字体放大
# ==========================================================
class PlaybackPanel(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._video_list   = []
        self._video_cursor = 0
        self._play_cap     = None
        self._play_timer   = None
        self._play_paused  = False
        self._play_fps     = 15.0
        self._play_total   = 0
        self._last_frame_t = 0.0
        self._build_ui()

    def _build_ui(self):
        root = QHBoxLayout(self)
        root.setContentsMargins(8, 8, 8, 8)
        root.setSpacing(8)

        # ---- 左侧列表 ----
        left = QWidget()
        left.setFixedWidth(260)
        ll = QVBoxLayout(left)
        ll.setContentsMargins(0, 0, 0, 0)
        ll.setSpacing(6)

        list_title = QLabel("📋  录像列表")
        list_title.setStyleSheet(
            "font-size:18px; color:#e94560; font-weight:bold;")
        ll.addWidget(list_title)

        self._list_widget = QListWidget()
        self._list_widget.setFocusPolicy(Qt.NoFocus)
        # ★ 行高增大，字体加大
        self._list_widget.setStyleSheet("""
            QListWidget {
                background:#0d1117; border:1px solid #0f3460;
                font-size:15px; color:#ccc;
            }
            QListWidget::item {
                padding:8px 4px;
                border-bottom:1px solid #1a2540;
                min-height:36px;
            }
            QListWidget::item:selected {
                background:#e94560; color:#fff;
            }
        """)
        ll.addWidget(self._list_widget, stretch=1)

        self._disk_label = QLabel("")
        self._disk_label.setStyleSheet("color:#aaa; font-size:13px;")
        self._disk_label.setWordWrap(True)
        ll.addWidget(self._disk_label)

        self._disk_bar = QProgressBar()
        self._disk_bar.setRange(0, 100)
        self._disk_bar.setFixedHeight(10)
        self._disk_bar.setTextVisible(False)
        ll.addWidget(self._disk_bar)

        hint = QLabel("↑↓选择  OK播放/暂停  返回退出")
        hint.setStyleSheet("color:#555; font-size:13px;")
        hint.setWordWrap(True)
        ll.addWidget(hint)

        root.addWidget(left)

        # ---- 右侧播放区 ----
        right = QWidget()
        right.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        rl = QVBoxLayout(right)
        rl.setContentsMargins(0, 0, 0, 0)
        rl.setSpacing(4)

        play_title = QLabel("▶  回放画面")
        play_title.setFixedHeight(32)
        play_title.setStyleSheet(
            "font-size:18px; color:#e94560; font-weight:bold;")
        rl.addWidget(play_title)

        # ★ label 充满右侧：Expanding + setScaledContents(True)
        #   setScaledContents(True) 让 pixmap 自动随 label 尺寸拉伸
        self._play_label = QLabel("选择左侧录像后按 OK 播放")
        self._play_label.setAlignment(Qt.AlignCenter)
        self._play_label.setStyleSheet(
            "background:#0d1117; color:#555; font-size:16px;"
            " border:1px solid #0f3460;")
        self._play_label.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Expanding)
        self._play_label.setScaledContents(True)   # ★ 自动拉伸填满
        rl.addWidget(self._play_label, stretch=1)

        self._status_label = QLabel("")
        self._status_label.setAlignment(Qt.AlignCenter)
        self._status_label.setFixedHeight(28)
        self._status_label.setStyleSheet(
            "color:#aaa; font-size:15px;")
        rl.addWidget(self._status_label)

        root.addWidget(right, stretch=1)

    def load(self):
        self._stop_play()
        self._video_list = RecordWorker.list_videos()
        self._list_widget.clear()
        self._video_cursor = 0

        if not self._video_list:
            self._list_widget.addItem("  （暂无录像）")
        else:
            for path in self._video_list:
                ts   = RecordWorker.parse_ts(path)
                sz   = RecordWorker.file_size_str(path)
                item = QListWidgetItem("  {}{}".format(ts, sz))
                self._list_widget.addItem(item)
            self._list_widget.setCurrentRow(0)

        self._update_disk_ui()
        self._play_label.setScaledContents(False)
        self._play_label.setText("选择左侧录像后按 OK 播放")
        self._status_label.setText("")

    def _update_disk_ui(self):
        info_str, pct = get_disk_info_str(VIDEO_DIR)
        self._disk_label.setText(info_str)
        self._disk_bar.setValue(pct)
        color = "#e94560" if pct >= 90 else "#4CAF50"
        self._disk_bar.setStyleSheet("""
            QProgressBar {{ background:#1a2540; border-radius:4px; }}
            QProgressBar::chunk {{ background:{}; border-radius:4px; }}
        """.format(color))

    def _stop_play(self):
        if self._play_timer:
            self._play_timer.stop()
            self._play_timer = None
        if self._play_cap:
            self._play_cap.release()
            self._play_cap = None
        self._play_paused = False

    def _start_play(self):
        if not self._video_list:
            return
        self._stop_play()
        path = self._video_list[self._video_cursor]
        self._play_cap = cv2.VideoCapture(path)
        if not self._play_cap.isOpened():
            self._status_label.setText("无法打开文件")
            return
        self._play_total  = int(self._play_cap.get(cv2.CAP_PROP_FRAME_COUNT))
        self._play_fps    = self._play_cap.get(cv2.CAP_PROP_FPS) or RECORD_FPS
        self._play_paused = False

        # ★ 开始播放时启用 ScaledContents 填满 label
        self._play_label.setScaledContents(True)
        self._play_label.setText("")

        interval_ms = max(1, int(1000.0 / self._play_fps))
        self._play_timer = QTimer()
        self._play_timer.timeout.connect(self._tick)
        self._play_timer.start(interval_ms)
        self._status_label.setText("▶ 播放中  |  {}".format(
            RecordWorker.parse_ts(path)))

    def _tick(self):
        if self._play_cap is None or self._play_paused:
            return
        ret, frame = self._play_cap.read()
        if not ret:
            self._play_timer.stop()
            self._status_label.setText("✅  播放完毕")
            return

        # ★ 直接设置 pixmap，label.setScaledContents(True) 自动填满
        rgb  = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        h, w = rgb.shape[:2]
        qimg = QImage(rgb.tobytes(), w, h, w * 3, QImage.Format_RGB888)
        self._play_label.setPixmap(QPixmap.fromImage(qimg))

        cur = int(self._play_cap.get(cv2.CAP_PROP_POS_FRAMES))
        es  = cur / self._play_fps
        ts  = self._play_total / self._play_fps
        ps  = "  【已暂停】" if self._play_paused else ""
        self._status_label.setText(
            "{:02d}:{:02d} / {:02d}:{:02d}{}".format(
                int(es)//60, int(es)%60,
                int(ts)//60, int(ts)%60, ps))

    def on_key(self, code):
        if code == KEY_UP:
            if self._video_list:
                self._video_cursor = max(0, self._video_cursor - 1)
                self._list_widget.setCurrentRow(self._video_cursor)
                self._stop_play()
                self._play_label.setScaledContents(False)
                self._play_label.setText("按 OK 播放")
                self._status_label.setText("")
        elif code == KEY_DOWN:
            if self._video_list:
                self._video_cursor = min(
                    len(self._video_list) - 1, self._video_cursor + 1)
                self._list_widget.setCurrentRow(self._video_cursor)
                self._stop_play()
                self._play_label.setScaledContents(False)
                self._play_label.setText("按 OK 播放")
                self._status_label.setText("")
        elif code == KEY_OK:
            if self._play_cap and self._play_cap.isOpened():
                self._play_paused = not self._play_paused
                self._status_label.setText(
                    "⏸  已暂停" if self._play_paused else "▶  播放中")
            else:
                self._start_play()

    def cleanup(self):
        self._stop_play()

# ==========================================================
# 13. 设置面板
#  ★ 字体全面放大（列表 20px、标题 26px、值标签 36px）
# ==========================================================
class SettingsPanel(QWidget):
    SUB_MAIN     = 0
    SUB_PLAYBACK = 1

    def __init__(self, cfg, parent=None):
        super().__init__(parent)
        self._cfg      = cfg
        self._cursor   = 0
        self._sub_page = self.SUB_MAIN
        self._build_ui()

    def _define_menu(self):
        return [
            {"name":"显示模式",       "type":"toggle",
             "key":"display_mode",
             "values":[MODE_360, MODE_QUAD],
             "labels":["360环视", "四分屏"]},
            {"name":"轮播间隔（秒）", "type":"range",
             "key":"carousel_interval", "min":1, "max":10, "step":1},
            {"name":"画面亮度",       "type":"range",
             "key":"brightness",       "min":0, "max":100, "step":5},
            {"name":"雷达报警距离",   "type":"range",
             "key":"radar_alarm_dist", "min":10, "max":200, "step":10},
            {"name":"雷达距离单位",   "type":"toggle",
             "key":"radar_unit",
             "values":[0, 1], "labels":["分米", "厘米"]},
            {"name":"显示雷达距离",   "type":"toggle",
             "key":"show_radar",
             "values":[0, 1], "labels":["关闭", "开启"]},
            {"name":"查看录像回放",   "type":"page",
             "action": self._enter_playback},
        ]

    def _build_ui(self):
        # ★ 全面放大字号
        self.setStyleSheet("""
            QWidget {
                background:#1a1a2e;
                color:#e0e0e0;
                font-size:20px;
            }
            QLabel { font-size:20px; }
            QListWidget {
                background:#16213e;
                border:1px solid #0f3460;
                font-size:20px;
                color:#e0e0e0;
            }
            QListWidget::item {
                padding:10px 8px;
                border-bottom:1px solid #0f3460;
                min-height:44px;
            }
            QListWidget::item:selected {
                background:#e94560;
                color:#ffffff;
            }
        """)
        self._stack = QStackedWidget(self)
        root = QVBoxLayout(self)
        root.setContentsMargins(0, 0, 0, 0)
        root.addWidget(self._stack)

        # 主菜单页
        mw = QWidget()
        ml = QVBoxLayout(mw)
        ml.setContentsMargins(50, 24, 50, 24)
        ml.setSpacing(14)

        title = QLabel("⚙  系统设置")
        title.setAlignment(Qt.AlignCenter)
        title.setFixedHeight(50)
        title.setStyleSheet(
            "color:#e94560; font-size:30px; font-weight:bold;")
        ml.addWidget(title)

        sep = QFrame()
        sep.setFrameShape(QFrame.HLine)
        sep.setFixedHeight(2)
        sep.setStyleSheet("background:#0f3460;")
        ml.addWidget(sep)

        self._list_widget = QListWidget()
        self._list_widget.setFocusPolicy(Qt.NoFocus)
        ml.addWidget(self._list_widget, stretch=1)

        # ★ 当前值标签放大
        self._val_label = QLabel("")
        self._val_label.setAlignment(Qt.AlignCenter)
        self._val_label.setFixedHeight(52)
        self._val_label.setStyleSheet(
            "color:#e94560; font-size:36px; font-weight:bold;")
        ml.addWidget(self._val_label)

        hint = QLabel("↑↓ 移动    ←→ 调值    OK 切换/进入    返回 退出保存")
        hint.setAlignment(Qt.AlignCenter)
        hint.setFixedHeight(30)
        hint.setStyleSheet("color:#555; font-size:16px;")
        ml.addWidget(hint)

        self._stack.addWidget(mw)

        # 回放页
        self._playback_panel = PlaybackPanel()
        self._stack.addWidget(self._playback_panel)

        self._stack.setCurrentIndex(0)
        self._menu_items = self._define_menu()

    def _refresh(self):
        self._list_widget.clear()
        for item in self._menu_items:
            vstr = self._item_val_str(item)
            self._list_widget.addItem(
                "    {}    {}".format(item["name"], vstr))
        self._list_widget.setCurrentRow(self._cursor)
        item = self._menu_items[self._cursor]
        if item["type"] == "range":
            self._val_label.setText(str(self._cfg.get(item["key"], "")))
        elif item["type"] == "toggle":
            try:
                idx = item["values"].index(
                    self._cfg.get(item["key"], item["values"][0]))
            except ValueError:
                idx = 0
            self._val_label.setText(item["labels"][idx])
        else:
            self._val_label.setText("")

    def _item_val_str(self, item):
        if item["type"] == "range":
            return "[ {} ]".format(self._cfg.get(item["key"], "?"))
        elif item["type"] == "toggle":
            try:
                idx = item["values"].index(
                    self._cfg.get(item["key"], item["values"][0]))
            except ValueError:
                idx = 0
            return "[ {} ]".format(item["labels"][idx])
        return "→"

    def _enter_playback(self):
        self._playback_panel.load()
        self._sub_page = self.SUB_PLAYBACK
        self._stack.setCurrentIndex(1)

    def on_key(self, code):
        if self._sub_page == self.SUB_MAIN:
            n = len(self._menu_items)
            if code == KEY_UP:
                self._cursor = (self._cursor - 1) % n
                self._refresh()
            elif code == KEY_DOWN:
                self._cursor = (self._cursor + 1) % n
                self._refresh()
            elif code == KEY_LEFT:
                item = self._menu_items[self._cursor]
                if item["type"] == "range":
                    v = self._cfg.get(item["key"], item["min"])
                    self._cfg[item["key"]] = max(
                        item["min"], v - item["step"])
                    self._refresh()
            elif code == KEY_RIGHT:
                item = self._menu_items[self._cursor]
                if item["type"] == "range":
                    v = self._cfg.get(item["key"], item["min"])
                    self._cfg[item["key"]] = min(
                        item["max"], v + item["step"])
                    self._refresh()
            elif code == KEY_OK:
                item = self._menu_items[self._cursor]
                if item["type"] == "toggle":
                    cur = self._cfg.get(item["key"], item["values"][0])
                    try:
                        idx = item["values"].index(cur)
                    except ValueError:
                        idx = 0
                    self._cfg[item["key"]] = \
                        item["values"][(idx+1) % len(item["values"])]
                    self._refresh()
                elif item["type"] == "page":
                    item["action"]()
        elif self._sub_page == self.SUB_PLAYBACK:
            if code == KEY_BACK:
                self._playback_panel.cleanup()
                self._sub_page = self.SUB_MAIN
                self._stack.setCurrentIndex(0)
                self._refresh()
            else:
                self._playback_panel.on_key(code)

    def load(self):
        self._sub_page = self.SUB_MAIN
        self._stack.setCurrentIndex(0)
        self._cursor = 0
        self._refresh()

    def save(self):
        save_config(self._cfg)

    def get_settings(self):
        return dict(self._cfg)

# ==========================================================
# 14. 主窗口
#  ★ 真全屏：showFullScreen() + image_label 用 setScaledContents(True)
#            渲染线程按真实屏幕尺寸生成图像，label 自动填满无黑边
# ==========================================================
class SurroundViewWindow(QMainWindow):
    PAGE_MAIN     = 0
    PAGE_SETTINGS = 1

    def __init__(self, multi_cam, serial_worker, remote_worker, cfg):
        super().__init__()
        self.multi_cam     = multi_cam
        self.serial_worker = serial_worker
        self.remote_worker = remote_worker
        self.cfg           = cfg
        self.carousel      = CarouselScheduler()

        self.setWindowTitle("环视系统")
        # ★ 先全屏，再取实际尺寸传给渲染线程
        self.showFullScreen()
        self.sw = 1024
        self.sh = 600
        print("[INFO] 实际屏幕尺寸: {}x{}".format(self.sw, self.sh))

        self.stack = QStackedWidget()
        self.setCentralWidget(self.stack)

        # 主显示页
        main_page = QWidget()
        main_page.setStyleSheet("background:black;")
        ml = QVBoxLayout(main_page)
        ml.setContentsMargins(0, 0, 0, 0)
        ml.setSpacing(0)

        # ★ setScaledContents(True) 让 pixmap 自动充满 label，无黑边
        self.image_label = QLabel()
        self.image_label.setAlignment(Qt.AlignCenter)
        self.image_label.setStyleSheet("background:black;")
        self.image_label.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.image_label.setScaledContents(True)   # ★ 关键
        ml.addWidget(self.image_label)
        self.stack.addWidget(main_page)

        # 设置页（全屏）
        self.settings_panel = SettingsPanel(cfg)
        self.stack.addWidget(self.settings_panel)
        self.stack.setCurrentIndex(self.PAGE_MAIN)

        # 工作线程（传入实际屏幕尺寸）
        self.raw_worker = RawCaptureWorker(multi_cam.caps, multi_cam.names)
        self.raw_worker.start()

        self.bird_worker = BirdViewWorker(
            self.raw_worker, multi_cam.camera_models,
            multi_cam.birdview, framestep=2)
        self.bird_worker.start()

        # ★ 渲染线程用实际屏幕尺寸
        self.render_360 = RenderWorker(
            self.bird_worker, self.raw_worker,
            serial_worker, self.carousel, cfg,
            self.sw, self.sh)
        self.render_360.start()

        self.render_quad = QuadRenderWorker(
            self.raw_worker, serial_worker, cfg,
            self.sw, self.sh)
        self.render_quad.start()

        self.record_worker = RecordWorker(self.raw_worker)
        self.record_worker.start()

        self.timer = QTimer()
        self.timer.timeout.connect(self._flush)
        self.timer.start(16)

        remote_worker.signals.key_pressed.connect(self._on_key)
        remote_worker.signals.enter_settings.connect(self._on_enter_settings)
        remote_worker.signals.exit_settings.connect(self._on_exit_settings)

        self._apply_display_mode()

    def _apply_display_mode(self):
        mode = self.cfg.get("display_mode", MODE_360)
        self.serial_worker.set_rx_enabled(mode == MODE_360)
        print("[UI] 显示模式: {}".format("360环视" if mode == MODE_360
                                        else "四分屏"))

    def _flush(self):
        if self.stack.currentIndex() != self.PAGE_MAIN:
            return
        mode = self.cfg.get("display_mode", MODE_360)
        qimg = (self.render_quad.get_qimage()
                if mode == MODE_QUAD
                else self.render_360.get_qimage())
        if qimg is None:
            return
        # ★ 直接 setPixmap，setScaledContents(True) 自动填满，无黑边
        self.image_label.setPixmap(QPixmap.fromImage(qimg))

    def _on_key(self, code):
        if self.stack.currentIndex() == self.PAGE_SETTINGS:
            if (code == KEY_BACK and
                    self.settings_panel._sub_page == SettingsPanel.SUB_MAIN):
                self._on_exit_settings()
            else:
                self.settings_panel.on_key(code)

    def _on_enter_settings(self):
        if self.stack.currentIndex() == self.PAGE_SETTINGS:
            return
        self.settings_panel.load()
        self.stack.setCurrentIndex(self.PAGE_SETTINGS)
        print("[UI] 进入设置界面")

    def _on_exit_settings(self):
        if self.stack.currentIndex() != self.PAGE_SETTINGS:
            return
        self.settings_panel.save()
        CarouselScheduler.INTERVAL = float(
            self.cfg.get("carousel_interval", 4))
        self._apply_display_mode()
        self.stack.setCurrentIndex(self.PAGE_MAIN)
        print("[UI] 退出设置，返回主视图")

    def closeEvent(self, event):
        self.timer.stop()
        self.render_360.stop()
        self.render_quad.stop()
        self.bird_worker.stop()
        self.raw_worker.stop()
        self.record_worker.stop()
        self.serial_worker.stop()
        self.remote_worker.stop()
        self.multi_cam.running = False
        for cap in self.multi_cam.caps:
            cap.release()
        event.accept()

# ==========================================================
# 15. 主函数
# ==========================================================
def main():
    parser = argparse.ArgumentParser(description="环视系统")
    parser.add_argument("--mode",        type=str, required=True,
                        choices=["realtime", "static"])
    parser.add_argument("--port",        type=str, default="/dev/ttyS0")
    parser.add_argument("--remote-port", type=str, default="/dev/ttyS3")
    parser.add_argument("--baudrate",    type=int, default=115200)
    parser.add_argument("--framestep",   type=int, default=2)
    args = parser.parse_args()

    if args.mode == "realtime":
        cfg = load_config()
        CarouselScheduler.INTERVAL = float(cfg.get("carousel_interval", 4))

        serial_worker = SerialWorker(port=args.port, baudrate=args.baudrate)
        serial_worker.start()
        print("[INFO] 雷达串口: {} @ {}  发送周期: {}ms".format(
            args.port, args.baudrate, SEND_INTERVAL_MS))

        remote_worker = RemoteWorker(port=args.remote_port,
                                     baudrate=args.baudrate)
        remote_worker.start()
        print("[INFO] 遥控串口: {} @ {}".format(
            args.remote_port, args.baudrate))

        multi_cam = MultiCameraBirdView()
        if not multi_cam.running:
            print("[ERROR] 摄像头初始化失败")
            serial_worker.stop()
            remote_worker.stop()
            return

        app = QApplication(sys.argv)

        # ★ 禁用系统缩放干扰，确保全屏像素对齐
        app.setAttribute(Qt.AA_DisableHighDpiScaling, True)

        win = SurroundViewWindow(
            multi_cam, serial_worker, remote_worker, cfg)
        win.bird_worker.framestep = args.framestep
        sys.exit(app.exec_())

    elif args.mode == "static":
        print("[INFO] Static mode not implemented.")

if __name__ == "__main__":
    main()
