From 99cb0ca6e75f8ac29ee2c2affa33a6cdbe2c0a54 Mon Sep 17 00:00:00 2001 From: m-guberina <gubi.guberina@gmail.com> Date: Thu, 23 Jan 2025 19:13:07 +0100 Subject: [PATCH] double serialization is goin on because shared memory in python is weird and protobuf doesn't delimit the messages by itself. the message lenth should cut the buffer, but something evil happens and then i doesn't wokr. idk what any of this is about. the pickling works on time, it's just stupid. --- python/convenience_tool_box/freedrive_v2.0.py | 24 +- python/examples/graz/joint_copier_client.py | 78 ++++++ .../examples/graz/point_impedance_server.py | 231 ++++++++++++++++++ .../__pycache__/managers.cpython-311.pyc | Bin 61911 -> 61103 bytes python/ur_simple_control/managers.py | 47 +++- .../ur_simple_control/networking/__init__.py | 6 +- python/ur_simple_control/networking/client.py | 57 ++++- .../networking/message_specs.proto | 1 + .../networking/message_specs_pb2.py | 10 +- python/ur_simple_control/networking/server.py | 2 - python/ur_simple_control/networking/util.py | 35 ++- 11 files changed, 438 insertions(+), 53 deletions(-) create mode 100644 python/examples/graz/joint_copier_client.py create mode 100644 python/examples/graz/point_impedance_server.py diff --git a/python/convenience_tool_box/freedrive_v2.0.py b/python/convenience_tool_box/freedrive_v2.0.py index ea35610..5c7c519 100644 --- a/python/convenience_tool_box/freedrive_v2.0.py +++ b/python/convenience_tool_box/freedrive_v2.0.py @@ -3,23 +3,31 @@ import numpy as np import time import argparse from functools import partial -from ur_simple_control.managers import getMinimalArgParser, ControlLoopManager, RobotManager +from ur_simple_control.managers import ( + getMinimalArgParser, + ControlLoopManager, + RobotManager, +) from ur_simple_control.basics.basics import freedriveUntilKeyboard import pickle + def get_args(): parser = getMinimalArgParser() - parser.description = 'nothing' + parser.description = ( + "freedrive with keyboard bindings to save poses and joint angles" + ) # add more arguments here from different Simple Manipulator Control modules args = parser.parse_args() return args -if __name__ == "__main__": + +if __name__ == "__main__": args = get_args() robot = RobotManager(args) - pose_n_q_dict = freedriveUntilKeyboard(args, robot) - file = open("./pose_n_q_dict.pickle", 'wb') + pose_n_q_dict = freedriveUntilKeyboard(args, robot) + file = open("./pose_n_q_dict.pickle", "wb") pickle.dump(pose_n_q_dict, file) file.close() print(pose_n_q_dict) @@ -31,9 +39,9 @@ if __name__ == "__main__": if args.save_log: robot.log_manager.plotAllControlLoops() - if args.visualize_manipulator: + if args.visualizer: robot.killManipulatorVisualizer() - + if args.save_log: robot.log_manager.saveLog() - #loop_manager.stopHandler(None, None) + # loop_manager.stopHandler(None, None) diff --git a/python/examples/graz/joint_copier_client.py b/python/examples/graz/joint_copier_client.py new file mode 100644 index 0000000..cd95e82 --- /dev/null +++ b/python/examples/graz/joint_copier_client.py @@ -0,0 +1,78 @@ +import pinocchio as pin +import numpy as np +from functools import partial +from ur_simple_control.networking.client import client +from ur_simple_control.managers import ( + ProcessManager, + getMinimalArgParser, + ControlLoopManager, + RobotManager, +) + + +def get_args(): + parser = getMinimalArgParser() + parser.description = "the robot will received joint angles from a socket and go to them in joint space" + # add more arguments here from different Simple Manipulator Control modules + 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 + + +def controlLoopExternalQ(robot: RobotManager, receiver: ProcessManager, i, past_data): + """ + controlLoop + ----------------------------- + controller description + """ + breakFlag = False + log_item = {} + save_past_dict = {} + + q = robot.getQ() + + q_desired = receiver.getData()["q"] + q_error = q_desired - q + K = 120 + qd_cmd = K * q_error * robot.dt + + robot.sendQd(qd_cmd) + + log_item["qs"] = q.reshape((robot.model.nq,)) + log_item["dqs"] = robot.getQd().reshape((robot.model.nv,)) + log_item["q_error"] = q_error.reshape((robot.model.nq,)) + return breakFlag, save_past_dict, log_item + + +if __name__ == "__main__": + args = get_args() + robot = RobotManager(args) + + # get expected behaviour here (library can't know what the end is - you have to do this here) + if not args.pinocchio_only: + robot.stopRobot() + + # VERY important that the first q we'll pass as desired is the current q, meaning the robot won't move + # this is set with init_value + receiver = ProcessManager( + args, client, {"q": robot.q.copy()}, 4, init_value={"q": robot.q.copy()} + ) + log_item = { + "qs": np.zeros((robot.model.nq,)), + "dqs": np.zeros((robot.model.nv,)), + "q_error": np.zeros((robot.model.nq,)), + } + control_loop = partial(controlLoopExternalQ, robot, receiver) + loop_manager = ControlLoopManager(robot, control_loop, args, {}, log_item) + loop_manager.run() + + if args.save_log: + robot.log_manager.plotAllControlLoops() + + if args.visualize_manipulator: + robot.killManipulatorVisualizer() + + if args.save_log: + robot.log_manager.saveLog() + # loop_manager.stopHandler(None, None) diff --git a/python/examples/graz/point_impedance_server.py b/python/examples/graz/point_impedance_server.py new file mode 100644 index 0000000..f525008 --- /dev/null +++ b/python/examples/graz/point_impedance_server.py @@ -0,0 +1,231 @@ +# PYTHON_ARGCOMPLETE_OK +import pinocchio as pin +import numpy as np +import copy +import argparse +from functools import partial +from ur_simple_control.clik.clik import ( + getClikArgs, + getClikController, + moveL, + moveUntilContact, +) +from ur_simple_control.managers import ( + ProcessManager, + getMinimalArgParser, + ControlLoopManager, + RobotManager, +) +from ur_simple_control.networking.server import server + + +def getArgs(): + parser = getMinimalArgParser() + parser = getClikArgs(parser) + parser.description = "the robot will stay in place, with impedance. it sends it's joint angles over tcp to whomever connects to the port" + parser.add_argument( + "--kp", + type=float, + help="proportial control constant for position errors", + default=1.0, + ) + parser.add_argument( + "--kv", type=float, help="damping in impedance control", default=0.001 + ) + parser.add_argument( + "--cartesian-space-impedance", + action=argparse.BooleanOptionalAction, + help="is the impedance computed and added in cartesian or in joint space", + default=False, + ) + parser.add_argument( + "--z-only", + action=argparse.BooleanOptionalAction, + help="whether you have general impedance or just ee z axis", + default=False, + ) + 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 + + +# feedforward velocity, feedback position and force for impedance +def controller(): + pass + + +def controlLoopPointImpedanceServer( + args, q_init, sender: ProcessManager, controller, robot: RobotManager, i, past_data +): + breakFlag = False + # TODO rename this into something less confusing + save_past_dict = {} + log_item = {} + q = robot.getQ() + Mtool = robot.getT_w_e() + wrench = robot.getWrench() + log_item["wrench_raw"] = wrench.reshape((6,)) + # deepcopy for good coding practise (and correctness here) + save_past_dict["wrench"] = copy.deepcopy(wrench) + # rolling average + # wrench = np.average(np.array(past_data['wrench']), axis=0) + # first-order low pass filtering instead + # beta is a smoothing coefficient, smaller values smooth more, has to be in [0,1] + # wrench = args.beta * wrench + (1 - args.beta) * past_data['wrench'][-1] + wrench = args.beta * wrench + (1 - args.beta) * np.average( + np.array(past_data["wrench"]), axis=0 + ) + if not args.z_only: + Z = np.diag(np.array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0])) + else: + Z = np.diag(np.array([0.0, 0.0, 1.0, 0.0, 0.0, 0.0])) + # Z = np.diag(np.ones(6)) + + wrench = Z @ wrench + + # this jacobian might be wrong + J = pin.computeFrameJacobian(robot.model, robot.data, q, robot.ee_frame_id) + dq = robot.getQd()[:6].reshape((6, 1)) + # get joint + tau = J.T @ wrench + tau = tau[:6].reshape((6, 1)) + # compute control law: + # - feedback the position + # kv is not needed if we're running velocity control + vel_cmd = ( + args.kp * (q_init[:6].reshape((6, 1)) - q[:6].reshape((6, 1))) + + args.alpha * tau + ) + # vel_cmd = np.zeros(6) + robot.sendQd(vel_cmd) + sender.sendCommand({"q": q}) + + # immediatelly stop if something weird happened (some non-convergence) + if np.isnan(vel_cmd[0]): + breakFlag = True + + # log what you said you'd log + # TODO fix the q6 situation (hide this) + log_item["qs"] = q[:6].reshape((6,)) + log_item["dqs"] = dq.reshape((6,)) + log_item["wrench_used"] = wrench.reshape((6,)) + + return breakFlag, save_past_dict, log_item + + +def controlLoopCartesianPointImpedanceServer( + args, + Mtool_init, + sender: ProcessManager, + clik_controller, + robot: RobotManager, + i, + past_data, +): + breakFlag = False + # TODO rename this into something less confusing + save_past_dict = {} + log_item = {} + q = robot.getQ() + Mtool = robot.getT_w_e() + wrench = robot.getWrench() + log_item["wrench_raw"] = wrench.reshape((6,)) + save_past_dict["wrench"] = copy.deepcopy(wrench) + # wrench = args.beta * wrench + (1 - args.beta) * past_data['wrench'][-1] + wrench = args.beta * wrench + (1 - args.beta) * np.average( + np.array(past_data["wrench"]), axis=0 + ) + # good generic values + # Z = np.diag(np.array([1.0, 1.0, 2.0, 1.0, 1.0, 1.0])) + # but let's stick to the default for now + if not args.z_only: + Z = np.diag(np.array([1.0, 1.0, 1.0, 10.0, 10.0, 10.0])) + else: + Z = np.diag(np.array([0.0, 0.0, 1.0, 0.0, 0.0, 0.0])) + # Z = np.diag(np.array([1.0, 1.0, 1.0, 10.0, 10.0, 10.0])) + # Z = np.diag(np.array([0.0, 0.0, 1.0, 0.0, 0.0, 0.0])) + + wrench = Z @ wrench + + J = pin.computeFrameJacobian(robot.model, robot.data, q, robot.ee_frame_id) + + SEerror = Mtool.actInv(Mtool_init) + err_vector = pin.log6(SEerror).vector + v_cartesian_body_cmd = args.kp * err_vector + args.alpha * wrench + + vel_cmd = clik_controller(J, v_cartesian_body_cmd) + robot.sendQd(vel_cmd) + sender.sendCommand({"q": q}) + + # immediatelly stop if something weird happened (some non-convergence) + if np.isnan(vel_cmd[0]): + breakFlag = True + + dq = robot.getQd()[:6].reshape((6, 1)) + # log what you said you'd log + # TODO fix the q6 situation (hide this) + log_item["qs"] = q[:6].reshape((6,)) + log_item["dqs"] = dq.reshape((6,)) + log_item["wrench_used"] = wrench.reshape((6,)) + + return breakFlag, save_past_dict, log_item + + +if __name__ == "__main__": + args = getArgs() + robot = RobotManager(args) + clikController = getClikController(args, robot) + + # TODO: and NOTE the weight, TCP and inertial matrix needs to be set on the robot + # you already found an API in rtde_control for this, just put it in initialization + # under using/not-using gripper parameters + # NOTE: to use this you need to change the version inclusions in + # ur_rtde due to a bug there in the current ur_rtde + robot firmware version + # (the bug is it works with the firmware verion, but ur_rtde thinks it doesn't) + # here you give what you're saving in the rolling past window + # it's initial value. + # controlLoopManager will populate the queue with these initial values + save_past_dict = { + "wrench": np.zeros(6), + } + # here you give it it's initial value + log_item = { + "qs": np.zeros(robot.n_arm_joints), + "dqs": np.zeros(robot.n_arm_joints), + "wrench_raw": np.zeros(6), + "wrench_used": np.zeros(6), + } + q_init = robot.getQ() + Mtool_init = robot.getT_w_e() + sender = ProcessManager(args, server, {"q": np.zeros(6)}, 0) + + if not args.cartesian_space_impedance: + controlLoop = partial( + controlLoopPointImpedanceServer, args, q_init, sender, controller, robot + ) + else: + controlLoop = partial( + controlLoopCartesianPointImpedanceServer, + args, + Mtool_init, + sender, + clikController, + robot, + ) + + loop_manager = ControlLoopManager( + robot, controlLoop, args, save_past_dict, log_item + ) + loop_manager.run() + + if not args.pinocchio_only: + robot.stopRobot() + + if args.save_log: + robot.log_manager.saveLog() + robot.log_manager.plotAllControlLoops() + + if args.visualize_manipulator: + robot.killManipulatorVisualizer() diff --git a/python/ur_simple_control/__pycache__/managers.cpython-311.pyc b/python/ur_simple_control/__pycache__/managers.cpython-311.pyc index 69c68cad978f4dfb5a8e0f4a8ea599a674315448..2f3690a363cf4d9c51db5e28701a6d2339495f7b 100644 GIT binary patch delta 2920 zcmccqn0ftOX1?XTyj%<n3=Cy;lhTEMOyrYbys}YUhEd!gg&~D8$1ut$)hNptqyU5$ zm`rZu6rG&TDCTIIYZhh3$dJO2B9vpEYY}AuW(((7=2}HrF*0y6q=>XIL|HR2xHF`P zwlJiKEn{L}Sj_}71%#t)ChudEl(q%SOSUkiNTJEwZT`eq!_Cj5t6P>?T$-4ZS(RE; z<uUoMn9}4`yy{$>y1E59`6VT(MVp`THgU0eGcYg|S8mReaAahk!MC>JioWmU?UHKj zGq~4!T+#8`{8aKiKL<Nh%jD;pGZ?RJZqVXjWR%(5shz>f#Ra!zGn*kdBjfkULPk#+ zl{fz|l4N95*eqmnnuXD7@;!^Uj53o2oTWBDvpmesn7DbBV=5z9M3GT`vXS!|#?_ne zI}3@jtz~3jDE8a@Jz^a*Q;+@T6)`!COg;6J#p4bzI!?Y3x02Cib5nc=E2H~n?$kUc z#_Y{i>4B__d7IDVxN|a2+ALDm#>9AF^X!VJER3f&H`X$7GQQi~(srJeQFU`jPZBeu z`{wQa>lhhBH>XY1VP=foJbB7-ZpQr0(F<Z&881xkT^_cXZ&@89<E_oT%YSn-zS!Kk z*^h})e)Em3S<H+oo1J$~W@I$le0^69BctWyvb_qMOZG4_GTLu0+ndZJ>dL~vP#nd; zz|g?(L4birpvUqGxBTXJ`*j%^gEz|@6lLTLXJKF{vSnmom>hURdUF0@tI7R`<}yZa z);-L_$QVD_>c|JiiJP^L3NuZV<%|hqU|^WekjfCnn8Fanl)@OroWha9+`<^elERY8 zn#Bf64IrG#p30HM31We88j~c50^>BM6xJ4&D6SN?7KSM96!sQ|D4rCiU<OUj%@dFR zV_}kF+dT118?#FdOA1dmIE58$OJPjm%@NO)sO4m2U}C7@T*kn_uo`R{h^S$RhZ|qR zkOgNikeb}cDKc4Zu7E%dL%cLdDF~<V)v`@CJTK16mcpLGzlLKOGXukF=E({nB9k-E zi%Hb5E@Nb1Sj`Ah3c|HqHC#0eSu&HSpI7ALLXn$vUWHM1^4+u2lOLRC;g$ud2H`Z8 z$%SWSm`ZFXAFPwAFR=rOK`_)m;x$ZJ3J?(nhAc%GtA-(q6~>NNg0b>MYM2meQUt0P z7#OgttYNKTh*yT`gQ{T%^B1U0cHE{i`R)Z?byctc0|P^mT!}A?&A^Z%2-N@<%2Jvv zcu|#6XtMdmNP{AIMg|5=##^E;naL#qNk*=D$@wX%MJ}me>K1QuPG)LeNqljBa&~G- zCIidlzKeQ{>XY|gJfC<=v9u_@I5W2(CpA7fKd+=HKSwVwwWK`1C_6JRU9YqxGpER( zk%8eBe=5Y#_!O9_DVfP7A^E<=>CX8nskb<Ci__yl_T6GH&d80=El$71l9QjDJ-O$S zxdL}l1js)=AmS(k1H&zz%)HE!_~iWD+{C<;qM*r-FKH`4>=7!+OwP_pO^FAqC`-&K zO)chTU|>*Cm@Ir*N4^o1TWpv?x#fn4L<dU`=M6cX4wfq%(jV9uc%&xBT()I}u;yI0 zX8phg61#Od8YCv6IN9RLqk6FHjKE9Wau>MeZt(bDKt(r{EiNcqTvWEYqHNXSa)n3X zhK#}$8H4LGc9&%AcGO&y@wp=7)8W$N`#@Z+!{aL(gN)`CY10iE2TCpkg<X&izbGAk zMLN90rN`$h7lV?<5}6fQ7Zok9C|X|Nk-K5(-Qnrd<8y^a=7Oy0+~f;NCKu&QugIBR zkTt!?V|G_UW+vMlwhosb&$|lhD{61(8*hj?U~wYkOymW<@DI$4ijj;T89?M05II@o z>c+_r?sGEEnaugbn=6GW8=NwVq$VpqSDI{aO=z;iOy0@sr*bgYm2gh}cwbr>T=Rex zF(3*8NRlsR@?vOWtYMrSkRT?O1xb?(3=GJUlP^A3nq1J!$HT}_!<fRH!raOv$uQY5 zQ-lw~v4Ny}&dJ;_^mtR4gBd_rlO?f^d9(kuP(}qV1t`c&Qvk($d`f0fYH~?tex8D@ zt%9-D<iKkpleHi7Pj<)>nY?Bj*W?Y`1U4%?+{GAO^b?eDK-Hq3CQnfiBLhQGFepv3 z78Yk#rQYHyC@qQ4%P&vNEGg0escZ)kXF)_HNNqHThyiI}PfRW>%`8g2#a@(}lbTqZ zsyX@Wb&L87phR^AQUq%;@CaPtmRq2BiCg&sxAF~s;Vb-#*ZI{i@vC3t*Sx~7xx(=h zzwQNo-5UZTR|J%<3+P@F&|Q&!QNZkqfY}8Bvj^P#NZRiTi_c)6XgkGrsnZg#OTu~= zVCaUh)D+tVE>~2{uc+|aUJ|yw07H`}JQJ0?p{lbY5CoS*ffAwo0!9$L$fE?3<-5S6 zcY#Om12+SY$^~xa$!{M^izP9Eiv1dfEJ*Y+FfgPrW;1M-dt$;k`R)ZCz7!@%R2F5` zFl6yej(?yx`PLK3$<c|NOj%r$FY1V~)iQ!*xL@comB>!sST3cO!ko<lGOmcdhOver z9?S;uOXNW$1ZN3MHhds5`KAtEeK3P2iytJaK;=;psA|v@ghW>;DD+uVa|=o;Z*irk z!lNr47BxkMAXXe$Sx$apN^wyvNFWMC6oH6T5Rn2Rv_M1<D0YJx7#K8}z-7xV4!*q9 z^7xd*lEh+=#R`*OK2?|e2#Q!XMsR5Xi5J03+)5X?l_qOG3t)UYx$2pA{SAl&EWUXJ zdOWZ5$X?=+y~rbfg-8AZkNj6Y1_`+-H49vp_*_vjxhQUWMclN*;|3O$clm`TSajC* z)Xpfp#IJCHU*U$b_5_wI0t#O_7z9LmYG;I8k=4D(uXlxC?*fNjkv#(g!{)uub~3Sn z({#k-nwJU!At3Life26<FDjV4=A|bi_vUXeD;ODhCl?%%RK3MkQdy9hmwt;iG%qth zugDFg7?I{PK|xb7dERTq`c#nrJHQFMfdK+PFiEg-d|&_(W*&^JevBU&uu*YLa;*9b z+?M!k5ZO?)rRsvA;}sRB4-9gw`sjocqXbA0PMGkqIx~J?0Fjf!-pDfAZ!URrjgbu$ zwMDg)z2D8?n!?Dy0II!<UvGZ<?gZoHH6I>%mM}4bbm68xGJxg2K&TJcC~$ZJ02U)l AnE(I) delta 3793 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 diff --git a/python/ur_simple_control/managers.py b/python/ur_simple_control/managers.py index 7aa7927..e09c5dc 100644 --- a/python/ur_simple_control/managers.py +++ b/python/ur_simple_control/managers.py @@ -101,9 +101,9 @@ def getMinimalArgParser(): help="frequency of the control loop", default=500) parser.add_argument('--fast-simulation', action=argparse.BooleanOptionalAction, \ help="do you want simulation to as fast as possible? (real-time viz relies on 500Hz)", default=False) - parser.add_argument('--visualize-manipulator', action=argparse.BooleanOptionalAction, \ + parser.add_argument('--visualizer', action=argparse.BooleanOptionalAction, \ help="whether you want to visualize the manipulator and workspace with meshcat", default=True) - parser.add_argument('--real-time-plotting', action=argparse.BooleanOptionalAction, \ + parser.add_argument('--plotter', action=argparse.BooleanOptionalAction, \ help="whether you want to have some real-time matplotlib graphs (parts of log_dict you select)", default=True) parser.add_argument('--gripper', type=str, \ help="gripper you're using (no gripper is the default)", \ @@ -240,7 +240,7 @@ class ControlLoopManager: for key in log_item: self.log_dict[key] = [] - if self.args.real_time_plotting: + if self.args.plotter: self.plotter_manager = ProcessManager(args, realTimePlotter, log_item, 0) @@ -285,7 +285,7 @@ class ControlLoopManager: # so this should be an argument that is use ONLY if we're in simulation if i % 20 == 0: # don't send what wasn't ready - if self.args.visualize_manipulator: + if self.args.visualizer: if self.robot_manager.robot_name != "yumi": self.robot_manager.visualizer_manager.sendCommand({"q" : self.robot_manager.q, "T_w_e" : self.robot_manager.getT_w_e()}) @@ -304,7 +304,7 @@ class ControlLoopManager: pass # TODO: here call robot manager's update ellipses function - if self.args.real_time_plotting: + if self.args.plotter: # don't put new stuff in if it didn't handle the previous stuff. # it's a plotter, who cares if it's late. # the number 5 is arbitrary @@ -339,7 +339,7 @@ class ControlLoopManager: ###################################################################### # for over ###################################################################### - if self.args.real_time_plotting: + if self.args.plotter: self.plotter_manager.terminateProcess() if self.args.save_log: self.robot_manager.log_manager.storeControlLoopRun(self.log_dict, self.controlLoop.func.__name__, self.final_iteration) @@ -385,10 +385,10 @@ class ControlLoopManager: self.robot_manager.log_manager.storeControlLoopRun(self.log_dict, self.controlLoop.func.__name__, self.final_iteration) self.robot_manager.log_manager.saveLog() - if self.args.real_time_plotting: + if self.args.plotter: self.plotter_manager.terminateProcess() - if self.args.visualize_manipulator: + if self.args.visualizer: self.robot_manager.visualizer_manager.terminateProcess() # TODO: this obviously only makes sense if you're on ur robots @@ -528,7 +528,7 @@ class RobotManager: # start visualize manipulator process if selected. # has to be started here because it lives throughout the whole run - if args.visualize_manipulator: + if args.visualizer: side_function = partial(manipulatorVisualizer, self.model, self.collision_model, self.visual_model) self.visualizer_manager = ProcessManager(args, side_function, {"q" : self.q.copy()}, 0) if args.visualize_collision_approximation: @@ -563,7 +563,7 @@ class RobotManager: q.append(0.0) q = np.array(q) self.q = q - if args.visualize_manipulator: + if args.visualizer: self.visualizer_manager.sendCommand({"q" : q}) @@ -1051,7 +1051,7 @@ class RobotManager: # NOTE i'm not deepcopying this on purpose # but that might be the preferred thing, we'll see self.Mgoal = Mgoal - if self.args.visualize_manipulator: + if self.args.visualizer: # TODO document this somewhere self.visualizer_manager.sendCommand( {"Mgoal" : Mgoal}) @@ -1098,7 +1098,7 @@ class RobotManager: NOTE: this function does not change internal variables! because it shouldn't - it should only update the visualizer """ - if self.args.visualize_manipulator: + if self.args.visualizer: self.visualizer_manager.sendCommand(viz_dict) else: if self.args.debug_prints: @@ -1154,14 +1154,19 @@ class ProcessManager: self.args = args self.comm_direction = comm_direction + # send command to slave process if comm_direction == 0: self.command_queue = Queue() self.side_process = Process(target=side_function, args=(args, init_command, self.command_queue,)) + # get data from slave process if comm_direction == 1: self.data_queue = Queue() self.side_process = Process(target=side_function, args=(args, init_command, self.data_queue,)) + # share data in both directions via shared memory with 2 buffers + # - one buffer for master to slave + # - one buffer for slave to master if comm_direction == 2: self.command_queue = Queue() self.data_queue = Queue() @@ -1190,6 +1195,7 @@ class ProcessManager: # the process has to create its shared memory self.side_process = Process(target=side_function, args=(args, init_command, shm_name, self.shared_command_lock, self.shm_data,)) + # networking client (server can use comm_direction 0) if comm_direction == 4: from ur_simple_control.networking.util import DictPb2EncoderDecoder self.encoder_decoder = DictPb2EncoderDecoder() @@ -1199,6 +1205,12 @@ class ProcessManager: # NOTE: size is max size of the recv buffer too, # and the everything blows up if you manage to fill it atm self.shm_msg = shared_memory.SharedMemory(shm_name, create=True, size=1024) + # need to initialize shared memory with init value + # NOTE: EVIL STUFF SO PICKLING ,READ NOTES IN networking/client.py + #init_val_as_msg = self.encoder_decoder.dictToSerializedPb2Msg(init_value) + #self.shm_msg.buf[:len(init_val_as_msg)] = init_val_as_msg + pickled_init_value = pickle.dumps(init_value) + self.shm_msg.buf[:len(pickled_init_value)] = pickled_init_value self.lock = Lock() self.side_process = Process(target=side_function, args=(args, init_command, shm_name, self.lock)) @@ -1262,8 +1274,17 @@ class ProcessManager: if self.comm_direction == 4: self.lock.acquire() #data_copy = copy.deepcopy(self.shm_msg.buf) - self.latest_data = self.encoder_decoder.serializedPb2MsgToDict(self.shm_msg.buf, self.msg_code) + # REFUSES TO WORK IF YOU DON'T PRE-CROP HERE!!! + # MAKES ABSOLUTELY NO SENSE!!! READ MORE IN ur_simple_control/networking/client.py + # so we're decoding there, pickling, and now unpickling. + # yes, it's incredibly stupid + #new_data = self.encoder_decoder.serializedPb2MsgToDict(self.shm_msg.buf, self.msg_code) + new_data = pickle.loads(self.shm_msg.buf) self.lock.release() + if len(new_data) > 0: + self.latest_data = new_data + #print("new_data", new_data) + #print("self.latest_data", self.latest_data) #self.latest_data = self.encoder_decoder.serializedPb2MsgToDict(data_copy, self.msg_code) return copy.deepcopy(self.latest_data) diff --git a/python/ur_simple_control/networking/__init__.py b/python/ur_simple_control/networking/__init__.py index df760a3..c466656 100644 --- a/python/ur_simple_control/networking/__init__.py +++ b/python/ur_simple_control/networking/__init__.py @@ -1,3 +1,3 @@ -from client import * -from server import * -from util import * +from .client import * +from .server import * +from .util import * diff --git a/python/ur_simple_control/networking/client.py b/python/ur_simple_control/networking/client.py index 201dd32..30545be 100644 --- a/python/ur_simple_control/networking/client.py +++ b/python/ur_simple_control/networking/client.py @@ -2,6 +2,8 @@ import socket from multiprocessing import shared_memory from google.protobuf.internal.encoder import _VarintBytes from google.protobuf.internal.decoder import _DecodeVarint32 +from ur_simple_control.networking.util import DictPb2EncoderDecoder +import pickle def client(args, init_command, shm_name, lock): @@ -23,6 +25,16 @@ def client(args, init_command, shm_name, lock): """ def parse_message(msg_raw): + """ + 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 msg_len = len(msg_raw) msg_in_bytes = b"" @@ -32,20 +44,18 @@ def client(args, init_command, shm_name, lock): if pos + next_pos > msg_len: print("NETWORKING CLIENT: BUFFER OVERFLOW, DROPPING MSG!") break - # NOTE: we only keep the latest message, 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. - # hence we only deserialize the last message - if pos >= len(msg_raw): - # why i am encoding the length back in ??? - msg_in_bytes = ( - _VarintBytes(pos + next_pos) + msg_raw[pos : pos + next_pos] - ) + msg_in_bytes = _VarintBytes(pos + next_pos) + msg_raw[pos : pos + next_pos] + pos += next_pos + if pos >= msg_len: break return msg_in_bytes + # TODO: deltte + encoder_decoder = DictPb2EncoderDecoder() + msg_code = encoder_decoder.dictToMsgCode(init_command) + shm = shared_memory.SharedMemory(name=shm_name) - host_addr = (args.host_addr, args.host_port) + host_addr = (args.host, args.port) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(host_addr) if args.debug_prints: @@ -55,8 +65,33 @@ def client(args, init_command, shm_name, lock): while True: msg_raw = s.recv(1024) msg_in_bytes = parse_message(msg_raw) + dict_msg = encoder_decoder.serializedPb2MsgToDict(msg_in_bytes, msg_code) + + # print( + # "NETWORKING CLIENT: putting new message in:", + # encoder_decoder.serializedPb2MsgToDict(msg_in_bytes, 1), + # ) + # TODO: I CAN'T READ THIS WITHOUT pre-CROPPING THE MESSAGE + # WHEN SENDING TO serializedPb2MsgToDict!!!!!!!!!!! + # THE FUNCTION CAN CORRECTLY DECODE THE LENGTH OF THE MESSAGE, + # AND I CROP IT THERE, BUT FOR SOME REASON IT REFUSES TO COORPERATE + # so fck it i'm reserializing with pickling to avoid wasting more time + # shm.buf[: len(msg_in_bytes)] = msg_in_bytes[:] + dict_msg_pickle = pickle.dumps(dict_msg) lock.acquire() - shm.buf[: len(msg_in_bytes)] = msg_in_bytes + shm.buf[: len(dict_msg_pickle)] = dict_msg_pickle + # print("NETWORKING CLIENT: message in bytes length", len(msg_in_bytes)) + # mem = shm.buf[: len(msg_in_bytes)] + ## print(mem) + # print("NETWORKING CLIENT: i can read back from shm:") + # print("NETWORKING_CLIENT", encoder_decoder.serializedPb2MsgToDict(mem, 1)) + # NOTE: this works just fine, but not, but you have to crop here with len(msg_in_bytes), + # even though the message begins with the int which is the same number. + # but cropping in serializedPb2MsgToDict refuses to work + # print( + # "NETWORKING_CLIENT", + # encoder_decoder.serializedPb2MsgToDict(shm.buf[: len(msg_in_bytes)], 1), + # ) lock.release() except KeyboardInterrupt: s.close() diff --git a/python/ur_simple_control/networking/message_specs.proto b/python/ur_simple_control/networking/message_specs.proto index 730a91b..3048168 100644 --- a/python/ur_simple_control/networking/message_specs.proto +++ b/python/ur_simple_control/networking/message_specs.proto @@ -1,4 +1,5 @@ edition = "2023"; +// syntax = "proto2"; message joint_angles { repeated double q = 1; } diff --git a/python/ur_simple_control/networking/message_specs_pb2.py b/python/ur_simple_control/networking/message_specs_pb2.py index 5ae3bf8..3d2d21d 100644 --- a/python/ur_simple_control/networking/message_specs_pb2.py +++ b/python/ur_simple_control/networking/message_specs_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: message_specs.proto -# Protobuf Python Version: 5.29.2 +# Protobuf Python Version: 5.28.3 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, 5, - 29, - 2, + 28, + 3, '', 'message_specs.proto' ) @@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13message_specs.proto\"\x19\n\x0cjoint_angles\x12\t\n\x01q\x18\x01 \x03(\x01\"8\n\x08wrenches\x12\x13\n\x0bwrench_mine\x18\x01 \x03(\x01\x12\x17\n\x0fwrench_estimate\x18\x02 \x03(\x01\x62\x08\x65\x64itionsp\xe8\x07') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13message_specs.proto\"\x19\n\x0cjoint_angles\x12\t\n\x01q\x18\x01 \x03(\x01\"3\n\x08wrenches\x12\x0e\n\x06wrench\x18\x01 \x03(\x01\x12\x17\n\x0fwrench_estimate\x18\x02 \x03(\x01\x62\x08\x65\x64itionsp\xe8\x07') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -34,5 +34,5 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['_JOINT_ANGLES']._serialized_start=23 _globals['_JOINT_ANGLES']._serialized_end=48 _globals['_WRENCHES']._serialized_start=50 - _globals['_WRENCHES']._serialized_end=106 + _globals['_WRENCHES']._serialized_end=101 # @@protoc_insertion_point(module_scope) diff --git a/python/ur_simple_control/networking/server.py b/python/ur_simple_control/networking/server.py index 13c2340..f3bd566 100644 --- a/python/ur_simple_control/networking/server.py +++ b/python/ur_simple_control/networking/server.py @@ -1,6 +1,4 @@ import socket -import time -import message_specs_pb2 from google.protobuf.internal.encoder import _VarintBytes from ur_simple_control.networking.util import DictPb2EncoderDecoder diff --git a/python/ur_simple_control/networking/util.py b/python/ur_simple_control/networking/util.py index 46397f3..909a3ba 100644 --- a/python/ur_simple_control/networking/util.py +++ b/python/ur_simple_control/networking/util.py @@ -1,6 +1,7 @@ -import message_specs_pb2 +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 """ to make the data API uniform across the library (dictionaries), @@ -70,19 +71,19 @@ class DictPb2EncoderDecoder: """ # pb2_msg = int_to_message[msg_code] msg_code = self.dictToMsgCode(dict_msg) - if msg_code == 0: + if msg_code == 1: pb2_msg = message_specs_pb2.joint_angles() # if i could go from string to class atribute somehow # that'd be amazing # TODO: see if that's possible - pb2_msg.q = dict_msg["q"] + pb2_msg.q.extend(dict_msg["q"]) if msg_code == 6: pb2_msg = message_specs_pb2.wrenches() # if i could go from string to class atribute somehow # that'd be amazing # TODO: see if that's possible - pb2_msg.wrench = dict_msg["wrench"] - pb2_msg.wrench_estimate = dict_msg["wrench_estimate"] + pb2_msg.wrench.extend(dict_msg["wrench"]) + pb2_msg.wrench_estimate.extend(dict_msg["wrench_estimate"]) # NOTE: possible extension: # prepend the message code as well so that i can send different @@ -115,20 +116,32 @@ class DictPb2EncoderDecoder: different messages over the same socket. """ dict_msg = {} - if msg_code == 0: + # who knows what goes on in the rest of the shared memory + # protobuf is not self delimiting + # so the first value always have to the length, + # and we only read pass just the actual message to ParseFromString() + next_pos, pos = 0, 0 + next_pos, pos = _DecodeVarint32(pb2_msg_in_bytes, pos) + # print("did decode the int") + # print("pos", pos, "next_pos", next_pos) + pb2_msg_in_bytes_cut = pb2_msg_in_bytes[pos : pos + next_pos] + if msg_code == 1: pb2_msg = message_specs_pb2.joint_angles() - pb2_msg.ParseFromString(pb2_msg_in_bytes) + # pb2_msg.ParseFromString(pb2_msg_in_bytes[pos : pos + next_pos]) + # print("msg", pb2_msg) + pb2_msg.ParseFromString(pb2_msg_in_bytes_cut) + # print("msg", pb2_msg) # if i could go from string to class atribute somehow # that'd be amazing # TODO: see if that's possible - dict_msg["q"] = pb2_msg.q + dict_msg["q"] = np.array(pb2_msg.q) if msg_code == 6: pb2_msg = message_specs_pb2.wrenches() - pb2_msg.ParseFromString(pb2_msg_in_bytes) + pb2_msg.ParseFromString(pb2_msg_in_bytes_cut) # if i could go from string to class atribute somehow # that'd be amazing # TODO: see if that's possible - dict_msg["wrench"] = pb2_msg.wrench - dict_msg["wrench_estimate"] = pb2_msg.wrench_estimate + dict_msg["wrench"] = np.array(pb2_msg.wrench) + dict_msg["wrench_estimate"] = np.array(pb2_msg.wrench_estimate) return dict_msg -- GitLab