diff --git a/pyrobolearn/utils/parsers/robots/data_structures.py b/pyrobolearn/utils/parsers/robots/data_structures.py index 53a1085..03ca331 100644 --- a/pyrobolearn/utils/parsers/robots/data_structures.py +++ b/pyrobolearn/utils/parsers/robots/data_structures.py @@ -4,6 +4,7 @@ """ import copy +from enum import Enum import numpy as np import trimesh from collections import OrderedDict, Iterable @@ -870,8 +871,10 @@ class MultiBody(object): self.materials = {} self.frame = Frame(position=position, orientation=orientation, dtype='world') + # sensors and actuators (with their transmissions) self.sensors = {} self.actuators = {} + self.transmissions = {} @property def name(self): @@ -1068,6 +1071,13 @@ class MultiBody(object): """ return len(self.actuators) > 0 + def has_transmissions(self): + """ + Return True if the multi-body data structure has some transmissions (transmission between joint and motor + actuator). + """ + return len(self.transmissions) > 0 + def add_sensor(self, sensor): """ Add a sensor to the multi-body data structure. @@ -1092,6 +1102,18 @@ class MultiBody(object): "{}".format(type(actuator))) self.actuators[actuator.id] = actuator + def add_transmission(self, transmission): + """ + Add a transmission element (i.e. link between joint and motor actuator). + + Args: + transmission (Transmission): transmission data structure. + """ + if not isinstance(transmission, Transmission): + raise TypeError("Expecting the given 'transmission' to be an instance of `Transmission`, but got instead: " + "{}".format(type(transmission))) + self.transmissions[transmission.name] = transmission + # alias Tree = MultiBody @@ -2879,10 +2901,41 @@ class Noise(object): class GaussianNoise(Noise): """Gaussian noise distribution used in sensors and actuators.""" - def __init__(self, mean, stddev): + def __init__(self, mean=None, stddev=None): + """ + Initialize the Gaussian noise. + + Args: + mean (float): mean value. + stddev (float): standard deviation value. + """ self.mean = mean self.stddev = stddev + @property + def mean(self): + """Return the Gaussian noise mean value.""" + return self._mean + + @mean.setter + def mean(self, mean): + """Set the Gaussian noise mean value.""" + if mean is not None: + mean = float(mean) + self._mean = mean + + @property + def stddev(self): + """Return the Gaussian noise standard deviation value.""" + return self._stddev + + @stddev.setter + def stddev(self, stddev): + """Set the Gaussian noise standard deviation value.""" + if stddev is not None: + stddev = float(stddev) + self._stddev = stddev + class Sensor(object): r"""Sensor (abstract) class. @@ -2918,6 +2971,18 @@ class Sensor(object): raise TypeError("Expecting the given 'name' to be a string, instead got: {}".format(type(name))) self._name = name + @property + def update_rate(self): + """Return the update rate.""" + return self._rate + + @update_rate.setter + def update_rate(self, rate): + """Set the update rate.""" + if rate is not None: + rate = float(rate) + self._rate = rate + @property def noise(self): """Return the sensor noise.""" @@ -3005,6 +3070,59 @@ class LinkSensor(Sensor): self._link = link +class Image(object): + r"""Image data structure.""" + + def __init__(self, width=None, height=None, format=None): + """ + Initialize the image data structure. + + Args: + width (int, None): width of the image in pixels. + height (int, None): height of the image in pixels. + format (str, None): format of the image. Ex: 'R8G8B8'. + """ + self.width = width + self.height = height + self.format = format + + @property + def width(self): + """Return the width of the image.""" + return self._width + + @width.setter + def width(self, width): + """Set the width of the image.""" + if width is not None: + width = int(width) + self._width = width + + @property + def height(self): + """Return the height of the image.""" + return self._height + + @height.setter + def height(self, height): + """Set the height of the image.""" + if height is not None: + height = int(height) + self._height = height + + @property + def format(self): + """Return the image format.""" + return self._format + + @format.setter + def format(self, format): + """Set the image format.""" + if format is not None and not isinstance(format, str): + raise TypeError("Expecting the given 'format' to be a string, but instead got: {}".format(type(format))) + self._format = format + + class CameraSensor(LinkSensor): """Camera sensor @@ -3028,10 +3146,8 @@ class CameraSensor(LinkSensor): self.visualize = False # intrinsic properties of camera + self.image = Image(width=None, height=None, format=None) self.horizontal_fov = None - self.width = None - self.height = None - self.format = None # R8G8B8 self.near = None self.far = None @@ -3041,12 +3157,171 @@ class CameraSensor(LinkSensor): self.image_topic = None # added to the camera_base_topic self.camera_info_topic = None # added to the camera_base_topic self.frame_name = None # check 'name' attribute in Frame class + self.hack_baseline = None self.distortion_k1 = None self.distortion_k2 = None self.distortion_k3 = None self.distortion_t1 = None self.distortion_t2 = None + self.focal_length = None + self.cx_prime = None + self.cx = None + self.cy = None + + @property + def visualize(self): + """Return if we should visualize in the simulator the sensor data.""" + return self._visualize + + @visualize.setter + def visualize(self, visualize): + """Set if we should visualize in the simulator the sensor data.""" + if visualize is None: + visualize = False + self._visualize = bool(visualize) + + @property + def image(self): + """Return the image data structure.""" + return self._image + + @image.setter + def image(self, image): + if image is not None and not isinstance(image, Image): + raise TypeError("Expecting the given 'image' to be an instance of `Image` but got instead: " + "{}".format(type(image))) + self._image = image + + @property + def horizontal_fov(self): + return self._horizontal_fov + + @horizontal_fov.setter + def horizontal_fov(self, fov): + if fov is not None: + fov = float(fov) + self._horizontal_fov = fov + + @property + def near(self): + return self._near + + @near.setter + def near(self, near): + if near is not None: + near = float(near) + self._near = near + + @property + def far(self): + return self._far + + @far.setter + def far(self, far): + if far is not None: + far = float(far) + self._far = far + + @property + def hack_baseline(self): + return self._hack_baseline + + @hack_baseline.setter + def hack_baseline(self, baseline): + if baseline is not None: + baseline = float(baseline) + self._hack_baseline = baseline + + @property + def distortion_k1(self): + return self._distortion_k1 + + @distortion_k1.setter + def distortion_k1(self, distortion_k1): + if distortion_k1 is not None: + distortion_k1 = float(distortion_k1) + self._distortion_k1 = distortion_k1 + + @property + def distortion_k2(self): + return self._distortion_k2 + + @distortion_k2.setter + def distortion_k2(self, distortion_k2): + if distortion_k2 is not None: + distortion_k2 = float(distortion_k2) + self._distortion_k2 = distortion_k2 + + @property + def distortion_k3(self): + return self._distortion_k3 + + @distortion_k3.setter + def distortion_k3(self, distortion_k3): + if distortion_k3 is not None: + distortion_k3 = float(distortion_k3) + self._distortion_k3 = distortion_k3 + + @property + def distortion_t1(self): + return self._distortion_t1 + + @distortion_t1.setter + def distortion_t1(self, distortion_t1): + if distortion_t1 is not None: + distortion_t1 = float(distortion_t1) + self._distortion_t1 = distortion_t1 + + @property + def distortion_t2(self): + return self._distortion_t2 + + @distortion_t2.setter + def distortion_t2(self, distortion_t2): + if distortion_t2 is not None: + distortion_t2 = float(distortion_t2) + self._distortion_t2 = distortion_t2 + + @property + def focal_length(self): + return self._focal_length + + @focal_length.setter + def focal_length(self, focal_length): + if focal_length is not None: + focal_length = float(focal_length) + self._focal_length = focal_length + + @property + def cx_prime(self): + return self._cx_prime + + @cx_prime.setter + def cx_prime(self, cx_prime): + if cx_prime is not None: + cx_prime = float(cx_prime) + self._cx_prime = cx_prime + + @property + def cx(self): + return self._cx + + @cx.setter + def cx(self, cx): + if cx is not None: + cx = float(cx) + self._cx = cx + + @property + def cy(self): + return self._cy + + @cy.setter + def cy(self, cy): + if cy is not None: + cy = float(cy) + self._cy = cy class DepthCameraSensor(LinkSensor): @@ -3090,7 +3365,7 @@ class GPURay(LinkSensor): """ super(GPURay, self).__init__(sensor_id, link, name, update_rate, noise) - self.frame = Frame() + self.frame = Frame() # pose self.visualize = False # @@ -3101,14 +3376,159 @@ class GPURay(LinkSensor): # self.range = None # and - self.range_resolution = 0.01 + self.range_resolution = None # plugin self.plugin_filename = None self.plugin_name = None - self.topic = None + self.topic_name = None self.frame_name = None # Check 'name' attribute in Frame class + @property + def pose(self): + pose = self.frame.pose + if pose is not None: + pos, rpy = pose + if pos is None: + pos = np.zeros(3) + if rpy is None: + rpy = np.zeros(3) + return np.concatenate((pos, rpy)) + + @pose.setter + def pose(self, pose): + self.frame.pose = pose + + @property + def visualize(self): + """Return if we should visualize in the simulator the sensor data.""" + return self._visualize + + @visualize.setter + def visualize(self, visualize): + """Set if we should visualize in the simulator the sensor data.""" + if visualize is None: + visualize = False + elif isinstance(visualize, str): + visualize = visualize.lower() + if visualize == 'false': + visualize = False + elif visualize == 'true': + visualize = True + self._visualize = bool(visualize) + + @property + def samples(self): + return self._samples + + @samples.setter + def samples(self, samples): + if samples is not None: + samples = int(samples) + self._samples = samples + + @property + def scan_resolution(self): + return self._scan_resolution + + @scan_resolution.setter + def scan_resolution(self, resolution): + if resolution is not None: + resolution = float(resolution) + self._scan_resolution = resolution + + @property + def range_angle(self): + return self._range_angle + + @range_angle.setter + def range_angle(self, range_angle): + if range_angle is not None: + if not isinstance(range_angle, (tuple, list, np.ndarray)): + raise TypeError("Expecting the given 'range_angle' to be a tuple/list/np.array of 2 float, but got " + "instead: {}".format(type(range_angle))) + if len(range_angle) != 2: + raise ValueError("Expecting the given 'range_angle' to be of length 2 but got instead a length of: " + "{}".format(len(range_angle))) + self._range_angle = range_angle + + @property + def range(self): + return self._range + + @range.setter + def range(self, range_distance): + if range_distance is not None: + if not isinstance(range_distance, (tuple, list, np.ndarray)): + raise TypeError("Expecting the given 'range' to be a tuple/list/np.array of 2 float, but got " + "instead: {}".format(type(range_distance))) + if len(range_distance) != 2: + raise ValueError("Expecting the given 'range' to be of length 2 but got instead a length of: " + "{}".format(len(range_distance))) + self._range = range_distance + + @property + def min_angle(self): + if self.range_angle is not None: + return self.range_angle[0] + + @min_angle.setter + def min_angle(self, angle): + if angle is not None: + angle = float(angle) + if self.range_angle is None: + self.range_angle = (None, None) + self.range_angle[0] = angle + + @property + def max_angle(self): + if self.range_angle is not None: + return self.range_angle[1] + + @max_angle.setter + def max_angle(self, angle): + if angle is not None: + angle = float(angle) + if self.range_angle is None: + self.range_angle = (None, None) + self.range_angle[1] = angle + + @property + def min_range(self): + if self.range is not None: + return self.range[0] + + @min_range.setter + def min_range(self, dist): + if dist is not None: + dist = float(dist) + if self.range is None: + self.range = (None, None) + self.range[0] = dist + + @property + def max_range(self): + if self.range is not None: + return self.range[1] + + @max_range.setter + def max_range(self, dist): + if dist is not None: + dist = float(dist) + if self.range is None: + self.range = (None, None) + self.range[1] = dist + + @property + def range_resolution(self): + return self._range_resolution + + @range_resolution.setter + def range_resolution(self, resolution): + if resolution is not None: + resolution = float(resolution) + self._range_resolution = resolution + class IMUSensor(LinkSensor): """IMU sensor. @@ -3130,6 +3550,18 @@ class IMUSensor(LinkSensor): """ super(IMUSensor, self).__init__(sensor_id, link, name, update_rate, noise) + self.frame = Frame() # pose + self.visualize = False + self.gravity = False + + # plugin + self.plugin_filename = None + self.plugin_name = None + self.topic_name = None + self.body_name = None # Check 'name' of link + self.gaussian_noise = None + self.frame_name = None # Check 'name' attribute in Frame class + class ForceTorqueSensor(JointSensor): """Force torque sensor. @@ -3164,7 +3596,7 @@ class Actuator(object): # Motor Args: actuator_id (int): actuator unique id. name (str): name of the actuator/motor. - actuators (list[Sensor]): inner list of actuators. + actuators (list[Actuator]): inner list of actuators. """ self.id = actuator_id self.name = name @@ -3220,7 +3652,7 @@ class JointActuator(Actuator): class MotorJointActuator(JointActuator): """Motor joint actuator.""" - def __init__(self, actuator_id, joint, name=None): + def __init__(self, actuator_id, joint, name=None, hardware_interface=None, mechanical_reduction=None): """ Initialize the joint actuator. @@ -3228,12 +3660,38 @@ class MotorJointActuator(JointActuator): actuator_id (int): unique actuator id. joint (Joint): joint to which the actuator is attached. name (str): name of the actuator. + hardware_interface (str): hardware interface used in ROS control. + mechanical_reduction (float): mechanical reduction factor. """ super(MotorJointActuator, self).__init__(actuator_id, joint, name) - self.transmission_type = None - self.hardware_interface = None # EffortJointInterface - self.mechanical_reduction = None + self.hardware_interface = hardware_interface # EffortJointInterface + self.mechanical_reduction = mechanical_reduction + + @property + def hardware_interface(self): + """Return the hardware interface.""" + return self._hardware_interface + + @hardware_interface.setter + def hardware_interface(self, interface): + """Set the hardware interface.""" + if interface is not None and not isinstance(interface, str): + raise TypeError("Expecting the given 'hardware_interface' to be a string but got instead: " + "{}".format(type(interface))) + self._hardware_interface = interface + + @property + def mechanical_reduction(self): + """Get the mechanical reduction factor.""" + return self._mechanical_reduction + + @mechanical_reduction.setter + def mechanical_reduction(self, reduction): + """Set the mechanical reduction factor.""" + if reduction is not None: + reduction = float(reduction) + self._mechanical_reduction = reduction class PositionJointActuator(MotorJointActuator): @@ -3270,6 +3728,13 @@ class Constraint(object): pass +class TransmissionType(Enum): + """Transmission type: simple, four-bar linkage, differential.""" + SIMPLE = 1 + FOUR_BAR_LINKAGE = 2 + DIFFERENTIAL = 3 + + class Transmission(object): r"""Transmission interface. @@ -3294,20 +3759,250 @@ class Transmission(object): - [3] URDF Transmissions: https://wiki.ros.org/urdf/XML/Transmission """ - def __init__(self, name, joint, transmission_type=None, actuator_name=None, hardware_interface=None): + def __init__(self, name, transmission_type=None, joint=None, actuator=None): """ Initialize the transmission. Args: name (str): name of the transmission. - joint (Joint): joint to which is attached the transmission. transmission_type (str): transmission type; select between {'simple', 'four-bar linkage', 'differential'}. - actuator_name (str): name of the actuator. - hardware_interface (str): hardware interface. + joint (Joint): joint to which the transmission is related to. + actuator (MotorJointActuator): actuator to which the transmission is connected to. """ self.name = name self.type = transmission_type - self.actuator = actuator_name - self.mechanical_reduction = None self.joint = joint - self.hardware_interface = hardware_interface + self.actuator = actuator + + @property + def name(self): + """Return the transmission unique name.""" + return self._name + + @name.setter + def name(self, name): + """Set the transmission unique name.""" + if not isinstance(name, str): + raise TypeError("Expecting the given 'name' to be a string, instead got: {}".format(type(name))) + self._name = name + + @property + def type(self): + """Return the transmission type.""" + return self._type + + @type.setter + def type(self, dtype): + """Set the transmission type.""" + # if dtype is not None: + # if isinstance(dtype, str): + # dtype = dtype.lower() + # for t in TransmissionType: + # if dtype == t.name.lower(): + # dtype = t + # break + # elif not isinstance(dtype, TransmissionType): + # raise TypeError("Expecting the given 'transmission_type' to be an instance of a string or an " + # "instance of `TransmissionType`, but got instead: {}".format(type(dtype))) + if dtype is not None and not isinstance(dtype, str): + raise TypeError("Expecting the given 'transmission_type' to be a string, instead got: " + "{}".format(type(dtype))) + self._type = dtype + + @property + def joint(self): + """Return the joint data structure to which the transmission is related to.""" + return self._joint + + @joint.setter + def joint(self, joint): + """Set the joint data structure to which the transmission is attached.""" + if joint is not None and not isinstance(joint, Joint): + raise TypeError("Expecting the given 'joint' to be an instance of `Joint`, but got instead: " + "{}".format(type(joint))) + self._joint = joint + + @property + def actuator(self): + """Return the actuator data structure to which the transmission is connected to.""" + return self._actuator + + @actuator.setter + def actuator(self, actuator): + """Set the actuator data structure to which the transmission is connected to.""" + if actuator is not None and not isinstance(actuator, MotorJointActuator): + raise TypeError("Expecting the given 'actuator' to be an instance of `MotorJointActuator`, but got " + "instead: {}".format(type(actuator))) + self._actuator = actuator + + +class ControlMode(Enum): + """Control mode: position, velocity, effort.""" + NULL = 0 + POSITION = 1 + VELOCITY = 2 + EFFORT = 3 + + +class PID(object): + """PID control""" + + def __init__(self, p=None, i=None, d=None, pid=None): + """ + Initialize PID. + + Args: + p (float, int, str): p coefficient value. + i (float, int, str): i coefficient value. + d (float, int, str): d coefficient value. + pid (list/tuple/np.array[float[3]], str): pid values. + """ + if pid is not None: + self.pid = pid + else: + self.p = p + self.i = i + self.d = d + + @property + def p(self): + return self._p + + @p.setter + def p(self, p): + if p is not None: + if not isinstance(p, (float, str, int)): + raise TypeError("Expecting the p coefficient to be an int, float, or str, but got instead: " + "{}".format(type(p))) + p = float(p) + self._p = p + + @property + def i(self): + return self._i + + @i.setter + def i(self, i): + if i is not None: + if not isinstance(i, (float, str, int)): + raise TypeError("Expecting the i coefficient to be an int, float, or str, but got instead: " + "{}".format(type(i))) + i = float(i) + self._i = i + + @property + def d(self): + return self._d + + @d.setter + def d(self, d): + if d is not None: + if not isinstance(d, (float, str, int)): + raise TypeError("Expecting the d coefficient to be an int, float, or str, but got instead: " + "{}".format(type(d))) + d = float(d) + self._d = d + + @property + def pid(self): + pid = [self._p, self._i, self._d] + for i in range(len(pid)): + if pid[i] is None: + pid[i] = 0. + return np.array(pid) + + @pid.setter + def pid(self, pid): + if pid is not None: + if isinstance(pid, str): + pid = [float(y) for x in pid.split(',') for y in x.split()] + elif isinstance(pid, (list, tuple, np.ndarray)): + if len(pid) != 3: + raise ValueError("Expecting the given pid list to be of length 3, but got a length of: " + "{}".format(len(pid))) + else: + raise TypeError("Expecting the given pid to be a list/tuple/np.array of 3 floats, or a string, but " + "instead got: {}".format(type(pid))) + + self.p = pid[0] + self.i = pid[1] + self.d = pid[2] + else: + self.p = None + self.i = None + self.d = None + + +class Control(object): + r"""Control + + """ + + def __init__(self, mode=None, pid=None): + """ + Initialize the control. + + Args: + mode (ControlMode, None): control mode. + pid (PID, None): PID values. + """ + self.mode = mode + self.pid = pid + + @property + def mode(self): + return self._mode + + @mode.setter + def mode(self, mode): + if mode is not None: + if isinstance(mode, str): + mode = mode.lower() + for m in ControlMode: + if mode == m.name.lower(): + mode = m + break + elif not isinstance(mode, ControlMode): + raise TypeError("Expecting the given control mode to be an instance of `ControlMode` or a string, but " + "got instead: {}".format(type(mode))) + self._mode = mode + + @property + def pid(self): + return self._pid + + @pid.setter + def pid(self, pid): + if pid is None: + pid = PID() + elif isinstance(pid, (list, tuple, np.ndarray, str)): + pid = PID(pid=pid) + elif not isinstance(pid, PID): + raise TypeError("Expecting the given PID values to be a list/tuple/np.array of 3 float, an instance of " + "`PID`, or a string, but got instead: {}".format(type(pid))) + self._pid = pid + + +class ROSControl(object): + """ROS control data structure as specified in URDFs.""" + + def __init__(self, namespace=None, control_period=None, robot_parameters=None, robot_sim_interface=None, + control_config_path=None): + """ + Initialize the ROS control data structure. + + Args: + namespace (str, None): ROS namespace, default to robot name in URDF. + control_period (int, None): period of the controller update (in seconds). If not specified, it will be + the default one. + robot_parameters (str, None): location of the robot_description (URDF) on the parameter server, default to + `/robot_description`. + robot_sim_interface (str, None): name of custum robot simulator interface to be used. Default to + `DefaultRobotHWSim`. + control_config_path (str, None): control configuration file path (usually it is a YAML file). + """ + self.namespace = namespace + self.control_period = control_period + self.robot_parameters = robot_parameters + self.robot_sim_interface = robot_sim_interface + self.config_path = control_config_path diff --git a/pyrobolearn/utils/parsers/robots/urdf_parser.py b/pyrobolearn/utils/parsers/robots/urdf_parser.py index dffaaec..2f087e3 100644 --- a/pyrobolearn/utils/parsers/robots/urdf_parser.py +++ b/pyrobolearn/utils/parsers/robots/urdf_parser.py @@ -97,7 +97,24 @@ class URDFParser(RobotParser): child_body = joint.child child_body.parent_joints[joint.name] = joint - # TODO: check sensor, plugins, transmission, etc + # check transmissions (for control) + for i, transmission_tag in enumerate(root.findall('transmission')): + # get transmission instance + transmission = self._parse_transmission(transmission_tag, idx=i, tree=tree) + + # add transmission + tree.add_transmission(transmission) + + # check sensors + for i, sensor_tag in enumerate(root.findall('gazebo')): + # get sensor instance + sensor = self._parse_sensor(sensor_tag, idx=i, tree=tree) + + # add sensor in tree + if sensor is not None: + tree.add_sensor(sensor) + + # TODO: check plugins # set the root element key = next(iter(tree.bodies)) @@ -247,7 +264,7 @@ class URDFParser(RobotParser): Args: joint_tag (ET.Element): joint XML element. idx (int): joint index. - tree (Tree): tree data structure. + tree (Tree): MultiBody data structure. Returns: Joint: joint data structure. @@ -295,6 +312,186 @@ class URDFParser(RobotParser): return joint + @staticmethod + def _parse_transmission(transmission_tag, idx, tree): + """ + Return the transmission data structure instance from a tag. See `ros_control` for more info. + + Args: + transmission_tag (ET.Element): transmission XML element. + idx (int): joint/motor index. + tree (Tree): tree data structure. + + Returns: + Transmission: transmission data structure. + """ + transmission = Transmission(name=transmission_tag.attrib.get('name')) + + # get type tag + type_tag = transmission_tag.find('type') + if type_tag is not None: + transmission.type = type_tag.text + + # get joint + joint_tag = transmission_tag.find('joint') + if joint_tag is not None: + transmission.joint = tree.joints[joint_tag.attrib.get('name')] + + # get actuator + actuator_tag = transmission_tag.find('actuator') + if actuator_tag is not None: + hardware_interface = actuator_tag.find('hardwareInterface').text + mechanical_reduction = actuator_tag.find('mechanicalReduction').text + actuator = MotorJointActuator(actuator_id=idx, joint=transmission.joint, + name=actuator_tag.attrib.get('name'), hardware_interface=hardware_interface, + mechanical_reduction=mechanical_reduction) + transmission.actuator = actuator + tree.add_actuator(actuator) + + return transmission + + @staticmethod + def _parse_sensor(gazebo_tag, idx, tree): + """ + Return the sensor data structure instance from a tag. + + Args: + gazebo_tag (ET.Element): gazebo sensor XML element. + idx (int): joint/motor index. + tree (Tree): tree data structure. + + Returns: + Sensor: sensor data structure. + """ + reference = gazebo_tag.attrib.get('reference') + + # get sensor + sensor_tag = gazebo_tag.find('sensor') + if sensor_tag is None: + return None + + sensor_type = sensor_tag.attrib.get('type') + + # define set_attribute text function + def set_attribute(obj, attribute_name, parent_tag, tag=None): + """Set XML tag text to object's attribute.""" + if tag is None: + tag = attribute_name + tag = parent_tag.find(tag) + if tag is not None: + setattr(obj, attribute_name, tag.text) + + # define check_noise tag function + def check_noise(tag): + """Check the tag and returns the Noise instance or None if no noise tags were found.""" + noise_tag = tag.find('noise') + noise = None + if noise_tag is not None: + type_tag = noise_tag.find('type') + if type_tag is not None: + noise_type = type_tag.text + if noise_type == 'gaussian': + noise = GaussianNoise() + for t in ['mean', 'stddev']: + set_attribute(noise, t, noise_tag) + return noise + + # check sensor type # + sensor = None + + if sensor_type == 'camera': # CAMERA sensor + sensor = CameraSensor(sensor_id=idx, link=tree.bodies[reference], name=sensor_tag.attrib.get('name')) + + # check tag + set_attribute(sensor, 'update_rate', sensor_tag) + + # check tag + camera_tag = sensor_tag.find('camera') + if camera_tag is not None: + + # check tag + image_tag = camera_tag.find('image') + if image_tag is not None: + image = Image() + for tag in ['width', 'height', 'format']: + set_attribute(image, tag, image_tag) + sensor.image = image + + # check tag + clip_tag = camera_tag.find('clip') + if clip_tag is not None: + for tag in ['near', 'far']: + set_attribute(sensor, tag, clip_tag) + + # check tag + sensor.noise = check_noise(camera_tag) + + # check tag + plugin_tag = sensor_tag.find('plugin') + if plugin_tag is not None: + sensor.plugin_filename = plugin_tag.attrib.get('filename') + sensor.plugin_name = plugin_tag.attrib.get('name') + + attributes = ['camera_base_topic', 'image_topic', 'camera_info_topic', 'frame_name', 'hack_baseline', + 'distortion_k1', 'distortion_k2', 'distortion_k3', 'distortion_t1', 'distortion_t2', + 'focal_length', 'cx_prime', 'cx', 'cy'] + tags = ['cameraName', 'imageTopicName', 'cameraInfoTopicName', 'frameName', 'hackBaseline', + 'distortionK1', 'distortionK2', 'distortionK3', 'distortionT1', 'distortionT2', 'focalLength', + 'CxPrime', 'Cx', 'Cy'] + for attrib, tag in zip(attributes, tags): + set_attribute(sensor, attrib, plugin_tag, tag) + + elif sensor_type == 'gpu_ray': # GPU RAY sensor + sensor = GPURay(sensor_id=idx, link=tree.bodies[reference], name=sensor_tag.attrib.get('name')) + + set_attribute(sensor, 'pose', sensor_tag) + set_attribute(sensor, 'visualize', sensor_tag) + set_attribute(sensor, 'update_rate', sensor_tag) + + # check tag + ray_tag = sensor_tag.find('ray') + if ray_tag is not None: + + # check tag + scan_tag = ray_tag.find('scan') + if scan_tag is not None: + # check tag + horizontal_tag = scan_tag.find('horizontal') + if horizontal_tag is not None: + attributes = ['samples', 'min_angle', 'max_angle'] + for attrib in attributes: + set_attribute(sensor, attrib, horizontal_tag) + set_attribute(sensor, 'scan_resolution', horizontal_tag, 'resolution') + + # check tag + range_tag = ray_tag.find('range') + if range_tag is not None: + attributes = ['min_range', 'max_range', 'range_resolution'] + tags = ['min', 'max', 'resolution'] + for attrib, tag in zip(attributes, tags): + set_attribute(sensor, attrib, range_tag, tag) + + # check tag + sensor.noise = check_noise(ray_tag) + + # check tag + plugin_tag = sensor_tag.find('plugin') + if plugin_tag is not None: + sensor.plugin_filename = plugin_tag.attrib.get('filename') + sensor.plugin_name = plugin_tag.attrib.get('name') + + attributes = ['topic_name', 'frame_name'] + tags = ['topicName', 'frameName'] + + for attrib, tag in zip(attributes, tags): + set_attribute(sensor, attrib, plugin_tag, tag) + + # elif sensor_type == 'imu': # imu sensor + else: + print("WARNING: THE SENSOR {} WAS NOT PARSED. PLEASE IMPLEMENT THE METHOD.") + + return sensor + def generate(self, tree=None): """ Generate the XML tree from the `Tree` data structure. @@ -472,5 +669,42 @@ class URDFParser(RobotParser): kwargs['upper'] = str(joint.limits[1]) ET.SubElement(joint_tag, 'limit', attrib=kwargs) + # generate transmission + for transmission in tree.transmissions.values(): + # set transmission name + transmission_tag = ET.SubElement(root, 'transmission', attrib={'name': transmission.name}) + + # set transmission type + type_tag = ET.SubElement(transmission_tag, 'type') + type_tag.text = transmission.type + + # set transmission joint + joint_tag = ET.SubElement(transmission_tag, 'joint', attrib={'name': transmission.joint.name}) + joint_hw_tag = ET.SubElement(joint_tag, 'hardwareInterface') + joint_hw_tag.text = transmission.actuator.hardware_interface + + # set transmission actuator + actuator_tag = ET.SubElement(transmission_tag, 'actuator', attrib={'name': transmission.actuator.name}) + hw_tag = ET.SubElement(actuator_tag, 'hardwareInterface') + hw_tag.text = transmission.actuator.hardware_interface + mechanical_reduction_tag = ET.SubElement(actuator_tag, 'mechanicalReduction') + mechanical_reduction_tag.text = str(transmission.actuator.mechanical_reduction) + + # generate sensors + # for sensor in tree.sensors.values(): + # # set sensor reference link/joint + # if isinstance(sensor, JointSensor): + # attrib = {'reference': sensor.joint.name} + # elif isinstance(sensor, LinkSensor): + # attrib = {'reference': sensor.link.name} + # else: + # raise TypeError("Expecting the sensor to be a joint sensor or link sensor...") + # gazebo_tag = ET.SubElement(root, 'gazebo', attrib=attrib) + # + # # set sensor name and type + # sensor_tag = set_name_and_type(gazebo_tag, 'sensor', sensor) + # + # # depending on the sensor generate the necessary tags + # return root XML element return root