# -*- coding: utf-8 -*- """ Task 模块完整演示 ───────────────────────────────────────────────────────────────── 运行: python main.py 依赖: pip install PyQt5 ───────────────────────────────────────────────────────────────── """ import logging import random import sys import time as _time from PyQt5.QtCore import Qt, QTimer, pyqtSlot from PyQt5.QtGui import QColor, QFont from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QTableWidget, QTableWidgetItem, QHeaderView, QGroupBox, QLabel, QPushButton, QTextEdit, QProgressBar, QStatusBar, QTabWidget, QTreeWidget, QTreeWidgetItem ) from uas.action.action_executor import action_executor # ── Action 模块 ──────────────────────────────────────────────────── from uas.action.action_type import ActionType from uas.action.registry.action_registry import action_factory # ── Event 模块 ───────────────────────────────────────────────────── from uas.event.event_bus import event_bus from uas.event.handlers.device_handlers import ( DeviceErrorHandler, DeviceTimeoutHandler, EquipmentFaultHandler ) from uas.route.base_waypoint import AirWaypoint as Waypoint # ── 航线模块 ─────────────────────────────────────────────────────── from uas.route.routes.air_route import FlightRoute from uas.task.task_action_plan import TriggerMoment from uas.task.task_executor import task_executor from uas.task.task_model import Task, TaskState, TaskArea # ── Task 模块 ────────────────────────────────────────────────────── from uas.task.task_type import TaskType, TaskPriority, task_type_registry logging.basicConfig( level = logging.INFO, format = "%(asctime)s [%(levelname)-8s] %(name)s - %(message)s", ) logger = logging.getLogger("main") # ───────────────────────────────────────────────────────────────── # 构建示例航线(北京→上海,加速时间轴用于演示) # ───────────────────────────────────────────────────────────────── def build_demo_route() -> FlightRoute: """ 构建北京→上海示例航线 为便于演示,将速度调大使总时长压缩到约 60s """ route = FlightRoute(start_time=0.0, default_turn_radius=2000.0) waypoints = [ Waypoint(identifier="ZBAA", latitude=40.0799, longitude=116.6031, altitude=500, speed=2000.0), Waypoint(identifier="WP01", latitude=39.5, longitude=117.5, altitude=8000, speed=2200.0), Waypoint(identifier="WP02", latitude=37.0, longitude=118.8, altitude=10000, speed=2400.0), Waypoint(identifier="WP03", latitude=34.0, longitude=120.0, altitude=8000, speed=2200.0), Waypoint(identifier="ZSPD", latitude=31.1443, longitude=121.8083, altitude=500, speed=1400.0), ] route.add_waypoints(waypoints) return route # ───────────────────────────────────────────────────────────────── # 构建示例任务 # ───────────────────────────────────────────────────────────────── def build_demo_task(route: FlightRoute) -> Task: """ 构建照相侦察任务: 航线级 Actions(整条航线): ├─ [串行 order=0] 系统自检 ON_ENTER ├─ [串行 order=1] 设备校准 ON_ENTER └─ [并行] 系统诊断 CONTINUOUS 每30s 航段[0] ZBAA→WP01(爬升段): ├─ [并行] 拍照 ON_ENTER └─ [串行 order=0] 系统诊断 ON_ENTER 航段[1] WP01→WP02(巡航段): ├─ [并行] 目标识别 ON_ENTER ├─ [并行] 环境扫描 ON_ENTER ├─ [串行 order=0] 红外扫描 SCHEDULED 偏移5s └─ [串行 order=1] 拍照 SCHEDULED 偏移10s 航段[2] WP02→WP03(下降段): ├─ [并行] 目标识别 ON_ENTER └─ [并行] 持续拍照 CONTINUOUS 每8s 航段[3] WP03→ZSPD(进近段): ├─ [串行 order=0] 系统诊断 ON_ENTER └─ [串行 order=1] 暂停 ON_EXIT """ area = TaskArea.from_polygon( points = [(40.0, 116.0), (40.0, 122.0), (31.0, 122.0), (31.0, 116.0)], name = "华东侦察区", alt_min = 500, alt_max = 12000, ) task = Task( name = "华东侦察任务-001", task_type = TaskType.RECON_PHOTO, description = "北京至上海沿线照相侦察任务", target_area = area, priority = TaskPriority.HIGH, tags = ["reconnaissance", "photo", "east-china"], ) # 配置航线 task.set_route(route) # ── 航线级 Actions ──────────────────────────────────────────── task.add_route_action( action_factory.create(ActionType.SYSTEM_SELF_CHECK, source="system"), order=0, trigger=TriggerMoment.ON_ENTER, description="起飞前系统自检(串行第1步)", ) task.add_route_action( action_factory.create(ActionType.SYSTEM_CALIBRATE, source="system", sensors=["imu", "gps"]), order=1, trigger=TriggerMoment.ON_ENTER, description="起飞前设备校准(串行第2步)", ) task.add_route_action( action_factory.create(ActionType.SYSTEM_DIAGNOSE, source="system"), order=None, trigger=TriggerMoment.CONTINUOUS, interval_s=30.0, description="全程系统诊断(并行,每30s)", ) # ── 航段[0] ZBAA→WP01 爬升段 ───────────────────────────────── task.add_segment_action( 0, action_factory.create(ActionType.CAPTURE_PHOTO, source="camera-01", resolution="4K"), order=None, trigger=TriggerMoment.ON_ENTER, description="爬升段入口拍照(并行)", ) task.add_segment_action( 0, action_factory.create(ActionType.SYSTEM_DIAGNOSE, source="system"), order=0, trigger=TriggerMoment.ON_ENTER, description="爬升段系统诊断(串行)", ) # ── 航段[1] WP01→WP02 巡航段 ───────────────────────────────── task.add_segment_action( 1, action_factory.create(ActionType.RECOGNIZE_TARGET, source="sensor-01", model="yolov8"), order=None, trigger=TriggerMoment.ON_ENTER, description="巡航段目标识别(并行)", ) task.add_segment_action( 1, action_factory.create(ActionType.SCAN_ENVIRONMENT, source="lidar-01", range_m=150), order=None, trigger=TriggerMoment.ON_ENTER, description="巡航段环境扫描(并行)", ) task.add_segment_action( 1, action_factory.create("perception.thermal_scan", source="ir-01"), order=0, trigger=TriggerMoment.SCHEDULED, time_offset_s=5.0, description="巡航段红外扫描(串行第1步,5s后)", ) task.add_segment_action( 1, action_factory.create(ActionType.CAPTURE_PHOTO, source="camera-01", resolution="1080P"), order=1, trigger=TriggerMoment.SCHEDULED, time_offset_s=10.0, description="巡航段拍照(串行第2步,10s后)", ) # ── 航段[2] WP02→WP03 下降段 ───────────────────────────────── task.add_segment_action( 2, action_factory.create(ActionType.RECOGNIZE_TARGET, source="sensor-01", model="yolov8"), order=None, trigger=TriggerMoment.ON_ENTER, description="下降段目标识别(并行)", ) task.add_segment_action( 2, action_factory.create(ActionType.CAPTURE_PHOTO, source="camera-02", resolution="4K"), order=None, trigger=TriggerMoment.CONTINUOUS, interval_s=8.0, description="下降段持续拍照(并行,每8s)", ) # ── 航段[3] WP03→ZSPD 进近段 ───────────────────────────────── task.add_segment_action( 3, action_factory.create(ActionType.SYSTEM_DIAGNOSE, source="system"), order=0, trigger=TriggerMoment.ON_ENTER, description="进近段系统诊断(串行第1步)", ) task.add_segment_action( 3, action_factory.create(ActionType.PAUSE, source="device-001"), order=1, trigger=TriggerMoment.ON_EXIT, description="进近段结束暂停(串行第2步)", ) return task # ───────────────────────────────────────────────────────────────── # 主窗口 # ───────────────────────────────────────────────────────────────── class TaskMonitorWindow(QMainWindow): STATE_COLORS = { TaskState.RUNNING: "#2196F3", TaskState.COMPLETED: "#4CAF50", TaskState.FAILED: "#f44336", TaskState.CANCELLED: "#FF9800", TaskState.PAUSED: "#9E9E9E", TaskState.READY: "#607D8B", TaskState.PLANNED: "#795548", TaskState.CREATED: "#455A64", } def __init__(self): super().__init__() self.setWindowTitle("🛰️ Task 监控系统 — PyQt5 演示") self.resize(1400, 860) self._current_task = None self._action_count = 0 self._event_count = 0 self._setup_modules() self._setup_ui() self._auto_demo() # ── 模块初始化 ──────────────────────────────────────────────── def _setup_modules(self): # EventBus 处理器 event_bus.register_handler(DeviceErrorHandler()) event_bus.register_handler(DeviceTimeoutHandler()) event_bus.register_handler(EquipmentFaultHandler()) event_bus.scan_decorators() # 注入依赖链:EventBus → ActionExecutor → TaskExecutor action_executor.set_event_bus(event_bus) task_executor.set_action_executor(action_executor) # 注册自定义 Action 类型(热成像) from uas.action.action_context import ActionContext from uas.action.action_model import Action from uas.event.event_type import EventType from uas.event.event_model import make_event task_type_registry.register( "perception.thermal_scan", category="perception", description="红外热成像扫描" ) def handle_thermal(action: Action, ctx: ActionContext): import time ctx.report_progress(30, "预热红外传感器...") time.sleep(0.05) ctx.check_cancel() ctx.report_progress(80, "采集热成像数据...") time.sleep(0.05) ctx.emit_event(make_event( EventType.DEVICE_DATA_RECEIVED, source = action.source, message = "热成像采集完成", max_temp = round(random.uniform(20, 80), 1), min_temp = round(random.uniform(-10, 20), 1), )) ctx.report_progress(100, "热成像完成") return {"thermal": "ok"} action_factory.register("perception.thermal_scan", handle_thermal) # ── 连接 TaskExecutor 信号 ──────────────────────────────── task_executor.task_submitted.connect(self._on_task_submitted) task_executor.task_started.connect(self._on_task_started) task_executor.task_completed.connect(self._on_task_completed) task_executor.task_failed.connect(self._on_task_failed) task_executor.task_cancelled.connect(self._on_task_cancelled) task_executor.task_paused.connect(self._on_task_paused) task_executor.task_resumed.connect(self._on_task_resumed) task_executor.segment_entered.connect(self._on_segment_entered) task_executor.segment_exited.connect(self._on_segment_exited) # ── 连接 ActionExecutor 信号 ────────────────────────────── action_executor.action_completed.connect(self._on_action_completed) action_executor.action_progress.connect(self._on_action_progress) action_executor.events_produced.connect(self._on_events_produced) # ── UI 构建 ─────────────────────────────────────────────────── def _setup_ui(self): central = QWidget() self.setCentralWidget(central) root = QVBoxLayout(central) root.setContentsMargins(6, 6, 6, 6) splitter = QSplitter(Qt.Horizontal) # ── 左侧:信息面板 ──────────────────────────────────────── left = QWidget() left.setFixedWidth(330) ll = QVBoxLayout(left) # 统计卡片 stats_box = QGroupBox("📊 实时统计") sl = QHBoxLayout(stats_box) self._lbl_tasks = self._card("任务数", "0", "#2196F3") self._lbl_actions = self._card("Action", "0", "#4CAF50") self._lbl_events = self._card("Event", "0", "#9C27B0") for w in [self._lbl_tasks, self._lbl_actions, self._lbl_events]: sl.addWidget(w) ll.addWidget(stats_box) # 当前任务信息 info_box = QGroupBox("📋 当前任务") il = QVBoxLayout(info_box) self._task_name_lbl = QLabel("—") self._task_name_lbl.setStyleSheet( "font-weight:bold; font-size:13px; color:#569cd6;" ) self._task_type_lbl = QLabel("类型: —") self._task_state_lbl = QLabel("状态: —") self._task_prio_lbl = QLabel("优先级: —") self._task_seg_lbl = QLabel("当前航段: —") self._task_area_lbl = QLabel("目标区域: —") self._task_elapsed = QLabel("耗时: —") for w in [self._task_name_lbl, self._task_type_lbl, self._task_state_lbl, self._task_prio_lbl, self._task_seg_lbl, self._task_area_lbl, self._task_elapsed]: il.addWidget(w) ll.addWidget(info_box) # Action 进度 prog_box = QGroupBox("⚙️ Action 进度") pl = QVBoxLayout(prog_box) self._prog_lbl = QLabel("等待任务...") self._prog_lbl.setStyleSheet("color:gray; font-size:11px;") self._prog_bar = QProgressBar() self._prog_bar.setRange(0, 100) self._prog_bar.setValue(0) pl.addWidget(self._prog_lbl) pl.addWidget(self._prog_bar) ll.addWidget(prog_box) # 控制按钮 ctrl_box = QGroupBox("🎮 任务控制") cl = QVBoxLayout(ctrl_box) btn_defs = [ ("🚀 启动侦察任务", self._start_recon, "#4CAF50"), ("⏸ 暂停任务", self._pause_task, "#607D8B"), ("▶️ 恢复任务", self._resume_task, "#2196F3"), ("🚫 取消任务", self._cancel_task, "#f44336"), ] for text, slot, color in btn_defs: btn = QPushButton(text) btn.setMinimumHeight(34) btn.setStyleSheet( f"QPushButton{{background:{color};color:white;" f"border-radius:4px;font-weight:bold;font-size:12px;}}" f"QPushButton:hover{{opacity:0.85;}}" ) btn.clicked.connect(slot) cl.addWidget(btn) ll.addWidget(ctrl_box) ll.addStretch() splitter.addWidget(left) # ── 右侧:监控面板 ──────────────────────────────────────── right = QWidget() rl = QVBoxLayout(right) tabs = QTabWidget() # Tab1: Action 计划树 tree_tab = QWidget() tree_layout = QVBoxLayout(tree_tab) self._action_tree = QTreeWidget() self._action_tree.setHeaderLabels( ["Action / 航段", "Action类型", "执行策略", "触发时机", "描述"] ) hdr = self._action_tree.header() hdr.setSectionResizeMode(0, QHeaderView.Stretch) hdr.setSectionResizeMode(1, QHeaderView.ResizeToContents) hdr.setSectionResizeMode(2, QHeaderView.ResizeToContents) hdr.setSectionResizeMode(3, QHeaderView.ResizeToContents) hdr.setSectionResizeMode(4, QHeaderView.Stretch) self._action_tree.setAlternatingRowColors(True) tree_layout.addWidget(self._action_tree) tabs.addTab(tree_tab, "🗂️ Action 计划树") # Tab2: 执行日志 log_tab = QWidget() log_layout = QVBoxLayout(log_tab) self._log_edit = QTextEdit() self._log_edit.setReadOnly(True) self._log_edit.setFont(QFont("Consolas", 10)) self._log_edit.setStyleSheet( "background:#1e1e1e; color:#d4d4d4;" "border-radius:4px; padding:4px;" ) log_layout.addWidget(self._log_edit) tabs.addTab(log_tab, "📝 执行日志") # Tab3: 执行记录表 rec_tab = QWidget() rec_layout = QVBoxLayout(rec_tab) self._rec_table = QTableWidget(0, 4) self._rec_table.setHorizontalHeaderLabels( ["时间", "类型", "描述", "数据"] ) self._rec_table.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch ) self._rec_table.setAlternatingRowColors(True) self._rec_table.setEditTriggers(QTableWidget.NoEditTriggers) self._rec_table.setSelectionBehavior(QTableWidget.SelectRows) rec_layout.addWidget(self._rec_table) tabs.addTab(rec_tab, "📋 执行记录") rl.addWidget(tabs) splitter.addWidget(right) root.addWidget(splitter) # 状态栏 self._statusbar = QStatusBar() self.setStatusBar(self._statusbar) self._statusbar.showMessage( "就绪 | TaskExecutor + ActionExecutor + EventBus 运行中" ) # 周期刷新定时器(刷新任务信息面板) self._refresh_timer = QTimer(self) self._refresh_timer.timeout.connect(self._refresh_task_info) self._refresh_timer.start(500) # ── Action 计划树构建 ────────────────────────────────────────── def _build_action_tree(self, task: Task): """根据 Task 的 RouteActionPlan 构建可视化树""" self._action_tree.clear() plan = task.action_plan # ── 航线级节点 ──────────────────────────────────────────── route_node = QTreeWidgetItem( ["🛣️ 航线级 Actions", "", "", "", ""] ) route_node.setForeground(0, QColor("#569cd6")) self._action_tree.addTopLevelItem(route_node) for entry in plan.route_entries: policy = (f"🔗 串行[{entry.order}]" if entry.is_sequential else "⚡ 并行") child = QTreeWidgetItem([ f" {entry.action.type_key}", entry.action.type_key, policy, entry.trigger.value, entry.description, ]) child.setForeground(1, QColor("#4ec9b0")) child.setForeground(2, QColor( "#FF9800" if entry.is_sequential else "#4CAF50" )) route_node.addChild(child) # ── 各航段节点 ──────────────────────────────────────────── if task.has_route: for idx, seg in enumerate(task.route.segments): seg_plan = plan.segment_plans.get(idx) n_par = len(seg_plan.parallel_entries) if seg_plan else 0 n_seq = len(seg_plan.sequential_entries) if seg_plan else 0 seg_label = ( f"✈️ 航段[{idx}] " f"{seg.wp_from.identifier} → {seg.wp_to.identifier} " f"({seg.distance/1000:.1f}km / {seg.duration:.0f}s) " f"[并行:{n_par} 串行:{n_seq}]" ) seg_node = QTreeWidgetItem([seg_label, "", "", "", ""]) seg_node.setForeground(0, QColor("#dcdcaa")) self._action_tree.addTopLevelItem(seg_node) if seg_plan and seg_plan.entries: # 并行组 if seg_plan.parallel_entries: par_node = QTreeWidgetItem( [" ⚡ 并行组", "", "", "", ""] ) par_node.setForeground(0, QColor("#4CAF50")) seg_node.addChild(par_node) for entry in seg_plan.parallel_entries: child = QTreeWidgetItem([ f" {entry.action.type_key}", entry.action.type_key, "⚡ 并行", entry.trigger.value, entry.description, ]) child.setForeground(1, QColor("#4ec9b0")) child.setForeground(2, QColor("#4CAF50")) par_node.addChild(child) # 串行组 if seg_plan.sequential_entries: seq_node = QTreeWidgetItem( [" 🔗 串行组", "", "", "", ""] ) seq_node.setForeground(0, QColor("#FF9800")) seg_node.addChild(seq_node) for entry in seg_plan.sequential_entries: child = QTreeWidgetItem([ f" [{entry.order}] {entry.action.type_key}", entry.action.type_key, f"🔗 串行[{entry.order}]", entry.trigger.value, entry.description, ]) child.setForeground(1, QColor("#4ec9b0")) child.setForeground(2, QColor("#FF9800")) seq_node.addChild(child) else: empty = QTreeWidgetItem( [" (无 Action 配置)", "", "", "", ""] ) empty.setForeground(0, QColor("#555555")) seg_node.addChild(empty) self._action_tree.expandAll() # ── 任务控制槽 ──────────────────────────────────────────────── @pyqtSlot() def _start_recon(self): if (self._current_task and self._current_task.is_running): self._log("⚠️ 已有任务正在执行,请先取消", "#FF9800") return route = build_demo_route() task = build_demo_task(route) self._current_task = task self._build_action_tree(task) self._rec_table.setRowCount(0) task_executor.submit(task) self._log( f"🚀 提交任务: {task.name} [{task.task_id[:8]}]", "#569cd6" ) @pyqtSlot() def _pause_task(self): if self._current_task: ok = task_executor.pause(self._current_task.task_id) if not ok: self._log("⚠️ 暂停失败(任务未在运行中)", "#FF9800") @pyqtSlot() def _resume_task(self): if self._current_task: ok = task_executor.resume(self._current_task.task_id) if not ok: self._log("⚠️ 恢复失败(任务未在暂停状态)", "#FF9800") @pyqtSlot() def _cancel_task(self): if self._current_task: task_executor.cancel(self._current_task.task_id) # ── TaskExecutor 信号槽 ─────────────────────────────────────── @pyqtSlot(object) def _on_task_submitted(self, task): self._log( f"📥 任务提交: {task.name} [{task.task_id[:8]}]", "#569cd6" ) self._lbl_tasks.setText(self._card_html( len(task_executor.get_running_tasks()) + 1, "#2196F3", "任务数" )) @pyqtSlot(object) def _on_task_started(self, task): self._log(f"▶ 任务开始执行: {task.name}", "#4ec9b0") self._statusbar.showMessage(f"▶ 执行中: {task.name}") self._add_rec_row("state_change", "任务开始执行", "") @pyqtSlot(object) def _on_task_completed(self, task): self._log( f"✅ 任务完成: {task.name} 耗时={task.elapsed_s:.1f}s", "#4CAF50" ) self._add_rec_row( "completed", f"任务完成,耗时 {task.elapsed_s:.1f}s", f"actions={self._action_count} events={self._event_count}", ) self._statusbar.showMessage( f"✅ 完成: {task.name} 耗时={task.elapsed_s:.1f}s" ) @pyqtSlot(object, str) def _on_task_failed(self, task, error): self._log(f"❌ 任务失败: {task.name} {error}", "#f44747") self._add_rec_row("failed", f"任务失败: {error}", "") self._statusbar.showMessage(f"❌ 失败: {task.name}") @pyqtSlot(object) def _on_task_cancelled(self, task): self._log(f"🚫 任务取消: {task.name}", "#FF9800") self._add_rec_row("cancelled", "任务已取消", "") self._statusbar.showMessage(f"🚫 取消: {task.name}") @pyqtSlot(object) def _on_task_paused(self, task): self._log(f"⏸ 任务暂停: {task.name}", "#9E9E9E") self._add_rec_row("state_change", "任务已暂停", "") @pyqtSlot(object) def _on_task_resumed(self, task): self._log(f"▶ 任务恢复: {task.name}", "#4ec9b0") self._add_rec_row("state_change", "任务已恢复", "") @pyqtSlot(object, int) def _on_segment_entered(self, task, seg_index): if task.has_route and seg_index < len(task.route.segments): seg = task.route.segments[seg_index] msg = (f"进入航段[{seg_index}] " f"{seg.wp_from.identifier}→{seg.wp_to.identifier} " f"({seg.distance/1000:.1f}km)") else: msg = f"进入航段[{seg_index}]" self._log(f"✈️ {msg}", "#dcdcaa") self._add_rec_row("segment", msg, f"seg_index={seg_index}") @pyqtSlot(object, int) def _on_segment_exited(self, task, seg_index): self._log(f"🏁 离开航段[{seg_index}]", "#808080") # ── ActionExecutor 信号槽 ───────────────────────────────────── @pyqtSlot(object) def _on_action_completed(self, action): self._action_count += 1 self._lbl_actions.setText( self._card_html(self._action_count, "#4CAF50", "Action") ) self._log( f" ✅ Action完成: {action.type_key} " f"src={action.source} {action.elapsed_ms:.0f}ms", "#6a9955", ) self._add_rec_row( "action_done", f"Action完成: {action.type_key}", f"src={action.source} elapsed={action.elapsed_ms:.0f}ms", ) @pyqtSlot(str, int, str) def _on_action_progress(self, action_id: str, percent: int, message: str): self._prog_bar.setValue(percent) self._prog_lbl.setText(f"[{action_id[:8]}] {message}") @pyqtSlot(object, list) def _on_events_produced(self, action, events): self._event_count += len(events) self._lbl_events.setText( self._card_html(self._event_count, "#9C27B0", "Event") ) for ev in events: self._log( f" ⚡ Event产生: {ev.type_key} src={ev.source}", "#c586c0", ) # ── 周期刷新 ────────────────────────────────────────────────── @pyqtSlot() def _refresh_task_info(self): """每500ms刷新左侧任务信息面板""" task = self._current_task if not task: return self._task_name_lbl.setText(task.name) self._task_type_lbl.setText(f"类型: {task.type_key}") state_color = self.STATE_COLORS.get(task.state, "#ffffff") self._task_state_lbl.setText( f'状态: ' f'{task.state.value}' ) self._task_state_lbl.setTextFormat(Qt.RichText) self._task_prio_lbl.setText(f"优先级: {task.priority.name}") self._task_elapsed.setText(f"耗时: {task.elapsed_s:.1f}s") if task.target_area: self._task_area_lbl.setText( f"目标区域: {task.target_area.name}" ) # 当前航段 if task.has_route: cursor = task_executor._seg_cursor.get(task.task_id, -1) n_segs = len(task.route.segments) if 0 <= cursor < n_segs: seg = task.route.segments[cursor] self._task_seg_lbl.setText( f"当前航段: [{cursor}] " f"{seg.wp_from.identifier}→{seg.wp_to.identifier}" ) elif cursor >= n_segs: self._task_seg_lbl.setText("当前航段: 已完成所有航段") else: self._task_seg_lbl.setText("当前航段: 等待进入") # ── 工具方法 ────────────────────────────────────────────────── def _log(self, text: str, color: str = "#d4d4d4"): ts = _time.strftime("%H:%M:%S") self._log_edit.append( f'[{ts}] ' f'{text}' ) sb = self._log_edit.verticalScrollBar() sb.setValue(sb.maximum()) def _add_rec_row(self, rec_type: str, description: str, data: str): ts = _time.strftime("%H:%M:%S") row = self._rec_table.rowCount() self._rec_table.insertRow(row) self._rec_table.setItem(row, 0, QTableWidgetItem(ts)) type_item = QTableWidgetItem(rec_type) color_map = { "completed": "#4CAF50", "failed": "#f44336", "cancelled": "#FF9800", "state_change": "#2196F3", "segment": "#dcdcaa", "action_done": "#6a9955", } type_item.setForeground(QColor(color_map.get(rec_type, "#d4d4d4"))) self._rec_table.setItem(row, 1, type_item) self._rec_table.setItem(row, 2, QTableWidgetItem(description)) self._rec_table.setItem(row, 3, QTableWidgetItem(data)) self._rec_table.scrollToBottom() def _card(self, title: str, value: str, color: str) -> QLabel: lbl = QLabel(self._card_html(int(value), color, title)) lbl.setAlignment(Qt.AlignCenter) lbl.setMinimumWidth(80) return lbl @staticmethod def _card_html(value: int, color: str, title: str) -> str: return (f"