120 lines
4.4 KiB
Python
120 lines
4.4 KiB
Python
from __future__ import annotations
|
||
|
||
from uas.uas_control import command_registry
|
||
from uas.uas_control.base_controller import BaseUASController
|
||
from uas.uas_control.command import Command, CommandResult, CommandType, Commands
|
||
from uas.uas_control.telemetry import BaseTelemetry, UAVTelemetry
|
||
|
||
|
||
# ═════════════════════════════════════════════════════════════════
|
||
# 无人机控制器
|
||
# ═════════════════════════════════════════════════════════════════
|
||
|
||
class UAVController(BaseUASController):
|
||
"""
|
||
无人机控制器
|
||
|
||
扩展无人机专有 API:
|
||
takeoff / land / rtl / loiter
|
||
set_altitude / set_gimbal / camera_shoot / follow_route
|
||
"""
|
||
|
||
@property
|
||
def device_type(self) -> str:
|
||
return "controller"
|
||
|
||
@command_registry.register
|
||
def takeoff(self, altitude: float, speed: float = 3.0) -> CommandResult:
|
||
altitude += self.get_telemetry().altitude
|
||
return self.send(Commands.takeoff(self.device_id, altitude, speed))
|
||
|
||
@command_registry.register
|
||
def land(self) -> CommandResult:
|
||
return self.send(Commands.land(self.device_id))
|
||
|
||
@command_registry.register
|
||
def rtl(self) -> CommandResult:
|
||
return self.send(Commands.rtl(self.device_id))
|
||
|
||
@command_registry.register
|
||
def loiter(self, radius: float = 50.0,
|
||
turns: int = 0) -> CommandResult:
|
||
return self.send(Command(
|
||
command_type = CommandType.LOITER,
|
||
target_id = self.device_id,
|
||
params = {"radius": radius, "turns": turns},
|
||
))
|
||
|
||
@command_registry.register
|
||
def set_altitude(self, altitude: float) -> CommandResult:
|
||
return self.send(Command(
|
||
command_type = CommandType.SET_ALTITUDE,
|
||
target_id = self.device_id,
|
||
params = {"altitude": altitude},
|
||
))
|
||
|
||
@command_registry.register
|
||
def set_gimbal(self, pitch: float, yaw: float = 0.0) -> CommandResult:
|
||
return self.send(Command(
|
||
command_type = CommandType.GIMBAL_CONTROL,
|
||
target_id = self.device_id,
|
||
params = {"pitch": pitch, "yaw": yaw},
|
||
))
|
||
|
||
@command_registry.register
|
||
def camera_shoot(self) -> CommandResult:
|
||
return self.send(Command(
|
||
command_type = CommandType.CAMERA_SHOOT,
|
||
target_id = self.device_id,
|
||
))
|
||
|
||
@command_registry.register
|
||
def camera_record(self, start: bool = True) -> CommandResult:
|
||
return self.send(Command(
|
||
command_type = CommandType.CAMERA_RECORD,
|
||
target_id = self.device_id,
|
||
params = {"start": start},
|
||
))
|
||
|
||
@command_registry.register
|
||
def follow_route(self, waypoints: list) -> CommandResult:
|
||
"""
|
||
执行预设航线
|
||
waypoints: [(lat, lon, alt, speed), ...]
|
||
"""
|
||
return self.send(Command(
|
||
command_type = CommandType.FOLLOW_ROUTE,
|
||
target_id = self.device_id,
|
||
params = {"waypoints": waypoints},
|
||
timeout_s = 3600.0,
|
||
))
|
||
|
||
# ── 遥测钩子 ──────────────────────────────────────────────────
|
||
|
||
def _on_telemetry(self, telem: BaseTelemetry) -> None:
|
||
if not isinstance(telem, UAVTelemetry):
|
||
return
|
||
if 0 < telem.battery_percent < 20:
|
||
self.error_occurred.emit(
|
||
f"{self.device_id}: 低电量告警 {telem.battery_percent}%,"
|
||
f"建议立即返航"
|
||
)
|
||
|
||
def _pre_send(self, cmd: Command) -> bool:
|
||
"""紧急停止指令跳过所有前置检查"""
|
||
if cmd.command_type == CommandType.EMERGENCY_STOP:
|
||
return True
|
||
telem = self._telemetry
|
||
if isinstance(telem, UAVTelemetry):
|
||
motion_cmds = {
|
||
CommandType.GOTO,
|
||
CommandType.SET_VELOCITY,
|
||
CommandType.FOLLOW_ROUTE,
|
||
}
|
||
if cmd.command_type in motion_cmds and not telem.armed:
|
||
self.error_occurred.emit(
|
||
f"{self.device_id}: 设备未解锁,拒绝运动指令"
|
||
)
|
||
return False
|
||
return True
|