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