539 lines
19 KiB
Python
539 lines
19 KiB
Python
# -*- 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())
|
||
|
||
|