Go2 DDS bridge is added.

This commit is contained in:
Rooholla-KhorramBakht 2025-01-25 12:01:51 +08:00
parent a5749756c4
commit 030326f0b4
13 changed files with 2452 additions and 22 deletions

354
Go2Py/control/neuro_diff.py Normal file
View File

@ -0,0 +1,354 @@
import os
import time
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import onnxruntime
from models import model_utils
class ActorStochasticMLP(nn.Module):
def __init__(self):
super(ActorStochasticMLP, self).__init__()
self.hidden_size = 256
obs_dim = 49
action_dim = 12
self.gru = nn.GRU(obs_dim, self.hidden_size, batch_first=True)
self.layer_dims = [obs_dim + self.hidden_size] + [256, 128, 64] + [action_dim]
init_ = lambda m: model_utils.init(m, nn.init.orthogonal_, lambda x: nn.init.
constant_(x, 0), np.sqrt(2))
modules = []
for i in range(len(self.layer_dims) - 1):
modules.append(nn.Linear(self.layer_dims[i], self.layer_dims[i + 1]))
if i < len(self.layer_dims) - 2:
modules.append(model_utils.get_activation_func('elu'))
modules.append(torch.nn.LayerNorm(self.layer_dims[i+1]))
else:
modules.append(model_utils.get_activation_func('identity'))
self.mu_net = nn.Sequential(*modules)
logstd = -1.0
self.logstd = torch.nn.Parameter(torch.ones(action_dim, dtype=torch.float32) * logstd)
self.action_dim = action_dim
self.obs_dim = obs_dim
self.min_logstd = -1.427
print(self.mu_net)
print(self.logstd)
def forward(self, obs, l):
out, time_latent = self.gru(obs, l)
x = torch.cat([obs, out], dim=-1)
mu = self.mu_net(x)
return mu, time_latent
class Policy:
def __init__(self, checkpoint_path):
self.agent = ActorStochasticMLP()
agent_init, _, _, self.obs_rms, _, _ = torch.load(checkpoint_path, map_location="cpu")
self.agent.load_state_dict(agent_init.state_dict())
onnx_file_name = checkpoint_path.replace(".pt", ".onnx")
self.actor_hidden_in = np.zeros((1, 256), dtype=np.float32)
dummy_input = torch.randn(1, 49)
with torch.no_grad():
torch_out, torch_hidden_out = self.agent(dummy_input, l = torch.tensor(self.actor_hidden_in))
torch.onnx.export(
self.agent, # The model being converted
(dummy_input, torch.tensor(self.actor_hidden_in)), # An example input for the model
onnx_file_name, # Output file name
export_params=True, # Store trained parameter weights inside the model file
opset_version=11, # ONNX version (opset) to export to, adjust as needed
do_constant_folding=True, # Whether to perform constant folding optimization
input_names=['input', 'hidden_in'], # Name of the input in the ONNX graph (can be customized)
output_names=['action', 'hidden_out'], # Name of the output (assuming get_action and get_value are key outputs)
)
self.ort_session = onnxruntime.InferenceSession(onnx_file_name, providers=["CPUExecutionProvider"])
ort_inputs = {'input': dummy_input.numpy(), 'hidden_in': self.actor_hidden_in}
ort_outs = self.ort_session.run(None, ort_inputs)
action, hidden_out = ort_outs[0], ort_outs[1]
np.testing.assert_allclose(torch_out.numpy(), action, rtol=1e-03, atol=1e-05)
np.testing.assert_allclose(torch_hidden_out.numpy(), hidden_out, rtol=1e-03, atol=1e-05)
print("Exported model has been tested with ONNXRuntime, and the result looks good!")
def __call__(self, obs, info):
obs = torch.cat(
[torch.zeros((1, 3)), torch.tensor(obs[np.newaxis]), torch.zeros((1, 9))],
dim = -1
)
obs = self.obs_rms.normalize(obs)[:, 3:-9].numpy().astype(np.float32)
ort_inputs = {'input': obs, 'hidden_in': self.actor_hidden_in}
ort_outs = self.ort_session.run(None, ort_inputs)
self.actor_hidden_in = ort_outs[1]
return ort_outs[0]
class CommandInterface:
def __init__(self, limits=None):
self.limits = limits
self.x_vel_cmd, self.y_vel_cmd, self.yaw_vel_cmd = 0.0, 0.0, 0.0
def get_command(self):
command = np.zeros((3,))
command[0] = self.x_vel_cmd
command[1] = self.y_vel_cmd
command[2] = self.yaw_vel_cmd
return command, False
class NeuroDiffAgent:
def __init__(self, command_profile, robot):
self.robot = robot
self.command_profile = command_profile
# self.lcm_bridge = LCMBridgeClient(robot_name=self.robot_name)
self.sim_dt = 0.001
self.decimation = 20
self.dt = self.sim_dt * self.decimation
self.timestep = 0
self.device = "cpu"
joint_names = [
"FL_hip_joint",
"FL_thigh_joint",
"FL_calf_joint",
"FR_hip_joint",
"FR_thigh_joint",
"FR_calf_joint",
"RL_hip_joint",
"RL_thigh_joint",
"RL_calf_joint",
"RR_hip_joint",
"RR_thigh_joint",
"RR_calf_joint",
]
policy_joint_names = joint_names
unitree_joint_names = [
"FR_hip_joint",
"FR_thigh_joint",
"FR_calf_joint",
"FL_hip_joint",
"FL_thigh_joint",
"FL_calf_joint",
"RR_hip_joint",
"RR_thigh_joint",
"RR_calf_joint",
"RL_hip_joint",
"RL_thigh_joint",
"RL_calf_joint",
]
policy_to_unitree_map = []
unitree_to_policy_map = []
for i, policy_joint_name in enumerate(policy_joint_names):
id = np.where([name == policy_joint_name for name in unitree_joint_names])[0][0]
policy_to_unitree_map.append((i, id))
self.policy_to_unitree_map = np.array(policy_to_unitree_map).astype(np.uint32)
for i, unitree_joint_name in enumerate(unitree_joint_names):
id = np.where([name == unitree_joint_name for name in policy_joint_names])[0][0]
unitree_to_policy_map.append((i, id))
self.unitree_to_policy_map = np.array(unitree_to_policy_map).astype(np.uint32)
default_joint_angles = {
"FL_hip_joint": 0.1,
"RL_hip_joint": 0.1,
"FR_hip_joint": -0.1,
"RR_hip_joint": -0.1,
"FL_thigh_joint": 0.8,
"RL_thigh_joint": 1.0,
"FR_thigh_joint": 0.8,
"RR_thigh_joint": 1.0,
"FL_calf_joint": -1.5,
"RL_calf_joint": -1.5,
"FR_calf_joint": -1.5,
"RR_calf_joint": -1.5
}
self.default_dof_pos = np.array(
[
default_joint_angles[name]
for name in joint_names
]
)
self.default_dof_pos = self.default_dof_pos
self.p_gains = np.zeros(12)
self.d_gains = np.zeros(12)
for i in range(12):
self.p_gains[i] = 20.0
self.d_gains[i] = 0.5
print(f"p_gains: {self.p_gains}")
self.commands = np.zeros(3)
self.actions = np.zeros((1, 12))
self.last_actions = np.zeros((1,12))
self.gravity_vector = np.zeros(3)
self.dof_pos = np.zeros(12)
self.dof_vel = np.zeros(12)
self.pos = np.zeros(3)
self.body_linear_vel = np.zeros(3)
self.body_angular_vel = np.zeros(3)
self.joint_pos_target = np.zeros(12)
self.joint_vel_target = np.zeros(12)
self.prev_joint_acc = None
self.torques = np.zeros(12)
self.contact_state = np.ones(4)
self.foot_contact_forces_mag = np.zeros(4)
self.prev_foot_contact_forces_mag = np.zeros(4)
self.test = 0
self.gait_indices = np.zeros(1, dtype=np.float32)
self.clock_inputs = np.zeros(4, dtype=np.float32)
def wait_for_state(self):
# return self.lcm_bridge.getStates(timeout=2)
pass
def get_obs(self):
cmds, reset_timer = self.command_profile.get_command()
self.commands[:] = cmds
# self.state = self.wait_for_state()
joint_state = self.robot.getJointStates()
if joint_state is not None:
self.gravity_vector = self.robot.getGravityInBody()
self.prev_dof_pos = self.dof_pos.copy()
self.dof_pos = np.array(joint_state['q'])[self.unitree_to_policy_map[:, 1]]
self.prev_dof_vel = self.dof_vel.copy()
self.dof_vel = np.array(joint_state['dq'])[self.unitree_to_policy_map[:, 1]]
self.body_angular_vel = self.robot.getIMU()["gyro"]
try:
self.foot_contact_forces_mag = self.robot.getFootContact()
self.body_linear_vel = self.robot.getLinVel()
self.pos, _ = self.robot.getPose()
except:
pass
ob = np.concatenate(
(
self.body_angular_vel * 0.25,
self.commands * np.array([2.0, 2.0, 0.25]),
self.gravity_vector[:, 0],
self.dof_pos * 1.0,
#((self.dof_pos - self.prev_dof_pos) / self.dt) * 0.05,
self.dof_vel * 0.05,
self.last_actions[0],
self.clock_inputs
),
axis=0,
)
#return torch.tensor(ob, device=self.device).float()
return ob
def publish_action(self, action, hard_reset=False):
# command_for_robot = UnitreeLowCommand()
#self.joint_pos_target = (
# action[0, :12].detach().cpu().numpy() * 0.25
#).flatten()
self.joint_pos_target = (
action[0, :12] * 0.25
).flatten()
self.joint_pos_target += self.default_dof_pos
self.joint_vel_target = np.zeros(12)
# command_for_robot.q_des = self.joint_pos_target
# command_for_robot.dq_des = self.joint_vel_target
# command_for_robot.kp = self.p_gains
# command_for_robot.kd = self.d_gains
# command_for_robot.tau_ff = np.zeros(12)
if hard_reset:
command_for_robot.id = -1
self.torques = (self.joint_pos_target - self.dof_pos) * self.p_gains + (
self.joint_vel_target - self.dof_vel
) * self.d_gains
# self.lcm_bridge.sendCommands(command_for_robot)
self.robot.setCommands(self.joint_pos_target[self.policy_to_unitree_map[:, 1]],
self.joint_vel_target[self.policy_to_unitree_map[:, 1]],
self.p_gains[self.policy_to_unitree_map[:, 1]],
self.d_gains[self.policy_to_unitree_map[:, 1]],
np.zeros(12))
def reset(self):
self.actions = torch.zeros((1, 12))
self.time = time.time()
self.timestep = 0
return self.get_obs()
def step(self, actions, hard_reset=False):
self.last_actions = self.actions[:]
self.actions = actions
self.publish_action(self.actions, hard_reset=hard_reset)
# time.sleep(max(self.dt - (time.time() - self.time), 0))
# if self.timestep % 100 == 0:
# print(f"frq: {1 / (time.time() - self.time)} Hz")
self.time = time.time()
#obs = self.get_obs()
joint_acc = np.abs(self.prev_dof_vel - self.dof_vel) / self.dt
if self.prev_joint_acc is None:
self.prev_joint_acc = np.zeros_like(joint_acc)
joint_jerk = np.abs(self.prev_joint_acc - joint_acc) / self.dt
self.prev_joint_acc = joint_acc.copy()
# clock accounting
frequencies = 3.
phases = 0.5
offsets = 0.
bounds = 0
self.gait_indices = np.remainder(
self.gait_indices + self.dt * frequencies, 1.0
)
self.foot_indices = [
self.gait_indices + phases + offsets + bounds,
self.gait_indices + offsets,
self.gait_indices + bounds,
self.gait_indices + phases,
]
self.clock_inputs[0] = np.sin(2 * np.pi * self.foot_indices[0])
self.clock_inputs[1] = np.sin(2 * np.pi * self.foot_indices[1])
self.clock_inputs[2] = np.sin(2 * np.pi * self.foot_indices[2])
self.clock_inputs[3] = np.sin(2 * np.pi * self.foot_indices[3])
foot_contact_rate = np.abs(self.foot_contact_forces_mag - self.prev_foot_contact_forces_mag)
self.prev_foot_contact_forces_mag = self.foot_contact_forces_mag.copy()
infos = {
"joint_pos": self.dof_pos[np.newaxis, :],
"joint_vel": self.dof_vel[np.newaxis, :],
"joint_pos_target": self.joint_pos_target[np.newaxis, :],
"joint_vel_target": self.joint_vel_target[np.newaxis, :],
"body_linear_vel": self.body_linear_vel[np.newaxis, :],
"body_angular_vel": self.body_angular_vel[np.newaxis, :],
"body_pos": self.pos[np.newaxis, :].copy(),
"contact_state": self.contact_state[np.newaxis, :],
"body_linear_vel_cmd": self.commands[np.newaxis, 0:2],
"body_angular_vel_cmd": self.commands[np.newaxis, 2:],
"torques": self.torques,
"foot_contact_forces_mag": self.foot_contact_forces_mag.copy(),
"joint_acc": joint_acc[np.newaxis, :],
"joint_jerk": joint_jerk[np.newaxis, :],
"foot_contact_rate": foot_contact_rate[np.newaxis, :],
}
self.timestep += 1
return None, None, None, infos

329
Go2Py/utils/point_cloud2.py Normal file
View File

@ -0,0 +1,329 @@
# Copyright 2008 Willow Garage, Inc.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# * Neither the name of the Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Serialization of sensor_msgs.PointCloud2 messages.
Author: Tim Field
ROS 2 port by Sebastian Grans
File originally ported from:
https://github.com/ros/common_msgs/blob/f48b00d43cdb82ed9367e0956db332484f676598/
sensor_msgs/src/sensor_msgs/point_cloud2.py
"""
import array
from collections import namedtuple
import sys
from typing import Iterable, List, NamedTuple, Optional
import numpy as np
try:
from numpy.lib.recfunctions import (structured_to_unstructured, unstructured_to_structured)
except ImportError:
from sensor_msgs_py.numpy_compat import (structured_to_unstructured,
unstructured_to_structured)
from sensor_msgs.msg import PointCloud2, PointField
from std_msgs.msg import Header
_DATATYPES = {}
_DATATYPES[PointField.INT8] = np.dtype(np.int8)
_DATATYPES[PointField.UINT8] = np.dtype(np.uint8)
_DATATYPES[PointField.INT16] = np.dtype(np.int16)
_DATATYPES[PointField.UINT16] = np.dtype(np.uint16)
_DATATYPES[PointField.INT32] = np.dtype(np.int32)
_DATATYPES[PointField.UINT32] = np.dtype(np.uint32)
_DATATYPES[PointField.FLOAT32] = np.dtype(np.float32)
_DATATYPES[PointField.FLOAT64] = np.dtype(np.float64)
DUMMY_FIELD_PREFIX = 'unnamed_field'
def read_points(
cloud: PointCloud2,
field_names: Optional[List[str]] = None,
skip_nans: bool = False,
uvs: Optional[Iterable] = None,
reshape_organized_cloud: bool = False) -> np.ndarray:
"""
Read points from a sensor_msgs.PointCloud2 message.
:param cloud: The point cloud to read from sensor_msgs.PointCloud2.
:param field_names: The names of fields to read. If None, read all fields.
(Type: Iterable, Default: None)
:param skip_nans: If True, then don't return any point with a NaN value.
(Type: Bool, Default: False)
:param uvs: If specified, then only return the points at the given
coordinates. (Type: Iterable, Default: None)
:param reshape_organized_cloud: Returns the array as an 2D organized point cloud if set.
:return: Structured NumPy array containing all points.
"""
assert isinstance(cloud, PointCloud2), \
'Cloud is not a sensor_msgs.msg.PointCloud2'
# Cast bytes to numpy array
points = np.ndarray(
shape=(cloud.width * cloud.height, ),
dtype=dtype_from_fields(cloud.fields, point_step=cloud.point_step),
buffer=cloud.data)
# Keep only the requested fields
if field_names is not None:
assert all(field_name in points.dtype.names for field_name in field_names), \
'Requests field is not in the fields of the PointCloud!'
# Mask fields
points = points[list(field_names)]
# Swap array if byte order does not match
if bool(sys.byteorder != 'little') != bool(cloud.is_bigendian):
points = points.byteswap(inplace=True)
# Check if we want to drop points with nan values
if skip_nans and not cloud.is_dense:
# Init mask which selects all points
not_nan_mask = np.ones(len(points), dtype=bool)
for field_name in points.dtype.names:
# Only keep points without any non values in the mask
not_nan_mask = np.logical_and(
not_nan_mask, ~np.isnan(points[field_name]))
# Select these points
points = points[not_nan_mask]
# Select points indexed by the uvs field
if uvs is not None:
# Don't convert to numpy array if it is already one
if not isinstance(uvs, np.ndarray):
uvs = np.fromiter(uvs, int)
# Index requested points
points = points[uvs]
# Cast into 2d array if cloud is 'organized'
if reshape_organized_cloud and cloud.height > 1:
points = points.reshape(cloud.width, cloud.height)
return points
def read_points_numpy(
cloud: PointCloud2,
field_names: Optional[List[str]] = None,
skip_nans: bool = False,
uvs: Optional[Iterable] = None,
reshape_organized_cloud: bool = False) -> np.ndarray:
"""
Read equally typed fields from sensor_msgs.PointCloud2 message as a unstructured numpy array.
This method is better suited if one wants to perform math operations
on e.g. all x,y,z fields.
But it is limited to fields with the same dtype as unstructured numpy arrays
only contain one dtype.
:param cloud: The point cloud to read from sensor_msgs.PointCloud2.
:param field_names: The names of fields to read. If None, read all fields.
(Type: Iterable, Default: None)
:param skip_nans: If True, then don't return any point with a NaN value.
(Type: Bool, Default: False)
:param uvs: If specified, then only return the points at the given
coordinates. (Type: Iterable, Default: None)
:param reshape_organized_cloud: Returns the array as an 2D organized point cloud if set.
:return: Numpy array containing all points.
"""
assert all(cloud.fields[0].datatype == field.datatype for field in cloud.fields[1:]), \
'All fields need to have the same datatype. Use `read_points()` otherwise.'
structured_numpy_array = read_points(
cloud, field_names, skip_nans, uvs, reshape_organized_cloud)
return structured_to_unstructured(structured_numpy_array)
def read_points_list(
cloud: PointCloud2,
field_names: Optional[List[str]] = None,
skip_nans: bool = False,
uvs: Optional[Iterable] = None) -> List[NamedTuple]:
"""
Read points from a sensor_msgs.PointCloud2 message.
This function returns a list of namedtuples. It operates on top of the
read_points method. For more efficient access use read_points directly.
:param cloud: The point cloud to read from. (Type: sensor_msgs.PointCloud2)
:param field_names: The names of fields to read. If None, read all fields.
(Type: Iterable, Default: None)
:param skip_nans: If True, then don't return any point with a NaN value.
(Type: Bool, Default: False)
:param uvs: If specified, then only return the points at the given
coordinates. (Type: Iterable, Default: None]
:return: List of namedtuples containing the values for each point
"""
assert isinstance(cloud, PointCloud2), \
'cloud is not a sensor_msgs.msg.PointCloud2'
if field_names is None:
field_names = [f.name for f in cloud.fields]
Point = namedtuple('Point', field_names)
return [Point._make(p) for p in read_points(cloud, field_names,
skip_nans, uvs)]
def dtype_from_fields(fields: Iterable[PointField], point_step: Optional[int] = None) -> np.dtype:
"""
Convert a Iterable of sensor_msgs.msg.PointField messages to a np.dtype.
:param fields: The point cloud fields.
(Type: iterable of sensor_msgs.msg.PointField)
:param point_step: Point step size in bytes. Calculated from the given fields by default.
(Type: optional of integer)
:returns: NumPy datatype
"""
# Create a lists containing the names, offsets and datatypes of all fields
field_names = []
field_offsets = []
field_datatypes = []
for i, field in enumerate(fields):
# Datatype as numpy datatype
datatype = _DATATYPES[field.datatype]
# Name field
if field.name == '':
name = f'{DUMMY_FIELD_PREFIX}_{i}'
else:
name = field.name
# Handle fields with count > 1 by creating subfields with a suffix consiting
# of "_" followed by the subfield counter [0 -> (count - 1)]
assert field.count > 0, "Can't process fields with count = 0."
for a in range(field.count):
# Add suffix if we have multiple subfields
if field.count > 1:
subfield_name = f'{name}_{a}'
else:
subfield_name = name
assert subfield_name not in field_names, 'Duplicate field names are not allowed!'
field_names.append(subfield_name)
# Create new offset that includes subfields
field_offsets.append(field.offset + a * datatype.itemsize)
field_datatypes.append(datatype.str)
# Create dtype
dtype_dict = {
'names': field_names,
'formats': field_datatypes,
'offsets': field_offsets
}
if point_step is not None:
dtype_dict['itemsize'] = point_step
return np.dtype(dtype_dict)
def create_cloud(
header: Header,
fields: Iterable[PointField],
points: Iterable) -> PointCloud2:
"""
Create a sensor_msgs.msg.PointCloud2 message.
:param header: The point cloud header. (Type: std_msgs.msg.Header)
:param fields: The point cloud fields.
(Type: iterable of sensor_msgs.msg.PointField)
:param points: The point cloud points. List of iterables, i.e. one iterable
for each point, with the elements of each iterable being the
values of the fields for that point (in the same order as
the fields parameter)
:return: The point cloud as sensor_msgs.msg.PointCloud2
"""
# Check if input is numpy array
if isinstance(points, np.ndarray):
# Check if this is an unstructured array
if points.dtype.names is None:
assert all(fields[0].datatype == field.datatype for field in fields[1:]), \
'All fields need to have the same datatype. Pass a structured NumPy array \
with multiple dtypes otherwise.'
# Convert unstructured to structured array
points = unstructured_to_structured(
points,
dtype=dtype_from_fields(fields))
else:
assert points.dtype == dtype_from_fields(fields), \
'PointFields and structured NumPy array dtype do not match for all fields! \
Check their field order, names and types.'
else:
# Cast python objects to structured NumPy array (slow)
points = np.array(
# Points need to be tuples in the structured array
list(map(tuple, points)),
dtype=dtype_from_fields(fields))
# Handle organized clouds
assert len(points.shape) <= 2, \
'Too many dimensions for organized cloud! \
Points can only be organized in max. two dimensional space'
height = 1
width = points.shape[0]
# Check if input points are an organized cloud (2D array of points)
if len(points.shape) == 2:
height = points.shape[1]
# Convert numpy points to array.array
memory_view = memoryview(points)
casted = memory_view.cast('B')
array_array = array.array('B')
array_array.frombytes(casted)
# Put everything together
cloud = PointCloud2(
header=header,
height=height,
width=width,
is_dense=False,
is_bigendian=sys.byteorder != 'little',
fields=fields,
point_step=points.dtype.itemsize,
row_step=(points.dtype.itemsize * width))
# Set cloud via property instead of the constructor because of the bug described in
# https://github.com/ros2/common_interfaces/issues/176
cloud.data = array_array
return cloud
def create_cloud_xyz32(header: Header, points: Iterable) -> PointCloud2:
"""
Create a sensor_msgs.msg.PointCloud2 message with (x, y, z) fields.
:param header: The point cloud header. (Type: std_msgs.msg.Header)
:param points: The point cloud points. (Type: Iterable)
:return: The point cloud as sensor_msgs.msg.PointCloud2.
"""
fields = [PointField(name='x', offset=0,
datatype=PointField.FLOAT32, count=1),
PointField(name='y', offset=4,
datatype=PointField.FLOAT32, count=1),
PointField(name='z', offset=8,
datatype=PointField.FLOAT32, count=1)]
return create_cloud(header, fields, points)

View File

@ -9,6 +9,85 @@ from rclpy.qos import QoSProfile
from rclpy.executors import MultiThreadedExecutor from rclpy.executors import MultiThreadedExecutor
from geometry_msgs.msg import TransformStamped from geometry_msgs.msg import TransformStamped
from scipy.spatial.transform import Rotation as R from scipy.spatial.transform import Rotation as R
from sensor_msgs.msg import PointCloud2
from sensor_msgs_py import point_cloud2
from sensor_msgs.msg import Image, CameraInfo
from cv_bridge import CvBridge
import cv2
from Go2Py.utils.point_cloud2 import read_points_numpy
import rclpy
from rclpy.node import Node
from nav_msgs.msg import Odometry
import rclpy
from rclpy.node import Node
from nav_msgs.msg import Odometry
import numpy as np
from scipy.spatial.transform import Rotation as R
class ROS2OdometryReader(Node):
"""
A class for subscribing to and retrieving Odometry messages.
Args:
odom_topic (str): The topic name for the odometry data.
node_name (str): A unique name for this node.
Attributes:
odom (Odometry): The latest odometry message received from the topic.
"""
def __init__(self, odom_topic, node_name):
super().__init__(f'{node_name}_odom_reader')
self.odom = None # Stores the latest odometry message
# Create a subscription to the specified odometry topic
self.odom_subscriber = self.create_subscription(
Odometry,
odom_topic,
self.odom_callback,
10
)
def odom_callback(self, msg):
"""
Callback function that stores the latest odometry message.
Args:
msg (Odometry): The odometry message received.
"""
self.odom = msg
def get_odometry(self):
"""
Returns the latest odometry message received from the topic.
Returns:
Odometry: The latest odometry message.
"""
return self.odom
def get_pose(self):
if self.odom is not None:
position = self.odom.pose.pose.position
orientation = self.odom.pose.pose.orientation
t = np.array([position.x, position.y, position.z])
q = np.array([orientation.x, orientation.y, orientation.z, orientation.w])
Rot = R.from_quat(q).as_matrix()
T = np.eye(4)
T[:3, :3] = Rot
T[:3, -1] = t
return T
else:
return None
def close(self):
"""
Destroys the node, cleaning up resources.
"""
self.destroy_node()
def ros2_init(args=None): def ros2_init(args=None):
rclpy.init(args=args) rclpy.init(args=args)
@ -49,10 +128,50 @@ class ROS2ExecutorManager:
if self.executor_thread: if self.executor_thread:
self.executor_thread.join() self.executor_thread.join()
class ROS2TFInterface(Node): # class ROS2TFInterface(Node):
# def __init__(self, parent_name, child_name, node_name):
# super().__init__(f'{node_name}_tf2_listener')
# self.parent_name = parent_name
# self.child_name = child_name
# self.tfBuffer = tf2_ros.Buffer()
# self.listener = tf2_ros.TransformListener(self.tfBuffer, self)
# self.T = None
# self.stamp = None
# self.running = True
# self.thread = threading.Thread(target=self.update_loop)
# self.thread.start()
# self.trans = None
# def update_loop(self):
# while self.running:
# try:
# self.trans = self.tfBuffer.lookup_transform(self.parent_name, self.child_name, rclpy.time.Time(), rclpy.time.Duration(seconds=0.1))
# except (tf2_ros.LookupException, tf2_ros.ConnectivityException, tf2_ros.ExtrapolationException) as e:
# print(e)
# time.sleep(0.01)
# def get_pose(self):
# if self.trans is None:
# return None
# else:
# translation = [self.trans.transform.translation.x, self.trans.transform.translation.y, self.trans.transform.translation.z]
# rotation = [self.trans.transform.rotation.x, self.trans.transform.rotation.y, self.trans.transform.rotation.z, self.trans.transform.rotation.w]
# self.T = np.eye(4)
# self.T[0:3, 0:3] = R.from_quat(rotation).as_matrix()
# self.T[:3, 3] = translation
# self.stamp = self.trans.header.stamp.nanosec * 1e-9 + self.trans.header.stamp.sec
# return self.T
# def close(self):
# self.running = False
# self.thread.join()
# self.destroy_node()
class ROS2TFInterface(Node):
def __init__(self, parent_name, child_name, node_name): def __init__(self, parent_name, child_name, node_name):
super().__init__(f'{node_name}_tf2_listener') super().__init__(f"{node_name}_tf2_listener")
self.parent_name = parent_name self.parent_name = parent_name
self.child_name = child_name self.child_name = child_name
self.tfBuffer = tf2_ros.Buffer() self.tfBuffer = tf2_ros.Buffer()
@ -60,31 +179,137 @@ class ROS2TFInterface(Node):
self.T = None self.T = None
self.stamp = None self.stamp = None
self.running = True self.running = True
self.thread = threading.Thread(target=self.update_loop)
self.thread.start()
self.trans = None self.trans = None
def update_loop(self):
while self.running:
try:
self.trans = self.tfBuffer.lookup_transform(self.parent_name, self.child_name, rclpy.time.Time(), rclpy.time.Duration(seconds=0.1))
except (tf2_ros.LookupException, tf2_ros.ConnectivityException, tf2_ros.ExtrapolationException) as e:
print(e)
time.sleep(0.01)
def get_pose(self): def get_pose(self):
if self.trans is None: try:
self.trans = self.tfBuffer.lookup_transform(
self.parent_name,
self.child_name,
rclpy.time.Time(),
rclpy.time.Duration(seconds=0.1),
)
except (
tf2_ros.LookupException,
tf2_ros.ConnectivityException,
tf2_ros.ExtrapolationException,
) as e:
return None return None
else:
translation = [self.trans.transform.translation.x, self.trans.transform.translation.y, self.trans.transform.translation.z] translation = [
rotation = [self.trans.transform.rotation.x, self.trans.transform.rotation.y, self.trans.transform.rotation.z, self.trans.transform.rotation.w] self.trans.transform.translation.x,
self.T = np.eye(4) self.trans.transform.translation.y,
self.T[0:3, 0:3] = R.from_quat(rotation).as_matrix() self.trans.transform.translation.z,
self.T[:3, 3] = translation ]
self.stamp = self.trans.header.stamp.nanosec * 1e-9 + self.trans.header.stamp.sec rotation = [
return self.T self.trans.transform.rotation.x,
self.trans.transform.rotation.y,
self.trans.transform.rotation.z,
self.trans.transform.rotation.w,
]
self.T = np.eye(4)
self.T[0:3, 0:3] = R.from_quat(rotation).as_matrix()
self.T[:3, 3] = translation
self.stamp = (
self.trans.header.stamp.nanosec * 1e-9 + self.trans.header.stamp.sec
)
return self.T
def close(self): def close(self):
self.running = False self.running = False
self.thread.join() self.destroy_node()
class PoindCloud2Bridge(Node):
def __init__(self, topic_name):
super().__init__("point_cloud_listener")
self.subscription = self.create_subscription(
PointCloud2, topic_name, self.listener_callback, 1
)
self.data = None
self.new_frame_flag = False
self.points = None
self._points = None
self.points = None
self.ready = False
self.running = True
def listener_callback(self, msg):
# Read the x, y, z fields from the PointCloud2 message
self._points = read_points_numpy(
msg,
field_names=("x", "y", "z"),
skip_nans=True,
reshape_organized_cloud=True,
)
# self._points = point_cloud2.read_points(
# msg,
# field_names=("x", "y", "z"),
# skip_nans=True,
# )
# _points = np.reshape(_points, (-1, 16, 3))
# self._points = np.reshape(_points, (-1, 16, 3))
self.new_frame_flag = True
@property
def state(self):
return self._points
class ROS2CameraReader(Node):
"""
A class for interacting with a ROS2 camera topics.
Args:
image_topic (str): The topic name for the image stream.
camera_info_topic (str, optional): The topic name for the camera information.
K (numpy.ndarray, optional): The intrinsic camera matrix for the raw (distorted) images.
D (numpy.ndarray, optional): The distortion coefficients.
Attributes:
color_frame (numpy.ndarray): The latest image frame received from the topic.
camera_info (CameraInfo): The latest camera information received from the topic.
K (numpy.ndarray): The intrinsic camera matrix.
D (numpy.ndarray): The distortion coefficients.
"""
def __init__(self, image_topic, node_name, camera_info_topic=None, K=None, D=None):
super().__init__(f'{node_name}_camera_reader')
self.bridge = CvBridge()
self.color_frame = None
self.camera_info = None
self.K = K
self.D = D
self.node_name = node_name
self.image_subscriber = self.create_subscription(Image, image_topic, self.image_callback, 10)
if camera_info_topic:
self.camera_info_subscriber = self.create_subscription(CameraInfo, camera_info_topic, self.camera_info_callback, 10)
def image_callback(self, msg):
self.color_frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding='passthrough')
def camera_info_callback(self, msg):
self.camera_info = msg
self.K = np.array(msg.k).reshape((3, 3))
self.D = np.array(msg.d)
def get_image(self):
"""
Returns the latest image frame received from the topic.
Returns:
numpy.ndarray: The latest image frame received from the topic.
"""
return self.color_frame
def get_intrinsics(self):
"""
Returns the intrinsic camera matrix.
Returns:
numpy.ndarray: The intrinsic camera matrix.
"""
return {'K': self.K, 'D': self.D}
def close(self):
self.destroy_node() self.destroy_node()

File diff suppressed because one or more lines are too long

43
dds_bridge/DDSMsgs.idl Normal file
View File

@ -0,0 +1,43 @@
module Go2DDSMsgs {
struct PointCloud {
sequence<float> x;
sequence<float> y;
sequence<float> z;
string timestamp;
};
struct DepthImage {
uint16 width;
uint16 height;
sequence<uint16> data;
string timestamp;
};
struct RGBImage {
uint16 width;
uint16 height;
sequence<uint8> data;
string timestamp;
};
struct Pose {
double quat[4];
double trans[3];
string timestamp;
};
struct HighCommand {
double vx;
double vy;
double omega;
string timestamp;
};
struct Imu {
double accel[3];
double gyro[3];
double quat[4];
string timestamp;
};
};

View File

@ -0,0 +1,8 @@
DDSMsgs
DepthImage
HighCommand
Imu
PointCloud
Pose
RGBImage

View File

@ -0,0 +1,79 @@
"""
Generated by Eclipse Cyclone DDS idlc Python Backend
Cyclone DDS IDL version: v0.10.5
Module: Go2DDSMsgs
IDL file: DDSMsgs.idl
"""
from enum import auto
from typing import TYPE_CHECKING, Optional
from dataclasses import dataclass
import cyclonedds.idl as idl
import cyclonedds.idl.annotations as annotate
import cyclonedds.idl.types as types
# root module import for resolving types
import Go2DDSMsgs
@dataclass
@annotate.final
@annotate.autoid("sequential")
class PointCloud(idl.IdlStruct, typename="Go2DDSMsgs.PointCloud"):
x: types.sequence[types.float32]
y: types.sequence[types.float32]
z: types.sequence[types.float32]
timestamp: str
@dataclass
@annotate.final
@annotate.autoid("sequential")
class DepthImage(idl.IdlStruct, typename="Go2DDSMsgs.DepthImage"):
width: types.uint16
height: types.uint16
data: types.sequence[types.uint16]
timestamp: str
@dataclass
@annotate.final
@annotate.autoid("sequential")
class RGBImage(idl.IdlStruct, typename="Go2DDSMsgs.RGBImage"):
width: types.uint16
height: types.uint16
data: types.sequence[types.uint8]
timestamp: str
@dataclass
@annotate.final
@annotate.autoid("sequential")
class Pose(idl.IdlStruct, typename="Go2DDSMsgs.Pose"):
quat: types.array[types.float64, 4]
trans: types.array[types.float64, 3]
timestamp: str
@dataclass
@annotate.final
@annotate.autoid("sequential")
class HighCommand(idl.IdlStruct, typename="Go2DDSMsgs.HighCommand"):
vx: types.float64
vy: types.float64
omega: types.float64
timestamp: str
@dataclass
@annotate.final
@annotate.autoid("sequential")
class Imu(idl.IdlStruct, typename="Go2DDSMsgs.Imu"):
accel: types.array[types.float64, 3]
gyro: types.array[types.float64, 3]
quat: types.array[types.float64, 4]
timestamp: str

View File

@ -0,0 +1,9 @@
"""
Generated by Eclipse Cyclone DDS idlc Python Backend
Cyclone DDS IDL version: v0.10.5
Module: Go2DDSMsgs
"""
from ._DDSMsgs import DepthImage, HighCommand, Imu, PointCloud, Pose, RGBImage
__all__ = ["DepthImage", "HighCommand", "Imu", "PointCloud", "Pose", "RGBImage", ]

116
dds_bridge/dds_bridge.py Normal file
View File

@ -0,0 +1,116 @@
import sys
sys.path.append('..')
from Go2Py.utils.ros2 import PoindCloud2Bridge, ros2_init, ros2_close, ROS2ExecutorManager, ROS2CameraReader
from Go2Py.utils.ros2 import ROS2OdometryReader
from Go2Py.robot.interface import GO2Real
from dds_telemetry import Go2DDSServer
import time
import numpy as np
import time
import cv2
import pickle
RECORD_DATASET = False
DATASET_LENGTH = 120
ros2_init()
map_bridge = PoindCloud2Bridge('/Laser_map')
fast_lio_odom = ROS2OdometryReader('/Odometry', 'odometry_subscriber')
rgb_camera_bridge = ROS2CameraReader('/camera/color/image_raw', 'rgb_reader')
depth_camera_bridge = ROS2CameraReader('/camera/depth/image_rect_raw', 'depth_reader')
ros2_exec_manager = ROS2ExecutorManager()
ros2_exec_manager.add_node(map_bridge)
ros2_exec_manager.add_node(fast_lio_odom)
ros2_exec_manager.add_node(rgb_camera_bridge)
ros2_exec_manager.add_node(depth_camera_bridge)
ros2_exec_manager.start()
# The extrinsics of the MID360 LiDAR with respect to the robot body
body_T_lidar = np.array([
[ 0.9743701, 0.0, 0.2249511, 0.1870 ],
[ 0.0, 1.0, 0.0, 0.0 ],
[-0.2249511, 0.0, 0.9743701, 0.0803 ],
[ 0.0, 0.0, 0.0, 1.0 ]
])
robot = GO2Real(mode='highlevel')
dds_server = Go2DDSServer(robot_name='go2')
def GetLidarPose():
lio_T_lidar = fast_lio_odom.get_pose()
if lio_T_lidar is not None:
lio_T_body = lio_T_lidar @ np.linalg.inv(body_T_lidar)
return lio_T_body
else:
return None
def GetLidarMap():
pcd = map_bridge._points
if pcd is not None:
return pcd
else:
return None
print('Waitng for the ROS2 bridges to start up ...')
time.sleep(5)
print('Running the bridge loop')
lio_pcd = None
map_update_counter = 0
rgb_imgs = []
depth_imgs = []
lio_Ts_robot = []
pcd_maps = []
start_time = time.time()
while True:
tic = time.time()
lio_T_robot = GetLidarPose()
if lio_T_robot is not None:
dds_server.sendLidarOdom(lio_T_robot)
if RECORD_DATASET:
lio_Ts_robot.append(lio_T_robot)
if map_update_counter%10==0:
lio_pcd = GetLidarMap()
if lio_pcd is not None:
dds_server.sendMap(lio_pcd)
if RECORD_DATASET:
pcd_maps.append(lio_pcd.copy())
map_update_counter += 1
depth = depth_camera_bridge.get_image()
rgb = rgb_camera_bridge.get_image()
if depth is not None and rgb is not None:
depth = cv2.resize(depth, (320, 240))
rgb = cv2.resize(rgb, (320, 240))
# print('sending image')
dds_server.sendDepth(depth)
dds_server.sendRGB(rgb)
if RECORD_DATASET:
rgb_imgs.append(rgb.copy())
depth_imgs.append(depth.copy())
if time.time()-start_time >= DATASET_LENGTH:
break
# Forward the rgb, depth, lio, lio_T_lidar to the computer through DDS
toc = time.time()
command = dds_server.getHighCmd()
if command is not None:
print('Recievied a command from the client. Forwarding to the robot...')
robot.setCommandsHigh(command.vx, command.vy, command.omega)
print(f'{(toc-tic):0.3f}')
while(time.time()-tic) < 0.1:
time.sleep(0.001)

111
dds_bridge/dds_telemetry.py Normal file
View File

@ -0,0 +1,111 @@
import socket
import struct
import numpy as np
import time
import threading
from cyclonedds.domain import DomainParticipant
from cyclonedds.topic import Topic
from cyclonedds.sub import DataReader
from cyclonedds.pub import Publisher, DataWriter
from dataclasses import dataclass
from Go2DDSMsgs import PointCloud, RGBImage, DepthImage, Pose, HighCommand
from scipy.spatial.transform import Rotation as R
def get_last_msg(reader, topic_type):
""" """
last_msg = reader.take()
if last_msg:
while True:
a = reader.take()
if not a:
break
else:
last_msg = a
if last_msg:
msg = last_msg[0]
if type(msg) == topic_type:
return msg
else:
return None
else:
return None
class Go2DDSServer:
def __init__(self, robot_name):
self.robot_name = robot_name
self.domain = DomainParticipant()
self.rgb_topic = Topic(self.domain, f'{self.robot_name}_rgb', RGBImage)
self.depth_topic = Topic(self.domain, f'{self.robot_name}_depth', DepthImage)
self.map_topic = Topic(self.domain, f'{self.robot_name}_lio_map', PointCloud)
self.lidar_odom_topic = Topic(self.domain, f'{self.robot_name}_lio_odom', Pose)
self.high_cmd_topic = Topic(self.domain, f'{self.robot_name}_high_cmd', HighCommand)
self.rgb_publisher = Publisher(self.domain)
self.depth_publisher = Publisher(self.domain)
self.map_publisher = Publisher(self.domain)
self.lidar_odom_publisher = Publisher(self.domain)
self.high_cmd_reader = DataReader(self.domain, self.high_cmd_topic)
self.rgb_writer = DataWriter(self.rgb_publisher, self.rgb_topic)
self.depth_writer = DataWriter(self.depth_publisher, self.depth_topic)
self.map_writer = DataWriter(self.map_publisher, self.map_topic)
self.lidar_odom_writer = DataWriter(self.lidar_odom_publisher, self.lidar_odom_topic)
def sendRGB(self, rgb):
self.rgb_writer.write(RGBImage(data=rgb.reshape(-1).tolist(), width=rgb.shape[1], height=rgb.shape[0], timestamp=''))
def sendDepth(self, depth):
self.depth_writer.write(DepthImage(data=depth.reshape(-1).tolist(), width=depth.shape[1], height=depth.shape[0], timestamp=''))
def sendMap(self, pcd):
map_pcd_mgs = PointCloud(x = pcd[:,0].tolist(),
y = pcd[:,1].tolist(),
z = pcd[:,2].tolist(),
timestamp=''
)
self.map_writer.write(map_pcd_mgs)
def sendLidarOdom(self, lio_T_body):
q = R.from_matrix(lio_T_body[:3,:3]).as_quat()
t = lio_T_body[:3,3]
pose_msg = Pose(quat=q.tolist(), trans=t.tolist(), timestamp='')
self.lidar_odom_writer.write(pose_msg)
def getHighCmd(self):
return get_last_msg(self.high_cmd_reader, HighCommand)
class Go2DDSClient:
def __init__(self, robot_name):
self.robot_name = robot_name
self.domain = DomainParticipant()
self.rgb_topic = Topic(self.domain, f'{self.robot_name}_rgb', RGBImage)
self.depth_topic = Topic(self.domain, f'{self.robot_name}_depth', DepthImage)
self.map_topic = Topic(self.domain, f'{self.robot_name}_lio_map', PointCloud)
self.lidar_odom_topic = Topic(self.domain, f'{self.robot_name}_lio_odom', Pose)
self.high_cmd_topic = Topic(self.domain, f'{self.robot_name}_high_cmd', HighCommand)
self.rgb_reader = DataReader(self.domain, self.rgb_topic)
self.depth_reader = DataReader(self.domain, self.depth_topic)
self.map_reader = DataReader(self.domain, self.map_topic)
self.lidar_odom_reader = DataReader(self.domain, self.lidar_odom_topic)
self.high_cmd_writer = DataWriter(self.domain, self.high_cmd_topic)
def getRGB(self):
return get_last_msg(self.rgb_reader, RGBImage)
def getDepth(self):
return get_last_msg(self.depth_reader, DepthImage)
def getMap(self):
return get_last_msg(self.map_reader, PointCloud)
def getLidarOdom(self):
return get_last_msg(self.lidar_odom_reader, Pose)
def sendHighCmd(self, vx, vy, omega):
self.high_cmd_writer.write(HighCommand(vx=vx, vy=vy, omega=omega, timestamp=''))

View File

@ -0,0 +1,53 @@
from Go2Py.robot.fsm import FSM
from Go2Py.robot.safety import SafetyHypervisor
from Go2Py.robot.interface import GO2Real
from Go2Py.control.neuro_diff import NeuroDiffAgent
import torch
import numpy as np
import time
class NeuroDiffController:
def __init__(self, robot, remote, checkpoint):
self.remote = remote
self.robot = robot
self.policy = Policy(checkpoint)
self.command_profile = CommandInterface()
self.agent = NeuroDiffSimAgent(self.command_profile, self.robot)
self.hist_data = {}
def init(self):
self.obs = self.agent.reset()
self.policy_info = {}
self.command_profile.yaw_vel_cmd = 0.0
self.command_profile.x_vel_cmd = 0.0
self.command_profile.y_vel_cmd = 0.0
def update(self, robot, remote):
if not hasattr(self, "obs"):
self.init()
commands = getRemote(remote)
self.command_profile.yaw_vel_cmd = 0.0
self.command_profile.x_vel_cmd = 0.0
self.command_profile.y_vel_cmd = 0.0
self.obs = self.agent.get_obs()
action = self.policy(self.obs, self.policy_info)
_, self.ret, self.done, self.info = self.agent.step(action)
for key, value in self.info.items():
if key in self.hist_data:
self.hist_data[key].append(value)
else:
self.hist_data[key] = [value]
robot = GO2Real(mode='lowlevel')
safety_hypervisor = SafetyHypervisor(robot)
time.sleep(3)
print(robot.getJointStates())
robot.close()

26
dds_bridge/test.py Normal file
View File

@ -0,0 +1,26 @@
from cyclonedds.topic import Topic
from cyclonedds.pub import DataWriter
from cyclonedds.domain import DomainParticipant
from cyclonedds.topic import Topic
from cyclonedds.sub import DataReader
from cyclonedds.util import duration
import numpy as np
from Go2DDSMsgs import PointCloud, RGBImage, DepthImage, Pose, HighCommand
# from scipy.spatial.transform import Rotation as R
participant = DomainParticipant()
topic = Topic(participant, 'test_topic', RGBImage)
writer = DataWriter(participant, topic)
# domain = DomainParticipant()
# rgb_topic = Topic(domain, 'go2_rgb', RGBImage)
# rgb_publisher = Publisher(domain)
# rgb_writer = DataWriter(rgb_publisher, rgb_topic)
rgb = np.random.randint(0, 255, (10, 10, 3)).astype(np.uint8)
rgb_msg = RGBImage(data=[0, 1, 2], timestamp='')
writer.write(rgb_msg)

0
dds_bridge/test2.py Normal file
View File