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