base_agent/uas/model/model.py

539 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
装备模型抽象基类(核心)
─────────────────────────────────────────────────────────────────
BaseEquipmentModel 所有装备模型的统一抽象基类
职责:
- 生命周期管理start/stop/pause/resume/reset
- 仿真驱动step/step_to
- 参数管理set_params/update_param/get_param
- 参数版本快照save_snapshot/restore_snapshot
- PyQt5 信号槽state_updated/lifecycle_changed/
params_changed/error_occurred
─────────────────────────────────────────────────────────────────
"""
from __future__ import annotations
import asyncio
import copy
import logging
from abc import abstractmethod
from typing import Any, List, Optional, Type, Callable
from PyQt5.QtCore import QObject, pyqtSignal
from uas.comm import CommAdapterFactory
from uas.comm.base import CommBaseAdapter
from uas.comm.message import Message
from uas.model.lifecycle import LifecycleFSM, LifecycleState
from uas.model.params import EquipmentParams
from uas.model.state import BaseState
from uas.task.task_model import Task
logger = logging.getLogger(__name__)
# ─────────────────────────────────────────────────────────────────
# 装备模型抽象基类
# ─────────────────────────────────────────────────────────────────
class BaseEquipmentModel(QObject):
"""
装备模型统一抽象基类
── 子类必须实现 ──────────────────────────────────────────────
_do_step(dt) 核心仿真推进(更新 _state
_build_initial_state() 构建初始状态快照
params_class 类属性:对应的 EquipmentParams 子类
── 子类可选覆盖 ──────────────────────────────────────────────
_on_start() 启动时的额外初始化
_on_stop() 停止时的清理
_on_pause() 暂停时的处理
_on_resume() 恢复时的处理
_on_reset() 重置时的处理
_on_params_changed(key, value) 参数变更时的响应
_check_constraints() 约束检查(越界告警)
"""
# ── PyQt5 信号 ────────────────────────────────────────────────
state_updated = pyqtSignal(object) # BaseState 子类实例
lifecycle_changed = pyqtSignal(str, str) # (old_state, new_state)
params_changed = pyqtSignal(str, object) # (key, new_value)
error_occurred = pyqtSignal(str) # 错误消息
# ── 子类须声明的类属性 ────────────────────────────────────────
params_class: Type[EquipmentParams] = EquipmentParams
def __init__(
self,
params: Optional[EquipmentParams] = None,
parent: QObject = None,
):
super().__init__(parent)
# 参数
self._params: EquipmentParams = (
params if params is not None
else self.params_class()
)
# 状态
self._state: Optional[BaseState] = None
self._initial_state: Optional[BaseState] = None
self._sub_models: List[BaseEquipmentModel] = []
# 生命周期 FSM
self._fsm = LifecycleFSM(
initial = LifecycleState.CREATED,
on_changed = self._on_fsm_changed,
)
self._comm = None
# 仿真时钟
self._sim_time: float = 0.0 # 仿真绝对时刻
self._elapsed: float = 0.0 # 自启动累计时长
# 参数快照栈
self._snapshots: List[EquipmentParams] = []
# 日志
self.logger = logging.getLogger(
f"{self.__class__.__name__}[{self._params.equipment_id}]"
)
# 初始化状态 → READY
self.set_state(self._build_initial_state())
self._fsm.to_ready(0.0)
self._actions = {}
def action_is_available(self, action: str):
return True
def register_action(self,
object: BaseEquipmentModel,
action: str,
handler: Callable):
if (object.equipment_id, action) not in self._actions:
self._actions[(object.equipment_id, action) ].append((object, handler))
def dereigster_action(self, object: BaseEquipmentModel, action):
if (object.equipment_id, action) in self._actions:
del self._actions[(object.equipment_id, action)]
def call_action(self, object_id:str , action: str, **parameters):
if (object_id, action) not in self._actions:
raise ValueError()
object, handler = self._actions[(object_id, action)]
assert isinstance(object, BaseState)
if object.action_is_available(action):
return handler(**parameters)
else:
raise
def set_state(self, state: Optional[BaseState]):
self._state = state.copy_with()
self._initial_state = state.copy_with()
self.state_updated.emit(self._state)
def add_sub_model(self, model: BaseEquipmentModel):
if model not in self._sub_models:
self._sub_models.append(model)
def add_sub_models(self, *models):
for model in models:
self.add_sub_model(model)
# ── 属性(只读)──────────────────────────────────────────────
@property
def actions(self):
return self._actions
@property
def sub_models(self) -> List[BaseEquipmentModel]:
return self._sub_models
@property
def params(self) -> EquipmentParams:
return self._params
@property
def state(self) -> Optional[BaseState]:
return self._state
@property
def lifecycle(self) -> LifecycleState:
return self._fsm.state
@property
def sim_time(self) -> float:
return self._sim_time
@property
def is_running(self) -> bool:
return self._fsm.is_running
@property
def equipment_id(self) -> str:
return self._params.equipment_id
@property
def name(self) -> str:
return self._params.name
# ── 生命周期控制 ──────────────────────────────────────────────
def start(self) -> bool:
"""
启动模型READY → RUNNING
:return: True=成功False=当前状态不允许启动
"""
try:
for model in self._sub_models:
if not model.start():
raise
errors = self.validate_params()
if errors:
msg = f"参数校验失败,无法启动: {errors}"
self.logger.error(msg)
self.error_occurred.emit(msg)
return False
self._fsm.to_running(self._sim_time)
self._on_start()
self.logger.info(f"模型启动: {self.name}")
return True
except ValueError as e:
self._handle_error(str(e))
return False
def stop(self) -> bool:
"""停止模型RUNNING/PAUSED → STOPPED"""
try:
for model in self._sub_models:
if not model.stop():
raise
self._fsm.to_stopped(self._sim_time)
self._on_stop()
self.logger.info(f"模型停止: {self.name}")
return True
except ValueError as e:
self._handle_error(str(e))
return False
def pause(self) -> bool:
"""暂停模型RUNNING → PAUSED"""
try:
for model in self._sub_models:
if not model.pause():
raise
self._fsm.to_paused(self._sim_time)
self._on_pause()
self.logger.info(f"模型暂停: {self.name}")
return True
except ValueError as e:
self._handle_error(str(e))
return False
def resume(self) -> bool:
"""恢复模型PAUSED → RUNNING"""
try:
for model in self._sub_models:
if not model.resume():
raise
self._fsm.to_running(self._sim_time)
self._on_resume()
self.logger.info(f"模型恢复: {self.name}")
return True
except ValueError as e:
self._handle_error(str(e))
return False
def reset(self) -> bool:
"""
重置模型(任意状态 → READY
恢复初始状态、清零仿真时钟
"""
try:
for model in self._sub_models:
if not model.reset():
raise
self._fsm.reset(0.0)
self._sim_time = 0.0
self._elapsed = 0.0
self._state = copy.deepcopy(self._initial_state)
self._on_reset()
self.logger.info(f"模型重置: {self.name}")
return True
except Exception as e:
self._handle_error(str(e))
return False
def run_task(self, task: Task):
pass
# ── 仿真驱动 ──────────────────────────────────────────────────
def step(self, dt: float) -> Optional[BaseState]:
"""
推进仿真 dt 秒(核心驱动方法)
内部执行顺序:
① 检查生命周期(非 RUNNING 则跳过)
② 调用 _do_step(dt)(子类实现运动/系统更新)
③ 调用 _check_constraints()(约束检查)
④ 更新 sim_time 和 elapsed_time
⑤ 发射 state_updated 信号
:param dt: 时间步长(秒,必须 > 0
:return: 更新后的状态快照,或 None未在运行中
"""
if not self._fsm.can_step:
return None
if dt <= 0:
raise ValueError(f"dt 必须大于 0当前值: {dt}")
try:
sub_states = []
for model in self._sub_models:
sub_state = model.step(dt)
if not sub_state:
raise
sub_states.append(sub_state)
# ② 子类核心更新
self._do_step(dt)
# ③ 约束检查
self._check_constraints()
# ④ 更新时钟
self._sim_time += dt
self._elapsed += dt
# 同步状态时间戳
if self._state is not None:
self._state.timestamp = self._sim_time
self._state.elapsed_time = self._elapsed
self._state.lifecycle = self._fsm.state.value
self._state.sub_states = sub_states
# ⑤ 发射信号
if self._state is not None:
self.state_updated.emit(self._state)
return self._state
except Exception as e:
self._handle_error(f"step 执行异常: {e}")
return None
def step_to(self, t: float, dt: float = 0.1) -> Optional[BaseState]:
"""
推进到绝对时刻 t
:param t: 目标时刻(秒)
:param dt: 内部步长(秒)
"""
for model in self._sub_models:
model.step_to(t, dt)
if t <= self._sim_time:
return self._state
remaining = t - self._sim_time
while remaining > 1e-9:
step_dt = min(dt, remaining)
self.step(step_dt)
remaining -= step_dt
return self._state
def get_state(self) -> Optional[BaseState]:
"""获取当前状态快照(深拷贝)"""
return self._state.copy_with() if self._state else None
# ── 参数管理 ──────────────────────────────────────────────────
def set_params(self, params: EquipmentParams) -> None:
"""整体替换装备参数"""
old = self._params
self._params = params
errors = self.validate_params()
if errors:
self._params = old
raise ValueError(f"新参数校验失败: {errors}")
self.logger.info(f"参数已整体替换: {params}")
def update_param(self, key: str, value: Any) -> None:
"""
动态更新单个参数
自动触发 params_changed 信号和约束重校验
"""
old_value = self._params.get(key)
self._params.set(key, value)
# 重新校验
errors = self.validate_params()
if errors:
# 回滚
self._params.set(key, old_value)
raise ValueError(
f"参数 {key}={value} 校验失败: {errors}"
)
self.logger.debug(f"参数更新: {key} = {value}")
self.params_changed.emit(key, value)
self._on_params_changed(key, value)
def get_param(self, key: str, default: Any = None) -> Any:
"""读取单个参数值"""
return self._params.get(key, default)
def validate_params(self) -> List[str]:
"""校验当前参数合法性"""
return self._params.validate()
# ── 参数快照 ──────────────────────────────────────────────────
def save_snapshot(self) -> int:
"""
保存当前参数快照
:return: 快照索引
"""
snapshot = copy.deepcopy(self._params)
self._snapshots.append(snapshot)
idx = len(self._snapshots) - 1
self.logger.debug(f"参数快照已保存: index={idx}")
return idx
def restore_snapshot(self, index: int = -1) -> bool:
"""
恢复参数快照
:param index: 快照索引(-1=最新)
:return: True=成功
"""
if not self._snapshots:
self.logger.warning("没有可用的参数快照")
return False
try:
snapshot = self._snapshots[index]
self._params = copy.deepcopy(snapshot)
self.logger.info(f"参数快照已恢复: index={index}")
return True
except IndexError:
self.logger.error(f"快照索引越界: {index}")
return False
def clear_snapshots(self) -> None:
"""清除所有参数快照"""
self._snapshots.clear()
# ── 抽象方法(子类必须实现)──────────────────────────────────
@abstractmethod
def _do_step(self, dt: float) -> None:
"""
核心仿真推进(子类实现)
必须更新 self._state 的所有相关字段
"""
...
@abstractmethod
def _build_initial_state(self) -> BaseState:
"""
构建初始状态快照(子类实现)
在 __init__ 和 reset() 时调用
"""
...
# ── 可选覆盖钩子 ──────────────────────────────────────────────
def _on_start(self) -> None: pass
def _on_stop(self) -> None: pass
def _on_pause(self) -> None: pass
def _on_resume(self) -> None: pass
def _on_reset(self) -> None: pass
def _on_params_changed(self, key: str, value: Any) -> None:
"""参数变更时的响应钩子(子类可覆盖)"""
pass
def _check_constraints(self) -> None:
"""
约束检查钩子(子类可覆盖)
检查速度/高度等是否越界,发射 error_occurred 告警
"""
pass
# ── 内部工具 ──────────────────────────────────────────────────
def _on_fsm_changed(self, old: str, new: str) -> None:
"""FSM 状态变化回调 → 发射 lifecycle_changed 信号"""
self.lifecycle_changed.emit(old, new)
self.logger.debug(f"生命周期变化: {old}{new}")
def _handle_error(self, msg: str) -> None:
"""统一错误处理:记录日志 + 转 ERROR 状态 + 发射信号"""
self.logger.error(msg)
self._fsm.to_error(self._sim_time)
self.error_occurred.emit(msg)
def set_initial_position(
self,
lat: float, lon: float, alt: float = 0.0,
heading: float = 0.0,
) -> None:
"""
设置初始位置(必须在 start() 之前调用)
同时更新 _state 和 _initial_state
"""
for s in (self._state, self._initial_state):
if s is not None:
s.latitude = lat
s.longitude = lon
s.altitude = alt
s.heading = heading
def __repr__(self) -> str:
return (f"{self.__class__.__name__}("
f"id={self.equipment_id}, "
f"name={self.name}, "
f"lc={self.lifecycle.value})")
class NetworkModel:
def __init__(self, network_id, url, model: BaseEquipmentModel):
self.network_id = network_id
self.url = url
self.model = model
async def _publish_state(self, state):
await self._comm_adapter.publish()
async def on_call(self, msg: Message):
logger.warning(f"🚨 告警消息 | priority={msg.priority.name} | {msg.payload}")
async def subscribe_actions(self, adapter:CommBaseAdapter, model: BaseEquipmentModel):
for action in model.actions:
await adapter.subscribe(f"{self.network_id}/{model.equipment_id}/{action}", self.on_call)
async def register_network_actions(self, adapter:CommBaseAdapter, model: BaseEquipmentModel):
for action in model.actions:
await adapter.publish(Message(topic=f"{self.network_id}",
payload={
"model_id": model.equipment_id,
"action": action,
"description": ""
}))
async def _process(self):
async with CommAdapterFactory.create(self.url) as adapter:
await self.register_network_actions(adapter, self.model)
await adapter.subscribe(f"{self.network_id}/{self.model.equipment_id}")
await self.subscribe_actions(adapter, self.model)
for model in self.model.sub_models:
await self.register_network_actions(adapter, model)
await self.subscribe_actions(adapter, model)
def start(self):
self.model.start()
asyncio.run(self._process())