409 lines
18 KiB
Python
409 lines
18 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
Task 数据模型
|
|||
|
|
─────────────────────────────────────────────────────────────────
|
|||
|
|
Task —— 核心任务对象
|
|||
|
|
TaskArea —— 任务目标区域(多边形/圆形)
|
|||
|
|
TaskState —— 任务状态机
|
|||
|
|
─────────────────────────────────────────────────────────────────
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import logging
|
|||
|
|
import time
|
|||
|
|
import uuid
|
|||
|
|
from dataclasses import dataclass, field
|
|||
|
|
from datetime import datetime
|
|||
|
|
from enum import Enum
|
|||
|
|
from typing import Any, Dict, List, Optional, Union
|
|||
|
|
|
|||
|
|
from uas.task.task_action_plan import RouteActionPlan, TriggerMoment
|
|||
|
|
from uas.task.task_type import TaskType, TaskPriority, task_type_registry
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 任务状态机
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class TaskState(Enum):
|
|||
|
|
CREATED = "created" # 已创建
|
|||
|
|
PLANNED = "planned" # 已规划(航线/Action 已配置)
|
|||
|
|
READY = "ready" # 就绪(等待执行时间)
|
|||
|
|
RUNNING = "running" # 执行中
|
|||
|
|
PAUSED = "paused" # 已暂停
|
|||
|
|
COMPLETED = "completed" # 已完成
|
|||
|
|
FAILED = "failed" # 执行失败
|
|||
|
|
CANCELLED = "cancelled" # 已取消
|
|||
|
|
ABORTED = "aborted" # 中止
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 任务目标区域
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class GeoPoint:
|
|||
|
|
"""地理坐标点"""
|
|||
|
|
latitude: float
|
|||
|
|
longitude: float
|
|||
|
|
altitude: float = 0.0
|
|||
|
|
|
|||
|
|
def __repr__(self):
|
|||
|
|
return f"({self.latitude:.4f}°, {self.longitude:.4f}°, {self.altitude:.0f}m)"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class TaskArea:
|
|||
|
|
"""
|
|||
|
|
任务目标区域
|
|||
|
|
支持多边形(polygon)和圆形(circle)两种定义方式
|
|||
|
|
"""
|
|||
|
|
name: str = ""
|
|||
|
|
area_type: str = "polygon" # "polygon" | "circle"
|
|||
|
|
|
|||
|
|
# 多边形顶点(area_type="polygon")
|
|||
|
|
polygon: List[GeoPoint] = field(default_factory=list)
|
|||
|
|
|
|||
|
|
# 圆形定义(area_type="circle")
|
|||
|
|
center: Optional[GeoPoint] = None
|
|||
|
|
radius_m: float = 0.0
|
|||
|
|
|
|||
|
|
# 高度范围
|
|||
|
|
alt_min_m: float = 0.0
|
|||
|
|
alt_max_m: float = 99999.0
|
|||
|
|
|
|||
|
|
description: str = ""
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def from_polygon(cls, points: List[tuple], name: str = "",
|
|||
|
|
alt_min: float = 0.0, alt_max: float = 99999.0) -> "TaskArea":
|
|||
|
|
"""从经纬度元组列表创建多边形区域"""
|
|||
|
|
polygon = [GeoPoint(lat, lon) for lat, lon in points]
|
|||
|
|
return cls(name=name, area_type="polygon", polygon=polygon,
|
|||
|
|
alt_min_m=alt_min, alt_max_m=alt_max)
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def from_circle(cls, center_lat: float, center_lon: float,
|
|||
|
|
radius_m: float, name: str = "") -> "TaskArea":
|
|||
|
|
"""从圆心+半径创建圆形区域"""
|
|||
|
|
return cls(name=name, area_type="circle",
|
|||
|
|
center=GeoPoint(center_lat, center_lon),
|
|||
|
|
radius_m=radius_m)
|
|||
|
|
|
|||
|
|
def __repr__(self):
|
|||
|
|
if self.area_type == "circle":
|
|||
|
|
return f"TaskArea(circle, center={self.center}, r={self.radius_m}m)"
|
|||
|
|
return f"TaskArea(polygon, points={len(self.polygon)})"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 任务执行记录
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class TaskExecutionRecord:
|
|||
|
|
"""任务执行过程记录条目"""
|
|||
|
|
timestamp: float
|
|||
|
|
event_type: str # "action_start" | "action_done" | "state_change" | "error"
|
|||
|
|
description: str
|
|||
|
|
data: Dict = field(default_factory=dict)
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def datetime_str(self) -> str:
|
|||
|
|
return datetime.fromtimestamp(self.timestamp).strftime("%H:%M:%S.%f")[:-3]
|
|||
|
|
|
|||
|
|
def __repr__(self):
|
|||
|
|
return f"[{self.datetime_str}] {self.event_type}: {self.description}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 核心 Task 对象
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class Task:
|
|||
|
|
"""
|
|||
|
|
核心任务对象
|
|||
|
|
─────────────────────────────────────────────────────────────
|
|||
|
|
字段说明:
|
|||
|
|
task_type —— 任务类型(TaskType 枚举 或 自定义字符串)
|
|||
|
|
name —— 任务名称
|
|||
|
|
description —— 任务描述
|
|||
|
|
target_area —— 任务目标区域
|
|||
|
|
priority —— 执行优先级
|
|||
|
|
scheduled_at —— 计划执行时间(Unix 时间戳)
|
|||
|
|
deadline —— 任务截止时间(Unix 时间戳)
|
|||
|
|
route —— 关联的 FlightRoute 对象
|
|||
|
|
action_plan —— 航线/航段 Action 配置计划
|
|||
|
|
─────────────────────────────────────────────────────────────
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# ── 基本信息 ──────────────────────────────────────────────────
|
|||
|
|
name: str
|
|||
|
|
task_type: Union[TaskType, str]
|
|||
|
|
description: str = ""
|
|||
|
|
target_area: Optional[TaskArea] = None
|
|||
|
|
priority: TaskPriority = TaskPriority.NORMAL
|
|||
|
|
|
|||
|
|
# ── 时间约束 ──────────────────────────────────────────────────
|
|||
|
|
scheduled_at: Optional[float] = None # 计划执行时间(None=立即)
|
|||
|
|
deadline: Optional[float] = None # 截止时间
|
|||
|
|
|
|||
|
|
# ── 航线配置 ──────────────────────────────────────────────────
|
|||
|
|
route: Optional[object] = None # FlightRoute 实例
|
|||
|
|
action_plan: RouteActionPlan = field(
|
|||
|
|
default_factory=RouteActionPlan
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── 扩展参数 ──────────────────────────────────────────────────
|
|||
|
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|||
|
|
tags: List[str] = field(default_factory=list)
|
|||
|
|
|
|||
|
|
# ── 自动填充 ──────────────────────────────────────────────────
|
|||
|
|
task_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|||
|
|
created_at: float = field(default_factory=time.time)
|
|||
|
|
state: TaskState = field(default=TaskState.CREATED)
|
|||
|
|
|
|||
|
|
# ── 运行时字段 ────────────────────────────────────────────────
|
|||
|
|
_start_time: Optional[float] = field(default=None, repr=False)
|
|||
|
|
_end_time: Optional[float] = field(default=None, repr=False)
|
|||
|
|
_records: List[TaskExecutionRecord] = field(default_factory=list, repr=False)
|
|||
|
|
_cancel_flag: bool = field(default=False, repr=False)
|
|||
|
|
|
|||
|
|
def __post_init__(self):
|
|||
|
|
# 若未指定优先级,从注册表读取默认值
|
|||
|
|
meta = task_type_registry.get(self.type_key)
|
|||
|
|
if meta and self.priority == TaskPriority.NORMAL:
|
|||
|
|
self.priority = meta.default_priority
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── 属性 ──────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def type_key(self) -> str:
|
|||
|
|
return (self.task_type.value
|
|||
|
|
if isinstance(self.task_type, TaskType)
|
|||
|
|
else str(self.task_type))
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def elapsed_s(self) -> float:
|
|||
|
|
if self._start_time and self._end_time:
|
|||
|
|
return self._end_time - self._start_time
|
|||
|
|
if self._start_time:
|
|||
|
|
return time.time() - self._start_time
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def is_running(self) -> bool:
|
|||
|
|
return self.state == TaskState.RUNNING
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def is_done(self) -> bool:
|
|||
|
|
return self.state in (
|
|||
|
|
TaskState.COMPLETED, TaskState.FAILED,
|
|||
|
|
TaskState.CANCELLED, TaskState.ABORTED,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def created_datetime(self) -> str:
|
|||
|
|
return datetime.fromtimestamp(self.created_at).strftime("%Y-%m-%d %H:%M:%S")
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def has_route(self) -> bool:
|
|||
|
|
return self.route is not None and bool(
|
|||
|
|
getattr(self.route, "segments", None)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def records(self) -> List[TaskExecutionRecord]:
|
|||
|
|
return list(self._records)
|
|||
|
|
|
|||
|
|
# ── 航线配置 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def set_route(self, route) -> "Task":
|
|||
|
|
"""
|
|||
|
|
配置航线(FlightRoute 实例)
|
|||
|
|
:return: self(链式调用)
|
|||
|
|
"""
|
|||
|
|
self.route = route
|
|||
|
|
self._add_record("state_change", f"航线已配置,航段数={len(route.segments)}")
|
|||
|
|
if self.state == TaskState.CREATED:
|
|||
|
|
self.state = TaskState.PLANNED
|
|||
|
|
return self
|
|||
|
|
|
|||
|
|
# ── Action 计划配置(快捷方法)────────────────────────────────
|
|||
|
|
|
|||
|
|
def add_route_action(
|
|||
|
|
self,
|
|||
|
|
action,
|
|||
|
|
order: Optional[int] = None,
|
|||
|
|
trigger: TriggerMoment = TriggerMoment.ON_ENTER,
|
|||
|
|
interval_s: float = 10.0,
|
|||
|
|
time_offset_s: float = 0.0,
|
|||
|
|
description: str = "",
|
|||
|
|
) -> "Task":
|
|||
|
|
"""
|
|||
|
|
为整条航线添加 Action
|
|||
|
|
order=None → 并行执行
|
|||
|
|
order=整数 → 串行执行(按 order 升序)
|
|||
|
|
"""
|
|||
|
|
self.action_plan.add_route_action(
|
|||
|
|
action=action, order=order, trigger=trigger,
|
|||
|
|
interval_s=interval_s, time_offset_s=time_offset_s,
|
|||
|
|
description=description,
|
|||
|
|
)
|
|||
|
|
return self
|
|||
|
|
|
|||
|
|
def add_segment_action(
|
|||
|
|
self,
|
|||
|
|
segment_index: int,
|
|||
|
|
action,
|
|||
|
|
order: Optional[int] = None,
|
|||
|
|
trigger: TriggerMoment = TriggerMoment.ON_ENTER,
|
|||
|
|
interval_s: float = 10.0,
|
|||
|
|
time_offset_s: float = 0.0,
|
|||
|
|
description: str = "",
|
|||
|
|
) -> "Task":
|
|||
|
|
"""
|
|||
|
|
为指定航段添加 Action
|
|||
|
|
segment_index: FlightRoute.segments 的下标
|
|||
|
|
order=None → 并行执行
|
|||
|
|
order=整数 → 串行执行(按 order 升序)
|
|||
|
|
"""
|
|||
|
|
self.action_plan.add_segment_action(
|
|||
|
|
segment_index=segment_index, action=action,
|
|||
|
|
order=order, trigger=trigger,
|
|||
|
|
interval_s=interval_s, time_offset_s=time_offset_s,
|
|||
|
|
description=description,
|
|||
|
|
)
|
|||
|
|
return self
|
|||
|
|
|
|||
|
|
# ── 状态操作 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def mark_ready(self):
|
|||
|
|
self.state = TaskState.READY
|
|||
|
|
self._add_record("state_change", "任务就绪")
|
|||
|
|
|
|||
|
|
def mark_running(self):
|
|||
|
|
self.state = TaskState.RUNNING
|
|||
|
|
self._start_time = time.time()
|
|||
|
|
self._add_record("state_change", "任务开始执行")
|
|||
|
|
|
|||
|
|
def mark_completed(self):
|
|||
|
|
self.state = TaskState.COMPLETED
|
|||
|
|
self._end_time = time.time()
|
|||
|
|
self._add_record("state_change", f"任务完成,耗时={self.elapsed_s:.1f}s")
|
|||
|
|
|
|||
|
|
def mark_failed(self, reason: str = ""):
|
|||
|
|
self.state = TaskState.FAILED
|
|||
|
|
self._end_time = time.time()
|
|||
|
|
self._add_record("error", f"任务失败: {reason}")
|
|||
|
|
|
|||
|
|
def mark_cancelled(self):
|
|||
|
|
self._cancel_flag = True
|
|||
|
|
self.state = TaskState.CANCELLED
|
|||
|
|
self._end_time = time.time()
|
|||
|
|
self._add_record("state_change", "任务已取消")
|
|||
|
|
|
|||
|
|
def mark_paused(self):
|
|||
|
|
self.state = TaskState.PAUSED
|
|||
|
|
self._add_record("state_change", "任务已暂停")
|
|||
|
|
|
|||
|
|
def mark_resumed(self):
|
|||
|
|
self.state = TaskState.RUNNING
|
|||
|
|
self._add_record("state_change", "任务已恢复")
|
|||
|
|
|
|||
|
|
def request_cancel(self) -> bool:
|
|||
|
|
self._cancel_flag = True
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def cancel_requested(self) -> bool:
|
|||
|
|
return self._cancel_flag
|
|||
|
|
|
|||
|
|
# ── 记录 ──────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _add_record(self, event_type: str, description: str, data: dict = None):
|
|||
|
|
self._records.append(TaskExecutionRecord(
|
|||
|
|
timestamp = time.time(),
|
|||
|
|
event_type = event_type,
|
|||
|
|
description = description,
|
|||
|
|
data = data or {},
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
def add_record(self, event_type: str, description: str, data: dict = None):
|
|||
|
|
"""外部调用:添加执行记录"""
|
|||
|
|
self._add_record(event_type, description, data)
|
|||
|
|
|
|||
|
|
# ── 序列化 ────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def to_dict(self) -> dict:
|
|||
|
|
return {
|
|||
|
|
"task_id": self.task_id,
|
|||
|
|
"name": self.name,
|
|||
|
|
"task_type": self.type_key,
|
|||
|
|
"description": self.description,
|
|||
|
|
"priority": self.priority.name,
|
|||
|
|
"state": self.state.value,
|
|||
|
|
"created_at": self.created_datetime,
|
|||
|
|
"elapsed_s": self.elapsed_s,
|
|||
|
|
"has_route": self.has_route,
|
|||
|
|
"route_actions": len(self.action_plan.route_entries),
|
|||
|
|
"segment_plans": len(self.action_plan.segment_plans),
|
|||
|
|
"tags": self.tags,
|
|||
|
|
"metadata": self.metadata,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ── 摘要 ──────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def info(self) -> str:
|
|||
|
|
meta = task_type_registry.get(self.type_key)
|
|||
|
|
lines = [
|
|||
|
|
f"{'═'*60}",
|
|||
|
|
f" 任务: {self.name} [{self.task_id[:8]}]",
|
|||
|
|
f"{'═'*60}",
|
|||
|
|
f" 类型 : {self.type_key}"
|
|||
|
|
f" ({meta.description if meta else ''})",
|
|||
|
|
f" 优先级 : {self.priority.name}",
|
|||
|
|
f" 状态 : {self.state.value}",
|
|||
|
|
f" 描述 : {self.description}",
|
|||
|
|
f" 创建时间 : {self.created_datetime}",
|
|||
|
|
f" 目标区域 : {self.target_area}",
|
|||
|
|
f" 标签 : {self.tags}",
|
|||
|
|
]
|
|||
|
|
if self.has_route:
|
|||
|
|
r = self.route
|
|||
|
|
lines += [
|
|||
|
|
f"{'─'*60}",
|
|||
|
|
f" 航线 : {len(r.segments)} 段 / "
|
|||
|
|
f"{sum(s.distance for s in r.segments)/1000:.1f} km / "
|
|||
|
|
f"{r.total_duration:.0f}s",
|
|||
|
|
f" 航线Actions: {len(self.action_plan.route_entries)} 个"
|
|||
|
|
f" (并行={len(self.action_plan.parallel_route_entries)}, "
|
|||
|
|
f"串行={len(self.action_plan.sequential_route_entries)})",
|
|||
|
|
f" 已配置航段: {len(self.action_plan.segment_plans)} 个",
|
|||
|
|
]
|
|||
|
|
for idx, plan in sorted(self.action_plan.segment_plans.items()):
|
|||
|
|
seg = r.segments[idx] if idx < len(r.segments) else None
|
|||
|
|
seg_name = (f"{seg.wp_from.identifier}→{seg.wp_to.identifier}"
|
|||
|
|
if seg else f"seg[{idx}]")
|
|||
|
|
lines.append(
|
|||
|
|
f" [{idx}] {seg_name}: "
|
|||
|
|
f"{len(plan.entries)} actions "
|
|||
|
|
f"(并行={len(plan.parallel_entries)}, "
|
|||
|
|
f"串行={len(plan.sequential_entries)})"
|
|||
|
|
)
|
|||
|
|
if self._records:
|
|||
|
|
lines += [f"{'─'*60}", " 执行记录:"]
|
|||
|
|
for rec in self._records[-5:]:
|
|||
|
|
lines.append(f" {rec}")
|
|||
|
|
lines.append(f"{'═'*60}")
|
|||
|
|
return "\n".join(lines)
|
|||
|
|
|
|||
|
|
def __repr__(self):
|
|||
|
|
return (f"Task(id={self.task_id[:8]}, name={self.name!r}, "
|
|||
|
|
f"type={self.type_key}, state={self.state.value}, "
|
|||
|
|
f"priority={self.priority.name})")
|