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()
|