base_agent/uas/task/main.py

810 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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'状态: <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()