810 lines
34 KiB
Python
810 lines
34 KiB
Python
# -*- 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() |