"""
Orisys SDK 实时监控 — Qt 统一界面

将所有可视化和曲线整合到一个窗口中，适合销售演示使用。
SDK version: 0.3.1
"""

import os
import sys
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
import orisys
import numpy as np
import time
import argparse
from datetime import datetime

try:
    from scipy.signal import butter, filtfilt
except Exception:
    butter = None
    filtfilt = None

# 下面这些参数只影响右侧三条力曲线的显示，不改变传感器读数、光流图或幅值图数据。
FORCE_SAMPLE_RATE_HZ = 30.0
PLOT_BUFFER_POINTS = 120
PLOT_WINDOW_SEC = PLOT_BUFFER_POINTS / FORCE_SAMPLE_RATE_HZ
Y_AXIS_MIN_SPAN = 20000.0
CURVE_THEME_COLOR = '#00E5FF'

from PyQt5.QtWidgets import (
    QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
    QLabel, QApplication, QSplitter, QPlainTextEdit,
    QPushButton, QFileDialog, QLineEdit,
)
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QImage, QPixmap, QFont, QIcon

try:
    import pyqtgraph as pg
except Exception:
    print("PyQtGraph is not installed. Please install it using 'pip install pyqt5 pyqtgraph'.")
    sys.exit(1)

# ---------------------------------------------------------------------------
# 工具
# ---------------------------------------------------------------------------

def cv2_to_pixmap(img_bgr: np.ndarray) -> QPixmap:
    rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
    h, w, ch = rgb.shape
    qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888)
    return QPixmap.fromImage(qimg)

def draw_magnitude_map(vfield, threshold=1.5, colormap=cv2.COLORMAP_JET) -> np.ndarray:
    flow_field = vfield
    magnitude = np.sqrt(flow_field[:, :, 0] ** 2 + flow_field[:, :, 1] ** 2)

    downsample_factor = 10
    h, w = magnitude.shape
    magnitude_downsampled = magnitude[::downsample_factor, ::downsample_factor]
    magnitude_smoothed = cv2.resize(magnitude_downsampled, (w, h), interpolation=cv2.INTER_CUBIC)

    magnitude_thresholded = magnitude_smoothed.copy()
    magnitude_thresholded[magnitude_smoothed < threshold] = 0

    mag_min, mag_max = magnitude_thresholded.min(), magnitude_thresholded.max()
    if mag_max > mag_min:
        magnitude_norm = ((magnitude_thresholded - mag_min) / (mag_max - mag_min) * 255).astype(np.uint8)
    else:
        magnitude_norm = np.zeros_like(magnitude_thresholded, dtype=np.uint8)

    magnitude_colored = cv2.applyColorMap(magnitude_norm, colormap)
    out_img = cv2.resize(magnitude_colored, (400, 400), interpolation=cv2.INTER_CUBIC)
    return out_img


def lowpass_filter(data, cutoff_hz, sample_rate_hz=FORCE_SAMPLE_RATE_HZ, order=2):
    """和 qt_viewer_2.py 一样用于三条力曲线显示的低通滤波。"""
    y = np.asarray(data, dtype=np.float64)
    if cutoff_hz == 0 or y.size == 0:
        return y.copy()

    nyquist = 0.5 * sample_rate_hz
    if cutoff_hz < 0 or cutoff_hz >= nyquist:
        raise ValueError(
            f"cutoff_hz must be in (0, {nyquist}) for sample_rate_hz={sample_rate_hz}"
        )

    if butter is not None and filtfilt is not None:
        b, a = butter(order, cutoff_hz / nyquist, btype="low")
        padlen = 3 * (max(len(b), len(a)) - 1)
        if y.size <= padlen:
            return y.copy()
        return filtfilt(b, a, y)

    window = min(max(int(sample_rate_hz / max(cutoff_hz, 1e-6)), 3), y.size)
    if window % 2 == 0:
        window -= 1
    if window <= 1:
        return y.copy()
    kernel = np.ones(window, dtype=np.float64) / window
    pad = window // 2
    forward = np.convolve(np.pad(y, (pad, pad), mode="edge"), kernel, mode="valid")
    return np.convolve(np.pad(forward[::-1], (pad, pad), mode="edge"), kernel, mode="valid")[::-1]


class TeeStream:
    """同时写入多个流"""
    def __init__(self, *streams):
        self.streams = streams

    def write(self, text):
        for s in self.streams:
            s.write(text)

    def flush(self):
        for s in self.streams:
            s.flush()


class WidgetLogHandler:
    """将 print 输出实时显示到 QPlainTextEdit"""
    def __init__(self, widget: QPlainTextEdit):
        self.widget = widget
        self.buf = ""

    def write(self, text: str):
        self.buf += text
        while "\n" in self.buf:
            line, self.buf = self.buf.split("\n", 1)
            if line:
                self.widget.appendPlainText(line)

    def flush(self):
        if self.buf:
            self.widget.appendPlainText(self.buf)
            self.buf = ""

# ---------------------------------------------------------------------------
# 样式表
# ---------------------------------------------------------------------------

STYLE = """
QMainWindow {
    background-color: #000000;
}

/* ---- 图像面板 ---- */
QLabel#arrowsLabel, QLabel#magLabel {
    border: 1px solid #262626;
    border-radius: 4px;
    background-color: #0b0b0b;
}

/* ---- 顶栏按钮 & 输入 - — */
QPushButton {
    background-color: #1a1a1a;
    color: #999999;
    border: 1px solid #333333;
    border-radius: 4px;
    padding: 6px 16px;
    font-family: "SimHei";
    font-size: 13px;
}
QPushButton:hover {
    background-color: #262626;
    color: #cccccc;
    border: 1px solid #555555;
}
QPushButton#btnStart {
    background-color: #0d2818;
    color: #4caf84;
    border: 1px solid #1a4d30;
}
QPushButton#btnStart:hover {
    background-color: #143d24;
    color: #6fd4a0;
}
QPushButton#btnStart:disabled {
    background-color: #0f0f0f;
    color: #333333;
    border: 1px solid #222222;
}
QPushButton#btnStop {
    background-color: #2b1010;
    color: #e05555;
    border: 1px solid #4d1a1a;
}
QPushButton#btnStop:hover {
    background-color: #3d1818;
    color: #f07070;
}
QPushButton#btnStop:disabled {
    background-color: #0f0f0f;
    color: #333333;
    border: 1px solid #222222;
}
QLineEdit {
    background-color: #0d0d0d;
    color: #999999;
    border: 1px solid #333333;
    border-radius: 4px;
    padding: 5px 10px;
    font-size: 13px;
}

/* ---- 力曲线容器 ---- */
GraphicsLayoutWidget {
    border: 1px solid #262626;
    border-radius: 4px;
}

/* ---- 日志面板 ---- */
QPlainTextEdit {
    background-color: #0b0b0b;
    color: #777777;
    border: 1px solid #262626;
    border-radius: 4px;
    font-family: "Cascadia Code", "Consolas", "Courier New", monospace;
    font-size: 13px;
    padding: 8px;
    selection-background-color: #1a3a4a;
    selection-color: #cccccc;
}

QPlainTextEdit QScrollBar:vertical {
    background-color: #0b0b0b;
    width: 6px;
    margin: 1px;
    border-radius: 3px;
}
QPlainTextEdit QScrollBar::handle:vertical {
    background-color: #262626;
    border-radius: 3px;
    min-height: 30px;
}
QPlainTextEdit QScrollBar::handle:vertical:hover {
    background-color: #3a3a3a;
}
QPlainTextEdit QScrollBar::add-line:vertical,
QPlainTextEdit QScrollBar::sub-line:vertical {
    height: 0px;
}
QPlainTextEdit QScrollBar::add-page:vertical,
QPlainTextEdit QScrollBar::sub-page:vertical {
    background: none;
}

QSplitter::handle {
    background-color: #1a1a1a;
}
QSplitter::handle:vertical {
    height: 1px;
}
QSplitter::handle:horizontal {
    width: 1px;
}
"""

# ---------------------------------------------------------------------------
# 主窗口
# ---------------------------------------------------------------------------

class MainWindow(QMainWindow):
    def __init__(self, args, log_file):
        super().__init__()
        self.setWindowTitle("Orisys SDK")
        self.setStyleSheet(STYLE)

        logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
        if os.path.exists(logo_path):
            self.setWindowIcon(QIcon(logo_path))

        self.sensor = None
        self.start_time = None
        self.args = args

        # 录像状态
        self.is_save = False
        self.frame_count = 0
        self.writer = None
        self.save_path = None

        # 曲线数据缓冲：只存原始 fn/fx/fy，滤波只在绘图副本上做。
        self.npoints = PLOT_BUFFER_POINTS
        self.lowpass_cutoff_hz = getattr(args, "cutoff", 10.0)
        self._init_plot_buffer()

        self._setup_ui()
        self._create_plots()

        # 同步参数到 UI
        self.camera_edit.setText(args.video)
        self.config_edit.setText(args.config)
        self.cal_edit.setText(args.cal)

        # stdout → 日志文件 + 界面日志面板
        self.log_handler = WidgetLogHandler(self.log_widget)
        sys.stdout = TeeStream(log_file, self.log_handler)
        sys.stderr = sys.stdout

        self._y_range_alpha = 0.5

        self.timer = QTimer()
        self.timer.timeout.connect(self._on_timer)
        self._running = False

    def _init_plot_buffer(self):
        self.data = np.full((self.npoints, 2, 3), np.nan, dtype=np.float64)
        self._plot_sample_count = 0
        self._y_range_smooth = [None, None, None]

    # ---- UI 构建 ----

    def _setup_ui(self):
        central = QWidget()
        self.setCentralWidget(central)
        root_layout = QVBoxLayout(central)
        root_layout.setContentsMargins(6, 6, 6, 6)
        root_layout.setSpacing(6)

        # ---- 顶栏 ----
        bar = QHBoxLayout()
        bar.setSpacing(8)

        self.btn_camera = QPushButton("摄像机")
        self.btn_camera.setObjectName("btnCamera")
        self.btn_camera.clicked.connect(self._select_camera)
        bar.addWidget(self.btn_camera)
        self.camera_edit = QLineEdit("1")
        self.camera_edit.setPlaceholderText("摄像头编号 或 视频路径")
        bar.addWidget(self.camera_edit, 1)

        self.btn_config = QPushButton("配置")
        self.btn_config.setObjectName("btnConfig")
        self.btn_config.clicked.connect(self._select_config)
        bar.addWidget(self.btn_config)
        self.config_edit = QLineEdit("./config/ddjx01.json")
        bar.addWidget(self.config_edit, 1)

        self.btn_reset = QPushButton("重置")
        self.btn_reset.setObjectName("btnReset")
        self.btn_reset.setEnabled(False)
        self.btn_reset.clicked.connect(self._on_reset)
        bar.addWidget(self.btn_reset)
        self.cal_edit = QLineEdit("./config/ddjx01.npy")
        bar.addWidget(self.cal_edit, 1)

        self.btn_start = QPushButton("启动")
        self.btn_start.setObjectName("btnStart")
        self.btn_start.clicked.connect(self._on_start)
        bar.addWidget(self.btn_start)

        self.btn_stop = QPushButton("暂停")
        self.btn_stop.setObjectName("btnStop")
        self.btn_stop.setEnabled(False)
        self.btn_stop.clicked.connect(self._on_stop)
        bar.addWidget(self.btn_stop)

        root_layout.addLayout(bar)

        # ---- 外层纵向 splitter：主区域 / 日志 ----
        v_splitter = QSplitter(Qt.Vertical)

        # ---- 主区域：图像 (左) | 曲线 (右) ----
        h_splitter = QSplitter(Qt.Horizontal)

        # 左侧图像面板
        left = QWidget()
        left_layout = QVBoxLayout(left)
        left_layout.setContentsMargins(0, 0, 0, 0)
        left_layout.setSpacing(6)

        self.arrows_label = QLabel()
        self.arrows_label.setObjectName("arrowsLabel")
        self.arrows_label.setMinimumSize(160, 160)
        self.arrows_label.setScaledContents(True)
        self.arrows_label.setAlignment(Qt.AlignCenter)
        left_layout.addWidget(self.arrows_label)

        self.mag_label = QLabel()
        self.mag_label.setObjectName("magLabel")
        self.mag_label.setMinimumSize(160, 160)
        self.mag_label.setScaledContents(True)
        self.mag_label.setAlignment(Qt.AlignCenter)
        left_layout.addWidget(self.mag_label)

        h_splitter.addWidget(left)

        # 右侧力曲线
        pg.setConfigOption('background', '#000000')
        pg.setConfigOption('foreground', '#777777')
        pg.setConfigOptions(antialias=True)
        self.graph_widget = pg.GraphicsLayoutWidget()
        self.graph_widget.setMinimumWidth(300)
        h_splitter.addWidget(self.graph_widget)

        self.h_splitter = h_splitter

        v_splitter.addWidget(h_splitter)

        # ---- 底部日志面板 ----
        self.log_widget = QPlainTextEdit()
        self.log_widget.setReadOnly(True)
        self.log_widget.setMaximumBlockCount(2000)
        font = QFont("Consolas", 12)
        font.setStyleHint(QFont.Monospace)
        self.log_widget.setFont(font)
        v_splitter.addWidget(self.log_widget)

        v_splitter.setStretchFactor(0, 4)
        v_splitter.setStretchFactor(1, 1)

        root_layout.addWidget(v_splitter)

    def _create_plots(self):
        titles = ["法向力", "切向力 X", "切向力 Y"]
        self.plots = []
        self.curves = []
        self.dot_glows = []
        self.dot_cores = []

        axis_pen = pg.mkPen(color=(38, 38, 38), width=1)  # #262626
        label_font = QFont("SimHei", 9)

        for i, title in enumerate(titles):
            p = self.graph_widget.addPlot(title=None)

            # 标题：黑体
            p.setTitle(title, color='#999999', size='14pt')
            p.titleLabel.setFont(QFont("SimHei", 14))

            # X 轴标签
            p.setLabel('bottom', '时间', units='s')
            p.getAxis('bottom').label.setFont(label_font)

            # 仅保留左 + 下轴线，隐藏右 + 上边框
            p.showAxis('top', False)
            p.showAxis('right', False)
            p.getAxis('left').setPen(axis_pen)
            p.getAxis('bottom').setPen(axis_pen)

            # 仅横向网格，极淡
            p.showGrid(x=False, y=True, alpha=0.12)

            # 等宽刻度字体
            tick_font = QFont("Consolas", 11)
            tick_font.setStyleHint(QFont.Monospace)
            p.getAxis('left').setTickFont(tick_font)
            p.getAxis('bottom').setTickFont(tick_font)

            # 曲线样式与 qt_viewer_2.py 对齐
            curve = p.plot(pen=pg.mkPen(color=CURVE_THEME_COLOR, width=2), fillLevel=0)

            # 终点光晕: #00e5ff 30% 透明，半径 8
            dot_glow = p.plot([], [], pen=None, symbol='o', symbolSize=8,
                              symbolPen=None, symbolBrush=(0, 229, 255, 76))
            # 终点核心: 纯白，半径 4
            dot_core = p.plot([], [], pen=None, symbol='o', symbolSize=4,
                              symbolPen=None, symbolBrush=(255, 255, 255, 255))

            p.setXRange(0, PLOT_WINDOW_SEC, padding=0)
            p.enableAutoRange(x=False, y=False)

            self.plots.append(p)
            self.curves.append(curve)
            self.dot_glows.append(dot_glow)
            self.dot_cores.append(dot_core)

            if i < 2:
                self.graph_widget.nextRow()

    def adjust_square_images(self):
        """根据窗口高度调整左侧面板宽度，使两张图保持正方形"""
        top_bar_h = 48
        log_h = self.log_widget.height()
        spacing = 20
        available = self.height() - top_bar_h - log_h - spacing
        each_h = max(available // 2, 160)
        self.h_splitter.setSizes([each_h, self.width() - each_h])

    def _make_force_plot_data(self, data_plot):
        for i in range(3):
            data_plot[:, 1, i] = lowpass_filter(
                data_plot[:, 1, i],
                self.lowpass_cutoff_hz,
                sample_rate_hz=FORCE_SAMPLE_RATE_HZ,
            )
        return data_plot

    # ---- 帧处理 ----

    def _on_timer(self):
        if self.sensor is None:
            return
        sensor = self.sensor

        sensor.get_img()

        # 录像写入
        if self.is_save and self.writer is not None and sensor.frame is not None:
            self.frame_count += 1
            save_img = sensor.frame
            if len(save_img.shape) == 2 or (len(save_img.shape) == 3 and save_img.shape[2] == 1):
                if len(save_img.shape) == 3:
                    save_img = save_img.squeeze(2)
                save_img = cv2.cvtColor(save_img, cv2.COLOR_GRAY2BGR)
            self.writer.write(save_img)

        sensor.compute_deformation(check_motion=True, threshold=0)

        fps, fn, fx, fy, flow, vnormal, vshear = sensor.read_info(
            sensor.info.FPS, sensor.info.FNORMAL,
            sensor.info.FSHEARX, sensor.info.FSHEARY,
            sensor.info.VRAW, sensor.info.VNORMAL,
            sensor.info.VSHEAR,
        )
        print(f"FPS={fps:.2f}, 法向力={fn:.4f}, 切向力X={fx:.4f}, 切向力Y={fy:.4f}")

        # 图像
        _bg_bgr = (22, 16, 11)
        _img = sensor.img
        _h, _w = _img.shape[:2]
        black_img = np.empty((_h, _w, 3), dtype=_img.dtype)
        black_img[:, :, 0] = _bg_bgr[0]
        black_img[:, :, 1] = _bg_bgr[1]
        black_img[:, :, 2] = _bg_bgr[2]

        arrows = orisys.util.draw_arrows(
            black_img, flow,
            threshold=2, grid_spacing=20,
            arrow_scale=0.5,
            below_threshold_color=(160, 160, 160),
        )
        arrows = cv2.resize(arrows, (400, 400), interpolation=cv2.INTER_CUBIC)
        self.arrows_label.setPixmap(cv2_to_pixmap(arrows))

        mag = draw_magnitude_map(flow)
        self.mag_label.setPixmap(cv2_to_pixmap(mag))

        # 曲线：只对右侧三条力曲线的显示数据做缓冲和低通滤波。
        current_time = time.time() - self.start_time
        if self._plot_sample_count < self.npoints:
            idx = self._plot_sample_count
            self.data[idx, 0, :] = current_time
            self.data[idx, 1, 0] = fn
            self.data[idx, 1, 1] = fx
            self.data[idx, 1, 2] = fy
            self._plot_sample_count += 1
        else:
            for i in range(3):
                self.data[:-1, 0, i] = self.data[1:, 0, i]
                self.data[:-1, 1, i] = self.data[1:, 1, i]
            self.data[-1, 0, :] = current_time
            self.data[-1, 1, 0] = fn
            self.data[-1, 1, 1] = fx
            self.data[-1, 1, 2] = fy

        n = self._plot_sample_count
        if n == 0:
            return

        data_plot = self._make_force_plot_data(self.data[:n].copy())

        for i, curve in enumerate(self.curves):
            x = data_plot[:, 0, i]
            y = data_plot[:, 1, i]
            curve.setData(x, y)
            ymin, ymax = y.min(), y.max()
            if self._y_range_smooth[i] is None:
                self._y_range_smooth[i] = (ymin, ymax)
            else:
                prev_min, prev_max = self._y_range_smooth[i]
                alpha = self._y_range_alpha
                ymin = alpha * ymin + (1 - alpha) * prev_min
                ymax = alpha * ymax + (1 - alpha) * prev_max
            if ymax - ymin < Y_AXIS_MIN_SPAN:
                mid = (ymin + ymax) / 2
                ymin = mid - Y_AXIS_MIN_SPAN / 2
                ymax = mid + Y_AXIS_MIN_SPAN / 2
            self._y_range_smooth[i] = (ymin, ymax)
            pad = max((ymax - ymin) * 0.12, 0.01)
            self.plots[i].setYRange(ymin - pad, ymax + pad)
            # 终点标记
            last_x = x[-1:]
            last_y = y[-1:]
            self.dot_glows[i].setData(last_x, last_y)
            self.dot_cores[i].setData(last_x, last_y)

        if n < self.npoints:
            for p in self.plots:
                p.setXRange(0, PLOT_WINDOW_SEC, padding=0)
        else:
            x0 = float(data_plot[0, 0, 0])
            x1 = float(data_plot[-1, 0, 0])
            if x1 <= x0:
                x1 = x0 + PLOT_WINDOW_SEC
            for p in self.plots:
                p.setXRange(x0, x1, padding=0)

    # ---- 键盘 & 录像 ----

    def _on_start(self):
        if self._running:
            return
        self.btn_start.setText("启动中...")
        self.btn_start.setEnabled(False)
        QApplication.processEvents()

        try:
            src = self.camera_edit.text()
            cfg = self.config_edit.text()
            cal = self.cal_edit.text()
            verbose = getattr(self.args, 'verbose', True)
            try:
                src = int(src)
            except ValueError:
                pass
            self.sensor = orisys.Sensor(src, config_name=cfg, cal_path=cal, verbose=verbose)
        except Exception as e:
            self.btn_start.setText("启动")
            self.btn_start.setEnabled(True)
            print(f"启动失败: {e}")
            return

        self.start_time = time.time()
        self._init_plot_buffer()
        self._running = True
        self.timer.start(10)
        self.btn_start.setText("启动")
        self.btn_stop.setEnabled(True)
        self.btn_reset.setEnabled(True)
        print(f"▶ 启动 — 摄像机={src}, 配置={cfg}, 标定={cal}")

    def _on_stop(self):
        if not self._running:
            return
        self._running = False
        self.timer.stop()
        self.btn_stop.setText("停止中...")
        self.btn_stop.setEnabled(False)
        QApplication.processEvents()

        if self.sensor is not None:
            self.sensor.disconnect()
            self.sensor = None

        self.btn_start.setEnabled(True)
        self.btn_stop.setText("暂停")
        self.btn_reset.setEnabled(False)
        print("⏸ 已暂停")

    def _select_camera(self):
        text = self.camera_edit.text()
        path, _ = QFileDialog.getOpenFileName(
            self, "选择视频文件", "data", "视频 (*.mp4 *.avi *.mov);;所有文件 (*)",
        )
        if path:
            self.camera_edit.setText(path)

    def _select_config(self):
        path, _ = QFileDialog.getOpenFileName(
            self, "选择配置文件", "config", "JSON (*.json);;所有文件 (*)",
        )
        if path:
            self.config_edit.setText(path)

    def _select_cal(self):
        path, _ = QFileDialog.getOpenFileName(
            self, "选择标定文件", "config", "NumPy (*.npy);;所有文件 (*)",
        )
        if path:
            self.cal_edit.setText(path)

    def _on_reset(self):
        """Reset optical flow tracker when drift occurs (same as basic_pipeline 'r' key)."""
        if self.sensor is None or not self._running:
            print("请先启动传感器后再重置追踪器。")
            return
        self.sensor.reset()
        print("追踪器已重置。")

    def keyPressEvent(self, event):
        key = event.key()
        if key == Qt.Key_Escape:
            if self.isFullScreen():
                self.showNormal()
            else:
                self.showFullScreen()
        elif key == Qt.Key_Q:
            print("\n正在退出示例程序...")
            self.close()
        elif key == Qt.Key_S:
            self._start_recording()
        elif key == Qt.Key_E:
            self._stop_recording()
        elif key == Qt.Key_R:
            self._on_reset()
        else:
            super().keyPressEvent(event)

    def _start_recording(self):
        if self.sensor is None:
            print("请先启动采集")
            return
        self.save_path = "./data/pulse_test.mp4"
        os.makedirs(os.path.dirname(self.save_path), exist_ok=True)

        save_img = self.sensor.frame
        if save_img is None:
            print("当前没有可用帧，无法确定录像尺寸。")
            return

        temp_img = save_img.copy()
        if len(temp_img.shape) == 2 or (len(temp_img.shape) == 3 and temp_img.shape[2] == 1):
            if len(temp_img.shape) == 3:
                temp_img = temp_img.squeeze(2)
            temp_img = cv2.cvtColor(temp_img, cv2.COLOR_GRAY2BGR)

        fh, fw = temp_img.shape[:2]
        self.writer = cv2.VideoWriter(
            self.save_path, cv2.VideoWriter_fourcc(*'mp4v'), 30, (fw, fh),
        )
        if self.writer.isOpened():
            self.is_save = True
            print(f"开始录像：{self.save_path}")
        else:
            print(f"无法创建录像文件：{self.save_path}")
            self.writer = None

    def _stop_recording(self):
        self.is_save = False
        if self.writer is not None:
            self.writer.release()
            self.writer = None
        if self.save_path is not None:
            print(f"录像结束：{self.save_path}，总帧数：{self.frame_count}")
        else:
            print(f"录像结束，总帧数：{self.frame_count}")
        self.frame_count = 0

    def closeEvent(self, event):
        self.timer.stop()
        if self.writer is not None:
            self.writer.release()
        if self.sensor is not None:
            self.sensor.disconnect()
        event.accept()

# ---------------------------------------------------------------------------
# 入口
# ---------------------------------------------------------------------------

def main():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    log_dir = os.path.join(script_dir, "logs")
    os.makedirs(log_dir, exist_ok=True)
    log_path = os.path.join(log_dir, f"qt_viewer_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
    log_file = open(log_path, 'w', encoding='utf-8')
    # stdout 到文件的临时重定向（创建 MainWindow 后会加上日志面板）
    sys.stdout = log_file
    sys.stderr = sys.stdout

    parser = argparse.ArgumentParser()
    parser.add_argument("--video", "-v", type=str, default="0")
    parser.add_argument("--config", "-c", type=str, default="./config/ddjx01.json")
    parser.add_argument("--cal", "-cal", type=str, default="./config/ddjx01.npy")
    parser.add_argument("--verbose", "-verbose", type=bool, default=True)
    parser.add_argument(
        "--cutoff",
        type=float,
        default=10.0,
        help="Low-pass cutoff for force curves (Hz), sample rate 30 Hz",
    )
    args = parser.parse_args()

    print("\n按键说明：Q 退出 | R 重置追踪器 | S 开始录像 | E 结束录像 | ESC 全屏")

    # 高 DPI 适配（笔记本高分屏）
    QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
    QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
    app = QApplication(sys.argv)
    window = MainWindow(args, log_file)
    window.showFullScreen()
    QApplication.processEvents()
    window.adjust_square_images()
    window._on_start()

    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
