base_agent/uas/task/main.py

810 lines
34 KiB
Python
Raw Normal View History

2026-06-18 03:28:14 +00:00
# -*- 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] ZBAAWP01爬升段
[并行] 拍照 ON_ENTER
[串行 order=0] 系统诊断 ON_ENTER
航段[1] WP01WP02巡航段
[并行] 目标识别 ON_ENTER
[并行] 环境扫描 ON_ENTER
[串行 order=0] 红外扫描 SCHEDULED 偏移5s
[串行 order=1] 拍照 SCHEDULED 偏移10s
航段[2] WP02WP03下降段
[并行] 目标识别 ON_ENTER
[并行] 持续拍照 CONTINUOUS 每8s
航段[3] WP03ZSPD进近段
[串行 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'状态: <span style="color:{state_color}; font-weight:bold;">'
f'{task.state.value}</span>'
)
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'<span style="color:#555555;">[{ts}]</span> '
f'<span style="color:{color};">{text}</span>'
)
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"<center>"
f"<b style='font-size:20px;color:{color};'>{value}</b>"
f"<br><small style='color:gray;'>{title}</small>"
f"</center>")
# ── 自动演示 ──────────────────────────────────────────────────
def _auto_demo(self):
"""启动后 1s 自动开始演示任务"""
QTimer.singleShot(1000, self._start_recon)
def closeEvent(self, event):
if self._current_task:
task_executor.cancel(self._current_task.task_id)
action_executor.wait_for_done(2000)
event.accept()
# ─────────────────────────────────────────────────────────────────
# 入口
# ─────────────────────────────────────────────────────────────────
def main():
app = QApplication(sys.argv)
app.setStyle("Fusion")
win = TaskMonitorWindow()
win.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()