172 lines
6.2 KiB
Python
172 lines
6.2 KiB
Python
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import math
|
|||
|
|
import random
|
|||
|
|
import time
|
|||
|
|
from typing import Any, Dict, List
|
|||
|
|
|
|||
|
|
from uas.uas_control.base_adapter import BaseProtocolAdapter, ProtocolType
|
|||
|
|
from uas.uas_control.command import Command, CommandResult, CommandType
|
|||
|
|
from uas.uas_control.telemetry import (
|
|||
|
|
BaseTelemetry, UAVTelemetry, RobotDogTelemetry, ConnectionState
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 仿真适配器(无需真实硬件,用于测试)
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class SimulationAdapter(BaseProtocolAdapter):
|
|||
|
|
"""
|
|||
|
|
仿真协议适配器
|
|||
|
|
|
|||
|
|
无需任何外部依赖,完全在内存中模拟设备行为。
|
|||
|
|
支持所有 CommandType,适合单元测试和集成演示。
|
|||
|
|
|
|||
|
|
config 字段:
|
|||
|
|
device_type : "controller" | "robot_dog"
|
|||
|
|
fail_rate : 指令随机失败概率(0.0~1.0,默认 0.0)
|
|||
|
|
latency_ms : 模拟通信延迟(ms,默认 20)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
_ALL_COMMANDS = list(CommandType)
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
device_id: str,
|
|||
|
|
device_type: str = "controller",
|
|||
|
|
fail_rate: float = 0.0,
|
|||
|
|
latency_ms: float = 20.0,
|
|||
|
|
parent = None,
|
|||
|
|
):
|
|||
|
|
config = dict(
|
|||
|
|
device_type = device_type,
|
|||
|
|
fail_rate = fail_rate,
|
|||
|
|
latency_ms = latency_ms,
|
|||
|
|
)
|
|||
|
|
super().__init__(device_id, ProtocolType.SIMULATION, config, parent)
|
|||
|
|
self._state: Dict[str, Any] = {
|
|||
|
|
"armed": False,
|
|||
|
|
"mode": "STABILIZE",
|
|||
|
|
"lat": 39.9,
|
|||
|
|
"lon": 116.4,
|
|||
|
|
"alt": 0.0,
|
|||
|
|
"heading": 0.0,
|
|||
|
|
"speed": 0.0,
|
|||
|
|
"battery": 100,
|
|||
|
|
"gait": "STAND",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def connect(self) -> bool:
|
|||
|
|
self.logger.info(
|
|||
|
|
f"[仿真] 设备连接: {self.device_id} "
|
|||
|
|
f"type={self.config['device_type']}"
|
|||
|
|
)
|
|||
|
|
self._set_connected(True)
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def disconnect(self) -> bool:
|
|||
|
|
self._set_connected(False)
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def send_command(self, cmd: Command) -> CommandResult:
|
|||
|
|
# 模拟延迟
|
|||
|
|
lat_s = self.config["latency_ms"] / 1000.0
|
|||
|
|
time.sleep(lat_s + random.uniform(0, lat_s * 0.2))
|
|||
|
|
|
|||
|
|
# 模拟随机失败
|
|||
|
|
if random.random() < self.config["fail_rate"]:
|
|||
|
|
return CommandResult.fail(cmd, "[仿真] 随机故障注入")
|
|||
|
|
|
|||
|
|
# 更新内部状态
|
|||
|
|
ct = cmd.command_type
|
|||
|
|
p = cmd.params
|
|||
|
|
|
|||
|
|
if ct == CommandType.ARM:
|
|||
|
|
self._state["armed"] = True
|
|||
|
|
elif ct == CommandType.DISARM:
|
|||
|
|
self._state["armed"] = False
|
|||
|
|
elif ct == CommandType.EMERGENCY_STOP:
|
|||
|
|
self._state["armed"] = False
|
|||
|
|
self._state["speed"] = 0.0
|
|||
|
|
elif ct == CommandType.TAKEOFF:
|
|||
|
|
self._state["alt"] = p.get("altitude", 10.0)
|
|||
|
|
self._state["speed"] = p.get("speed", 3.0)
|
|||
|
|
self._state["mode"] = "GUIDED"
|
|||
|
|
elif ct == CommandType.LAND:
|
|||
|
|
self._state["alt"] = 0.0
|
|||
|
|
self._state["speed"] = 0.0
|
|||
|
|
self._state["mode"] = "LAND"
|
|||
|
|
elif ct == CommandType.RTL:
|
|||
|
|
self._state["mode"] = "RTL"
|
|||
|
|
elif ct == CommandType.GOTO:
|
|||
|
|
self._state["lat"] = p.get("lat", self._state["lat"])
|
|||
|
|
self._state["lon"] = p.get("lon", self._state["lon"])
|
|||
|
|
self._state["alt"] = p.get("alt", self._state["alt"])
|
|||
|
|
self._state["speed"] = p.get("speed", 5.0)
|
|||
|
|
elif ct == CommandType.SET_VELOCITY:
|
|||
|
|
vx = p.get("vx", 0); vy = p.get("vy", 0)
|
|||
|
|
self._state["speed"] = math.hypot(vx, vy)
|
|||
|
|
self._state["heading"] = (
|
|||
|
|
math.degrees(math.atan2(vy, vx)) + 360
|
|||
|
|
) % 360
|
|||
|
|
elif ct == CommandType.SET_MODE:
|
|||
|
|
self._state["mode"] = p.get("mode", "STABILIZE")
|
|||
|
|
elif ct in (CommandType.SIT, CommandType.STAND,
|
|||
|
|
CommandType.WALK, CommandType.TROT):
|
|||
|
|
self._state["gait"] = ct.value.upper()
|
|||
|
|
self._state["speed"] = (
|
|||
|
|
p.get("speed", 1.0)
|
|||
|
|
if ct in (CommandType.WALK, CommandType.TROT) else 0.0
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return CommandResult.ok(
|
|||
|
|
cmd, f"[仿真] {ct.value} 执行成功",
|
|||
|
|
data=dict(self._state)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def get_telemetry(self) -> BaseTelemetry:
|
|||
|
|
t = time.time()
|
|||
|
|
s = self._state
|
|||
|
|
dt = self.config["device_type"]
|
|||
|
|
|
|||
|
|
# 模拟位置漂移
|
|||
|
|
s["lat"] += math.sin(t * 0.01) * 0.00001
|
|||
|
|
s["lon"] += math.cos(t * 0.01) * 0.00001
|
|||
|
|
s["battery"] = max(0, int(100 - self.uptime_s * 0.05))
|
|||
|
|
|
|||
|
|
base = dict(
|
|||
|
|
device_id = self.device_id,
|
|||
|
|
timestamp = t,
|
|||
|
|
connection_state = ConnectionState.CONNECTED.value,
|
|||
|
|
latitude = s["lat"],
|
|||
|
|
longitude = s["lon"],
|
|||
|
|
altitude = s["alt"],
|
|||
|
|
heading = s["heading"],
|
|||
|
|
ground_speed = s["speed"],
|
|||
|
|
battery_percent = s["battery"],
|
|||
|
|
armed = s["armed"],
|
|||
|
|
mode = s["mode"],
|
|||
|
|
gps_fix_type = 3,
|
|||
|
|
satellites_visible = 14,
|
|||
|
|
signal_strength = 90,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if dt == "controller":
|
|||
|
|
return UAVTelemetry(**{
|
|||
|
|
k: v for k, v in base.items()
|
|||
|
|
if k in UAVTelemetry.__dataclass_fields__
|
|||
|
|
}, flight_mode=s["mode"])
|
|||
|
|
else:
|
|||
|
|
return RobotDogTelemetry(**{
|
|||
|
|
k: v for k, v in base.items()
|
|||
|
|
if k in RobotDogTelemetry.__dataclass_fields__
|
|||
|
|
}, gait_mode=s["gait"])
|
|||
|
|
|
|||
|
|
def supported_commands(self) -> List[CommandType]:
|
|||
|
|
return self._ALL_COMMANDS
|
|||
|
|
|
|||
|
|
def inject_fault(self, fail_rate: float) -> None:
|
|||
|
|
"""动态注入故障率(测试用)"""
|
|||
|
|
self.config["fail_rate"] = max(0.0, min(1.0, fail_rate))
|
|||
|
|
self.logger.warning(f"故障注入: fail_rate={fail_rate:.0%}")
|