860 lines
35 KiB
Python
860 lines
35 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
Task 执行器
|
|||
|
|
─────────────────────────────────────────────────────────────────
|
|||
|
|
基于 PyQt5 QObject/pyqtSignal + QThreadPool 实现:
|
|||
|
|
|
|||
|
|
核心调度逻辑:
|
|||
|
|
1. 任务启动时,按时间轴扫描航线各段
|
|||
|
|
2. 进入航段时:
|
|||
|
|
a. 并行组 → 同时提交所有 Action 到 ActionExecutor
|
|||
|
|
b. 串行组 → 按 order 顺序依次提交,等待上一个完成再提交下一个
|
|||
|
|
c. 串/并行组的执行顺序由 serial_first 控制
|
|||
|
|
3. 航线级 Action 在整条航线 ON_ENTER/ON_EXIT 时触发
|
|||
|
|
4. CONTINUOUS 类型 Action 由 QTimer 周期触发
|
|||
|
|
5. SCHEDULED 类型 Action 由 QTimer.singleShot 在指定偏移时刻触发
|
|||
|
|
─────────────────────────────────────────────────────────────────
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import functools
|
|||
|
|
import logging
|
|||
|
|
import threading
|
|||
|
|
import time
|
|||
|
|
from typing import Dict, List, Optional
|
|||
|
|
|
|||
|
|
from PyQt5.QtCore import (
|
|||
|
|
QObject, QRunnable, QThreadPool,
|
|||
|
|
pyqtSignal, pyqtSlot, QTimer
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
from uas.event.event_handler import on_event
|
|||
|
|
from uas.event.event_model import Event
|
|||
|
|
from uas.event.event_type import EventType
|
|||
|
|
from uas.task.task_action_plan import (
|
|||
|
|
ActionEntry, SegmentActionPlan, RouteActionPlan,
|
|||
|
|
TriggerMoment
|
|||
|
|
)
|
|||
|
|
from uas.task.task_model import Task, TaskState
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 串行 Action 链执行器(在工作线程中顺序执行)
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class _SerialChainSignals(QObject):
|
|||
|
|
finished = pyqtSignal(str, list) # task_id, [ActionResult]
|
|||
|
|
error = pyqtSignal(str, str) # task_id, error_msg
|
|||
|
|
|
|||
|
|
|
|||
|
|
class _SerialChainWorker(QRunnable):
|
|||
|
|
"""
|
|||
|
|
在 QThreadPool 工作线程中串行执行一组 ActionEntry
|
|||
|
|
每个 Action 完成后才执行下一个(严格顺序保证)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
task_id: str,
|
|||
|
|
entries: List[ActionEntry],
|
|||
|
|
executor, # ActionExecutor 引用
|
|||
|
|
label: str = "", # 调试标签(如 "route" / "seg[0]")
|
|||
|
|
):
|
|||
|
|
super().__init__()
|
|||
|
|
self.task_id = task_id
|
|||
|
|
self.entries = entries # 已按 order 排序
|
|||
|
|
self.executor = executor
|
|||
|
|
self.label = label
|
|||
|
|
self.signals = _SerialChainSignals()
|
|||
|
|
self.setAutoDelete(True)
|
|||
|
|
|
|||
|
|
@pyqtSlot()
|
|||
|
|
def run(self):
|
|||
|
|
results = []
|
|||
|
|
for entry in self.entries:
|
|||
|
|
if not entry.enabled:
|
|||
|
|
continue
|
|||
|
|
try:
|
|||
|
|
logger.debug(
|
|||
|
|
f"[串行/{self.label}] 执行 Action: "
|
|||
|
|
f"{entry.action.type_key} order={entry.order}"
|
|||
|
|
)
|
|||
|
|
# submit_sync 阻塞当前工作线程,保证严格串行
|
|||
|
|
result = self.executor.submit_sync(entry.action)
|
|||
|
|
results.append(result)
|
|||
|
|
logger.debug(
|
|||
|
|
f"[串行/{self.label}] 完成 Action: "
|
|||
|
|
f"{entry.action.type_key} success={result.success}"
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(
|
|||
|
|
f"[串行/{self.label}] Action 执行异常: "
|
|||
|
|
f"{entry.action.type_key} → {e}",
|
|||
|
|
exc_info=True,
|
|||
|
|
)
|
|||
|
|
self.signals.error.emit(self.task_id, str(e))
|
|||
|
|
return # 串行链中断
|
|||
|
|
|
|||
|
|
self.signals.finished.emit(self.task_id, results)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 航段调度器(管理单个航段的 Action 触发)
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class _SegmentScheduler(QObject):
|
|||
|
|
"""
|
|||
|
|
管理单个航段的 Action 调度
|
|||
|
|
负责:ON_ENTER 触发、CONTINUOUS 定时器、SCHEDULED 单次定时器、ON_EXIT 触发
|
|||
|
|
继承 QObject 以便在内部安全使用 QTimer
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
serial_done = pyqtSignal(str, int, list) # task_id, seg_index, results
|
|||
|
|
serial_error = pyqtSignal(str, int, str) # task_id, seg_index, error
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
task: Task,
|
|||
|
|
seg_index: int,
|
|||
|
|
plan: SegmentActionPlan,
|
|||
|
|
action_executor,
|
|||
|
|
thread_pool: QThreadPool,
|
|||
|
|
parent: QObject = None,
|
|||
|
|
):
|
|||
|
|
super().__init__(parent)
|
|||
|
|
self.task = task
|
|||
|
|
self.seg_index = seg_index
|
|||
|
|
self.plan = plan
|
|||
|
|
self.action_executor = action_executor
|
|||
|
|
self.pool = thread_pool
|
|||
|
|
self._timers: List[QTimer] = []
|
|||
|
|
self._active = False
|
|||
|
|
|
|||
|
|
# ── 进入航段 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def start(self):
|
|||
|
|
"""进入航段时调用:触发 ON_ENTER + 启动定时器"""
|
|||
|
|
self._active = True
|
|||
|
|
logger.info(
|
|||
|
|
f"[Seg{self.seg_index}] 进入,启动 Action 调度 "
|
|||
|
|
f"task={self.task.task_id[:8]}"
|
|||
|
|
)
|
|||
|
|
self.task.add_record(
|
|||
|
|
"action_start",
|
|||
|
|
f"进入航段[{self.seg_index}],开始 Action 调度",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ON_ENTER 触发
|
|||
|
|
on_enter = self.plan.entries_by_trigger(TriggerMoment.ON_ENTER)
|
|||
|
|
if on_enter:
|
|||
|
|
self._dispatch_entries(on_enter, label=f"seg{self.seg_index}/enter")
|
|||
|
|
|
|||
|
|
# SCHEDULED 触发(单次定时器)
|
|||
|
|
for entry in self.plan.entries_by_trigger(TriggerMoment.SCHEDULED):
|
|||
|
|
delay_ms = max(0, int(entry.time_offset_s * 1000))
|
|||
|
|
t = QTimer(self)
|
|||
|
|
t.setSingleShot(True)
|
|||
|
|
t.timeout.connect(lambda e=entry: self._dispatch_single(e))
|
|||
|
|
t.start(delay_ms)
|
|||
|
|
self._timers.append(t)
|
|||
|
|
logger.debug(
|
|||
|
|
f"[Seg{self.seg_index}] SCHEDULED Action "
|
|||
|
|
f"{entry.action.type_key} 将在 {delay_ms}ms 后触发"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# CONTINUOUS 触发(周期定时器)
|
|||
|
|
for entry in self.plan.entries_by_trigger(TriggerMoment.CONTINUOUS):
|
|||
|
|
interval_ms = max(100, int(entry.interval_s * 1000))
|
|||
|
|
t = QTimer(self)
|
|||
|
|
t.timeout.connect(lambda e=entry: self._dispatch_single(e))
|
|||
|
|
t.start(interval_ms)
|
|||
|
|
self._timers.append(t)
|
|||
|
|
logger.debug(
|
|||
|
|
f"[Seg{self.seg_index}] CONTINUOUS Action "
|
|||
|
|
f"{entry.action.type_key} 每 {interval_ms}ms 触发"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── 离开航段 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def stop(self):
|
|||
|
|
"""离开航段时调用:停止所有定时器 + 触发 ON_EXIT"""
|
|||
|
|
if not self._active:
|
|||
|
|
return
|
|||
|
|
self._active = False
|
|||
|
|
|
|||
|
|
# 停止并销毁所有定时器
|
|||
|
|
for t in self._timers:
|
|||
|
|
t.stop()
|
|||
|
|
t.deleteLater()
|
|||
|
|
self._timers.clear()
|
|||
|
|
|
|||
|
|
# ON_EXIT 触发
|
|||
|
|
on_exit = self.plan.entries_by_trigger(TriggerMoment.ON_EXIT)
|
|||
|
|
if on_exit:
|
|||
|
|
self._dispatch_entries(on_exit, label=f"seg{self.seg_index}/exit")
|
|||
|
|
|
|||
|
|
self.task.add_record(
|
|||
|
|
"action_start",
|
|||
|
|
f"离开航段[{self.seg_index}],ON_EXIT Actions 已触发",
|
|||
|
|
)
|
|||
|
|
logger.info(f"[Seg{self.seg_index}] 离开,定时器已清理")
|
|||
|
|
|
|||
|
|
# ── 核心分发逻辑 ──────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _dispatch_entries(self, entries: List[ActionEntry], label: str = ""):
|
|||
|
|
"""
|
|||
|
|
按策略分发一组 ActionEntry:
|
|||
|
|
┌─────────────────────────────────────────────────────┐
|
|||
|
|
│ 并行组(order=None)→ 同时提交到 ActionExecutor │
|
|||
|
|
│ 串行组(order!=None)→ 提交到 SerialChainWorker │
|
|||
|
|
│ serial_first 控制两组相对启动顺序 │
|
|||
|
|
└─────────────────────────────────────────────────────┘
|
|||
|
|
注意:串行组提交到线程池后立即返回(非阻塞),
|
|||
|
|
线程池内部保证串行顺序,不阻塞主线程。
|
|||
|
|
"""
|
|||
|
|
parallel_entries = [e for e in entries if e.is_parallel and e.enabled]
|
|||
|
|
sequential_entries = sorted(
|
|||
|
|
[e for e in entries if e.is_sequential and e.enabled],
|
|||
|
|
key=lambda e: e.order,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _run_parallel():
|
|||
|
|
for entry in parallel_entries:
|
|||
|
|
self.action_executor.submit(entry.action)
|
|||
|
|
logger.debug(
|
|||
|
|
f"[并行/{label}] 提交 Action: {entry.action.type_key}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _run_sequential():
|
|||
|
|
if not sequential_entries:
|
|||
|
|
return
|
|||
|
|
worker = _SerialChainWorker(
|
|||
|
|
task_id = self.task.task_id,
|
|||
|
|
entries = sequential_entries,
|
|||
|
|
executor = self.action_executor,
|
|||
|
|
label = label,
|
|||
|
|
)
|
|||
|
|
worker.signals.finished.connect(
|
|||
|
|
lambda tid, res, si=self.seg_index:
|
|||
|
|
self.serial_done.emit(tid, si, res)
|
|||
|
|
)
|
|||
|
|
worker.signals.error.connect(
|
|||
|
|
lambda tid, err, si=self.seg_index:
|
|||
|
|
self.serial_error.emit(tid, si, err)
|
|||
|
|
)
|
|||
|
|
self.pool.start(worker)
|
|||
|
|
logger.debug(
|
|||
|
|
f"[串行/{label}] 提交串行链,共 {len(sequential_entries)} 个 Action"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 根据 serial_first 决定启动顺序
|
|||
|
|
if self.plan.serial_first:
|
|||
|
|
_run_sequential()
|
|||
|
|
_run_parallel()
|
|||
|
|
else:
|
|||
|
|
_run_parallel()
|
|||
|
|
_run_sequential()
|
|||
|
|
|
|||
|
|
def _dispatch_single(self, entry: ActionEntry):
|
|||
|
|
"""分发单个 ActionEntry(CONTINUOUS/SCHEDULED 回调)"""
|
|||
|
|
if entry.enabled and self._active:
|
|||
|
|
self.action_executor.submit(entry.action)
|
|||
|
|
logger.debug(
|
|||
|
|
f"[Seg{self.seg_index}] 触发 Action: {entry.action.type_key} "
|
|||
|
|
f"trigger={entry.trigger.value}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 航线级 Action 调度器
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class _RouteScheduler(QObject):
|
|||
|
|
"""
|
|||
|
|
管理整条航线级别的 Action 调度
|
|||
|
|
负责:ON_ENTER/ON_EXIT/CONTINUOUS/SCHEDULED 触发
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
serial_done = pyqtSignal(str, list) # task_id, results
|
|||
|
|
serial_error = pyqtSignal(str, str) # task_id, error
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
task: Task,
|
|||
|
|
action_executor,
|
|||
|
|
thread_pool: QThreadPool,
|
|||
|
|
parent: QObject = None,
|
|||
|
|
):
|
|||
|
|
super().__init__(parent)
|
|||
|
|
self.task = task
|
|||
|
|
self.action_executor = action_executor
|
|||
|
|
self.pool = thread_pool
|
|||
|
|
self._timers: List[QTimer] = []
|
|||
|
|
self._active = False
|
|||
|
|
|
|||
|
|
def start(self):
|
|||
|
|
"""航线开始时调用"""
|
|||
|
|
self._active = True
|
|||
|
|
plan = self.task.action_plan
|
|||
|
|
|
|||
|
|
# ON_ENTER 触发
|
|||
|
|
on_enter = plan.route_entries_by_trigger(TriggerMoment.ON_ENTER)
|
|||
|
|
if on_enter:
|
|||
|
|
self._dispatch_entries(on_enter, plan)
|
|||
|
|
|
|||
|
|
# SCHEDULED 触发
|
|||
|
|
for entry in plan.route_entries_by_trigger(TriggerMoment.SCHEDULED):
|
|||
|
|
delay_ms = max(0, int(entry.time_offset_s * 1000))
|
|||
|
|
t = QTimer(self)
|
|||
|
|
t.setSingleShot(True)
|
|||
|
|
t.timeout.connect(lambda e=entry: self._dispatch_single(e))
|
|||
|
|
t.start(delay_ms)
|
|||
|
|
self._timers.append(t)
|
|||
|
|
|
|||
|
|
# CONTINUOUS 触发
|
|||
|
|
for entry in plan.route_entries_by_trigger(TriggerMoment.CONTINUOUS):
|
|||
|
|
interval_ms = max(100, int(entry.interval_s * 1000))
|
|||
|
|
t = QTimer(self)
|
|||
|
|
t.timeout.connect(lambda e=entry: self._dispatch_single(e))
|
|||
|
|
t.start(interval_ms)
|
|||
|
|
self._timers.append(t)
|
|||
|
|
|
|||
|
|
logger.info(
|
|||
|
|
f"[FlightRoute] 航线级调度启动 task={self.task.task_id[:8]} "
|
|||
|
|
f"entries={len(plan.route_entries)}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def stop(self):
|
|||
|
|
"""航线结束时调用"""
|
|||
|
|
if not self._active:
|
|||
|
|
return
|
|||
|
|
self._active = False
|
|||
|
|
|
|||
|
|
for t in self._timers:
|
|||
|
|
t.stop()
|
|||
|
|
t.deleteLater()
|
|||
|
|
self._timers.clear()
|
|||
|
|
|
|||
|
|
# ON_EXIT 触发
|
|||
|
|
plan = self.task.action_plan
|
|||
|
|
on_exit = plan.route_entries_by_trigger(TriggerMoment.ON_EXIT)
|
|||
|
|
if on_exit:
|
|||
|
|
self._dispatch_entries(on_exit, plan)
|
|||
|
|
|
|||
|
|
logger.info(f"[FlightRoute] 航线级调度停止 task={self.task.task_id[:8]}")
|
|||
|
|
|
|||
|
|
def _dispatch_entries(self, entries: List[ActionEntry], plan: RouteActionPlan):
|
|||
|
|
parallel_entries = [e for e in entries if e.is_parallel and e.enabled]
|
|||
|
|
sequential_entries = sorted(
|
|||
|
|
[e for e in entries if e.is_sequential and e.enabled],
|
|||
|
|
key=lambda e: e.order,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _run_parallel():
|
|||
|
|
for entry in parallel_entries:
|
|||
|
|
self.action_executor.submit(entry.action)
|
|||
|
|
logger.debug(f"[并行/route] 提交: {entry.action.type_key}")
|
|||
|
|
|
|||
|
|
def _run_sequential():
|
|||
|
|
if not sequential_entries:
|
|||
|
|
return
|
|||
|
|
worker = _SerialChainWorker(
|
|||
|
|
task_id = self.task.task_id,
|
|||
|
|
entries = sequential_entries,
|
|||
|
|
executor = self.action_executor,
|
|||
|
|
label = "route",
|
|||
|
|
)
|
|||
|
|
worker.signals.finished.connect(
|
|||
|
|
lambda tid, res: self.serial_done.emit(tid, res)
|
|||
|
|
)
|
|||
|
|
worker.signals.error.connect(
|
|||
|
|
lambda tid, err: self.serial_error.emit(tid, err)
|
|||
|
|
)
|
|||
|
|
self.pool.start(worker)
|
|||
|
|
|
|||
|
|
if plan.serial_first:
|
|||
|
|
_run_sequential()
|
|||
|
|
_run_parallel()
|
|||
|
|
else:
|
|||
|
|
_run_parallel()
|
|||
|
|
_run_sequential()
|
|||
|
|
|
|||
|
|
def _dispatch_single(self, entry: ActionEntry):
|
|||
|
|
if entry.enabled and self._active:
|
|||
|
|
self.action_executor.submit(entry.action)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# Task 执行器主类
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class TaskExecutor(QObject):
|
|||
|
|
"""
|
|||
|
|
Task 执行器(单例)
|
|||
|
|
─────────────────────────────────────────────────────────────
|
|||
|
|
使用方式:
|
|||
|
|
executor = TaskExecutor.instance()
|
|||
|
|
executor.set_action_executor(action_executor)
|
|||
|
|
executor.submit(task)
|
|||
|
|
─────────────────────────────────────────────────────────────
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
_instance = None
|
|||
|
|
_lock = threading.Lock()
|
|||
|
|
|
|||
|
|
# ── 对外信号 ──────────────────────────────────────────────────
|
|||
|
|
task_submitted = pyqtSignal(object) # Task
|
|||
|
|
task_started = pyqtSignal(object) # Task
|
|||
|
|
task_completed = pyqtSignal(object) # Task
|
|||
|
|
task_failed = pyqtSignal(object, str) # Task, error
|
|||
|
|
task_cancelled = pyqtSignal(object) # Task
|
|||
|
|
task_paused = pyqtSignal(object) # Task
|
|||
|
|
task_resumed = pyqtSignal(object) # Task
|
|||
|
|
segment_entered = pyqtSignal(object, int) # Task, seg_index
|
|||
|
|
segment_exited = pyqtSignal(object, int) # Task, seg_index
|
|||
|
|
action_triggered = pyqtSignal(object, object) # Task, ActionEntry
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def instance(cls) -> "TaskExecutor":
|
|||
|
|
with cls._lock:
|
|||
|
|
if cls._instance is None:
|
|||
|
|
cls._instance = cls()
|
|||
|
|
return cls._instance
|
|||
|
|
|
|||
|
|
def __init__(self, max_threads: int = 8, parent=None):
|
|||
|
|
super().__init__(parent)
|
|||
|
|
self._pool = QThreadPool.globalInstance()
|
|||
|
|
self._pool.setMaxThreadCount(max_threads)
|
|||
|
|
self._action_executor = None
|
|||
|
|
|
|||
|
|
# 运行中的任务 { task_id: Task }
|
|||
|
|
self._running: Dict[str, Task] = {}
|
|||
|
|
self._run_lock = threading.Lock()
|
|||
|
|
|
|||
|
|
# 航线推进定时器 { task_id: QTimer }
|
|||
|
|
self._route_timers: Dict[str, QTimer] = {}
|
|||
|
|
|
|||
|
|
# 航段调度器列表 { task_id: List[_SegmentScheduler | None] }
|
|||
|
|
self._seg_schedulers: Dict[str, List] = {}
|
|||
|
|
|
|||
|
|
# 航线级调度器 { task_id: _RouteScheduler }
|
|||
|
|
self._route_schedulers: Dict[str, _RouteScheduler] = {}
|
|||
|
|
|
|||
|
|
# # 当前活跃航段索引 { task_id: int }
|
|||
|
|
# self._seg_cursor: Dict[str, int] = {}
|
|||
|
|
|
|||
|
|
# # 航段时间边界缓存 { task_id: [(t_start, t_end), ...] }
|
|||
|
|
# self._seg_times: Dict[str, List[Tuple[float, float]]] = {}
|
|||
|
|
|
|||
|
|
# 任务实际启动时刻(用于时间轴对齐){ task_id: float }
|
|||
|
|
# self._task_wall_start: Dict[str, float] = {}
|
|||
|
|
|
|||
|
|
logger.info(f"✅ TaskExecutor 初始化 max_threads={max_threads}")
|
|||
|
|
|
|||
|
|
@on_event(EventType.ROUTE_SEGMENT_EXITED,EventType.ROUTE_SEGMENT_ENTERED,EventType.ROUTE_WAYPOINT_REACHED)
|
|||
|
|
def handle_route_event(self, event: Event):
|
|||
|
|
segment_index = event.params.get("segment_index")
|
|||
|
|
task = event.params.get("task")
|
|||
|
|
self.start_segment_actions(event.event_type, segment_index, task)
|
|||
|
|
|
|||
|
|
def set_action_executor(self, executor) -> None:
|
|||
|
|
"""注入 ActionExecutor"""
|
|||
|
|
self._action_executor = executor
|
|||
|
|
logger.info("✅ ActionExecutor 已注入 TaskExecutor")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── 提交接口 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def submit(self, task: Task) -> str:
|
|||
|
|
"""
|
|||
|
|
异步提交任务(立即返回)
|
|||
|
|
:return: task_id
|
|||
|
|
"""
|
|||
|
|
if not self._action_executor:
|
|||
|
|
raise RuntimeError("请先调用 set_action_executor() 注入 ActionExecutor")
|
|||
|
|
|
|||
|
|
task.mark_ready()
|
|||
|
|
self.task_submitted.emit(task)
|
|||
|
|
|
|||
|
|
with self._run_lock:
|
|||
|
|
self._running[task.task_id] = task
|
|||
|
|
|
|||
|
|
_task_procedure = functools.partial(self._start_task, task)
|
|||
|
|
# 若有计划执行时间,延迟启动
|
|||
|
|
if task.scheduled_at and task.scheduled_at > time.time():
|
|||
|
|
delay_ms = int((task.scheduled_at - time.time()) * 1000)
|
|||
|
|
QTimer.singleShot(delay_ms, _task_procedure)
|
|||
|
|
logger.info(f"任务已调度,{delay_ms}ms 后启动: {task.name}")
|
|||
|
|
else:
|
|||
|
|
# 使用 singleShot(0) 确保在 Qt 事件循环中启动,避免递归
|
|||
|
|
QTimer.singleShot(0, _task_procedure)
|
|||
|
|
|
|||
|
|
return task.task_id
|
|||
|
|
|
|||
|
|
def cancel(self, task_id: str) -> bool:
|
|||
|
|
"""取消任务"""
|
|||
|
|
with self._run_lock:
|
|||
|
|
task = self._running.get(task_id)
|
|||
|
|
if task:
|
|||
|
|
task.request_cancel()
|
|||
|
|
self._cleanup_task(task_id)
|
|||
|
|
task.mark_cancelled()
|
|||
|
|
self.task_cancelled.emit(task)
|
|||
|
|
logger.info(f"🚫 任务已取消: {task.name}")
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def pause(self, task_id: str) -> bool:
|
|||
|
|
"""暂停任务(暂停航线推进定时器)"""
|
|||
|
|
timer = self._route_timers.get(task_id)
|
|||
|
|
task = self._running.get(task_id)
|
|||
|
|
if timer and task and task.is_running:
|
|||
|
|
timer.stop()
|
|||
|
|
task.mark_paused()
|
|||
|
|
self.task_paused.emit(task)
|
|||
|
|
logger.info(f"⏸ 任务已暂停: {task.name}")
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def resume(self, task_id: str) -> bool:
|
|||
|
|
"""恢复任务"""
|
|||
|
|
timer = self._route_timers.get(task_id)
|
|||
|
|
task = self._running.get(task_id)
|
|||
|
|
if timer and task and task.state == TaskState.PAUSED:
|
|||
|
|
timer.start(100)
|
|||
|
|
task.mark_resumed()
|
|||
|
|
self.task_resumed.emit(task)
|
|||
|
|
logger.info(f"▶ 任务已恢复: {task.name}")
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# ── 内部:任务启动 ────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
@pyqtSlot()
|
|||
|
|
def _start_task(self, task: Task):
|
|||
|
|
"""在 Qt 事件循环中启动任务"""
|
|||
|
|
if task.cancel_requested:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
task.mark_running()
|
|||
|
|
# self._task_wall_start[task.task_id] = time.time()
|
|||
|
|
self.task_started.emit(task)
|
|||
|
|
logger.info(f"▶ 任务开始执行: {task.name} [{task.task_id[:8]}]")
|
|||
|
|
|
|||
|
|
# ── 启动航线级调度器 ──────────────────────────────────────
|
|||
|
|
route_sched = _RouteScheduler(
|
|||
|
|
task = task,
|
|||
|
|
action_executor = self._action_executor,
|
|||
|
|
thread_pool = self._pool,
|
|||
|
|
parent = self,
|
|||
|
|
)
|
|||
|
|
route_sched.serial_done.connect(self._on_route_serial_done)
|
|||
|
|
route_sched.serial_error.connect(self._on_route_serial_error)
|
|||
|
|
self._route_schedulers[task.task_id] = route_sched
|
|||
|
|
route_sched.start()
|
|||
|
|
|
|||
|
|
# ── 无航线任务:直接完成 ──────────────────────────────────
|
|||
|
|
if not task.has_route:
|
|||
|
|
self._execute_no_route_task(task)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# ── 缓存各航段时间边界(相对于任务启动时刻的偏移)────────
|
|||
|
|
# seg_times: List[Tuple[float, float]] = []
|
|||
|
|
# for seg in task.route.segments:
|
|||
|
|
# # 航段时间 = 路由内部时间轴(相对于 route.start_time)
|
|||
|
|
# # 转换为相对于任务实际启动时刻的偏移
|
|||
|
|
# offset = self._task_wall_start[task.task_id] - task.route.start_time
|
|||
|
|
# seg_times.append((seg.t_start + offset, seg.t_end + offset))
|
|||
|
|
# self._seg_times[task.task_id] = seg_times
|
|||
|
|
|
|||
|
|
# ── 初始化各航段调度器 ────────────────────────────────────
|
|||
|
|
schedulers: List[Optional[_SegmentScheduler]] = []
|
|||
|
|
for idx in range(len(task.route.segments)):
|
|||
|
|
plan = task.action_plan.segment_plans.get(idx)
|
|||
|
|
if plan:
|
|||
|
|
sched = _SegmentScheduler(
|
|||
|
|
task = task,
|
|||
|
|
seg_index = idx,
|
|||
|
|
plan = plan,
|
|||
|
|
action_executor = self._action_executor,
|
|||
|
|
thread_pool = self._pool,
|
|||
|
|
parent = self,
|
|||
|
|
)
|
|||
|
|
sched.serial_done.connect(self._on_seg_serial_done)
|
|||
|
|
sched.serial_error.connect(self._on_seg_serial_error)
|
|||
|
|
else:
|
|||
|
|
sched = None
|
|||
|
|
schedulers.append(sched)
|
|||
|
|
|
|||
|
|
self._seg_schedulers[task.task_id] = schedulers
|
|||
|
|
# self._seg_cursor[task.task_id] = -1 # -1 表示尚未进入任何航段
|
|||
|
|
|
|||
|
|
# ── 启动航线推进定时器(100ms 轮询时间轴)────────────────
|
|||
|
|
# timer = QTimer(self)
|
|||
|
|
# _task_route = functools.partial(self._advance_route, task)
|
|||
|
|
# timer.timeout.connect(_task_route)
|
|||
|
|
# timer.start(100)
|
|||
|
|
# self._route_timers[task.task_id] = timer
|
|||
|
|
|
|||
|
|
def _execute_no_route_task(self, task: Task):
|
|||
|
|
"""
|
|||
|
|
执行无航线任务:
|
|||
|
|
直接调度所有 route_entries(并行 + 串行),完成后结束任务
|
|||
|
|
"""
|
|||
|
|
plan = task.action_plan
|
|||
|
|
|
|||
|
|
# 并行组
|
|||
|
|
for entry in plan.parallel_route_entries:
|
|||
|
|
if entry.enabled:
|
|||
|
|
self._action_executor.submit(entry.action)
|
|||
|
|
|
|||
|
|
# 串行组
|
|||
|
|
seq_entries = plan.sequential_route_entries
|
|||
|
|
if seq_entries:
|
|||
|
|
worker = _SerialChainWorker(
|
|||
|
|
task_id = task.task_id,
|
|||
|
|
entries = seq_entries,
|
|||
|
|
executor = self._action_executor,
|
|||
|
|
label = "no_route",
|
|||
|
|
)
|
|||
|
|
worker.signals.finished.connect(
|
|||
|
|
lambda tid, res: self._finish_task(task)
|
|||
|
|
)
|
|||
|
|
worker.signals.error.connect(
|
|||
|
|
lambda tid, err: self._fail_task(task, err)
|
|||
|
|
)
|
|||
|
|
self._pool.start(worker)
|
|||
|
|
else:
|
|||
|
|
# 无串行组,等待并行 Action 提交后直接完成
|
|||
|
|
QTimer.singleShot(500, lambda: self._finish_task(task))
|
|||
|
|
|
|||
|
|
# ── 内部:航线推进(时间轴轮询)──────────────────────────────
|
|||
|
|
|
|||
|
|
@pyqtSlot()
|
|||
|
|
def start_segment_actions(self, event_type: EventType, seg_index: int, task: Task):
|
|||
|
|
if task.cancel_requested or not task.is_running:
|
|||
|
|
self._cleanup_task(task.task_id)
|
|||
|
|
if task.cancel_requested:
|
|||
|
|
task.mark_cancelled()
|
|||
|
|
self.task_cancelled.emit(task)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# now = time.time()
|
|||
|
|
# seg_times = self._seg_times.get(task.task_id, [])
|
|||
|
|
schedulers = self._seg_schedulers.get(task.task_id, [])
|
|||
|
|
# cur_idx = self._seg_cursor.get(task.task_id, -1)
|
|||
|
|
# n_segs = len(seg_times)
|
|||
|
|
#
|
|||
|
|
# if not seg_times:
|
|||
|
|
# return
|
|||
|
|
#
|
|||
|
|
## ── 判断当前时刻落在哪个航段 ──────────────────────────────
|
|||
|
|
# new_idx = cur_idx
|
|||
|
|
# for i, (t_start, t_end) in enumerate(seg_times):
|
|||
|
|
# if t_start <= now <= t_end:
|
|||
|
|
# new_idx = i
|
|||
|
|
# break
|
|||
|
|
# else:
|
|||
|
|
# # 超出所有航段时间范围
|
|||
|
|
# if now > seg_times[-1][1]:
|
|||
|
|
# new_idx = n_segs # 标记为"已完成所有航段"
|
|||
|
|
|
|||
|
|
match event_type:
|
|||
|
|
case EventType.ROUTE_SEGMENT_ENTERED:
|
|||
|
|
if schedulers[seg_index]:
|
|||
|
|
schedulers[seg_index].start()
|
|||
|
|
self.segment_entered.emit(task, seg_index)
|
|||
|
|
logger.info(
|
|||
|
|
f"[FlightRoute] 进入航段[{seg_index}] task={task.task_id[:8]}"
|
|||
|
|
)
|
|||
|
|
case EventType.ROUTE_SEGMENT_EXITED:
|
|||
|
|
if schedulers[seg_index]:
|
|||
|
|
schedulers[seg_index].stop()
|
|||
|
|
self.segment_exited.emit(task, seg_index)
|
|||
|
|
logger.info(
|
|||
|
|
f"[FlightRoute] 离开航段[{seg_index}] task={task.task_id[:8]}"
|
|||
|
|
)
|
|||
|
|
case EventType.ROUTE_WAYPOINT_REACHED:
|
|||
|
|
pass
|
|||
|
|
if seg_index == len(task.route.segments):
|
|||
|
|
self._finish_task(task)
|
|||
|
|
|
|||
|
|
# # ── 航段切换处理 ──────────────────────────────────────────
|
|||
|
|
# if new_idx != cur_idx:
|
|||
|
|
# # 离开旧航段
|
|||
|
|
# if 0 <= cur_idx < len(schedulers) and schedulers[cur_idx]:
|
|||
|
|
# schedulers[cur_idx].stop()
|
|||
|
|
# self.segment_exited.emit(task, cur_idx)
|
|||
|
|
# logger.info(
|
|||
|
|
# f"[FlightRoute] 离开航段[{cur_idx}] task={task.task_id[:8]}"
|
|||
|
|
# )
|
|||
|
|
#
|
|||
|
|
# # 进入新航段
|
|||
|
|
# if 0 <= new_idx < len(schedulers):
|
|||
|
|
# if schedulers[new_idx]:
|
|||
|
|
# schedulers[new_idx].start()
|
|||
|
|
# self.segment_entered.emit(task, new_idx)
|
|||
|
|
# logger.info(
|
|||
|
|
# f"[FlightRoute] 进入航段[{new_idx}] task={task.task_id[:8]}"
|
|||
|
|
# )
|
|||
|
|
# self._seg_cursor[task.task_id] = new_idx
|
|||
|
|
#
|
|||
|
|
# # 所有航段执行完毕
|
|||
|
|
# elif new_idx >= n_segs:
|
|||
|
|
# self._seg_cursor[task.task_id] = new_idx
|
|||
|
|
# self._finish_task(task)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pyqtSlot()
|
|||
|
|
def _advance_route(self, task: Task):
|
|||
|
|
"""
|
|||
|
|
每 100ms 轮询一次:
|
|||
|
|
根据当前真实时刻与各航段时间边界,判断是否需要进入/离开航段
|
|||
|
|
"""
|
|||
|
|
if task.cancel_requested or not task.is_running:
|
|||
|
|
self._cleanup_task(task.task_id)
|
|||
|
|
if task.cancel_requested:
|
|||
|
|
task.mark_cancelled()
|
|||
|
|
self.task_cancelled.emit(task)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
now = time.time()
|
|||
|
|
seg_times = self._seg_times.get(task.task_id, [])
|
|||
|
|
schedulers = self._seg_schedulers.get(task.task_id, [])
|
|||
|
|
cur_idx = self._seg_cursor.get(task.task_id, -1)
|
|||
|
|
n_segs = len(seg_times)
|
|||
|
|
|
|||
|
|
if not seg_times:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# ── 判断当前时刻落在哪个航段 ──────────────────────────────
|
|||
|
|
new_idx = cur_idx
|
|||
|
|
for i, (t_start, t_end) in enumerate(seg_times):
|
|||
|
|
if t_start <= now <= t_end:
|
|||
|
|
new_idx = i
|
|||
|
|
break
|
|||
|
|
else:
|
|||
|
|
# 超出所有航段时间范围
|
|||
|
|
if now > seg_times[-1][1]:
|
|||
|
|
new_idx = n_segs # 标记为"已完成所有航段"
|
|||
|
|
|
|||
|
|
# ── 航段切换处理 ──────────────────────────────────────────
|
|||
|
|
if new_idx != cur_idx:
|
|||
|
|
# 离开旧航段
|
|||
|
|
if 0 <= cur_idx < len(schedulers) and schedulers[cur_idx]:
|
|||
|
|
schedulers[cur_idx].stop()
|
|||
|
|
self.segment_exited.emit(task, cur_idx)
|
|||
|
|
logger.info(
|
|||
|
|
f"[FlightRoute] 离开航段[{cur_idx}] task={task.task_id[:8]}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 进入新航段
|
|||
|
|
if 0 <= new_idx < len(schedulers):
|
|||
|
|
if schedulers[new_idx]:
|
|||
|
|
schedulers[new_idx].start()
|
|||
|
|
self.segment_entered.emit(task, new_idx)
|
|||
|
|
logger.info(
|
|||
|
|
f"[FlightRoute] 进入航段[{new_idx}] task={task.task_id[:8]}"
|
|||
|
|
)
|
|||
|
|
self._seg_cursor[task.task_id] = new_idx
|
|||
|
|
|
|||
|
|
# 所有航段执行完毕
|
|||
|
|
elif new_idx >= n_segs:
|
|||
|
|
self._seg_cursor[task.task_id] = new_idx
|
|||
|
|
self._finish_task(task)
|
|||
|
|
|
|||
|
|
# ── 内部:任务完成/失败 ───────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _finish_task(self, task: Task):
|
|||
|
|
"""正常完成任务"""
|
|||
|
|
if task.is_done:
|
|||
|
|
return
|
|||
|
|
self._cleanup_task(task.task_id)
|
|||
|
|
task.mark_completed()
|
|||
|
|
self.task_completed.emit(task)
|
|||
|
|
logger.info(f"✅ 任务完成: {task.name} 耗时={task.elapsed_s:.1f}s")
|
|||
|
|
|
|||
|
|
def _fail_task(self, task: Task, reason: str):
|
|||
|
|
"""任务失败"""
|
|||
|
|
if task.is_done:
|
|||
|
|
return
|
|||
|
|
self._cleanup_task(task.task_id)
|
|||
|
|
task.mark_failed(reason)
|
|||
|
|
self.task_failed.emit(task, reason)
|
|||
|
|
logger.error(f"❌ 任务失败: {task.name} reason={reason}")
|
|||
|
|
|
|||
|
|
def _cleanup_task(self, task_id: str):
|
|||
|
|
"""清理任务相关资源"""
|
|||
|
|
# 停止航线推进定时器
|
|||
|
|
timer = self._route_timers.pop(task_id, None)
|
|||
|
|
if timer:
|
|||
|
|
timer.stop()
|
|||
|
|
timer.deleteLater()
|
|||
|
|
|
|||
|
|
# 停止所有航段调度器
|
|||
|
|
for sched in self._seg_schedulers.pop(task_id, []):
|
|||
|
|
if sched:
|
|||
|
|
sched.stop()
|
|||
|
|
|
|||
|
|
# 停止航线级调度器
|
|||
|
|
route_sched = self._route_schedulers.pop(task_id, None)
|
|||
|
|
if route_sched:
|
|||
|
|
route_sched.stop()
|
|||
|
|
|
|||
|
|
# 清理游标和时间缓存
|
|||
|
|
self._seg_cursor.pop(task_id, None)
|
|||
|
|
# self._seg_times.pop(task_id, None)
|
|||
|
|
# self._task_wall_start.pop(task_id, None)
|
|||
|
|
|
|||
|
|
with self._run_lock:
|
|||
|
|
self._running.pop(task_id, None)
|
|||
|
|
|
|||
|
|
# ── 槽函数 ────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
@pyqtSlot(str, int, list)
|
|||
|
|
def _on_seg_serial_done(self, task_id: str, seg_index: int, results: list):
|
|||
|
|
logger.info(
|
|||
|
|
f"[串行链] 航段[{seg_index}] 完成 "
|
|||
|
|
f"results={len(results)} task={task_id[:8]}"
|
|||
|
|
)
|
|||
|
|
task = self._running.get(task_id)
|
|||
|
|
if task:
|
|||
|
|
task.add_record(
|
|||
|
|
"action_done",
|
|||
|
|
f"航段[{seg_index}] 串行 Action 链完成,共 {len(results)} 个",
|
|||
|
|
{"seg_index": seg_index, "results_count": len(results)},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
@pyqtSlot(str, int, str)
|
|||
|
|
def _on_seg_serial_error(self, task_id: str, seg_index: int, error: str):
|
|||
|
|
logger.error(f"[串行链] 航段[{seg_index}] 错误: {error}")
|
|||
|
|
task = self._running.get(task_id)
|
|||
|
|
if task:
|
|||
|
|
task.add_record("error", f"航段[{seg_index}] 串行链错误: {error}")
|
|||
|
|
|
|||
|
|
@pyqtSlot(str, list)
|
|||
|
|
def _on_route_serial_done(self, task_id: str, results: list):
|
|||
|
|
logger.info(
|
|||
|
|
f"[串行链] 航线级完成 results={len(results)} task={task_id[:8]}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
@pyqtSlot(str, str)
|
|||
|
|
def _on_route_serial_error(self, task_id: str, error: str):
|
|||
|
|
logger.error(f"[串行链] 航线级错误: {error}")
|
|||
|
|
|
|||
|
|
# ── 查询接口 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def get_running_tasks(self) -> List[Task]:
|
|||
|
|
with self._run_lock:
|
|||
|
|
return list(self._running.values())
|
|||
|
|
|
|||
|
|
def get_task(self, task_id: str) -> Optional[Task]:
|
|||
|
|
with self._run_lock:
|
|||
|
|
return self._running.get(task_id)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 全局执行器实例
|
|||
|
|
task_executor = TaskExecutor.instance()
|