base_agent/uas/task/task_model.py

409 lines
18 KiB
Python
Raw Normal View History

2026-06-18 03:28:14 +00:00
# -*- 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})")