diff --git a/python/examples/graz/clik_client.py b/python/examples/graz/clik_client.py index f7f29d0cab415101c62e4d6f8455a8b287fc66b2..1615311fde4a19fa890bd9e9dafaa9f55401e167 100644 --- a/python/examples/graz/clik_client.py +++ b/python/examples/graz/clik_client.py @@ -44,8 +44,8 @@ def controlLoopClikExternalGoal( T_w_e = robot.getT_w_e() data = receiver.getData() T_goal = data["T_goal"] - print("translation:", T_goal.translation) - print("rotation:", pin.Quaternion(T_goal.rotation)) + #print("translation:", T_goal.translation) + #print("rotation:", pin.Quaternion(T_goal.rotation)) SEerror = T_w_e.actInv(T_goal) err_vector = pin.log6(SEerror).vector J = pin.computeFrameJacobian(robot.model, robot.data, q, robot.ee_frame_id) diff --git a/python/examples/graz/joint_copier_client.py b/python/examples/graz/joint_copier_client.py index 0ed3acb8fff8131d216bd7717925a21cfd2abfcc..00bc0af5ec6ad272150b5b4d92fbf535e9be73f7 100644 --- a/python/examples/graz/joint_copier_client.py +++ b/python/examples/graz/joint_copier_client.py @@ -70,7 +70,7 @@ if __name__ == "__main__": if args.save_log: robot.log_manager.plotAllControlLoops() - if args.visualize_manipulator: + if args.visualizer: robot.killManipulatorVisualizer() if args.save_log: diff --git a/python/examples/graz/message_specs_pb2.py b/python/examples/graz/message_specs_pb2.py new file mode 120000 index 0000000000000000000000000000000000000000..ef7c007cfe238804ade48e005ff5d63b52bf974c --- /dev/null +++ b/python/examples/graz/message_specs_pb2.py @@ -0,0 +1 @@ +../../ur_simple_control/networking/message_specs_pb2.py \ No newline at end of file diff --git a/python/examples/graz/protobuf_to_ros1.py b/python/examples/graz/protobuf_to_ros1.py new file mode 100644 index 0000000000000000000000000000000000000000..c01d29d1e482fa3929566eed695b9e3a168575fa --- /dev/null +++ b/python/examples/graz/protobuf_to_ros1.py @@ -0,0 +1,74 @@ +import socket +from google.protobuf.internal.encoder import _VarintBytes +from google.protobuf.internal.decoder import _DecodeVarint32 +import time +import argparse +import message_specs_pb2 + +def getArgs(): + parser = argparse.ArgumentParser() + parser.description = "get ros1 message, make it a protobuf message, send it via tcp socket" + parser.add_argument("--host", type=str, help="host ip address", default="127.0.0.1") + parser.add_argument("--port", type=int, help="host's port", default=6666) + + args = parser.parse_args() + return args + + +def parse_message(buffer): + """ + parse_message + ------------- + here the message is what we got from recv(), + and parsing it refers to finding serialized pb2 messages in it + NOTE: we only keep the latest message because + we're not going to do anything with the missed message. + the assumption is that we only get the same kind of message from + a sensor or something similar, not files or whatever else needs to be whole + """ + pos, next_pos = 0, 0 + buffer_len = len(buffer) + msg_in_bytes = b"" + len_size_offset = 0 + while True: + next_pos, pos = _DecodeVarint32(buffer, pos) + if pos + next_pos > buffer_len: + return msg_in_bytes, pos - len_size_offset + msg_in_bytes = _VarintBytes(pos + next_pos) + buffer[pos : pos + next_pos] + len_size_offset = len(_VarintBytes(pos + next_pos)) + pos += next_pos + if pos >= buffer_len: + return msg_in_bytes, pos + +args = getArgs() +host_addr = (args.host, args.port) +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +buffer = b"" +while True: + try: + s.connect(host_addr) + break + except ConnectionRefusedError: + time.sleep(0.005) +print("NETWORKING CLIENT: connected to server") + +try: + while True: + msg_raw = s.recv(1024) + buffer += msg_raw + msg_in_bytes, pos = parse_message(buffer) + buffer = buffer[pos:] + + next_pos, pos = 0, 0 + next_pos, pos = _DecodeVarint32(msg_in_bytes, pos) + pb2_msg_in_bytes_cut = msg_in_bytes[pos : pos + next_pos] + pb2_msg = message_specs_pb2.wrench() + pb2_msg.ParseFromString(pb2_msg_in_bytes_cut) + + print(pb2_msg.wrench) + # here you send construct and send the ros message + time.sleep(0.002) + +except KeyboardInterrupt: + s.close() + print("NETWORKING_CLIENT: caught KeyboardInterrupt, i'm out") diff --git a/python/examples/graz/ros1_to_protobuf.py b/python/examples/graz/ros1_to_protobuf.py new file mode 100644 index 0000000000000000000000000000000000000000..a9659f99cf790c4e20701fb1256dba2a02d24652 --- /dev/null +++ b/python/examples/graz/ros1_to_protobuf.py @@ -0,0 +1,46 @@ +import socket +from google.protobuf.internal.encoder import _VarintBytes +import argparse +import message_specs_pb2 +import numpy as np +import time + + + +def getArgs(): + parser = argparse.ArgumentParser() + parser.description = "get ros1 message, make it a protobuf message, send it via tcp socket" + parser.add_argument("--host", type=str, help="host ip address", default="127.0.0.1") + parser.add_argument("--port", type=int, help="host's port", default=7777) + + args = parser.parse_args() + return args + +args = getArgs() +host_addr = (args.host, args.port) +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.bind(host_addr) + +print("listening on port ", args.port) +s.listen() +comm_socket, comm_addr = s.accept() +# we're only accepting a single connection +s.close() +print("NETWORKING_SERVER: accepted a client", comm_addr) +try: + while True: + # listen to a pre-determined ros1 message here + pb2_msg = message_specs_pb2.joint_angles() + pb2_msg.q.extend(np.random.random(8)) + print(pb2_msg) + msg_length = pb2_msg.ByteSize() + msg_serialized = pb2_msg.SerializeToString() + msg = _VarintBytes(msg_length) + msg_serialized + comm_socket.send(msg) + time.sleep(0.002) +except KeyboardInterrupt: + if args.debug_prints: + print("NETWORKING_SERVER: caugth KeyboardInterrupt, networking server out") + + +comm_socket.close() diff --git a/python/ur_simple_control/networking/util.py b/python/ur_simple_control/networking/util.py index a3b5e98aed4f1952615278d2719a78a07f6bfc62..4b3bf8b00e23653740fa4e0aadf0e9918ebd55ca 100644 --- a/python/ur_simple_control/networking/util.py +++ b/python/ur_simple_control/networking/util.py @@ -1,8 +1,11 @@ +# TODO: make protobuf an optional import +# then if it's not there use pickling as default import ur_simple_control.networking.message_specs_pb2 as message_specs_pb2 from google.protobuf.internal.encoder import _VarintBytes from google.protobuf.internal.decoder import _DecodeVarint32 import numpy as np import pinocchio as pin +import pickle """ to make the data API uniform across the library (dictionaries), @@ -32,7 +35,6 @@ want to look at protobuf in the rest of the code class DictPb2EncoderDecoder: def __init__(self): # TODO: fill this in with more messages - # int_to_message = {0: message_specs_pb2.joint_angles, 6: message_specs_pb2.wrenches} self.key_to_index = { "q": 0, "wrench": 1, @@ -43,6 +45,7 @@ class DictPb2EncoderDecoder: # if you want to embed message codes into messages, # you'll need to use this # stupidest possible solution + # TODO: implement own bidirectional dict for instead of this # self.index_to_key = {self.key_to_index[key]: key for key in self.key_to_index} @@ -76,7 +79,6 @@ class DictPb2EncoderDecoder: message. ask someone who actually knows network programming what makes more sense """ - # pb2_msg = int_to_message[msg_code] msg_code = self.dictToMsgCode(dict_msg) if msg_code == 1: pb2_msg = message_specs_pb2.joint_angles() @@ -179,3 +181,16 @@ class DictPb2EncoderDecoder: dict_msg["v"] = np.array(pb2_msg.velocity) return dict_msg + + +class DictPickleWithHeaderEncoderDecoder: + def __init__(self): + self.HEADERSIZE = 10 + self.buffer = b"" + + def dictToSerializedMsg(self, dict_msg : dict): + msg = pickle.dumps(dict_msg) + return bytes(f"{len(msg)}:<{self.HEADERSIZE}", 'utf-8')+msg + + def what(self): + pass diff --git a/python/ur_simple_control/robots/heron.py b/python/ur_simple_control/robots/heron.py index 5701db459e244e0d66b43c57e78e24d00ae55bd1..beb6909144d235ce416c1647bcd7f856daa075e3 100644 --- a/python/ur_simple_control/robots/heron.py +++ b/python/ur_simple_control/robots/heron.py @@ -4,3 +4,4 @@ class RobotManagerHeron(RobotManagerAbstract): self.model, self.collision_model, self.visual_model, self.data = ( heron_approximation() ) + self.ee_frame_id = self.model.getFrameId("tool0") diff --git a/python/ur_simple_control/robots/mobile_yumi.py b/python/ur_simple_control/robots/mobile_yumi.py index 3e087f162d084b2daa8d32decfb192f7cd92a88a..8abda3f0a984564f3ad277a1c077cea9ec2356c6 100644 --- a/python/ur_simple_control/robots/mobile_yumi.py +++ b/python/ur_simple_control/robots/mobile_yumi.py @@ -4,3 +4,5 @@ class RobotManagerMobileYumi(RobotManagerAbstract): self.args = args self.model, self.collision_model, self.visual_model, self.data = ( get_yumi_model() + self.r_ee_frame_id = self.model.getFrameId("robr_joint_7") + self.l_ee_frame_id = self.model.getFrameId("robl_joint_7") diff --git a/python/ur_simple_control/robots/robotmanager_abstract.py b/python/ur_simple_control/robots/robotmanager_abstract.py index a47a0e4422a87159d9d4f5b37377672ebe4ffda3..c3e60b2b1b72d7dca089591004ce238dd21f9453 100644 --- a/python/ur_simple_control/robots/robotmanager_abstract.py +++ b/python/ur_simple_control/robots/robotmanager_abstract.py @@ -1,12 +1,18 @@ import abc import argparse import pinocchio as pin +import numpy as np +from ur_simple_control.util.grippers.robotiq.robotiq_gripper import RobotiqGripper +from ur_simple_control.util.grippers.on_robot.twofg import TWOFG from ur_simple_control.util.logging_utils import LogManager +from functools import partial +from ur_simple_control.visualize.visualize import manipulatorVisualizer ################################### # TODO: make everything different on each robot an abstract method. # TODO: try to retain non-robot defined functions like _step, getters, setters etc +# TODO: rename pinocchio_only to real (also toggle the boolean!) ############################################ @@ -19,17 +25,8 @@ class RobotManagerAbstract(abc.ABC): - right now it is assumed you're running this on UR5e so some magic numbers are just put to it. this will be extended once there's a need for it. - - at this stage it's just a boilerplate reduction class - but the idea is to inherit it for more complicated things - with many steps, like dmp. - or just showe additional things in, this is python after all - - you write your controller separately, - and then drop it into this - there is a wrapper function you put - around the control loop which handles timing so that you - actually run at 500Hz and not more. - - this is probably not the most new-user friendly solution, - but it's designed for fastest idea to implementation rate. - - if this was a real programming language, all of these would really be private variables. + - it's just a boilerplate reduction class + - if this was a real programming language, a lot of variables would really be private variables. as it currently stands, "private" functions have the '_' prefix while the public getters don't have a prefix. - TODO: write out default arguments needed here as well @@ -40,56 +37,35 @@ class RobotManagerAbstract(abc.ABC): # might be changed later if that seems more appropriate @abc.abstractmethod def __init__(self, args): - self.args: argparse.Namespace - self.robot_name: str = args.robot - self.model: pin.Model + self.args: argparse.Namespace # const + self.robot_name: str = args.robot # const + self.model: pin.Model # const self.data: pin.Data self.visual_model: pin.pinocchio_pywrap_default.GeometryModel self.collision_model: pin.pinocchio_pywrap_default.GeometryModel if args.save_log: - self.log_manager = LogManager(args) + self._log_manager = LogManager(args) - # last joint because pinocchio adds base frame as 0th joint. - # and since this is unintuitive, we add the other variable too - # so that the control designer doesn't need to think about such bs - # self.JOINT_ID = 6 - self.ee_frame_id = self.model.getFrameId("tool0") - if self.robot_name == "yumi": - self.r_ee_frame_id = self.model.getFrameId("robr_joint_7") - self.l_ee_frame_id = self.model.getFrameId("robl_joint_7") - # JUST FOR TEST - # NOTE - # NOTE - # NOTE - self.ee_frame_id = self.model.getFrameId("robr_joint_7") - # self.ee_frame_id = self.model.getFrameId("hande_right_finger_joint") # TODO: add -1 option here meaning as fast as possible - self.update_rate = args.ctrl_freq # Hz - self.dt = 1 / self.update_rate - # you better not give me crazy stuff - # and i'm not clipping it, you're fixing it - self.MAX_ACCELERATION = 1.7 - assert args.acceleration <= self.MAX_ACCELERATION and args.acceleration > 0.0 - # this is the number passed to speedj - self.acceleration = args.acceleration - # NOTE: this is evil and everything only works if it's set to 1 - # you really should control the acceleration via the acceleration argument. - assert args.speed_slider <= 1.0 and args.acceleration > 0.0 - # TODO: these are almost certainly higher - # NOTE and TODO: speed slider is evil, put it to 1, handle the rest yourself. - # NOTE: i have no idea what's the relationship between max_qdd and speed slider - # self.max_qdd = 1.7 * args.speed_slider - # NOTE: this is an additional kinda evil speed limitation (by this code, not UR). - # we're clipping joint velocities with this. - # if your controllers are not what you expect, you might be commanding a very high velocity, - # which is clipped, resulting in unexpected movement. - self.MAX_QD = 3.14 - assert args.max_qd <= self.MAX_QD and args.max_qd > 0.0 - self.max_qd = args.max_qd + self._update_rate: int = self.args.ctrl_freq # Hz + self._dt = 1 / self._update_rate + self._t: float = 0.0 + # defined per robot - hardware limit + self._MAX_ACCELERATION: float | np.ndarray # in joint space + assert ( + self.args.acceleration <= self._MAX_ACCELERATION + and self.args.acceleration > 0.0 + ) + self._acceleration = self.args.acceleration + # defined per robot - hardware limit + self._MAX_QD: float | np.ndarray + assert self.args.max_qd <= self._MAX_QD and self.args.max_qd > 0.0 + # internally clipped to this + self._max_qd = args.max_qd self.gripper = None - if (self.args.gripper != "none") and not self.pinocchio_only: + if (self.args.gripper != "none") and self.args.real: if self.args.gripper == "robotiq": self.gripper = RobotiqGripper() self.gripper.connect(args.robot_ip, 63352) @@ -97,23 +73,16 @@ class RobotManagerAbstract(abc.ABC): if self.args.gripper == "onrobot": self.gripper = TWOFG() - # TODO: specialize for each robot, - # add reasonable home positions - self.q = pin.randomConfiguration( - self.model, -1 * np.ones(self.model.nq), np.ones(self.model.nq) - ) - if self.robot_name == "ur5e": - self.q[-1] = 0.0 - self.q[-2] = 0.0 - # v_q is the generalization of qd for every type of joint. - # for revolute joints it's qd, but for ex. the planar joint it's the body velocity. + # NOTE: make sure you've read pinocchio docs and understand why + # nq and nv are not necessarily the same number + self.q = np.zeros(self.model.nq) self.v_q = np.zeros(self.model.nv) - # same note as v_q, but it's a_q. self.a_q = np.zeros(self.model.nv) # start visualize manipulator process if selected. - # has to be started here because it lives throughout the whole run - if args.visualize_manipulator: + # since there is only one robot and one visualizer, this is robotmanager's job + self.visualizer_manager: None | ProcessManager + if args.visualizer: side_function = partial( manipulatorVisualizer, self.model, @@ -124,47 +93,44 @@ class RobotManagerAbstract(abc.ABC): args, side_function, {"q": self.q.copy()}, 0 ) if args.visualize_collision_approximation: - pass - # TODO: import the ellipses here, and write an update ellipse function - # then also call that in controlloopmanager - - # initialize and connect the interfaces - if not args.pinocchio_only: - # NOTE: you can't connect twice, so you can't have more than one RobotManager. - # if this produces errors like "already in use", and it's not already in use, - # try just running your new program again. it could be that the socket wasn't given - # back to the os even though you've shut off the previous program. - print("CONNECTING TO UR5e!") - self.rtde_control = RTDEControlInterface(args.robot_ip) - self.rtde_receive = RTDEReceiveInterface(args.robot_ip) - self.rtde_io = RTDEIOInterface(args.robot_ip) - self.rtde_io.setSpeedSlider(args.speed_slider) - # NOTE: the force/torque sensor just has large offsets for no reason, - # and you need to minus them to have usable readings. - # we provide this with calibrateFT - self.wrench_offset = self.calibrateFT() + # TODO: import the ellipses here, and write an update ellipse function + # then also call that in controlloopmanager + raise NotImplementedError( + "sorry, almost done - look at utils to get 90% of the solution!" + ) else: - self.wrench_offset = np.zeros(6) + self.visualizer_manager = None - self.speed_slider = args.speed_slider + self.connectToRobot() - if args.pinocchio_only and args.start_from_current_pose: - self.rtde_receive = RTDEReceiveInterface(args.robot_ip) - q = self.rtde_receive.getActualQ() - q.append(0.0) - q.append(0.0) - q = np.array(q) - self.q = q - if args.visualize_manipulator: - self.visualizer_manager.sendCommand({"q": q}) + # wrench being the force-torque sensor reading, if any + self.wrench_offset = np.zeros(6) - # do it once to get T_w_e + self.setInitialPose() self._step() ####################################################################### # getters which assume you called step() # ####################################################################### + # NOTE: just do nothing if you're only integrating with pinocchio, + # or start a physics simulator if you're using it + @abc.abstractmethod + def connectToRobot(self): + pass + + @abc.abstractmethod + def setInitialPose(self): + """ + for example, if just integrating, do: + self.q = pin.randomConfiguration( + self.model, -1 * np.ones(self.model.nq), np.ones(self.model.nq) + ) + NOTE: you probably want to specialize this for your robot + to have it reasonable (no-self collisions, particular home etc) + """ + pass + def getQ(self): return self.q.copy() diff --git a/python/ur_simple_control/robots/single_arm_interface.py b/python/ur_simple_control/robots/single_arm_interface.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c12ad68a1c44e1760120bbae6f506f5c536be76e 100644 --- a/python/ur_simple_control/robots/single_arm_interface.py +++ b/python/ur_simple_control/robots/single_arm_interface.py @@ -0,0 +1,9 @@ +import abs +from ur_simple_control.robots.robotmanager_abstract import RobotManagerAbstract + + +class SingleArmInterface(RobotManagerAbstract): + def __init__(self): + # idk if this is correct + super().__init__ + self.ee_frame_id = self.model.getFrameId("tool0") diff --git a/python/ur_simple_control/robots/ur5e.py b/python/ur_simple_control/robots/ur5e.py index 710db78556e1db8319c9c110eb5636f3b3ffd209..311314e2e04bd47ae1794a2615655df8baba045c 100644 --- a/python/ur_simple_control/robots/ur5e.py +++ b/python/ur_simple_control/robots/ur5e.py @@ -1,8 +1,63 @@ from ur_simple_control.robots.robotmanager_abstract import RobotManagerAbstract +from ur_simple_control.robots.single_arm_interface import SingleArmInterface from ur_simple_control.util.get_model import get_model +import numpy as np +import pinocchio as pin -class RobotManagerUR5e(RobotManagerAbstract): +# NOTE: this one assumes a jaw gripper +class RobotManagerUR5e(SingleArmInterface): def __init__(self, args): self.args = args self.model, self.collision_model, self.visual_model, self.data = get_model() + self._MAX_ACCELERATION = 1.7 # const + self._MAX_QD = 3.14 # const + # NOTE: this is evil and everything only works if it's set to 1 + # you really should control the acceleration via the acceleration argument. + # we need to set it to 1.0 with ur_rtde so that's why it's here and explicitely named + self._speed_slider = 1.0 # const + # TODO: remove gripper degrees of freedom from the model, it just ruins stuff + if self.robot_name == "ur5e": + self.q[-1] = 0.0 + self.q[-2] = 0.0 + + if self.args.real: + # NOTE: you can't connect twice, so you can't have more than one RobotManager per robot. + # if this produces errors like "already in use", and it's not already in use, + # try just running your new program again. it could be that the socket wasn't given + # back to the os even though you've shut off the previous program. + print("CONNECTING TO UR5e!") + self.rtde_control = RTDEControlInterface(self.args.robot_ip) + self.rtde_receive = RTDEReceiveInterface(self.args.robot_ip) + self.rtde_io = RTDEIOInterface(self.args.robot_ip) + self.rtde_io.setSpeedSlider(self.args.speed_slider) + # NOTE: the force/torque sensor just has large offsets for no reason, + # and you need to minus them to have usable readings. + # we provide this with calibrateFT + self.wrench_offset = self.calibrateFT() + else: + self.wrench_offset = np.zeros(6) + + self.setInitialPose() + if not args.real and args.start_from_current_pose: + self.rtde_receive = RTDEReceiveInterface(args.robot_ip) + q = self.rtde_receive.getActualQ() + q.append(0.0) + q.append(0.0) + q = np.array(q) + self.q = q + if args.visualize_manipulator: + self.visualizer_manager.sendCommand({"q": q}) + + def setInitialPose(self): + if not self.args.real and self.args.start_from_current_pose: + self.rtde_receive = RTDEReceiveInterface(args.robot_ip) + self.q = np.array(self.rtde_receive.getActualQ()) + if self.args.visualize_manipulator: + self.visualizer_manager.sendCommand({"q": self.q}) + if not self.args.real and not self.args.start_from_current_pose: + self.q = pin.randomConfiguration( + self.model, -1 * np.ones(self.model.nq), np.ones(self.model.nq) + ) + if self.args.real: + self.q = np.array(self.rtde_receive.getActualQ())