base_agent/uas/uas_control/protocols/ros_adapter.py

478 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.

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())