diff --git a/.gitignore b/.gitignore
index 9f12f4e633167f31196961d94b18dfff0896ff3f..75233b978650f06122fb38f1854a799ee3d8d480 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ build/
 **/.build
 **/build
 **__pycache__
+**/__pycache__/
 .vscode/
 **.pickle
 **.csv
diff --git a/python/examples/graz/clik_m.py b/python/examples/graz/clik_m.py
new file mode 100644
index 0000000000000000000000000000000000000000..82f28c67b8246803cafe5bfceb2014d20559f958
--- /dev/null
+++ b/python/examples/graz/clik_m.py
@@ -0,0 +1,596 @@
+import pinocchio as pin
+import numpy as np
+import copy
+import argparse
+from functools import partial
+from ur_simple_control.managers import ControlLoopManager, RobotManager
+import time
+from qpsolvers import solve_qp
+import argparse
+
+# CHECK THESE OTHER
+from spatialmath.base import rotz, skew, r2q, tr2angvec
+
+def getClikArgs(parser):
+    """
+    getClikArgs
+    ------------
+    Every clik-related magic number, flag and setting is put in here.
+    This way the rest of the code is clean.
+    Force filtering is considered a core part of these control loops
+    because it's not necessarily the same as elsewhere.
+    If you want to know what these various arguments do, just grep 
+    though the code to find them (but replace '-' with '_' in multi-word arguments).
+    All the sane defaults are here, and you can change any number of them as an argument
+    when running your program. If you want to change a drastic amount of them, and
+    not have to run a super long commands, just copy-paste this function and put your
+    own parameters as default ones.
+    """
+    #parser = argparse.ArgumentParser(description='Run closed loop inverse kinematics \
+    #        of various kinds. Make sure you know what the goal is before you run!',
+    #        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+
+    ####################
+    #  clik arguments  #
+    ####################
+    parser.add_argument('--goal-error', type=float, \
+            help="the final position error you are happy with", default=1e-2)
+    parser.add_argument('--tikhonov-damp', type=float, \
+            help="damping scalar in tikhonov regularization", default=1e-3)
+    # TODO add the rest
+    parser.add_argument('--clik-controller', type=str, \
+            help="select which click algorithm you want", \
+            default='dampedPseudoinverse', choices=['dampedPseudoinverse', 'jacobianTranspose', 'invKinmQP'])
+
+    ###########################################
+    #  force sensing and feedback parameters  #
+    ###########################################
+    parser.add_argument('--alpha', type=float, \
+            help="force feedback proportional coefficient", \
+            default=0.01)
+    parser.add_argument('--beta', type=float, \
+            help="low-pass filter beta parameter", \
+            default=0.01)
+
+    ###############################
+    #  path following parameters  #
+    ###############################
+    parser.add_argument('--max-init-clik-iterations', type=int, \
+            help="number of max clik iterations to get to the first point", default=10000)
+    parser.add_argument('--max-running-clik-iterations', type=int, \
+            help="number of max clik iterations between path points", default=1000)
+    
+    return parser
+
+#######################################################################
+#                             controllers                             #
+#######################################################################
+"""
+controllers general
+-----------------------
+really trully just the equation you are running.
+ideally, everything else is a placeholder,
+meaning you can swap these at will.
+if a controller has some additional arguments,
+those are put in via functools.partial in getClikController,
+so that you don't have to worry about how your controller is handled in
+the actual control loop after you've put it in getClikController,
+which constructs the controller from an argument to the file.
+"""
+
+def dampedPseudoinverse(tikhonov_damp, J, err_vector):
+    qd = J.T @ np.linalg.inv(J @ J.T + np.eye(J.shape[0], J.shape[0]) * tikhonov_damp) @ err_vector
+    return qd
+
+def jacobianTranspose(J, err_vector):
+    qd = J.T @ err_vector
+    return qd
+
+def invKinmQP(J, err_vector):
+    # maybe a lower precision dtype is equally good, but faster?
+    P = np.eye(J.shape[1], dtype="double")
+    # TODO: why is q all 0?
+    q = np.array([0] * J.shape[1], dtype="double")
+    G = None
+    # TODO: extend for orientation as well
+    b = err_vector#[:3]
+    A = J#[:3]
+    # TODO: you probably want limits here
+    lb = None
+    ub = None
+    h = None
+    #qd = solve_qp(P, q, G, h, A, b, lb, ub, solver="ecos")
+    qd = solve_qp(P, q, G, h, A, b, lb, ub, solver="quadprog")
+    return qd
+
+def getClikController(args):
+    """
+    getClikController
+    -----------------
+    A string argument is used to select one of these.
+    It's a bit ugly, bit totally functional and OK solution.
+    we want all of theme to accept the same arguments, i.e. the jacobian and the error vector.
+    if they have extra stuff, just map it in the beginning with partial
+    NOTE: this could be changed to something else if it proves inappropriate later
+    TODO: write out other algorithms
+    """
+    if args.clik_controller == "dampedPseudoinverse":
+        return partial(dampedPseudoinverse, args.tikhonov_damp)
+    if args.clik_controller == "jacobianTranspose":
+        return jacobianTranspose
+    # TODO implement and add in the rest
+    #if controller_name == "invKinmQPSingAvoidE_kI":
+    #    return invKinmQPSingAvoidE_kI
+    #if controller_name == "invKinm_PseudoInv":
+    #    return invKinm_PseudoInv
+    #if controller_name == "invKinm_PseudoInv_half":
+    #    return invKinm_PseudoInv_half
+    if args.clik_controller == "invKinmQP":
+        return invKinmQP
+    #if controller_name == "invKinmQPSingAvoidE_kI":
+    #    return invKinmQPSingAvoidE_kI
+    #if controller_name == "invKinmQPSingAvoidE_kM":
+    #    return invKinmQPSingAvoidE_kM
+    #if controller_name == "invKinmQPSingAvoidManipMax":
+    #    return invKinmQPSingAvoidManipMax
+
+    # default
+    return partial(dampedPseudoinverse, args.tikhonov_damp)
+
+
+def controlLoopClik(robot : RobotManager, clik_controller, i, past_data):
+    """
+    controlLoopClik
+    ---------------
+    generic control loop for clik (handling error to final point etc).
+    in some version of the universe this could be extended to a generic
+    point-to-point motion control loop.
+    """
+    breakFlag = False
+    log_item = {}
+    q = robot.getQ()
+    T_w_e = robot.getT_w_e()
+    # first check whether we're at the goal
+    SEerror = T_w_e.actInv(robot.Mgoal)
+    err_vector = pin.log6(SEerror).vector 
+    if np.linalg.norm(err_vector) < robot.args.goal_error:
+#      print("Convergence achieved, reached destionation!")
+        breakFlag = True
+    #J = pin.computeJointJacobian(robot.model, robot.data, q, robot.JOINT_ID)
+    J = pin.computeFrameJacobian(robot.model, robot.data, q, robot.ee_frame_id)
+    # compute the joint velocities based on controller you passed
+    qd = clik_controller(J, err_vector)
+    robot.sendQd(qd)
+    
+    log_item['qs'] = q.reshape((robot.model.nq,))
+    log_item['dqs'] = robot.getQd().reshape((robot.model.nq,))
+    # we're not saving here, but need to respect the API, 
+    # hence the empty dict
+    return breakFlag, {}, log_item
+    
+    
+def controlLoop_WholeBodyClik(robot : RobotManager, clik_controller, i, past_data):
+    """
+    controlLoop_WholeBodyClik
+    ---------------
+    generic control loop for clik for mobile robot
+    """
+    th = 10^-3
+    if e_norm > th: # trajectory_planner.has_next_position() and elapsed_time > max_time_limit
+        rospy.loginfo_once(f"Target {np.array2string(trajectory_planner.pf, precision=3)} raggiunto")
+        break
+    # breakFlag = False # CHECK the exit conditions
+    
+    log_item = {}
+    q = robot.getQ() # it should contain both manipulator and base variables
+    qb = q[:2]
+    qm = q[2:]
+    T_w_e = robot.getT_w_e().homogeneous() 
+    
+    # TODO: Handle the Desired trajectory    
+    '''
+    pos_d = trajectory_planner.next_position_sample()
+    linear_vel_d = trajectory_planner.next_linear_velocity_sample()
+
+    orient_d = trajectory_planner.next_orient_sample()
+    angular_vel_d = trajectory_planner.next_angular_velocity_sample()
+    '''
+    
+    # position error
+    ep = pos_d - T_w_e[:3,3]
+    # orientation error
+    e_angle, e_axis = tr2angvec(orient_d @ T_w_e[:3, :3].T)
+    eo = np.sin(e_angle) * e_axis
+
+    # error norm
+    e_norm = np.linalg.norm(np.concatenate((ep,eo)))
+
+    L = L_matrix(orient_d, current_orientation)
+
+    cmd_EE_twist = np.r_[
+        linear_vel_d + Kp @ ep,
+        inv(L) @ (L.T @ angular_vel_d + Ko @ eo)
+    ]
+    
+    J = robot.whole_body_jacobian(---) # INSERT input parameters
+    
+    ## Weight the sub-Jacobians
+    dof_base = 2
+    dof_manipulator = 6
+    
+    W_base = np.eye(dof_base) 
+    W_arm = np.eye(dof_manipulator)
+
+    W = np.block([
+        [W_base          , np.zeros((dof_base, dof_manipulator))],
+        [np.zeros((dof_manipulator, dof_base)), W_arm           ]
+    ])
+
+    W_inv = np.linalg.inv(W)
+    J_T = J.T
+
+    J_pinv_weighted = W_inv @ J_T @ inv(J @ W_inv @ J_T)
+
+    qd = clik_controller(J_pinv_weighted, err_vector)
+    robot.sendQd(qd)
+    
+    log_item['qs'] = q.reshape((robot.model.nq,))
+    log_item['dqs'] = robot.getQd().reshape((robot.model.nq,))
+    # we're not saving here, but need to respect the API, 
+    # hence the empty dict
+    return e_norm, {}, log_item
+
+
+def moveUntilContactControlLoop(args, robot : RobotManager, speed, clik_controller, i, past_data):
+    """
+    moveUntilContactControlLoop
+    ---------------
+    generic control loop for clik (handling error to final point etc).
+    in some version of the universe this could be extended to a generic
+    point-to-point motion control loop.
+    """
+    breakFlag = False
+    # know where you are, i.e. do forward kinematics
+    log_item = {}
+    q = robot.getQ()
+    # break if wrench is nonzero basically
+    #wrench = robot.getWrench()
+    # you're already giving the speed in the EE i.e. body frame
+    # so it only makes sense to have the wrench in the same frame
+    #wrench = robot._getWrenchInEE()
+    wrench = robot.getWrench()
+    # and furthermore it's a reasonable assumption that you'll hit the thing
+    # in the direction you're going in.
+    # thus we only care about wrenches in those direction coordinates
+    mask = speed != 0.0
+    # NOTE: contact getting force is a magic number
+    # it is a 100% empirical, with the goal being that it's just above noise.
+    # so far it's worked fine, and it's pretty soft too.
+    if np.linalg.norm(wrench[mask]) > args.contact_detecting_force:
+        print("hit with", np.linalg.norm(wrench[mask]))
+        breakFlag = True
+    if (args.pinocchio_only) and (i > 500):
+        print("let's say you hit something lule")
+        breakFlag = True
+    # pin.computeJointJacobian is much different than the C++ version lel
+    J = pin.computeJointJacobian(robot.model, robot.data, q, robot.JOINT_ID)
+    # compute the joint velocities.
+    qd = clik_controller(J, speed)
+    robot.sendQd(qd)
+    log_item['wrench'] = wrench.reshape((6,))
+    return breakFlag, {}, log_item
+
+def moveUntilContact(args, robot, speed):
+    """
+    moveUntilContact
+    -----
+    does clik until it feels something with the f/t sensor
+    """
+    assert type(speed) == np.ndarray 
+    clik_controller = getClikController(args)
+    controlLoop = partial(moveUntilContactControlLoop, args, robot, speed, clik_controller)
+    # we're not using any past data or logging, hence the empty arguments
+    log_item = {'wrench' : np.zeros(6)}
+    loop_manager = ControlLoopManager(robot, controlLoop, args, {}, log_item)
+    loop_manager.run()
+    # TODO: remove, this isn't doing anything
+    time.sleep(0.01)
+    print("Collision detected!!")
+
+def moveL(args, robot : RobotManager, goal_point):
+    """
+    moveL
+    -----
+    does moveL.
+    send a SE3 object as goal point.
+    if you don't care about rotation, make it np.zeros((3,3))
+    """
+    #assert type(goal_point) == pin.pinocchio_pywrap.SE3
+    robot.Mgoal = copy.deepcopy(goal_point)
+    clik_controller = getClikController(args)
+    controlLoop = partial(controlLoopClik, robot, clik_controller)
+    # we're not using any past data or logging, hence the empty arguments
+    log_item = {
+            'qs' : np.zeros(robot.model.nq),
+            'dqs' : np.zeros(robot.model.nq),
+        }
+    loop_manager = ControlLoopManager(robot, controlLoop, args, {}, log_item)
+    loop_manager.run()
+
+# TODO: implement
+def moveLFollowingLine(args, robot, goal_point):
+    """
+    moveLFollowingLine
+    ------------------
+    make a path from current to goal position, i.e.
+    just a straight line between them.
+    the question is what TODO with orientations.
+    i suppose it makes sense to have one function that enforces/assumes
+    that the start and end positions have the same orientation.
+    then another version goes in a line and linearly updates the orientation
+    as it goes
+    """
+    pass
+
+# TODO: implement
+def cartesianPathFollowing(args, robot, path):
+    pass
+
+def controlLoopCompliantClik(args, robot : RobotManager, i, past_data):
+    """
+    controlLoopClik
+    ---------------
+    generic control loop for clik (handling error to final point etc).
+    in some version of the universe this could be extended to a generic
+    point-to-point motion control loop.
+    """
+    breakFlag = False
+    log_item = {}
+    save_past_dict = {}
+    # know where you are, i.e. do forward kinematics
+    q = robot.getQ()
+    T_w_e = robot.getT_w_e()
+    wrench = robot.getWrench()
+    # we need to overcome noise if we want to converge
+    if np.linalg.norm(wrench) < args.minimum_detectable_force_norm:
+        wrench = np.zeros(6)
+    save_past_dict['wrench'] = copy.deepcopy(wrench)
+    wrench = args.beta * wrench + (1 - args.beta) * past_data['wrench'][-1]
+    #mapping = np.zeros((6,6))
+    #mapping[0:3, 0:3] = T_w_e.rotation
+    #mapping[3:6, 3:6] = T_w_e.rotation
+    #wrench = mapping.T @ wrench
+    #Z = np.diag(np.array([1.0, 1.0, 2.0, 1.0, 1.0, 1.0]))
+    Z = np.diag(np.array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0]))
+    wrench = Z @ wrench
+    #pin.forwardKinematics(robot.model, robot.data, q)
+    # first check whether we're at the goal
+    SEerror = T_w_e.actInv(robot.Mgoal)
+    err_vector = pin.log6(SEerror).vector 
+    if np.linalg.norm(err_vector) < robot.args.goal_error:
+#      print("Convergence achieved, reached destionation!")
+        breakFlag = True
+    # pin.computeJointJacobian is much different than the C++ version lel
+    J = pin.computeJointJacobian(robot.model, robot.data, q, robot.JOINT_ID)
+    # compute the joint velocities.
+    # just plug and play different ones
+    qd = J.T @ np.linalg.inv(J @ J.T + np.eye(J.shape[0], J.shape[0]) * args.tikhonov_damp) @ err_vector
+    tau = J.T @ wrench
+    #tau = tau[:6].reshape((6,1))
+    qd += args.alpha * tau
+    robot.sendQd(qd)
+    
+    log_item['qs'] = q.reshape((robot.model.nq,))
+    # get actual velocity, not the commanded one.
+    # although that would be fun to compare i guess
+    # TODO: have both, but call the compute control signal like it should be
+    log_item['dqs'] = robot.getQd().reshape((robot.model.nq,))
+    log_item['wrench'] = wrench.reshape((6,))
+    log_item['tau'] = tau.reshape((robot.model.nq,))
+    # we're not saving here, but need to respect the API, 
+    # hence the empty dict
+    return breakFlag, save_past_dict, log_item
+
+# add a threshold for the wrench
+def compliantMoveL(args, robot, goal_point):
+    """
+    compliantMoveL
+    -----
+    does compliantMoveL - a moveL, but with compliance achieved
+    through f/t feedback
+    send a SE3 object as goal point.
+    if you don't care about rotation, make it np.zeros((3,3))
+    """
+#    assert type(goal_point) == pin.pinocchio_pywrap.SE3
+    robot.Mgoal = copy.deepcopy(goal_point)
+    clik_controller = getClikController(args)
+    controlLoop = partial(controlLoopCompliantClik, args, robot)
+    # we're not using any past data or logging, hence the empty arguments
+    log_item = {
+            'qs' : np.zeros(robot.model.nq),
+            'wrench' : np.zeros(6),
+            'tau' : np.zeros(robot.model.nq),
+            'dqs' : np.zeros(robot.model.nq),
+        }
+    save_past_dict = {
+            'wrench': np.zeros(6),
+            }
+    loop_manager = ControlLoopManager(robot, controlLoop, args, save_past_dict, log_item)
+    loop_manager.run()
+
+
+def clikCartesianPathIntoJointPath(args, robot, path, \
+        clikController, q_init, plane_pose):
+    """
+    clikCartesianPathIntoJointPath
+    ------------------------------
+    functionality
+    ------------
+    Follows a provided Cartesian path,
+    creates a joint space trajectory for it.
+    Output format and timing works only for what the dmp code expects
+    because it's only used there,
+    and I never gave that code a lift-up it needs.
+
+    return
+    ------
+    - joint_space_trajectory to follow the given path.
+
+    arguments
+    ----------
+    - path: cartesian path given in task frame
+    """
+    # we don't know how many there will be, so a linked list is 
+    # clearly the best data structure here (instert is o(1) still,
+    # and we aren't pressed on time when turning it into an array later)
+    qs = []
+    # let's use the functions we already have. TODO so
+    # we need to create a new RobotManager with arguments for simulation,
+    # otherwise weird things will happen.
+    # we keep all the other args intact
+    sim_args = copy.deepcopy(args)
+    sim_args.pinocchio_only = True
+    sim_args.fast_simulation = True
+    sim_args.real_time_plotting = False
+    sim_args.visualize_manipulator = False
+    sim_args.save_log = False # we're not using sim robot outside of this
+    sim_args.max_iterations = 10000 # more than enough
+    sim_robot = RobotManager(sim_args)
+    sim_robot.q = q_init.copy()
+    sim_robot._step()
+    for pose in path:
+        moveL(sim_args, sim_robot, pose)
+        # loop logs is a dict, dict keys list preserves input order
+        new_q = sim_robot.q.copy() 
+        robot.updateViz(sim_robot.q, sim_robot.T_w_e)
+        time.sleep(0.05)
+        qs.append(new_q[:6])
+        # plot this on the real robot
+
+    ##############################################
+    #  save the obtained joint-space trajectory  #
+    ##############################################
+    qs = np.array(qs)
+    # we're putting a dmp over this so we already have the timing ready
+    # TODO: make this general, you don't want to depend on other random
+    # arguments (make this one traj_time, then put tau0 = traj_time there
+    t = np.linspace(0, args.tau0, len(qs)).reshape((len(qs),1))
+    joint_trajectory = np.hstack((t, qs))
+    # TODO handle saving more consistently/intentionally
+    # (although this definitely works right now and isn't bad, just mid)
+    # os.makedir -p a data dir and save there, this is ugly
+    np.savetxt("./joint_trajectory.csv", joint_trajectory, delimiter=',', fmt='%.5f')
+    return joint_trajectory
+    
+    
+    def whole_body_jacobian(robot : RobotManager, clik_controller, i, past_data):
+        # Whole-body jacobian calculation
+        I_p = np.array([1,0,0], dtype=np.float64).reshape(-1,1)
+        I_o = np.array([0,0,1], dtype=np.float64).reshape(-1,1)
+        O_3x1 = np.zeros((3, 1), dtype=np.float64)
+    
+        q = robot.getQ() # it should contain both base and manipulator/dual arm joint variables
+        qb = q[:2]
+        qm = q[2:] 
+        Rb = rotz(qb[2]) 
+            
+        if robot_name == ur                
+            T_w_b = robot.data.oMi[1].homogeneous() # mobile base frame wrt world frame
+            T_w_e = robot.getT_w_e().homogeneous() # ee frame wrt world frame
+
+            T_b_e = np.linalg.inv(T_w_b) @ T_w_e
+            
+            pos_EE_b = T_b_e[:3, 3]
+            
+            J_m = pin.computeFrameJacobian(robot.model, robot.data, qm, robot.ee_frame_id)
+
+            JP_m = J_m[:3]
+            JO_m = J_m[3:]
+
+            S = (-skew(Rb@pos_EE_b)[:, 2]).reshape(-1,1) # select 3rd column
+
+            J = np.block([
+                [I_p  , S   , Rb@JP_m],
+                [O_3x1, I_o ,    JO_m]
+            ])
+        else
+            """
+            FIX  
+            """
+            T_b_a = robot.getT_w_a().homogeneous() # transformation matrix between absolute frame and mobile base frame
+            pos_a_b = T_b_a[:3, 3] # absolute frame origin
+
+            J_m1 = pin.computeFrameJacobian(robot.model, robot.data, qm, robot.ee_frame_id) # Manipulator 1 Jacobian matrix
+            J_m2 = pin.computeFrameJacobian(robot.model, robot.data, qm, robot.ee_frame_id) # Manipulator 2 Jacobian matrix
+
+            JP_m1 = J_m1[:3]
+            JO_m1 = J_m1[3:]
+            JP_m2 = J_m2[:3]
+            JO_m2 = J_m2[3:]
+
+            S = (-skew(Rb@pos_a_b)[:, 2]).reshape(-1,1) # select 3rd column
+
+            J_b = np.block([
+                [I_p  , S  ],
+                [O_3x1, I_o]
+            ])
+            
+            T = 0.5 * np.block([
+                [np.identity(6), np.identity(6)]
+            ])
+
+            O_3x6 = np.zeros((3, 6), dtype=np.float64)
+
+            J_m = T @ np.block([
+                    [Rb@JP_m1, O_3x6],
+                    [JO_m1, O_3x6],
+                    [O_3x6, Rb@ JP_m2],
+                    [O_3x6, JO_m2]
+                    ])
+
+            J = np.block([[J_b,J_m]])
+
+    return J
+       
+    # L-matrix calculation for angle-axis error    
+    def L_matrix(orient_d, current_orientation):
+    '''Calcola la matrice L, usata nel calcolo dell'errore di orientamento'''
+    nd = orient_d[:, 0]  # Normal vector (x-axis)
+    ad = orient_d[:, 1]  # Approach vector (y-axis)
+    sd = orient_d[:, 2]  # Sliding vector (z-axis)
+
+    ne = current_orientation[:, 0]  # Normal vector (x-axis)
+    ae = current_orientation[:, 1]  # Approach vector (y-axis)
+    se = current_orientation[:, 2]  # Sliding vector (z-axis)
+
+    L = -0.5 * (skew(nd) @ skew(ne) + skew(sd) @ skew(se) + skew(ad) @ skew(ae))
+    return L
+
+    def traj(v,type):
+    '''
+    Calculates the time law coeffs
+    The v-vector should contain:
+     - "cubic": pi, vi, pf, vf, tf;
+     - "quintic": pi, vi, ai, pf, vf, af, tf;
+    '''
+    if type == "cubic"
+        t_end = v(end)
+        np.array([[np.zeros((1,3)), 1],
+        [np.zeros((1,2)), 1, 0],
+        t_end**np.array([3:-1:0]),
+        np.array([3:-1:0])* t_end ** np.array([2:-1:-1])
+        ])
+    # TO BE CONTINUED
+        
+    return L
+
+
+if __name__ == "__main__": 
+    args = get_args()
+    robot = RobotManager(args)
+    Mgoal = robot.defineGoalPointCLI()
+    clik_controller = getClikController(args)
+    controlLoop = partial(controlLoopClik, robot, clik_controller)
+    # we're not using any past data or logging, hence the empty arguments
+    loop_manager = ControlLoopManager(robot, controlLoop, args, {}, {})
+    loop_manager.run()
diff --git a/python/examples/graz/traiettoria.m b/python/examples/graz/traiettoria.m
new file mode 100644
index 0000000000000000000000000000000000000000..a9189f664fe858336812313b62923e93e80e2c79
--- /dev/null
+++ b/python/examples/graz/traiettoria.m
@@ -0,0 +1,209 @@
+clear;clc;
+close all
+
+% End-effector trajectory generation between 2 generic points of the space
+p_i = [0.5,-0.3,0.9]';
+o_i = pi* [0.2,1.3,-0.4]';
+x_i = [p_i;o_i];
+
+p_f = [1.8,1.2,0.8]';
+o_f = pi* [0.5,0.5,0.4]';
+x_f = [p_f;o_f];
+
+%% Position traj
+% Unit vector
+v = (p_i - p_f)/norm(p_i - p_f);
+v_xy = [v(1);v(2);0];
+
+% New end_point 
+distance = 0.05;
+p_f_new = p_f + distance *v_xy;
+p_f_new(3) = p_f(3);
+
+%% Time law
+tf = 20;
+ti = 0;
+time_step = 10^-3;
+time = (0 : time_step : tf);
+
+% Choose the polynomial law degree
+% "cubic" -- > pi, pf, vi, vf, tf;
+% "quintic" -- > pi, pf, vi, vf, ai, af, tf;
+
+% 1� step: Generic position --> 5 cm to goal
+type = "quintic"; % cubic
+poly_v = [zeros(1,3),norm(p_f_new - p_i),zeros(1,2),tf];
+coeff = trajectory(poly_v,type);
+s_p = polyval(coeff,time);
+
+traj_p1 = p_i + s_p .* (p_f_new - p_i)/norm(p_f_new - p_i);
+
+% 2� step: 5 cm to goal --> goal
+type = "quintic"; % cubic
+poly_v = [zeros(1,3),norm(p_f - p_f_new),zeros(1,2),tf];
+coeff = trajectory(poly_v,type);
+s_p = polyval(coeff,time);
+
+traj_p2 = p_f_new + s_p .* (p_f - p_f_new)/norm(p_f - p_f_new);
+
+%% Orientation traj
+Ri = eultoR(o_i);
+Rf = eultoR(o_f);
+Rfi = Ri'*Rf;
+
+[theta_tot,r] = angle_axis(Rfi);
+
+type = "quintic"; % cubic
+poly_v = [zeros(1,3),theta_tot,zeros(1,2),tf];
+coeff = trajectory(poly_v,type);
+s_o = polyval(coeff,time);
+
+traj_o = cell([length(time),1]);
+for i = 1 : length(time)
+    traj_o{i} = Ri * aatoR(s_o(i),r);
+end
+
+
+%% Plot
+% Time law
+figure;
+subplot(3,1,1)
+plot(time,traj_p1(1,:));grid on
+subplot(3,1,2)
+plot(time,traj_p1(2,:));grid on
+subplot(3,1,3)
+plot(time,traj_p1(3,:));grid on
+
+% Path
+s = linspace(0,norm(p_i - p_f));
+v_r = cell([3,1]); 
+
+for i = 1:length(p_i)
+    v_r{i} = v(i)*s + p_f(i);
+end
+figure
+plot3(p_i(1),p_i(2),p_i(3),'rx'); hold on
+plot3(p_f(1),p_f(2),p_f(3),'bx'); grid on
+plot3(v_r{1},v_r{2},v_r{3},'c','linew',0.5)
+
+% Unit-vector for new end_point 
+s = linspace(0,0.05);
+v_r = cell([3,1]); 
+
+for i = 1:length(p_i)
+    v_r{i} = v_xy(i)*s + p_f(i);
+end
+
+plot3(v_r{1},v_r{2},v_r{3},'k','linew',0.5)
+plot3(p_f_new(1),p_f_new(2),p_f_new(3),'gx'); grid on
+
+% trajectories plot
+plot3(traj_p1(1,:), traj_p1(2,:), traj_p1(3,:),'r.')
+plot3(traj_p2(1,:), traj_p2(2,:), traj_p2(3,:),'k.')
+
+function [coeff] = trajectory(v,type)
+%     The v-vector should contain:
+%     - "cubic": pi, vi, pf, vf, tf;
+%     - "quintic": pi, vi, ai, pf, vf, af, tf;
+
+    if type == "cubic"
+        t_end = v(end);
+        A_b = [zeros(1,3) 1; zeros(1,2) 1 0; t_end.^(3:-1:0); (3:-1:0).*t_end.^(2:-1:-1)];
+        b_b = v(1:end - 1);
+        coeff = A_b\b_b;
+    else
+        if type == "quintic"
+             t_end = v(end);
+             A_b = [zeros(1,5) 1; 
+                   zeros(1,4) 1 0;
+                   zeros(1,3) 2 0 0;
+                   t_end.^(5:-1:0);
+                   (5:-1:0).*t_end.^(4:-1:-1);
+                   (5:-1:0).*(4:-1:-1).*t_end.^(3:-1:-2);       
+                   ];
+            b_b = v(1:end - 1);
+%             disp(t_end)
+            coeff = A_b\b_b';
+        end
+    end      
+end
+
+function[theta_tot,r] = angle_axis(Ri_f)
+
+theta_tot = acos((sum(diag(Ri_f))-1)/2);
+
+% Caso di singolarit� di rappresentazione
+if theta_tot == 0
+    r = [1;0;0];
+elseif theta_tot == pi
+    % scegliendo la soluzione positiva
+    rx = sqrt((Ri_f(1,1)+1)/2); 
+    if rx == 0
+        disp('theta_tot=0 ed rx=0')
+        disp(Ri_f)
+        return;
+    end
+    ry =(Ri_f(2,1)/(2*rx));
+    rz = (Ri_f(3,1)/(2*rx));
+    r = [rx;ry;rz];
+    r = r/norm(r);
+else
+    % Non singolarit�
+    r = 1/(2*sin(theta_tot))*[Ri_f(3,2)-Ri_f(2,3);Ri_f(1,3)-Ri_f(3,1);Ri_f(2,1)-Ri_f(1,2)];
+    r = r/norm(r);
+end
+end
+
+function [RPY] = eultoR(v)
+
+%% Costruzione matrici con angoli RPY 
+% Trasformazione da gradi in radianti
+% fi = fi*pi/180;
+% theta = theta*pi/180;
+% psi = psi*pi/180;
+
+fi = v(1);
+theta = v(2);
+psi = v(3);
+
+RPY = [cos(fi)*cos(theta) cos(fi)*sin(theta)*sin(psi)-sin(fi)*cos(psi) cos(fi)*sin(theta)*cos(psi)+sin(fi)*sin(psi); 
+      sin(fi)*cos(theta) sin(fi)*sin(theta)*sin(psi)+cos(fi)*cos(psi) sin(fi)*sin(theta)*cos(psi)-cos(fi)*sin(psi); 
+      -sin(theta) cos(theta)*sin(psi) cos(theta)*cos(psi)];
+
+%% Verifica ortonormalit�
+v = zeros(1,3);
+v(1) = RPY(:,1)'*RPY(:,2)<10^-3;
+v(2) = RPY(:,3)'*RPY(:,2)<10^-3;
+v(3) = norm(RPY(:,1))-norm(RPY(:,2))<10^-3;
+
+if v == zeros(1,3)
+    pause
+end
+
+end
+
+function [RPY] = aatoR(t,r)
+
+%% Costruzione matrici con angoli RPY 
+% Trasformazione da gradi in radianti
+% fi = fi*pi/180;
+% theta = theta*pi/180;
+% psi = psi*pi/180;
+
+RPY = [r(1)^2*(1-cos(t)) + cos(t), r(1)*r(2)*(1-cos(t)) - r(3) * sin(t), r(1)*r(3)*(1-cos(t)) + r(2) * sin(t);
+       r(1)*r(2)*(1-cos(t)) + r(3) * sin(t), r(2)^2*(1-cos(t)) + cos(t), r(3)*r(2)*(1-cos(t))- r(1) * sin(t);
+       r(1)*r(3)*(1-cos(t)) - r(2) * sin(t), r(3)*r(2)*(1-cos(t)) + r(1) * sin(t), r(3)^2*(1-cos(t)) + cos(t)];
+
+%% Verifica ortonormalit�
+v = zeros(1,3);
+v(1) = RPY(:,1)'*RPY(:,2)<10^-3;
+v(2) = RPY(:,3)'*RPY(:,2)<10^-3;
+v(3) = norm(RPY(:,1))-norm(RPY(:,2))<10^-3;
+
+if v == zeros(1,3)
+    pause
+end
+
+end
+
+
diff --git a/python/ur_simple_control/__pycache__/managers.cpython-312.pyc b/python/ur_simple_control/__pycache__/managers.cpython-312.pyc
index e02e89e5479c52cad283376b57866178a3e4284e..27806b78056d710f4665ff2904927bdc83901eb5 100644
Binary files a/python/ur_simple_control/__pycache__/managers.cpython-312.pyc and b/python/ur_simple_control/__pycache__/managers.cpython-312.pyc differ
diff --git a/python/ur_simple_control/clik/__init__.py b/python/ur_simple_control/clik/__init__.py
index 89ff317dca2dcc6367230aa3b9feec75619ce399..05eae0dbb013f9dba70f1d1c9b8142b24bd900d9 100644
--- a/python/ur_simple_control/clik/__init__.py
+++ b/python/ur_simple_control/clik/__init__.py
@@ -1 +1,2 @@
 from .clik import *
+#from .whole_body_clik import *
diff --git a/python/ur_simple_control/clik/__pycache__/__init__.cpython-312.pyc b/python/ur_simple_control/clik/__pycache__/__init__.cpython-312.pyc
index f8bb567b50b60daa7ddfa22992641874ea904ffb..bf3603d7f8c81c60e49d3f5c04ec1426b72c8995 100644
Binary files a/python/ur_simple_control/clik/__pycache__/__init__.cpython-312.pyc and b/python/ur_simple_control/clik/__pycache__/__init__.cpython-312.pyc differ
diff --git a/python/ur_simple_control/clik/traiettoria.py b/python/ur_simple_control/clik/traiettoria.py
new file mode 100644
index 0000000000000000000000000000000000000000..87fb4f67f2643c704810bbe2d520a5257c8de583
--- /dev/null
+++ b/python/ur_simple_control/clik/traiettoria.py
@@ -0,0 +1,254 @@
+import numpy as np
+import pinocchio as pin
+
+def traj(v,type):
+    '''
+    Calculates the time law coeffs
+    The v-vector should contain:
+     - "cubic": pi, vi, pf, vf, tf;
+     - "quintic": pi, vi, ai, pf, vf, af, tf;
+    '''
+    if type == "cubic"
+        t_end = v(end)
+        A_b = np.array([[np.zeros((1,3)), 1],
+        [np.zeros((1,2)), 1, 0],
+        t_end**np.array([3:-1:0]),
+        np.array([3:-1:0])* t_end ** np.array([2:-1:-1])
+        ])
+        b_b = v[:-1]
+        coeff = np.linalg.inv(A_b) @ b_b
+    else
+    if type == "quintic"
+        t_end = v(end)
+        A_b = np.array([[np.zeros((1,5)), 1],
+        [np.zeros((1,4)), 1, 0],
+        [np.zeros((1,3)), 2, 0, 0],
+        t_end**np.array([5:-1:0]),
+        np.array([5:-1:0])* t_end ** np.array([4:-1:-1]),
+        np.array([5:-1:0])* np.array([4:-1:-1]) t_end ** np.array([3:-1:-2])
+        ])
+        b_b = v[:-1]
+        coeff = np.linalg.inv(A_b) @ b_b
+
+    return coeff
+
+#% End-effector trajectory generation between 2 generic points of the space
+# f := goal : pin.SE3
+# i := T_w_e : pin.SE3
+#p_i = [0.5,-0.3,0.9]';
+T_w_e = pin.SE3.Identity()
+p_i = T_w_e.translation
+#o_i = pi* [0.2,1.3,-0.4]';
+o_i = T_w_e.rotation
+#x_i = [p_i;o_i];
+
+#p_f = [1.8,1.2,0.8]';
+#o_f = pi* [0.5,0.5,0.4]';
+#x_f = [p_f;o_f];
+goal = pin.SE3.Identity()
+p_f = goal.translation
+#o_i = pi* [0.2,1.3,-0.4]';
+o_f = goal.rotation
+#x_i = [p_i;o_i];
+
+
+#%% Position traj
+#% Unit vector
+v = (p_i - p_f) / np.linalg.norm(p_i - p_f)
+#v_xy = [v(1);v(2);0];
+v_xy = np.array([v[0], v[1], 0.0])
+
+#% New end_point 
+distance = 0.05
+p_f_new = p_f + distance * v_xy
+p_f_new[2] = p_f[2]
+
+#%% Time law
+tf = 20.0
+ti = 0.0
+time_step = 1e-3
+#time = (0 : time_step : tf);
+time = np.linspace(0, tf, int(tf / time_step))
+
+#% Choose the polynomial law degree
+#% "cubic" -- > pi, pf, vi, vf, tf;
+#% "quintic" -- > pi, pf, vi, vf, ai, af, tf;
+
+#% 1� step: Generic position --> 5 cm to goal
+type = "quintic"; % cubic
+poly_v = [zeros(1,3),norm(p_f_new - p_i),zeros(1,2),tf];
+coeff = trajectory(poly_v,type);
+s_p = polyval(coeff,time);
+
+traj_p1 = p_i + s_p .* (p_f_new - p_i)/norm(p_f_new - p_i);
+
+% 2� step: 5 cm to goal --> goal
+type = "quintic"; % cubic
+poly_v = [zeros(1,3),norm(p_f - p_f_new),zeros(1,2),tf];
+coeff = trajectory(poly_v,type);
+s_p = polyval(coeff,time);
+
+traj_p2 = p_f_new + s_p .* (p_f - p_f_new)/norm(p_f - p_f_new);
+
+%% Orientation traj
+Ri = eultoR(o_i);
+Rf = eultoR(o_f);
+Rfi = Ri'*Rf;
+
+[theta_tot,r] = angle_axis(Rfi);
+
+type = "quintic"; % cubic
+poly_v = [zeros(1,3),theta_tot,zeros(1,2),tf];
+coeff = trajectory(poly_v,type);
+s_o = polyval(coeff,time);
+
+traj_o = cell([length(time),1]);
+for i = 1 : length(time)
+    traj_o{i} = Ri * aatoR(s_o(i),r);
+end
+
+
+%% Plot
+% Time law
+figure;
+subplot(3,1,1)
+plot(time,traj_p1(1,:));grid on
+subplot(3,1,2)
+plot(time,traj_p1(2,:));grid on
+subplot(3,1,3)
+plot(time,traj_p1(3,:));grid on
+
+% Path
+s = linspace(0,norm(p_i - p_f));
+v_r = cell([3,1]); 
+
+for i = 1:length(p_i)
+    v_r{i} = v(i)*s + p_f(i);
+end
+figure
+plot3(p_i(1),p_i(2),p_i(3),'rx'); hold on
+plot3(p_f(1),p_f(2),p_f(3),'bx'); grid on
+plot3(v_r{1},v_r{2},v_r{3},'c','linew',0.5)
+
+% Unit-vector for new end_point 
+s = linspace(0,0.05);
+v_r = cell([3,1]); 
+
+for i = 1:length(p_i)
+    v_r{i} = v_xy(i)*s + p_f(i);
+end
+
+plot3(v_r{1},v_r{2},v_r{3},'k','linew',0.5)
+plot3(p_f_new(1),p_f_new(2),p_f_new(3),'gx'); grid on
+
+% trajectories plot
+plot3(traj_p1(1,:), traj_p1(2,:), traj_p1(3,:),'r.')
+plot3(traj_p2(1,:), traj_p2(2,:), traj_p2(3,:),'k.')
+
+function [coeff] = trajectory(v,type)
+%     The v-vector should contain:
+%     - "cubic": pi, vi, pf, vf, tf;
+%     - "quintic": pi, vi, ai, pf, vf, af, tf;
+def trajectory(vector,type)
+
+    if type == "cubic"
+        t_end = v(end);
+        A_b = [zeros(1,3) 1; zeros(1,2) 1 0; t_end.^(3:-1:0); (3:-1:0).*t_end.^(2:-1:-1)];
+        b_b = v(1:end - 1);
+        coeff = A_b\b_b;
+    else
+        if type == "quintic"
+             t_end = v(end);
+             A_b = [zeros(1,5) 1; 
+                   zeros(1,4) 1 0;
+                   zeros(1,3) 2 0 0;
+                   t_end.^(5:-1:0);
+                   (5:-1:0).*t_end.^(4:-1:-1);
+                   (5:-1:0).*(4:-1:-1).*t_end.^(3:-1:-2);       
+                   ];
+            b_b = v(1:end - 1);
+%             disp(t_end)
+            coeff = A_b\b_b';
+        end
+    end      
+end
+
+function[theta_tot,r] = angle_axis(Ri_f)
+
+theta_tot = acos((sum(diag(Ri_f))-1)/2);
+
+% Caso di singolarit� di rappresentazione
+if theta_tot == 0
+    r = [1;0;0];
+elseif theta_tot == pi
+    % scegliendo la soluzione positiva
+    rx = sqrt((Ri_f(1,1)+1)/2); 
+    if rx == 0
+        disp('theta_tot=0 ed rx=0')
+        disp(Ri_f)
+        return;
+    end
+    ry =(Ri_f(2,1)/(2*rx));
+    rz = (Ri_f(3,1)/(2*rx));
+    r = [rx;ry;rz];
+    r = r/norm(r);
+else
+    % Non singolarit�
+    r = 1/(2*sin(theta_tot))*[Ri_f(3,2)-Ri_f(2,3);Ri_f(1,3)-Ri_f(3,1);Ri_f(2,1)-Ri_f(1,2)];
+    r = r/norm(r);
+end
+end
+
+function [RPY] = eultoR(v)
+
+%% Costruzione matrici con angoli RPY 
+% Trasformazione da gradi in radianti
+% fi = fi*pi/180;
+% theta = theta*pi/180;
+% psi = psi*pi/180;
+
+fi = v(1);
+theta = v(2);
+psi = v(3);
+
+RPY = [cos(fi)*cos(theta) cos(fi)*sin(theta)*sin(psi)-sin(fi)*cos(psi) cos(fi)*sin(theta)*cos(psi)+sin(fi)*sin(psi); 
+      sin(fi)*cos(theta) sin(fi)*sin(theta)*sin(psi)+cos(fi)*cos(psi) sin(fi)*sin(theta)*cos(psi)-cos(fi)*sin(psi); 
+      -sin(theta) cos(theta)*sin(psi) cos(theta)*cos(psi)];
+
+%% Verifica ortonormalit�
+v = zeros(1,3);
+v(1) = RPY(:,1)'*RPY(:,2)<10^-3;
+v(2) = RPY(:,3)'*RPY(:,2)<10^-3;
+v(3) = norm(RPY(:,1))-norm(RPY(:,2))<10^-3;
+
+if v == zeros(1,3)
+    pause
+end
+
+end
+
+function [RPY] = aatoR(t,r)
+
+%% Costruzione matrici con angoli RPY 
+% Trasformazione da gradi in radianti
+% fi = fi*pi/180;
+% theta = theta*pi/180;
+% psi = psi*pi/180;
+
+RPY = [r(1)^2*(1-cos(t)) + cos(t), r(1)*r(2)*(1-cos(t)) - r(3) * sin(t), r(1)*r(3)*(1-cos(t)) + r(2) * sin(t);
+       r(1)*r(2)*(1-cos(t)) + r(3) * sin(t), r(2)^2*(1-cos(t)) + cos(t), r(3)*r(2)*(1-cos(t))- r(1) * sin(t);
+       r(1)*r(3)*(1-cos(t)) - r(2) * sin(t), r(3)*r(2)*(1-cos(t)) + r(1) * sin(t), r(3)^2*(1-cos(t)) + cos(t)];
+
+%% Verifica ortonormalit�
+v = zeros(1,3);
+v(1) = RPY(:,1)'*RPY(:,2)<10^-3;
+v(2) = RPY(:,3)'*RPY(:,2)<10^-3;
+v(3) = norm(RPY(:,1))-norm(RPY(:,2))<10^-3;
+
+if v == zeros(1,3)
+    pause
+end
+
+end
+
+
diff --git a/python/ur_simple_control/clik/whole_body_clik.py b/python/ur_simple_control/clik/whole_body_clik.py
new file mode 100644
index 0000000000000000000000000000000000000000..74a66304dc755e4b5ac814887177765ad693108e
--- /dev/null
+++ b/python/ur_simple_control/clik/whole_body_clik.py
@@ -0,0 +1,197 @@
+import pinocchio as pin
+import numpy as np
+import copy
+import argparse
+from functools import partial
+from ur_simple_control.managers import ControlLoopManager, RobotManager
+import time
+from qpsolvers import solve_qp
+import argparse
+
+# CHECK THESE OTHER
+from spatialmath.base import rotz, skew, r2q, tr2angvec
+
+
+def controlLoop_WholeBodyClik(goal : pin.SE3, robot : RobotManager, clik_controller, i, past_data):
+    """
+    controlLoop_WholeBodyClik
+    ---------------
+    generic control loop for clik for mobile robot
+    """
+    breakFlag = False
+    log_item = {}
+    save_past_dict = {}
+    q = robot.getQ() # it should contain both manipulator and base variables
+    qb = q[:3]
+    qm = q[3:]
+    angle = np.arctan2(q[3], q[2])
+    qb[2] = angle
+
+    T_w_e = robot.getT_w_e()
+    SEerror = T_w_e.actInv(goal)
+    err_vector = pin.log6(SEerror).vector 
+    err_norm  = np.linalg.norm(err_vector)
+
+    orient_d = goal.rotation
+
+    # position error
+    #ep = pos_d - T_w_e[:3,3]
+    ## orientation error
+    #e_angle, e_axis = tr2angvec(orient_d @ T_w_e[:3, :3].T)
+    #eo = np.sin(e_angle) * e_axis
+
+    #L = L_matrix(orient_d, current_orientation)
+    ## error norm
+    #e_norm = np.linalg.norm(np.concatenate((ep,eo)))
+
+    th = 10^-3
+    if e_norm > th: 
+        breakFlag = True
+    
+
+    T_w_e = T_w_e.homogeneous() 
+    
+    # TODO: Handle the Desired trajectory    
+    '''
+    pos_d = trajectory_planner.next_position_sample()
+    linear_vel_d = trajectory_planner.next_linear_velocity_sample()
+
+    orient_d = trajectory_planner.next_orient_sample()
+    angular_vel_d = trajectory_planner.next_angular_velocity_sample()
+    '''
+    
+
+    J = whole_body_jacobian(robot) 
+    
+    ## Weight the sub-Jacobians
+    dof_base = 3
+    dof_manipulator = 6
+    
+    W_base = np.eye(dof_base) 
+    W_arm = np.eye(dof_manipulator)
+
+    W = np.block([
+        [W_base,                                np.zeros((dof_base, dof_manipulator))],
+        [np.zeros((dof_manipulator, dof_base)), W_arm                              ]])
+
+    W_inv = np.linalg.inv(W)
+    J_T = J.T
+
+    J_pinv_weighted = W_inv @ J_T @ np.linalg.inv(J @ W_inv @ J_T)
+
+    qd = J_pinv_weighted @ err_vector
+    robot.sendQd(qd)
+    
+    log_item['qs'] = q.reshape((robot.model.nq,))
+    log_item['dqs'] = robot.getQd().reshape((robot.model.nq,))
+    # we're not saving here, but need to respect the API, 
+    # hence the empty dict
+    return breakFlag, {}, log_item
+
+
+def whole_body_jacobian(robot : RobotManager):
+    # Whole-body jacobian calculation
+    I_p = np.array([1,0,0], dtype=np.float64).reshape(-1,1)
+    I_o = np.array([0,0,1], dtype=np.float64).reshape(-1,1)
+    O_3x1 = np.zeros((3, 1), dtype=np.float64)
+
+    q = robot.getQ() # it should contain both base and manipulator/dual arm joint variables
+    qb = q[:3]
+    qm = q[3:] 
+    angle = np.arctan2(q[3], q[2])
+    Rb = rotz(angle) 
+        
+    if robot.robot_name == "heron":
+        T_w_b = robot.data.oMi[1].homogeneous() # mobile base frame wrt world frame
+        T_w_e = robot.getT_w_e().homogeneous() # ee frame wrt world frame
+
+        T_b_e = np.linalg.inv(T_w_b) @ T_w_e
+        
+        pos_EE_b = T_b_e[:3, 3]
+        
+        J_m = pin.computeFrameJacobian(robot.model, robot.data, qm, robot.ee_frame_id, pin.WORLD)
+        J_m = J_m[:,:-2]
+        J_m = J_m[:,3:]
+
+        JP_m = J_m[:3]
+        JO_m = J_m[3:]
+
+        S = (-skew(Rb@pos_EE_b)[:, 2]).reshape(-1,1) # select 3rd column
+
+        J = np.block([
+            [I_p  , S   , Rb@JP_m],
+            [O_3x1, I_o ,    JO_m]
+        ])
+    else
+        """
+        FIX  
+        """
+        T_b_a = robot.getT_w_a().homogeneous() # transformation matrix between absolute frame and mobile base frame
+        pos_a_b = T_b_a[:3, 3] # absolute frame origin
+
+        # TODO: put left arm ee_frame_id
+        J_m1 = pin.computeFrameJacobian(robot.model, robot.data, qm, robot.ee_frame_id) # Manipulator 1 Jacobian matrix
+        # TODO: put right arm ee_frame_id
+        J_m2 = pin.computeFrameJacobian(robot.model, robot.data, qm, robot.ee_frame_id) # Manipulator 2 Jacobian matrix
+
+        JP_m1 = J_m1[:3]
+        JO_m1 = J_m1[3:]
+        JP_m2 = J_m2[:3]
+        JO_m2 = J_m2[3:]
+
+        S = (-skew(Rb@pos_a_b)[:, 2]).reshape(-1,1) # select 3rd column
+
+        J_b = np.block([
+            [I_p  , S  ],
+            [O_3x1, I_o]
+        ])
+        
+        T = 0.5 * np.block([
+            [np.identity(6), np.identity(6)]
+        ])
+
+        O_3x6 = np.zeros((3, 6), dtype=np.float64)
+
+        J_m = T @ np.block([
+                [Rb@JP_m1, O_3x6],
+                [JO_m1, O_3x6],
+                [O_3x6, Rb@ JP_m2],
+                [O_3x6, JO_m2]
+                ])
+
+        J = np.block([[J_b,J_m]])
+
+return J
+   
+# L-matrix calculation for angle-axis error    
+def L_matrix(orient_d, current_orientation):
+    '''Calcola la matrice L, usata nel calcolo dell'errore di orientamento'''
+    nd = orient_d[:, 0]  # Normal vector (x-axis)
+    ad = orient_d[:, 1]  # Approach vector (y-axis)
+    sd = orient_d[:, 2]  # Sliding vector (z-axis)
+
+    ne = current_orientation[:, 0]  # Normal vector (x-axis)
+    ae = current_orientation[:, 1]  # Approach vector (y-axis)
+    se = current_orientation[:, 2]  # Sliding vector (z-axis)
+
+    L = -0.5 * (skew(nd) @ skew(ne) + skew(sd) @ skew(se) + skew(ad) @ skew(ae))
+return L
+
+def traj(v,type):
+    '''
+    Calculates the time law coeffs
+    The v-vector should contain:
+     - "cubic": pi, vi, pf, vf, tf;
+     - "quintic": pi, vi, ai, pf, vf, af, tf;
+    '''
+    if type == "cubic"
+        t_end = v(end)
+        np.array([[np.zeros((1,3)), 1],
+        [np.zeros((1,2)), 1, 0],
+        t_end**np.array([3:-1:0]),
+        np.array([3:-1:0])* t_end ** np.array([2:-1:-1])
+        ])
+    # TO BE CONTINUED
+        
+    return 0
+