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})") |