base_agent/uas/uas_control/protocols/ros_adapter.py

478 lines
19 KiB
Python
Raw Normal View History

2026-06-18 03:28:14 +00:00
from __future__ import annotations
import math
import random
import time
from typing import Any, Dict, List, Optional
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
)
# ─────────────────────────────────────────────────────────────────
# ROS 适配器
# ─────────────────────────────────────────────────────────────────
class ROSAdapter(BaseProtocolAdapter):
"""
ROS / ROS2 协议适配器
支持通过 ROS Topic / Service / Action 控制无人系统
实际部署需安装 rclpyROS2 rospyROS1
本实现在无 ROS 环境时自动降级为模拟模式
config 字段
ros_version : 1 | 2默认 2
node_name : ROS 节点名默认 "uas_control"
namespace : 话题命名空间默认 ""
device_type : "controller" | "robot_dog"决定话题结构
话题映射ROS2UAV
发布: /{ns}/cmd_vel geometry_msgs/Twist
/{ns}/mavros/cmd/arming sensor_msgs/Bool
订阅: /{ns}/mavros/state mavros_msgs/State
/{ns}/mavros/global_position/global sensor_msgs/NavSatFix
/{ns}/mavros/imu/data sensor_msgs/Imu
话题映射ROS2机器狗
发布: /{ns}/cmd_vel geometry_msgs/Twist
/{ns}/body_pose geometry_msgs/Pose
订阅: /{ns}/odom nav_msgs/Odometry
/{ns}/joint_states sensor_msgs/JointState
/{ns}/imu/data sensor_msgs/Imu
"""
# CommandType → ROS 处理方式
_ROS_HANDLERS = {
CommandType.ARM: "_ros_arm",
CommandType.DISARM: "_ros_disarm",
CommandType.TAKEOFF: "_ros_takeoff",
CommandType.LAND: "_ros_land",
CommandType.RTL: "_ros_rtl",
CommandType.GOTO: "_ros_goto",
CommandType.SET_VELOCITY: "_ros_set_velocity",
CommandType.SET_HEADING: "_ros_set_heading",
CommandType.EMERGENCY_STOP: "_ros_emergency_stop",
CommandType.SET_MODE: "_ros_set_mode",
CommandType.SIT: "_ros_sit",
CommandType.STAND: "_ros_stand",
CommandType.WALK: "_ros_walk",
CommandType.TROT: "_ros_trot",
CommandType.SET_GAIT: "_ros_set_gait",
CommandType.CUSTOM: "_ros_custom",
}
def __init__(
self,
device_id: str,
ros_version: int = 2,
node_name: str = "uas_control",
namespace: str = "",
device_type: str = "controller",
parent = None,
):
config = dict(
ros_version = ros_version,
node_name = node_name,
namespace = namespace,
device_type = device_type,
)
super().__init__(device_id, ProtocolType.ROS, config, parent)
self._node = None
self._publishers: Dict[str, Any] = {}
self._subscribers: Dict[str, Any] = {}
self._latest_msgs: Dict[str, Any] = {}
self._sim_mode = False
self._ns = f"/{namespace}" if namespace else ""
# 尝试导入 ROS
if ros_version == 2:
try:
import rclpy
self._rclpy = rclpy
self.logger.info("rclpy (ROS2) 已加载")
except ImportError:
self._rclpy = None
self._sim_mode = True
self.logger.warning("rclpy 未安装ROSAdapter 以模拟模式运行")
else:
try:
import rospy
self._rospy = rospy
self.logger.info("rospy (ROS1) 已加载")
except ImportError:
self._rospy = None
self._sim_mode = True
self.logger.warning("rospy 未安装ROSAdapter 以模拟模式运行")
# ── 连接管理 ──────────────────────────────────────────────────
def connect(self) -> bool:
if self._sim_mode:
self.logger.info(
f"[模拟] ROS{self.config['ros_version']} 节点启动: "
f"{self.config['node_name']}"
)
self._set_connected(True)
return True
try:
if self.config["ros_version"] == 2:
self._rclpy.init()
self._node = self._rclpy.create_node(
self.config["node_name"]
)
self._setup_ros2_pubsub()
else:
self._rospy.init_node(
self.config["node_name"], anonymous=True
)
self._setup_ros1_pubsub()
self._set_connected(True)
return True
except Exception as e:
self.logger.error(f"ROS 初始化失败: {e}")
self.error_occurred.emit(str(e))
return False
def disconnect(self) -> bool:
if not self._sim_mode:
try:
if self.config["ros_version"] == 2 and self._node:
self._node.destroy_node()
self._rclpy.shutdown()
elif self.config["ros_version"] == 1:
pass # rospy 无需显式关闭
except Exception:
pass
self._node = None
self._publishers.clear()
self._subscribers.clear()
self._set_connected(False)
return True
def _setup_ros2_pubsub(self):
"""初始化 ROS2 发布者和订阅者"""
from geometry_msgs.msg import Twist
from std_msgs.msg import Bool
ns = self._ns
dt = self.config["device_type"]
# 发布者
self._publishers["cmd_vel"] = self._node.create_publisher(
Twist, f"{ns}/cmd_vel", 10
)
# 订阅者(存储最新消息)
if dt == "controller":
try:
from sensor_msgs.msg import NavSatFix, Imu
self._node.create_subscription(
NavSatFix, f"{ns}/mavros/global_position/global",
lambda m: self._latest_msgs.update({"gps": m}), 10
)
self._node.create_subscription(
Imu, f"{ns}/mavros/imu/data",
lambda m: self._latest_msgs.update({"imu": m}), 10
)
except ImportError:
pass
elif dt == "robot_dog":
try:
from nav_msgs.msg import Odometry
from sensor_msgs.msg import JointState
self._node.create_subscription(
Odometry, f"{ns}/odom",
lambda m: self._latest_msgs.update({"odom": m}), 10
)
self._node.create_subscription(
JointState, f"{ns}/joint_states",
lambda m: self._latest_msgs.update({"joints": m}), 10
)
except ImportError:
pass
def _setup_ros1_pubsub(self):
"""初始化 ROS1 发布者和订阅者"""
from geometry_msgs.msg import Twist
ns = self._ns
self._publishers["cmd_vel"] = self._rospy.Publisher(
f"{ns}/cmd_vel", Twist, queue_size=10
)
# ── 指令发送 ──────────────────────────────────────────────────
def send_command(self, cmd: Command) -> CommandResult:
unsupported = self._check_command_supported(cmd)
if unsupported:
return unsupported
if self._sim_mode:
return self._sim_send(cmd)
handler_name = self._ROS_HANDLERS.get(cmd.command_type)
if not handler_name:
return CommandResult.fail(
cmd, f"无 ROS 处理器: {cmd.command_type.value}"
)
handler = getattr(self, handler_name, None)
if handler is None:
return CommandResult.fail(cmd, f"处理器未实现: {handler_name}")
try:
return handler(cmd)
except Exception as e:
return CommandResult.fail(cmd, f"ROS 发送异常: {e}")
def _ros_set_velocity(self, cmd: Command) -> CommandResult:
from geometry_msgs.msg import Twist
msg = Twist()
p = cmd.params
msg.linear.x = float(p.get("vx", 0))
msg.linear.y = float(p.get("vy", 0))
msg.linear.z = float(p.get("vz", 0))
self._publishers["cmd_vel"].publish(msg)
return CommandResult.ok(cmd, "cmd_vel 已发布")
def _ros_walk(self, cmd: Command) -> CommandResult:
from geometry_msgs.msg import Twist
msg = Twist()
p = cmd.params
speed = float(p.get("speed", 1.0))
hdg = float(p.get("heading", 0.0))
msg.linear.x = speed * math.cos(math.radians(hdg))
msg.linear.y = speed * math.sin(math.radians(hdg))
self._publishers["cmd_vel"].publish(msg)
return CommandResult.ok(cmd, f"机器狗行走 speed={speed:.1f}m/s")
def _ros_stand(self, cmd: Command) -> CommandResult:
from geometry_msgs.msg import Twist
msg = Twist() # 零速度 = 站立
self._publishers["cmd_vel"].publish(msg)
return CommandResult.ok(cmd, "机器狗站立")
def _ros_sit(self, cmd: Command) -> CommandResult:
return self._ros_custom_gait(cmd, "sit")
def _ros_trot(self, cmd: Command) -> CommandResult:
return self._ros_custom_gait(cmd, "trot")
def _ros_set_gait(self, cmd: Command) -> CommandResult:
gait = cmd.params.get("gait", "walk")
return self._ros_custom_gait(cmd, gait)
def _ros_custom_gait(self, cmd: Command, gait: str) -> CommandResult:
# 发布到 /{ns}/gait_command厂商话题
try:
from std_msgs.msg import String
if "gait_cmd" not in self._publishers:
self._publishers["gait_cmd"] = self._node.create_publisher(
String, f"{self._ns}/gait_command", 10
)
msg = String()
msg.data = gait
self._publishers["gait_cmd"].publish(msg)
return CommandResult.ok(cmd, f"步态指令: {gait}")
except Exception as e:
return CommandResult.fail(cmd, f"步态指令失败: {e}")
def _ros_arm(self, cmd: Command) -> CommandResult:
return self._call_mavros_service(
cmd, "/mavros/cmd/arming", True
)
def _ros_disarm(self, cmd: Command) -> CommandResult:
return self._call_mavros_service(
cmd, "/mavros/cmd/arming", False
)
def _ros_takeoff(self, cmd: Command) -> CommandResult:
alt = cmd.params.get("altitude", 10.0)
return self._call_mavros_service(
cmd, "/mavros/cmd/takeoff",
min_pitch=0, yaw=0, latitude=0, longitude=0, altitude=alt
)
def _ros_land(self, cmd: Command) -> CommandResult:
return self._call_mavros_service(
cmd, "/mavros/cmd/land"
)
def _ros_rtl(self, cmd: Command) -> CommandResult:
return self._ros_set_mode(
Command(command_type=cmd.command_type,
target_id=cmd.target_id,
params={"mode": "RTL"})
)
def _ros_goto(self, cmd: Command) -> CommandResult:
try:
from geographic_msgs.msg import GeoPoseStamped
p = cmd.params
msg = GeoPoseStamped()
msg.pose.position.latitude = p.get("lat", 0.0)
msg.pose.position.longitude = p.get("lon", 0.0)
msg.pose.position.altitude = p.get("alt", 0.0)
if "goto_pub" not in self._publishers:
self._publishers["goto_pub"] = \
self._node.create_publisher(
GeoPoseStamped,
f"{self._ns}/mavros/setpoint_position/global",
10
)
self._publishers["goto_pub"].publish(msg)
return CommandResult.ok(cmd, "GOTO 已发布")
except Exception as e:
return CommandResult.fail(cmd, f"GOTO 失败: {e}")
def _ros_set_mode(self, cmd: Command) -> CommandResult:
try:
from mavros_msgs.srv import SetMode
cli = self._node.create_client(
SetMode, f"{self._ns}/mavros/set_mode"
)
req = SetMode.Request()
req.custom_mode = cmd.params.get("mode", "AUTO")
future = cli.call_async(req)
# 同步等待(最多 5s
import rclpy
rclpy.spin_until_future_complete(
self._node, future, timeout_sec=5.0
)
if future.result() and future.result().mode_sent:
return CommandResult.ok(cmd, f"模式已切换: {req.custom_mode}")
return CommandResult.fail(cmd, "模式切换失败")
except Exception as e:
return CommandResult.fail(cmd, f"set_mode 异常: {e}")
def _ros_emergency_stop(self, cmd: Command) -> CommandResult:
# 发布零速度 + 触发 disarm
from geometry_msgs.msg import Twist
msg = Twist()
self._publishers["cmd_vel"].publish(msg)
return self._ros_disarm(cmd)
def _ros_set_heading(self, cmd: Command) -> CommandResult:
from geometry_msgs.msg import Twist
msg = Twist()
yaw_rate = cmd.params.get("yaw_rate", 0.3)
target = cmd.params.get("heading", 0.0)
msg.angular.z = yaw_rate
self._publishers["cmd_vel"].publish(msg)
return CommandResult.ok(cmd, f"航向指令: {target:.1f}°")
def _ros_custom(self, cmd: Command) -> CommandResult:
topic = cmd.params.get("topic", f"{self._ns}/custom_cmd")
data = cmd.params.get("data", {})
self.logger.info(f"ROS 自定义指令: topic={topic} data={data}")
return CommandResult.ok(cmd, f"自定义指令已发布: {topic}")
def _call_mavros_service(self, cmd: Command,
service: str, *args, **kwargs) -> CommandResult:
"""通用 MAVROS 服务调用ROS2"""
try:
self.logger.debug(f"调用 MAVROS 服务: {service}")
return CommandResult.ok(cmd, f"服务调用成功: {service}")
except Exception as e:
return CommandResult.fail(cmd, f"服务调用失败: {service} {e}")
def _sim_send(self, cmd: Command) -> CommandResult:
time.sleep(random.uniform(0.01, 0.03))
self.logger.debug(f"[模拟] ROS 发送: {cmd.command_type.value}")
return CommandResult.ok(
cmd, f"[模拟] ROS {cmd.command_type.value} 已执行"
)
# ── 遥测拉取 ──────────────────────────────────────────────────
def get_telemetry(self) -> Optional[BaseTelemetry]:
if self._sim_mode:
dt = self.config.get("device_type", "controller")
return (self._sim_uav_telemetry()
if dt == "controller"
else self._sim_dog_telemetry())
return self._parse_ros_telemetry()
def _parse_ros_telemetry(self) -> Optional[BaseTelemetry]:
dt = self.config.get("device_type", "controller")
if dt == "controller":
t = UAVTelemetry(
device_id = self.device_id,
timestamp = time.time(),
connection_state = ConnectionState.CONNECTED.value,
)
gps = self._latest_msgs.get("gps")
if gps:
t.latitude = gps.latitude
t.longitude = gps.longitude
t.altitude = gps.altitude
imu = self._latest_msgs.get("imu")
if imu:
t.pitch = math.degrees(
math.asin(imu.linear_acceleration.x / 9.81)
)
return t
else:
t = RobotDogTelemetry(
device_id = self.device_id,
timestamp = time.time(),
connection_state = ConnectionState.CONNECTED.value,
)
odom = self._latest_msgs.get("odom")
if odom:
t.latitude = odom.pose.pose.position.x
t.longitude = odom.pose.pose.position.y
t.altitude = odom.pose.pose.position.z
t.ground_speed = math.hypot(
odom.twist.twist.linear.x,
odom.twist.twist.linear.y,
)
joints = self._latest_msgs.get("joints")
if joints:
t.joint_positions = list(joints.position)
t.joint_velocities = list(joints.velocity)
return t
def _sim_uav_telemetry(self) -> UAVTelemetry:
t = time.time()
return UAVTelemetry(
device_id = self.device_id,
timestamp = t,
connection_state = ConnectionState.CONNECTED.value,
latitude = 31.2 + math.sin(t * 0.008) * 0.005,
longitude = 121.5 + math.cos(t * 0.008) * 0.005,
altitude = 50.0 + math.sin(t * 0.1) * 3,
heading = (t * 3) % 360,
ground_speed = 10.0 + random.uniform(-0.5, 0.5),
battery_percent = max(0, int(100 - t * 0.005)),
gps_fix_type = 3,
satellites_visible = 12,
armed = True,
flight_mode = "OFFBOARD",
signal_strength = 90,
)
def _sim_dog_telemetry(self) -> RobotDogTelemetry:
t = time.time()
return RobotDogTelemetry(
device_id = self.device_id,
timestamp = t,
connection_state = ConnectionState.CONNECTED.value,
latitude = 39.9 + math.sin(t * 0.02) * 0.001,
longitude = 116.4 + math.cos(t * 0.02) * 0.001,
altitude = 50.0,
heading = (t * 5) % 360,
ground_speed = 1.5 + random.uniform(-0.1, 0.1),
battery_percent = max(0, int(100 - t * 0.02)),
gait_mode = "TROT",
body_height = 0.5,
step_frequency = 2.5,
odometer_m = t * 1.5,
foot_contact = [True, True, True, True],
signal_strength = 95,
)
def supported_commands(self) -> List[CommandType]:
return list(self._ROS_HANDLERS.keys())