
# =========================================================
# 这个原型界面的目标：
# 1) 不再只做“勾选 UI”，而是让界面直接生成接近
#    WorkshopSessionConfig / RequestedCalibrationTask 的数据结构预览；
# 2) 让你能一眼看到：当前界面上的输入，最后会怎么进入主控消息。
# 说明：
# - 这里不是 ROS2 运行节点，只是一个 PySide6 原型工具；
# - 右侧 JSON 预览，是为了帮助你核对“UI -> 消息结构”是否对齐。
# =========================================================

import json
import signal
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout,
    QHBoxLayout, QGridLayout, QTextEdit, QGroupBox, QCheckBox, QListWidget,
    QListWidgetItem, QProgressBar, QMessageBox, QTreeWidget, QTreeWidgetItem,
    QSplitter, QTabWidget, QComboBox, QFormLayout, QFileDialog
)

# =========================================================
# 底盘通用参数
# 这些最终会映射成 RequestedCalibrationTask：
# - stage_type = CHASSIS_CALIBRATION_STAGE
# - task_code = chassis.common.xxx
# =========================================================
CHASSIS_COMMON = [
    ("chassis.common.effective_wheel_base_m", "有效轴距"),
    ("chassis.common.effective_track_width_m", "有效轮距"),
    ("chassis.common.longitudinal_scale", "纵向里程比例补偿"),
    ("chassis.common.lateral_scale", "横向里程比例补偿"),
    ("chassis.common.yaw_scale", "航向比例补偿"),
    ("chassis.common.straight_line_bias", "直线跑偏补偿"),
]

# =========================================================
# 底盘专属参数
# 这些会根据底盘类型动态显示。
# =========================================================
CHASSIS_SPECIFIC = {
    "ACKERMANN_CHASSIS": [
        ("chassis.ackermann.front_left_steer_zero_offset_deg", "左前舵角零偏"),
        ("chassis.ackermann.front_right_steer_zero_offset_deg", "右前舵角零偏"),
        ("chassis.ackermann.rear_left_wheel_radius_m", "左后轮有效半径"),
        ("chassis.ackermann.rear_right_wheel_radius_m", "右后轮有效半径"),
        ("chassis.ackermann.steering_ratio", "转向传动比"),
    ],
    "DIFFERENTIAL_CHASSIS": [
        ("chassis.differential.left_wheel_radius_m", "左轮有效半径"),
        ("chassis.differential.right_wheel_radius_m", "右轮有效半径"),
        ("chassis.differential.axle_track_width_m", "驱动轮间距"),
        ("chassis.differential.left_encoder_scale", "左编码器比例系数"),
        ("chassis.differential.right_encoder_scale", "右编码器比例系数"),
    ],
    "SINGLE_STEER_WHEEL_CHASSIS": [
        ("chassis.single_steer.drive_wheel_radius_m", "驱动轮有效半径"),
        ("chassis.single_steer.steer_zero_offset_deg", "舵角零偏"),
        ("chassis.single_steer.steering_ratio", "转向比"),
        ("chassis.single_steer.drive_encoder_scale", "驱动编码器比例"),
    ],
    "MULTI_STEER_WHEEL_CHASSIS": [
        ("chassis.multi_steer.module_wheel_radius_m", "模块轮半径"),
        ("chassis.multi_steer.module_steer_zero_offset_deg", "模块舵角零偏"),
        ("chassis.multi_steer.module_pos_x_m", "模块在 base_link 下的 X"),
        ("chassis.multi_steer.module_pos_y_m", "模块在 base_link 下的 Y"),
    ],
}

# =========================================================
# 运控参数
# 按 控制轴 × 算法类型 组织。
# 最终会映射成 RequestedCalibrationTask：
# - stage_type = CONTROL_CALIBRATION_STAGE
# - task_code = control.lateral.pid / control.longitudinal.mpc 等
# - task_params = need_tuning / is_default
# =========================================================
CONTROL_OPTIONS = {
    "lateral": [
        ("control.lateral.pid", "PID"),
        ("control.lateral.mpc", "MPC"),
        ("control.lateral.lqr", "LQR"),
        ("control.lateral.pure_pursuit", "Pure Pursuit"),
    ],
    "longitudinal": [
        ("control.longitudinal.pid", "PID"),
        ("control.longitudinal.mpc", "MPC"),
    ],
}

# =========================================================
# 传感器标定树
# 最终会映射成 RequestedCalibrationTask：
# - stage_type = SENSOR_INTRINSIC_CALIBRATION_STAGE / SENSOR_EXTRINSIC_CALIBRATION_STAGE / HAND_EYE_CALIBRATION_STAGE
# - task_code / target_id 由树节点决定
# =========================================================
SENSOR_TREE = {
    "camera": {
        "front_camera": [
            ("sensor.front_camera.camera_intrinsic", "相机内参"),
            ("sensor.front_camera.sensor_to_base_extrinsic", "传感器到 base_link 外参"),
        ],
        "downward_camera": [
            ("sensor.downward_camera.camera_intrinsic", "相机内参"),
            ("sensor.downward_camera.sensor_to_base_extrinsic", "传感器到 base_link 外参"),
        ],
        "arm_camera": [
            ("sensor.arm_camera.camera_intrinsic", "相机内参"),
            ("sensor.arm_camera.sensor_to_base_extrinsic", "传感器到 base_link 外参"),
            ("sensor.arm_camera.hand_eye", "手眼标定"),
        ],
    },
    "lidar": {
        "lidar_3d": [
            ("sensor.lidar_3d.sensor_to_base_extrinsic", "传感器到 base_link 外参"),
        ],
        "lidar_2d": [
            ("sensor.lidar_2d.sensor_to_base_extrinsic", "传感器到 base_link 外参"),
        ],
    },
    "imu": {
        "imu_01": [
            ("sensor.imu.imu_intrinsic", "IMU 内参"),
            ("sensor.imu.sensor_to_base_extrinsic", "传感器到 base_link 外参"),
        ],
    }
}

# =========================================================
# 常量：用字符串模拟 WorkflowStageType / StageExecutionPolicy
# 作用：
# - 方便你直接在 JSON 预览里看懂每一项会落成什么；
# - 后面接 ROS2 时，再替换成真实枚举/消息值即可。
# =========================================================
STAGE_CHASSIS = "CHASSIS_CALIBRATION_STAGE"
STAGE_CONTROL = "CONTROL_CALIBRATION_STAGE"
STAGE_SENSOR_INTRINSIC = "SENSOR_INTRINSIC_CALIBRATION_STAGE"
STAGE_SENSOR_EXTRINSIC = "SENSOR_EXTRINSIC_CALIBRATION_STAGE"
STAGE_HAND_EYE = "HAND_EYE_CALIBRATION_STAGE"
POLICY_REQUIRED = "REQUIRED"
POLICY_OPTIONAL = "OPTIONAL"


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("自动化标定车间 · UI -> WorkshopSessionConfig 对齐版")
        self.resize(1700, 980)

        # 当前界面右侧显示的“整场任务运行状态”只是原型演示状态，不是 ROS 真状态。
        self.session_state = "DRAFT"
        self.current_step_index = -1
        self.steps = []
        self._timers = []

        central = QWidget()
        self.setCentralWidget(central)
        root = QVBoxLayout(central)
        root.setContentsMargins(14, 14, 14, 14)
        root.setSpacing(12)

        title = QLabel("自动化标定车间 · UI 与消息结构对齐原型")
        title.setFont(QFont("", 18, QFont.Bold))
        desc = QLabel(
            "这版界面不只是勾选任务，而是直接生成接近 WorkshopSessionConfig / RequestedCalibrationTask 的数据预览。"
        )
        desc.setWordWrap(True)
        root.addWidget(title)
        root.addWidget(desc)

        main_splitter = QSplitter(Qt.Horizontal)
        root.addWidget(main_splitter, 1)

        left = QWidget()
        right = QWidget()
        main_splitter.addWidget(left)
        main_splitter.addWidget(right)
        main_splitter.setSizes([980, 720])

        left_layout = QVBoxLayout(left)
        right_layout = QVBoxLayout(right)

        # =========================================================
        # 任务基础信息
        # 这些字段不会直接进入 WorkshopSessionConfig，
        # 但它们通常属于 CreateWorkshopSessionRequest 的上层输入。
        # =========================================================
        base_box = QGroupBox("任务基础信息")
        left_layout.addWidget(base_box)
        base_layout = QGridLayout(base_box)

        self.task_name = QLineEdit("AGV_01_完整标定任务")
        self.vehicle_id = QLineEdit("AGV_01")
        self.line_id = QLineEdit("LINE_A")
        self.operator = QLineEdit("张三")
        self.base_link = QLineEdit("base_link")

        base_layout.addWidget(QLabel("任务名称"), 0, 0)
        base_layout.addWidget(self.task_name, 0, 1)
        base_layout.addWidget(QLabel("车辆 ID"), 0, 2)
        base_layout.addWidget(self.vehicle_id, 0, 3)
        base_layout.addWidget(QLabel("产线 / 工位"), 1, 0)
        base_layout.addWidget(self.line_id, 1, 1)
        base_layout.addWidget(QLabel("操作者"), 1, 2)
        base_layout.addWidget(self.operator, 1, 3)
        base_layout.addWidget(QLabel("base_link"), 2, 0)
        base_layout.addWidget(self.base_link, 2, 1)

        # =========================================================
        # 固定步骤输入
        # 这几项会直接进入 WorkshopSessionConfig：
        # - localization_source_id
        # - workcell_zone_id
        # - reference_target_id
        # =========================================================
        fixed_box = QGroupBox("固定步骤输入 · 外部真值校核")
        left_layout.addWidget(fixed_box)
        fixed_layout = QFormLayout(fixed_box)

        self.localization_source = QLineEdit("truth_source_01")
        self.workcell_zone = QLineEdit("zone_A")
        self.reference_target = QLineEdit("board_01")

        fixed_layout.addRow("外部定位源 ID", self.localization_source)
        fixed_layout.addRow("工位 / 区域 ID", self.workcell_zone)
        fixed_layout.addRow("参考目标 ID", self.reference_target)

        # =========================================================
        # 运行策略
        # 这些会直接进入 WorkshopSessionConfig 的布尔开关。
        # =========================================================
        strategy_box = QGroupBox("运行策略（直接对应 WorkshopSessionConfig）")
        left_layout.addWidget(strategy_box)
        strategy_layout = QGridLayout(strategy_box)

        self.auto_commit = QCheckBox("自动写入参数")
        self.require_approval = QCheckBox("写入前要求人工审批")
        self.run_validation = QCheckBox("每阶段后自动复测")
        self.stop_on_first_failure = QCheckBox("首错即停")
        self.allow_optional_skip = QCheckBox("允许可选阶段跳过")
        self.enable_auto_rollback = QCheckBox("验证失败自动回滚")
        self.allow_rebuild_plan = QCheckBox("允许运行前重建计划")

        # 给几个更常见的默认值
        self.stop_on_first_failure.setChecked(True)
        self.allow_optional_skip.setChecked(True)

        checks = [
            self.auto_commit,
            self.require_approval,
            self.run_validation,
            self.stop_on_first_failure,
            self.allow_optional_skip,
            self.enable_auto_rollback,
            self.allow_rebuild_plan,
        ]
        for idx, cb in enumerate(checks):
            strategy_layout.addWidget(cb, idx // 2, idx % 2)

        # =========================================================
        # 任务定义区
        # 这里的所有选择，最终都会变成 requested_tasks[]
        # =========================================================
        tabs = QTabWidget()
        left_layout.addWidget(tabs, 1)

        # -------------------- 底盘标定 --------------------
        chassis_tab = QWidget()
        chassis_layout = QVBoxLayout(chassis_tab)

        type_row = QHBoxLayout()
        type_row.addWidget(QLabel("底盘类型"))
        self.chassis_type = QComboBox()
        self.chassis_type.addItems([
            "ACKERMANN_CHASSIS",
            "DIFFERENTIAL_CHASSIS",
            "SINGLE_STEER_WHEEL_CHASSIS",
            "MULTI_STEER_WHEEL_CHASSIS",
        ])
        self.chassis_type.currentTextChanged.connect(self.rebuild_chassis_tree)
        type_row.addWidget(self.chassis_type)
        type_row.addWidget(QLabel("多舵轮模块 ID"))
        self.multi_module_id = QLineEdit("steering_module_1")
        type_row.addWidget(self.multi_module_id)
        type_row.addStretch(1)
        chassis_layout.addLayout(type_row)

        self.chassis_tree = QTreeWidget()
        self.chassis_tree.setHeaderLabels(["对象", "说明"])
        self.chassis_tree.setColumnWidth(0, 320)
        chassis_layout.addWidget(self.chassis_tree)
        tabs.addTab(chassis_tab, "底盘标定")

        # -------------------- 运控参数 --------------------
        control_tab = QWidget()
        control_layout = QVBoxLayout(control_tab)
        control_tip = QLabel("勾选项最终会变成 RequestedCalibrationTask；need_tuning / is_default 会写入 task_params。")
        control_tip.setWordWrap(True)
        control_layout.addWidget(control_tip)

        self.control_rows = []
        for axis, items in CONTROL_OPTIONS.items():
            box = QGroupBox(axis)
            form = QGridLayout(box)
            row = 0
            for item_id, name in items:
                enable = QCheckBox(name)
                need_tuning = QCheckBox("need_tuning")
                is_default = QCheckBox("is_default")
                form.addWidget(enable, row, 0)
                form.addWidget(need_tuning, row, 1)
                form.addWidget(is_default, row, 2)
                self.control_rows.append((item_id, axis, name, enable, need_tuning, is_default))
                row += 1
            control_layout.addWidget(box)
        tabs.addTab(control_tab, "运控参数标定")

        # -------------------- 传感器标定 --------------------
        sensor_tab = QWidget()
        sensor_layout = QVBoxLayout(sensor_tab)
        sensor_tip = QLabel("每个勾选项会映射成 RequestedCalibrationTask，device 名称会作为 target_id。")
        sensor_tip.setWordWrap(True)
        sensor_layout.addWidget(sensor_tip)

        self.sensor_tree = QTreeWidget()
        self.sensor_tree.setHeaderLabels(["对象", "说明"])
        self.sensor_tree.setColumnWidth(0, 320)
        self.sensor_checks = {}
        self._build_sensor_tree()
        sensor_layout.addWidget(self.sensor_tree)
        tabs.addTab(sensor_tab, "传感器标定")

        # 按钮区
        btn_row = QHBoxLayout()
        self.refresh_btn = QPushButton("刷新预览")
        self.create_btn = QPushButton("创建任务（演示）")
        self.run_btn = QPushButton("运行流程（演示）")
        self.export_btn = QPushButton("导出 JSON")
        self.refresh_btn.clicked.connect(self.refresh_preview)
        self.create_btn.clicked.connect(self.create_session)
        self.run_btn.clicked.connect(self.run_demo)
        self.export_btn.clicked.connect(self.export_json)
        btn_row.addWidget(self.refresh_btn)
        btn_row.addWidget(self.create_btn)
        btn_row.addWidget(self.run_btn)
        btn_row.addWidget(self.export_btn)
        btn_row.addStretch(1)
        left_layout.addLayout(btn_row)

        # =========================================================
        # 右侧：概览 + JSON 预览
        # =========================================================
        overview_box = QGroupBox("当前任务概览")
        right_layout.addWidget(overview_box)
        overview_layout = QVBoxLayout(overview_box)

        state_row = QHBoxLayout()
        self.state_label = QLabel("DRAFT")
        self.selection_count_label = QLabel("当前选择：0 项")
        state_row.addWidget(QLabel("当前状态："))
        state_row.addWidget(self.state_label)
        state_row.addStretch(1)
        state_row.addWidget(self.selection_count_label)
        overview_layout.addLayout(state_row)

        self.progress = QProgressBar()
        overview_layout.addWidget(self.progress)

        self.step_list = QListWidget()
        overview_layout.addWidget(self.step_list, 1)

        bottom_splitter = QSplitter(Qt.Horizontal)
        right_layout.addWidget(bottom_splitter, 1)

        event_box = QGroupBox("运行过程通知")
        json_box = QGroupBox("WorkshopSessionConfig / RequestedCalibrationTask 预览")
        bottom_splitter.addWidget(event_box)
        bottom_splitter.addWidget(json_box)
        bottom_splitter.setSizes([360, 520])

        event_layout = QVBoxLayout(event_box)
        self.event_log = QTextEdit()
        self.event_log.setReadOnly(True)
        self.event_log.setPlainText("系统初始化完成，等待创建标定任务。")
        event_layout.addWidget(self.event_log)

        json_layout = QVBoxLayout(json_box)
        self.json_preview = QTextEdit()
        self.json_preview.setReadOnly(True)
        json_layout.addWidget(self.json_preview)

        self.rebuild_chassis_tree()
        self.refresh_preview()

    # =========================================================
    # 下面开始是“把 UI 选择转换成消息结构”的核心逻辑
    # =========================================================

    def rebuild_chassis_tree(self):
        # 重新构造底盘任务树：
        # 1) 通用参数始终存在；
        # 2) 专属参数按当前底盘类型显示。
        self.chassis_tree.clear()
        self.chassis_checks = {}

        common_item = QTreeWidgetItem(["通用参数", "所有底盘形式都可能会用到"])
        self.chassis_tree.addTopLevelItem(common_item)
        for task_id, name in CHASSIS_COMMON:
            item = QTreeWidgetItem([name, "会映射成 RequestedCalibrationTask"])
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(0, Qt.Unchecked)
            item.setData(0, Qt.UserRole, task_id)
            common_item.addChild(item)
            self.chassis_checks[task_id] = item

        chassis_type = self.chassis_type.currentText()
        specific_item = QTreeWidgetItem([f"{chassis_type} 专属参数", "按当前底盘类型展开"])
        self.chassis_tree.addTopLevelItem(specific_item)
        for task_id, name in CHASSIS_SPECIFIC[chassis_type]:
            item = QTreeWidgetItem([name, "会映射成 RequestedCalibrationTask"])
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(0, Qt.Unchecked)
            item.setData(0, Qt.UserRole, task_id)
            specific_item.addChild(item)
            self.chassis_checks[task_id] = item

        self.chassis_tree.expandAll()
        self.chassis_tree.itemChanged.connect(lambda *_: self.refresh_preview())
        self.refresh_preview()

    def _build_sensor_tree(self):
        # 构造传感器任务树：
        # 分类 -> 设备 -> 具体标定任务
        self.sensor_tree.clear()
        for category, devices in SENSOR_TREE.items():
            cat_item = QTreeWidgetItem([category, "传感器类型"])
            self.sensor_tree.addTopLevelItem(cat_item)
            for target_id, tasks in devices.items():
                device_item = QTreeWidgetItem([target_id, "具体设备 / target_id"])
                cat_item.addChild(device_item)
                for task_code, display_name, stage_type in tasks:
                    task_item = QTreeWidgetItem([display_name, "会映射成 RequestedCalibrationTask"])
                    task_item.setFlags(task_item.flags() | Qt.ItemIsUserCheckable)
                    task_item.setCheckState(0, Qt.Unchecked)
                    task_item.setData(0, Qt.UserRole, (task_code, target_id, stage_type))
                    device_item.addChild(task_item)
                    self.sensor_checks[(task_code, target_id)] = task_item
        self.sensor_tree.expandAll()
        self.sensor_tree.itemChanged.connect(lambda *_: self.refresh_preview())

    def make_task(self, stage_type, task_code, target_id="", task_params=None, enabled=True, require_manual_approval=False,
                  execution_policy=POLICY_REQUIRED, reason=""):
        # 这个函数的作用：
        # 把当前 UI 上的一个具体选择，转换成一个 RequestedCalibrationTask 对象（这里用 dict 预览）。
        return {
            "stage_type": stage_type,
            "enabled": enabled,
            "require_manual_approval": require_manual_approval,
            "execution_policy": execution_policy,
            "reason": reason,
            "task_code": task_code,
            "target_id": target_id,
            "task_params": task_params or [],
        }

    def selected_chassis_tasks(self):
        tasks = []
        chassis_type = self.chassis_type.currentText()
        for task_code, item in self.chassis_checks.items():
            if item.checkState(0) == Qt.Checked:
                # 多舵轮场景下，target_id 直接复用模块 ID 输入；
                # 其它底盘形式先用 chassis_main。
                target_id = self.multi_module_id.text().strip() if chassis_type == "MULTI_STEER_WHEEL_CHASSIS" else "chassis_main"
                params = [{"key": "chassis_type", "value": chassis_type}]
                tasks.append(self.make_task(
                    stage_type=STAGE_CHASSIS,
                    task_code=task_code,
                    target_id=target_id,
                    task_params=params,
                ))
        return tasks

    def selected_control_tasks(self):
        tasks = []
        for task_code, axis, alg_name, enable, need_tuning, is_default in self.control_rows:
            if enable.isChecked():
                params = [
                    {"key": "control_axis", "value": axis},
                    {"key": "algorithm_name", "value": alg_name},
                    {"key": "need_tuning", "value": "true" if need_tuning.isChecked() else "false"},
                    {"key": "is_default", "value": "true" if is_default.isChecked() else "false"},
                ]
                tasks.append(self.make_task(
                    stage_type=STAGE_CONTROL,
                    task_code=task_code,
                    target_id=f"{axis}_controller",
                    task_params=params,
                ))
        return tasks

    def selected_sensor_tasks(self):
        tasks = []
        for (task_code, target_id), item in self.sensor_checks.items():
            if item.checkState(0) == Qt.Checked:
                _, _, stage_type = item.data(0, Qt.UserRole)
                tasks.append(self.make_task(
                    stage_type=stage_type,
                    task_code=task_code,
                    target_id=target_id,
                ))
        return tasks

    def build_config_payload(self):
        # 这里生成的对象，就是当前这版 UI 对齐出来的 WorkshopSessionConfig 预览。
        requested_tasks = []
        requested_tasks.extend(self.selected_chassis_tasks())
        requested_tasks.extend(self.selected_control_tasks())
        requested_tasks.extend(self.selected_sensor_tasks())

        config = {
            "requested_tasks": requested_tasks,
            "auto_commit_parameters": self.auto_commit.isChecked(),
            "require_manual_approval_before_commit": self.require_approval.isChecked(),
            "run_validation_after_each_stage": self.run_validation.isChecked(),
            "stop_on_first_failure": self.stop_on_first_failure.isChecked(),
            "allow_optional_stage_skip": self.allow_optional_skip.isChecked(),
            "enable_auto_rollback_on_validation_failure": self.enable_auto_rollback.isChecked(),
            "allow_rebuild_execution_plan": self.allow_rebuild_plan.isChecked(),
            "localization_source_id": self.localization_source.text().strip(),
            "workcell_zone_id": self.workcell_zone.text().strip(),
            "reference_target_id": self.reference_target.text().strip(),
        }
        return config

    def build_steps(self):
        # 这部分只是为了右侧“执行顺序预览”。
        # 它不是 ROS 消息，而是把 config 里的 requested_tasks 转成用户更容易看懂的步骤列表。
        config = self.build_config_payload()
        steps = [("external_reference", "外部真值校核（固定必做）")]
        for task in config["requested_tasks"]:
            steps.append((task["task_code"], f'{task["stage_type"]} · {task["task_code"]} · {task["target_id"]}'))
        steps.append(("report", "生成最终报告"))
        return steps

    def refresh_preview(self):
        # 每次界面勾选发生变化，都刷新：
        # 1) 当前步骤预览
        # 2) JSON 预览
        self.steps = self.build_steps()
        self.step_list.clear()
        for idx, (_, name) in enumerate(self.steps, start=1):
            self.step_list.addItem(QListWidgetItem(f"{idx}. {name}"))

        selected_count = max(len(self.steps) - 2, 0)
        self.selection_count_label.setText(f"当前选择：{selected_count} 项")

        payload = {
            "create_request_preview": {
                "task_name": self.task_name.text().strip(),
                "vehicle_id": self.vehicle_id.text().strip(),
                "workshop_line_id": self.line_id.text().strip(),
                "operator": self.operator.text().strip(),
                "base_link": self.base_link.text().strip(),
                "config": self.build_config_payload(),
            }
        }
        self.json_preview.setPlainText(json.dumps(payload, ensure_ascii=False, indent=2))

    def log(self, text):
        self.event_log.setPlainText(text + "\n" + self.event_log.toPlainText())

    def create_session(self):
        # 这里只是演示“任务创建成功后的界面反应”，不是真的发 ROS service。
        self.clear_timers()
        self.refresh_preview()
        config = self.build_config_payload()
        if len(config["requested_tasks"]) == 0:
            QMessageBox.warning(self, "提示", "请至少选择一个具体标定项。")
            return

        self.session_state = "READY"
        self.state_label.setText(self.session_state)
        self.progress.setValue(0)
        self.log(f"已创建标定任务：{self.task_name.text().strip()}")
        self.log(f"固定步骤输入：定位源={config['localization_source_id']}，工位={config['workcell_zone_id']}，参考目标={config['reference_target_id']}")
        self.log(f"本次共生成 {len(config['requested_tasks'])} 个 RequestedCalibrationTask。")

    def run_demo(self):
        # 这里只做主流程演示，不接 ROS2。
        self.clear_timers()
        self.refresh_preview()
        config = self.build_config_payload()
        if len(config["requested_tasks"]) == 0:
            QMessageBox.warning(self, "提示", "请至少选择一个具体标定项。")
            return

        self.session_state = "PRECHECK_RUNNING"
        self.state_label.setText(self.session_state)
        self.progress.setValue(0)
        self.log("开始预检：检查固定步骤输入是否完整、以及 requested_tasks 是否非空。")

        t = QTimer(self)
        t.setSingleShot(True)
        t.timeout.connect(self._after_precheck)
        t.start(700)
        self._timers.append(t)

    def _after_precheck(self):
        self.session_state = "RUNNING"
        self.state_label.setText(self.session_state)
        self.log("预检通过，开始正式执行。")

        for index, (_, name) in enumerate(self.steps):
            t = QTimer(self)
            t.setSingleShot(True)
            t.timeout.connect(lambda idx=index, nm=name: self._run_step(idx, nm))
            t.start(850 * (index + 1))
            self._timers.append(t)

        end_t = QTimer(self)
        end_t.setSingleShot(True)
        end_t.timeout.connect(self._finish_run)
        end_t.start(850 * (len(self.steps) + 1))
        self._timers.append(end_t)

    def _run_step(self, index, name):
        # 运行过程演示：让右侧步骤列表看起来像在逐步推进。
        self.current_step_index = index
        self.log(f"开始执行：{name}")
        for i in range(self.step_list.count()):
            original = self.steps[i][1]
            prefix = "▶ " if i == index else ("✓ " if i < index else "○ ")
            self.step_list.item(i).setText(f"{prefix}{original}")
        self.progress.setValue(int(index / max(len(self.steps), 1) * 100))

    def _finish_run(self):
        # 演示整场任务完成后的界面变化。
        self.current_step_index = -1
        self.session_state = "SUCCEEDED"
        self.state_label.setText(self.session_state)
        self.progress.setValue(100)
        self.log("整场标定执行完成。")
        for i in range(self.step_list.count()):
            self.step_list.item(i).setText(f"✓ {self.steps[i][1]}")

    def export_json(self):
        # 导出当前 JSON 预览，方便你拿去和后端 / ROS 接口一起对字段。
        payload_text = self.json_preview.toPlainText().strip()
        if not payload_text:
            QMessageBox.warning(self, "提示", "当前没有可导出的内容。")
            return
        path, _ = QFileDialog.getSaveFileName(self, "导出 JSON", "workshop_session_config_preview.json", "JSON Files (*.json)")
        if not path:
            return
        with open(path, "w", encoding="utf-8") as f:
            f.write(payload_text)
        QMessageBox.information(self, "完成", f"已导出到：{path}")

    def clear_timers(self):
        for t in self._timers:
            t.stop()
            t.deleteLater()
        self._timers.clear()


if __name__ == "__main__":
    app = QApplication([])

    # 让终端里的 Ctrl+C 可以退出 Qt 程序。
    signal.signal(signal.SIGINT, lambda *args: app.quit())

    # 让 Python 有机会处理 SIGINT。
    sigint_timer = QTimer()
    sigint_timer.start(200)
    sigint_timer.timeout.connect(lambda: None)

    w = MainWindow()
    w.show()
    app.exec()
