diff --git a/examples/cart_pulling/control_sub_problems/fixed_paths/dual_arm_path_following.py b/examples/cart_pulling/control_sub_problems/fixed_paths/dual_arm_path_following.py
index 2e11a54104211e46764924edde04603c624414d5..40021e9420f356e4e10b5b85c5da0ebce7bb9361 100644
--- a/examples/cart_pulling/control_sub_problems/fixed_paths/dual_arm_path_following.py
+++ b/examples/cart_pulling/control_sub_problems/fixed_paths/dual_arm_path_following.py
@@ -38,7 +38,7 @@ if __name__ == "__main__":
     x0 = np.concatenate([robot.q, robot.v])
     robot._step()
 
-    CrocoEEPathFollowingMPC(args, robot, x0, path)
+    CrocoDualEEPathFollowingMPC(args, robot, x0, path)
 
     print("final position:", robot.T_w_e)
 
diff --git a/examples/cart_pulling/control_sub_problems/with_planner/base_and_dual_arm_ee_path_following_from_planner.py b/examples/cart_pulling/control_sub_problems/with_planner/base_and_dual_arm_ee_path_following_from_planner.py
new file mode 100644
index 0000000000000000000000000000000000000000..a728f36b3f9a4df81d1ab1b584895df0ca3104bb
--- /dev/null
+++ b/examples/cart_pulling/control_sub_problems/with_planner/base_and_dual_arm_ee_path_following_from_planner.py
@@ -0,0 +1,111 @@
+from smc.robots.implementations.mobile_yumi import SimulatedMobileYuMiRobotManager
+from smc import getMinimalArgParser
+from smc.path_generation.maps.premade_maps import createSampleStaticMap
+from smc.path_generation.path_math.path2d_to_6d import path2D_to_SE3
+from smc.control.optimal_control.util import get_OCP_args
+from smc.control.cartesian_space import getClikArgs
+from smc.path_generation.planner import starPlanner, getPlanningArgs
+from smc.control.optimal_control.croco_mpc_point_to_point import (
+    CrocoDualEEAndBaseP2PMPC,
+)
+from smc.control.optimal_control.croco_mpc_path_following import (
+    BaseAndDualEEPathFollowingMPC,
+)
+from smc.multiprocessing import ProcessManager
+
+import time
+import numpy as np
+from functools import partial
+import pinocchio as pin
+
+
+def get_args():
+    parser = getMinimalArgParser()
+    parser = get_OCP_args(parser)
+    parser = getClikArgs(parser)  # literally just for goal error
+    parser = getPlanningArgs(parser)
+    parser.add_argument(
+        "--handlebar-height",
+        type=float,
+        default=0.5,
+        help="heigh of handlebar of the cart to be pulled",
+    )
+    parser.add_argument(
+        "--base-to-handlebar-preferred-distance",
+        type=float,
+        default=0.5,
+        help="prefered path arclength from mobile base position to handlebar",
+    )
+    args = parser.parse_args()
+    return args
+
+
+if __name__ == "__main__":
+    args = get_args()
+    assert args.robot == "myumi"
+    robot = SimulatedMobileYuMiRobotManager(args)
+    # TODO: HOW IS IT POSSIBLE THAT T_W_E IS WRONG WITHOUT STEP CALLED HERE?????????????????
+    T_absgoal_l = pin.SE3.Identity()
+    T_absgoal_l.translation[1] = 0.1
+    T_absgoal_r = pin.SE3.Identity()
+    T_absgoal_r.translation[1] = -0.1
+    robot._step()
+    robot._q[0] = 9.0
+    robot._q[1] = 4.0
+    robot._step()
+    x0 = np.concatenate([robot.q, robot.v])
+    goal = np.array([0.5, 5.5])
+
+    T_w_abs = robot.T_w_abs
+    planning_function = partial(starPlanner, goal)
+    # here we're following T_w_e reference so that's what we send
+    path_planner = ProcessManager(
+        args, planning_function, T_w_abs.translation[:2], 3, None
+    )
+    _, map_as_list = createSampleStaticMap()
+    if args.visualizer:
+        robot.sendRectangular2DMapToVisualizer(map_as_list)
+        # time.sleep(5)
+
+    T_w_abs = robot.T_w_abs
+    data = None
+    # get first path
+    ##########################################3
+    #                initialize
+    ###########################################
+    while data is None:
+        path_planner.sendCommand(T_w_abs.translation[:2])
+        data = path_planner.getData()
+        # time.sleep(1 / args.ctrl_freq)
+        time.sleep(1)
+
+    _, path2D = data
+    path2D = np.array(path2D).reshape((-1, 2))
+    pathSE3 = path2D_to_SE3(path2D, args.handlebar_height)
+    if args.visualizer:
+        # TODO: document this somewhere
+        robot.visualizer_manager.sendCommand({"Mgoal": pathSE3[0]})
+    if np.linalg.norm(pin.log6(T_w_abs.actInv(pathSE3[0]))) > 1e-2:
+        print("going to initial path position")
+        p_base = pathSE3[0].translation.copy()
+        p_base[0] -= args.base_to_handlebar_preferred_distance
+        p_base[2] = 0.0
+        print(pathSE3[0].translation)
+        print(p_base)
+        CrocoDualEEAndBaseP2PMPC(
+            args, robot, pathSE3[0], T_absgoal_l, T_absgoal_r, p_base
+        )
+    print("initialized!")
+    BaseAndDualEEPathFollowingMPC(args, robot, path_planner, T_absgoal_l, T_absgoal_r)
+
+    print("final position:", robot.T_w_e)
+
+    if args.real:
+        robot.stopRobot()
+
+    if args.save_log:
+        robot._log_manager.saveLog()
+        robot._log_manager.plotAllControlLoops()
+
+    if args.visualizer:
+        robot.killManipulatorVisualizer()
diff --git a/examples/cart_pulling/disjoint_control/; b/examples/cart_pulling/disjoint_control/;
deleted file mode 100644
index b244f1397da7be08cc7d5a7ed068d1e3af2e1a95..0000000000000000000000000000000000000000
--- a/examples/cart_pulling/disjoint_control/;
+++ /dev/null
@@ -1,268 +0,0 @@
-from smc.robots.implementations import *
-
-import numpy as np
-import pinocchio as pin
-import argparse
-
-
-def getMinimalArgParser():
-    """
-    getDefaultEssentialArgs
-    ------------------------
-    returns a parser containing:
-        - essential arguments (run in real or in sim)
-        - parameters for (compliant)moveJ
-        - parameters for (compliant)moveL
-    """
-    parser = argparse.ArgumentParser(
-        description="Run something with \
-            Simple Manipulator Control",
-        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
-    )
-    #################################################
-    #  general arguments: connection, plotting etc  #
-    #################################################
-    parser.add_argument(
-        "--robot",
-        type=str,
-        help="which robot you're running or simulating",
-        default="ur5e",
-        choices=["ur5e", "heron", "heronros", "gripperlessur5e", "mirros", "yumi"],
-    )
-    parser.add_argument(
-        "--real",
-        action=argparse.BooleanOptionalAction,
-        help="whether you're running on the real robot or not",
-        default=False,
-    )
-    # if this ends up working, replace --real with --mode, which can be {real, integration (simulation), debugging}
-    parser.add_argument(
-        "--robot-ip",
-        type=str,
-        help="robot's ip address (only needed if running on the real robot)",
-        default="192.168.1.102",
-    )
-    parser.add_argument(
-        "--ctrl-freq",
-        type=int,
-        help="frequency of the control loop. select -1 if you want to go as fast as possible (useful for running tests in sim)",
-        default=500,
-    )
-    parser.add_argument(
-        "--visualizer",
-        action=argparse.BooleanOptionalAction,
-        help="whether you want to visualize the manipulator and workspace with meshcat",
-        default=True,
-    )
-    parser.add_argument(
-        "--viz-update-rate",
-        type=int,
-        help="frequency of visual updates. visualizer and plotter update every viz-update-rate^th iteration of the control loop.",
-        default=20,
-    )
-    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)",
-        default="none",
-        choices=["none", "robotiq", "onrobot"],
-    )
-    # TODO: make controlloop manager run in a while True loop and remove this
-    # ==> max-iterations actually needs to be an option. sometimes you want to simulate and exit
-    #     if the convergence does not happen in a reasonable amount of time,
-    #     ex. goal outside of workspace has been passed or something
-    # =======> if it's set to 0 then the loops run infinitely long
-    parser.add_argument(
-        "--max-iterations",
-        type=int,
-        help="maximum allowable iteration number (it runs at 500Hz)",
-        default=100000,
-    )
-    parser.add_argument(
-        "--start-from-current-pose",
-        action=argparse.BooleanOptionalAction,
-        help="if connected to the robot, read the current pose and set it as the initial pose for the robot. \
-                 very useful and convenient when running simulation before running on real",
-        default=False,
-    )
-    parser.add_argument(
-        "--acceleration",
-        type=float,
-        help="robot's joints acceleration. scalar positive constant, max 1.7, and default 0.3. \
-                   BE CAREFUL WITH THIS. the urscript doc says this is 'lead axis acceleration'.\
-                   TODO: check what this means",
-        default=0.3,
-    )
-    parser.add_argument(
-        "--max-v-percentage",
-        type=float,
-        help="select the percentage of the maximum joint velocity the robot can achieve to be the control input maximum (control inputs are clipped to perc * max_v)",
-        default=0.3,
-    )
-    parser.add_argument(
-        "--debug-prints",
-        action=argparse.BooleanOptionalAction,
-        help="print some debug info",
-        default=False,
-    )
-    parser.add_argument(
-        "--save-log",
-        action=argparse.BooleanOptionalAction,
-        help="whether you want to save the log of the run. it saves \
-                        what you pass to ControlLoopManager. check other parameters for saving directory and log name.",
-        default=False,
-    )
-    parser.add_argument(
-        "--save-dir",
-        type=str,
-        help="path to where you store your logs. default is ./data, but if that directory doesn't exist, then /tmp/data is created and used.",
-        default="./data",
-    )
-    parser.add_argument(
-        "--run-name",
-        type=str,
-        help="name the whole run/experiment (name of log file). note that indexing of runs is automatic and under a different argument.",
-        default="latest_run",
-    )
-    parser.add_argument(
-        "--index-runs",
-        action=argparse.BooleanOptionalAction,
-        help="if you want more runs of the same name, this option will automatically assign an index to a new run (useful for data collection).",
-        default=False,
-    )
-    parser.add_argument(
-        "--past-window-size",
-        type=int,
-        help="how many timesteps of past data you want to save",
-        default=5,
-    )
-    # maybe you want to scale the control signal (TODO: NOT HERE)
-    parser.add_argument(
-        "--controller-speed-scaling",
-        type=float,
-        default="1.0",
-        help="not actually_used atm",
-    )
-    ########################################
-    #  environment interaction parameters  #
-    ########################################
-    parser.add_argument(
-        "--contact-detecting-force",
-        type=float,  # default=1.3, help='the force used to detect contact (collision) in the moveUntilContact function')
-        default=2.8,
-        help="the force used to detect contact (collision) in the moveUntilContact function",
-    )
-    parser.add_argument(
-        "--minimum-detectable-force-norm",
-        type=float,
-        help="we need to disregard noise to converge despite filtering. \
-                  a quick fix is to zero all forces of norm below this argument threshold.",
-        default=3.0,
-    )
-    # TODO make this work without parsing (or make it possible to parse two times)
-    # if (args.gripper != "none") and args.simulation:
-    #    raise NotImplementedError('Did not figure out how to put the gripper in \
-    #            the simulation yet, sorry :/ . You can have only 1 these flags right now')
-    parser.add_argument(
-        "--visualize-collision-approximation",
-        action=argparse.BooleanOptionalAction,
-        help="whether you want to visualize the collision approximation used in controllers with obstacle avoidance",
-        default=False,
-    )
-    return parser
-
-
-# TODO: make robot-independent
-def defineGoalPointCLI(robot):
-    """
-    defineGoalPointCLI
-    ------------------
-    get a nice TUI-type prompt to put in a frame goal for p2p motion.
-    --> best way to handle the goal is to tell the user where the gripper is
-        in both UR tcp frame and with pinocchio and have them
-        manually input it when running.
-        this way you force the thinking before the moving,
-        but you also get to view and analyze the information first
-    TODO get the visual thing you did in ivc project with sliders also.
-    it's just text input for now because it's totally usable, just not superb.
-    but also you do want to have both options. obviously you go for the sliders
-    in the case you're visualizing, makes no sense otherwise.
-    """
-    robot._step()
-    q = robot.q
-    # define goal
-    T_w_e = robot.T_w_e
-    print("You can only specify the translation right now.")
-    if robot.args.real:
-        print(
-            "In the following, first 3 numbers are x,y,z position, and second 3 are r,p,y angles"
-        )
-        print(
-            "Here's where the robot is currently. Ensure you know what the base frame is first."
-        )
-        print(
-            "base frame end-effector pose from pinocchio:\n",
-            *robot.data.oMi[6].translation.round(4),
-            *pin.rpy.matrixToRpy(robot.data.oMi[6].rotation).round(4)
-        )
-        print("UR5e TCP:", *np.array(robot.rtde_receive.getActualTCPPose()).round(4))
-    # remain with the current orientation
-    # TODO: add something, probably rpy for orientation because it's the least number
-    # of numbers you need to type in
-    Mgoal = T_w_e.copy()
-    # this is a reasonable way to do it too, maybe implement it later
-    # Mgoal.translation = Mgoal.translation + np.array([0.0, 0.0, -0.1])
-    # do a while loop until this is parsed correctly
-    while True:
-        goal = input(
-            "Please enter the target end-effector position in the x.x,y.y,z.z format: "
-        )
-        try:
-            e = "ok"
-            goal_list = goal.split(",")
-            for i in range(len(goal_list)):
-                goal_list[i] = float(goal_list[i])
-        except:
-            e = exc_info()
-            print("The input is not in the expected format. Try again.")
-            print(e)
-        if e == "ok":
-            Mgoal.translation = np.array(goal_list)
-            break
-    print("this is goal pose you defined:\n", Mgoal)
-
-    # NOTE i'm not deepcopying this on purpose
-    # but that might be the preferred thing, we'll see
-    robot.Mgoal = Mgoal
-    if robot.args.visualizer:
-        # TODO document this somewhere
-        robot.visualizer_manager.sendCommand({"Mgoal": Mgoal})
-    return Mgoal
-
-
-# TODO: finish
-def getRobotFromArgs(args: argparse.Namespace) -> AbstractRobotManager:
-    if args.robot == "ur5e":
-        if args.real:
-            return RealUR5eRobotManager(args)
-        else:
-            return SimulatedUR5eRobotManager(args)
-    if args.robot == "heron":
-        if args.real:
-            pass
-            # TODO: finish it
-            # return RealHeronRobotManager(args)
-        else:
-            return SimulatedHeronRobotManager(args)
-
-
-#    if args.robot == "mir":
-#        return RealUR5eRobotManager(args)
-#    if args.robot == "yumi":
-#        return RealUR5eRobotManager(args)
diff --git a/examples/optimal_control/croco_ik_ocp.py b/examples/optimal_control/croco_ik_ocp.py
index 7e72f7488a878741e6ea53c2c3a1f7200b9fbb72..40f70f3357df988f00aacbef03a7e962552c9db7 100644
--- a/examples/optimal_control/croco_ik_ocp.py
+++ b/examples/optimal_control/croco_ik_ocp.py
@@ -39,7 +39,7 @@ if __name__ == "__main__":
     # this shouldn't really depend on x0 but i can't be bothered
     ocp = SingleArmIKOCP(args, robot, x0, T_w_goal)
     ocp.solveInitialOCP(x0)
-    reference = ocp.getSolvedReference(x0)
+    reference = ocp.getSolvedReference()
 
     # NOTE: IF YOU PLOT SOMETHING OTHER THAN REAL-TIME PLOTTING FIRST IT BREAKS EVERYTHING
     #    if args.solver == "boxfddp":
diff --git a/examples/optimal_control/test_by_running.sh b/examples/optimal_control/test_by_running.sh
new file mode 100755
index 0000000000000000000000000000000000000000..dd696d3793c3fe62489313fa619e49155ccb58e9
--- /dev/null
+++ b/examples/optimal_control/test_by_running.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+# the idea here is to run all the runnable things
+# and test for syntax errors 
+#
+# ################
+# single arm 
+# ###############
+# ocp
+runnable="croco_ik_ocp.py --ctrl-freq=-1 --no-plotter --no-visualizer --max-iterations=2000"
+echo $runnable
+python $runnable
+
+# mpc
+runnable="croco_ik_mpc.py --max-solver-iter 10 --n-knots 30  --ctrl-freq=-1 --no-visualizer --no-plotter --max-iterations=2000"
+echo $runnable
+python $runnable
+
+######################
+# whole body single arm
+######################
+# ocp
+runnable="croco_ik_ocp.py --robot=heron --ctrl-freq=-1 --no-visualizer --no-plotter --max-iterations=2000"
+echo $runnable
+python $runnable
+# mpc
+runnable="croco_ik_mpc.py --max-solver-iter 10 --n-knots 30 --robot=heron --ctrl-freq=-1 --no-visualizer --no-plotter --max-iterations=2000"
+echo $runnable
+python $runnable
+
+######################
+# dual arm
+# ####################
+# ocp TODO: missing
+
+# mpc
+runnable="dual_arm_ik_mpc.py --max-solver-iter 10 --n-knots 30 --robot=yumi --ctrl-freq=-1 --no-visualizer --no-plotter --max-iterations=2000"
+echo $runnable
+python $runnable
+
+# ##########################
+# whole body dual arm
+# ##########################
+# ocp TODO: missing
+#
+# mpc
+runnable="dual_arm_ik_mpc.py --max-solver-iter 10 --n-knots 30 --robot=myumi --ctrl-freq=-1 --no-visualizer --no-plotter --max-iterations=2000"
+echo $runnable
+python $runnable
+
+##############################
+# base only
+# ##################################
+# ocp TODO: missing
+# mpc TODO: missing
+
+##############################
+# whole body single arm base + ee reference
+# ##################################
+# ocp TODO: missing
+# mpc TODO: missing
+#
+##############################
+# whole body dual arm base + ee reference
+# ##################################
+# ocp TODO: missing
+# mpc TODO: missing
diff --git a/python/smc/control/joint_space/joint_space_trajectory_following.py b/python/smc/control/joint_space/joint_space_trajectory_following.py
index dc5ee95caff0ac889d611b87bbe027f3f9eb1969..fad6b5dadc04f038a82b367395a73c79aa9f7944 100644
--- a/python/smc/control/joint_space/joint_space_trajectory_following.py
+++ b/python/smc/control/joint_space/joint_space_trajectory_following.py
@@ -1,10 +1,10 @@
-from smc.robots.robotmanager_abstract import AbstractRobotManager
+from smc.robots.abstract_robotmanager import AbstractRobotManager
 from smc.control.control_loop_manager import ControlLoopManager
-from smc import getMinimalArgParser
 
 import pinocchio as pin
 import numpy as np
 from functools import partial
+from collections import deque
 
 
 # NOTE: it's probably a good idea to generalize this for different references:
@@ -17,8 +17,12 @@ from functools import partial
 # this is because joint positions will be more accurate.
 # if we integrate them with interpolated velocities.
 def followKinematicJointTrajPControlLoop(
-    stop_at_final: bool, robot: AbstractRobotManager, reference, i, past_data
-):
+    stop_at_final: bool,
+    robot: AbstractRobotManager,
+    reference,
+    i: int,
+    past_data: dict[str, deque[np.ndarray]],
+) -> tuple[bool, dict[str, np.ndarray], dict[str, np.ndarray]]:
     breakFlag = False
     save_past_dict = {}
     log_item = {}
diff --git a/python/smc/control/optimal_control/croco_mpc_path_following.py b/python/smc/control/optimal_control/croco_mpc_path_following.py
index 5e0fb60542795bc809249e236dd44f2438c33e56..cf890f7e0f8f5272209b54f67a70d39859f680f1 100644
--- a/python/smc/control/optimal_control/croco_mpc_path_following.py
+++ b/python/smc/control/optimal_control/croco_mpc_path_following.py
@@ -1,6 +1,6 @@
 from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
 from smc.robots.interfaces.single_arm_interface import SingleArmInterface
-from smc.robots.interfaces.whole_body_interface import SingleArmWholeBodyInterface
+from smc.robots.interfaces.whole_body_interface import SingleArmWholeBodyInterface, DualArmWholeBodyInterface
 from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface
 from smc.control.control_loop_manager import ControlLoopManager
 from smc.multiprocessing.process_manager import ProcessManager
@@ -294,24 +294,23 @@ def BaseAndEEPathFollowingFromPlannerMPCControlLoop(
 
     # NOTE: this one is obtained as the future path from path planner
     max_base_v = np.linalg.norm(robot._max_v[:2])
-    path_base = path2D_to_trajectory2D(args, path2D_untimed_base, max_base_v)
-    path_base = np.hstack((path_base, np.zeros((len(path_base), 1))))
-
-    pathSE3_handlebar = construct_EE_path(args, p, past_data["path2D_untimed"])
+    trajectory_base = path2D_to_trajectory2D(args, path2D_untimed_base, max_base_v)
+    trajectory_base = np.hstack((trajectory_base, np.zeros((len(trajectory_base), 1))))
 
+    trajectorySE3_handlebar = construct_EE_path(args, p, past_data["path2D_untimed"])
 
     if args.visualizer:
         if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
-            robot.visualizer_manager.sendCommand({"path": path_base})
-            robot.visualizer_manager.sendCommand({"frame_path": pathSE3_handlebar})
+            robot.visualizer_manager.sendCommand({"path": trajectory_base})
+            robot.visualizer_manager.sendCommand({"frame_path": trajectorySE3_handlebar})
 
     x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(x0, data=(path_base, pathSE3_handlebar))
+    ocp.warmstartAndReSolve(x0, data=(trajectory_base, trajectorySE3_handlebar))
     xs = np.array(ocp.solver.xs)
     v_cmd = xs[1, robot.model.nq :]
 
-    err_vector_ee = log6(robot.T_w_e.actInv(pathSE3_handlebar[0]))
-    err_vector_base = np.linalg.norm(p - path_base[0][:2])  # z axis is irrelevant
+    err_vector_ee = log6(robot.T_w_e.actInv(trajectorySE3_handlebar[0]))
+    err_vector_base = np.linalg.norm(p - trajectory_base[0][:2])  # z axis is irrelevant
     log_item = {}
     log_item["err_vec_ee"] = err_vector_ee
     log_item["err_norm_ee"] = np.linalg.norm(err_vector_ee).reshape((1,))
@@ -346,6 +345,7 @@ def BaseAndEEPathFollowingMPC(
     are actually extracted from the state x(q,dq).
     """
 
+    robot._mode = SingleArmWholeBodyInterface.control_mode.whole_body
     T_w_e = robot.T_w_e
     x0 = np.concatenate([robot.q, robot.v])
     ocp = BaseAndEEPathFollowingOCP(args, robot, x0)
@@ -393,144 +393,67 @@ def BaseAndEEPathFollowingMPC(
 
 # TODO: the robot put in is a whole body robot,
 # which you need to implement
-def BaseAndDualArmEEPathFollowingMPCControlLoop(
-    args,
-    robot,
+def BaseAndDualEEPathFollowingMPCControlLoop(
+    T_absgoal_l: SE3,
+    T_absgoal_r: SE3,
     ocp: CrocoOCP,
-    path_planner: ProcessManager,
-    grasp_pose,
-    goal_transform,
-    iter_n,
-    past_data,
-):
+    path2D_untimed_base: np.ndarray,
+    args: Namespace,
+    robot: DualArmWholeBodyInterface,
+    t: int,
+    past_data: dict[str, deque[np.ndarray]],
+) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, np.ndarray]]:
     """
-    CrocoPathFollowingMPCControlLoop
+    BaseAndDualEEPathFollowingMPCControlLoop
     -----------------------------
-    end-effector(s) follow their path(s).
-
-    path planner can either be a function which spits out a list of path points
-    or an instance of ProcessManager which spits out path points
-    by calling ProcessManager.getData()
+    cart pulling dual arm control loop
     """
-    raise NotImplementedError
-    breakFlag = False
-    log_item = {}
-    save_past_dict = {}
+    p = robot.q[:2]
 
-    q = robot.q
-    T_w_e = robot.T_w_e
-    p = q[:2]
-    # NOTE: this is the actual position, not what the path suggested
-    # whether this or path reference should be put in is debateable.
-    # this feels more correct to me.
-    save_past_dict["path2D_untimed"] = p
-    path_planner.sendCommand(p)
-
-    ###########################
-    #  get path from planner  #
-    ###########################
-    # NOTE: it's pointless to recalculate the path every time
-    # if it's the same 2D path
-    # get the path from the base from the current base position onward.
-    # but we're still fast so who cares
-    data = path_planner.getData()
-
-    if data == "done":
-        breakFlag = True
-    if data == "done" or data is None:
-        robot.sendVelocityCommand(np.zeros(robot.model.nv))
-        log_item["qs"] = q.reshape((robot.model.nq,))
-        log_item["dqs"] = robot.v.reshape((robot.model.nv,))
-        return breakFlag, save_past_dict, log_item
-
-    ##########################################
-    #  construct timed 2D path for the base  #
-    ##########################################
-    _, path2D_untimed_base = data
-    path2D_untimed_base = np.array(path2D_untimed_base).reshape((-1, 2))
-    # ideally should be precomputed somewhere
+    # NOTE: this one is obtained as the future path from path planner
     max_base_v = np.linalg.norm(robot._max_v[:2])
-    # base just needs timing on the path
-    path_base = path2D_to_trajectory2D(args, path2D_untimed_base, max_base_v)
-    # and it's of height 0 (i.e. the height of base's planar joint)
-    path_base = np.hstack((path_base, np.zeros((len(path_base), 1))))
-
-    ###################################################
-    #  construct timed SE3 path for the end-effector  #
-    ###################################################
-    # this works as follow
-    # 1) find the previous path point of arclength base_to_handlebar_preferred_distance.
-    # first part of the path is from there to current base position,
-    # second is just the current base's plan.
-    # 2) construct timing on the first part.
-    # 3) join that with the already timed second part.
-    # 4) turn the timed 2D path into an SE3 trajectory
-
-    # NOTE: this can be O(1) instead of O(n) but i can't be bothered
-    path_arclength = np.linalg.norm(p - past_data["path2D_untimed"])
-    handlebar_path_index = -1
-    for i in range(-2, -1 * len(past_data["path2D_untimed"]), -1):
-        if path_arclength > args.base_to_handlebar_preferred_distance:
-            handlebar_path_index = i
-            break
-        path_arclength += np.linalg.norm(
-            past_data["path2D_untimed"][i - 1] - past_data["path2D_untimed"][i]
-        )
-    # i shouldn't need to copy-paste everything but what can you do
-    path2D_handlebar_1_untimed = np.array(past_data["path2D_untimed"])
-    # TODO: try this
-    # path2D_handlebar_1_untimed = np.array(past_data["path2D_untimed"][handlebar_path_index:])
-    # NOTE: BIG ASSUMPTION
-    # let's say we're computing on time, and that's the time spacing
-    # of previous path points.
-    # this means you need to lower the control frequency argument
-    # if you're not meeting deadlines.
-    # if you really need timing information, you should actually
-    # get it from ControlLoopManager instead of i,
-    # but let's say this is better because you're forced to know
-    # how fast you are instead of ducktaping around delays.
-    # TODO: actually save timing, pass t instead of i to controlLoops
-    # from controlLoopManager
-    # NOTE: this might not working when rosified so check that first
-    time_past = np.linspace(
-        0.0, args.past_window_size * robot.dt, args.past_window_size
-    )
-    s = np.linspace(0.0, args.n_knots * args.ocp_dt, args.n_knots)
-    path2D_handlebar_1 = np.hstack(
-        (
-            np.interp(s, time_past, path2D_handlebar_1_untimed[:, 0]).reshape((-1, 1)),
-            np.interp(s, time_past, path2D_handlebar_1_untimed[:, 1]).reshape((-1, 1)),
-        )
-    )
+    trajectory_base = path2D_to_trajectory2D(args, path2D_untimed_base, max_base_v)
+    trajectory_base = np.hstack((trajectory_base, np.zeros((len(trajectory_base), 1))))
 
-    pathSE3_handlebar = path2D_to_SE3(path2D_handlebar_1, args.handlebar_height)
-    pathSE3_handlebar_left = []
-    pathSE3_handlebar_right = []
-    for pathSE3 in pathSE3_handlebar:
-        pathSE3_handlebar_left.append(goal_transform.act(pathSE3))
-        pathSE3_handlebar_right.append(goal_transform.inverse().act(pathSE3))
+    trajectorySE3_handlebar = construct_EE_path(args, p, past_data["path2D_untimed"])
+    trajectorySE3_l = []
+    trajectorySE3_r = []
+    for traj_pose in trajectorySE3_handlebar:
+        trajectorySE3_l.append(T_absgoal_l.act(traj_pose))
+        trajectorySE3_r.append(T_absgoal_r.act(traj_pose))
 
     if args.visualizer:
-        if iter_n % 20 == 0:
-            robot.visualizer_manager.sendCommand({"path": path_base})
-            robot.visualizer_manager.sendCommand({"frame_path": pathSE3_handlebar})
+        if t % int(np.ceil(args.ctrl_freq / 25)) == 0:
+            robot.visualizer_manager.sendCommand({"path": trajectory_base})
+            robot.visualizer_manager.sendCommand({"frame_path": trajectorySE3_handlebar})
 
     x0 = np.concatenate([robot.q, robot.v])
-    ocp.warmstartAndReSolve(
-        x0, data=(path_base, pathSE3_handlebar_left, pathSE3_handlebar_right)
-    )
+    ocp.warmstartAndReSolve(x0, data=(trajectory_base, trajectorySE3_l, trajectorySE3_r))
     xs = np.array(ocp.solver.xs)
-    us = np.array(ocp.solver.us)
-    vel_cmds = xs[1, robot.model.nq :]
-
-    robot.sendVelocityCommand(vel_cmds)
-
-    log_item["qs"] = q.reshape((robot.model.nq,))
-    log_item["dqs"] = robot.v.reshape((robot.model.nv,))
-    return breakFlag, save_past_dict, log_item
+    v_cmd = xs[1, robot.model.nq :]
 
+    err_vector_ee_l = log6(robot.T_w_l.actInv(trajectorySE3_l[0]))
+    err_norm_ee_l = np.linalg.norm(err_vector_ee_l)
+    err_vector_ee_r = log6(robot.T_w_l.actInv(trajectorySE3_r[0]))
+    err_norm_ee_r = np.linalg.norm(err_vector_ee_r)
+    err_vector_base = np.linalg.norm(p - trajectory_base[0][:2])  # z axis is irrelevant
+    log_item = {}
+    log_item["err_vec_ee_l"] = err_vector_ee_l
+    log_item["err_norm_ee_l"] = err_norm_ee_l.reshape((1,))
+    log_item["err_vec_ee_r"] = err_vector_ee_r
+    log_item["err_norm_ee_r"] = err_norm_ee_r.reshape((1,))
+    log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,))
+    save_past_item = {"path2D_untimed": p}
+    return v_cmd, log_item, save_past_item
 
-def BaseAndDualArmEEPathFollowingMPC(args, robot, path_planner):
+def BaseAndDualEEPathFollowingMPC(
+    args: Namespace,
+    robot: DualArmWholeBodyInterface,
+    path_planner: ProcessManager | types.FunctionType,
+    T_absgoal_l: SE3,
+    T_absgoal_r: SE3,
+    run=True,
+) -> None | ControlLoopManager:
     """
     CrocoEndEffectorPathFollowingMPC
     -----
@@ -539,22 +462,53 @@ def BaseAndDualArmEEPathFollowingMPC(args, robot, path_planner):
     a dynamics level, and velocities we command
     are actually extracted from the state x(q,dq).
     """
-    raise NotImplementedError("this has to be templetized like the previous cases")
+    robot._mode = DualArmWholeBodyInterface.control_mode.whole_body
+    # NOTE: i'm shoving these into the class here - bad style,
+    # but i don't 
+    T_w_abs = robot.T_w_abs
     x0 = np.concatenate([robot.q, robot.v])
     ocp = BaseAndDualArmEEPathFollowingOCP(args, robot, x0)
     ocp.solveInitialOCP(x0)
 
-    controlLoop = partial(
-        BaseAndEEPathFollowingMPCControlLoop, args, robot, ocp, path_planner
-    )
-    log_item = {"qs": np.zeros(robot.model.nq), "dqs": np.zeros(robot.model.nv)}
-    # TODO: put ensurance that save_past is not too small for this
-    # or make a specific argument for THIS past-moving-window size
-    # this is the end-effector's reference, so we should initialize that
-    # TODO: verify this initialization actually makes sense
-    T_w_e = robot.T_w_e
-    save_past_dict = {"path2D_untimed": T_w_e.translation[:2]}
+    max_base_v = np.linalg.norm(robot._max_v[:2])
+
+    path2D_handlebar = initializePastData(args, T_w_abs, robot.q[:2], float(max_base_v))
+
+    if type(path_planner) == types.FunctionType:
+        raise NotImplementedError
+    else:
+        get_position = lambda robot: robot.q[:2]
+        BaseAndDualEEPathFollowingMPCControlLoop_with_l_r = partial(BaseAndDualEEPathFollowingMPCControlLoop, T_absgoal_l, T_absgoal_r)
+        controlLoop = partial(
+            PathFollowingFromPlannerControlLoop,
+            path_planner,
+            get_position,
+            ocp,
+            BaseAndDualEEPathFollowingMPCControlLoop_with_l_r,
+            args,
+            robot,
+        )
+
+    log_item = {
+        "qs": np.zeros(robot.model.nq),
+        "dqs": np.zeros(robot.model.nv),
+        "err_vec_ee_l": np.zeros((6,)),
+        "err_norm_ee_l": np.zeros((1,)),
+        "err_vec_ee_r": np.zeros((6,)),
+        "err_norm_ee_r": np.zeros((1,)),
+        "err_norm_base": np.zeros((1,)),
+    }
+    save_past_dict = {"path2D_untimed": T_w_abs.translation[:2]}
     loop_manager = ControlLoopManager(
         robot, controlLoop, args, save_past_dict, log_item
     )
-    loop_manager.run()
+    # actually put past data into the past window
+    loop_manager.past_data["path2D_untimed"].clear()
+    loop_manager.past_data["path2D_untimed"].extend(
+        path2D_handlebar[i] for i in range(args.past_window_size)
+    )
+
+    if run:
+        loop_manager.run()
+    else:
+        return loop_manager
diff --git a/python/smc/control/optimal_control/croco_mpc_point_to_point.py b/python/smc/control/optimal_control/croco_mpc_point_to_point.py
index d79472566a2f9bdeda9e74dc0cac8c9fe218fcc1..a2cdd9f465ace9f60e30537749b6254e509342ee 100644
--- a/python/smc/control/optimal_control/croco_mpc_point_to_point.py
+++ b/python/smc/control/optimal_control/croco_mpc_point_to_point.py
@@ -220,9 +220,9 @@ def CrocoEEAndBaseP2PMPC(
 
 def CrocoP2PDualEEAndBaseMPCControlLoop(
     ocp,
-    T_w_abseegoal: pin.SE3,
-    T_absgoal_lgoal: pin.SE3,
-    T_absgoal_rgoal: pin.SE3,
+    T_w_absgoal: pin.SE3,
+    T_absgoal_l: pin.SE3,
+    T_absgoal_r: pin.SE3,
     p_basegoal: np.ndarray,
     args: Namespace,
     robot: DualArmWholeBodyInterface,
@@ -245,9 +245,9 @@ def CrocoP2PDualEEAndBaseMPCControlLoop(
 def CrocoDualEEAndBaseP2PMPC(
     args: Namespace,
     robot: DualArmWholeBodyInterface,
-    T_w_abseegoal: pin.SE3,
-    T_absgoal_lgoal: pin.SE3,
-    T_absgoal_rgoal: pin.SE3,
+    T_w_absgoal: pin.SE3,
+    T_absgoal_l: pin.SE3,
+    T_absgoal_r: pin.SE3,
     p_basegoal: np.ndarray,
     run=True,
 ) -> None | ControlLoopManager:
@@ -260,16 +260,18 @@ def CrocoDualEEAndBaseP2PMPC(
     are actually extracted from the state x(q,dq)
     """
     x0 = np.concatenate([robot.q, robot.v])
-    goal = (T_w_abseegoal, p_basegoal)
+    T_w_lgoal = T_absgoal_l.act(T_w_absgoal)
+    T_w_rgoal = T_absgoal_r.act(T_w_absgoal)
+    goal = (T_w_lgoal, T_w_rgoal, p_basegoal)
     ocp = BaseAndDualArmIKOCP(args, robot, x0, goal)
     ocp.solveInitialOCP(x0)
 
     controlLoop = partial(
         DualEEAndBaseP2PCtrlLoopTemplate,
         ocp,
-        T_w_abseegoal,
-        T_absgoal_lgoal,
-        T_absgoal_rgoal,
+        T_w_absgoal,
+        T_absgoal_l,
+        T_absgoal_r,
         p_basegoal,
         CrocoP2PDualEEAndBaseMPCControlLoop,
         args,
diff --git a/python/smc/control/optimal_control/path_following_croco_ocp.py b/python/smc/control/optimal_control/path_following_croco_ocp.py
index 698bf71ea0b5b8c25d157c6c1739199bc5bbee1f..99ed1175dfdcbc617b63166b736421d9f08f1b70 100644
--- a/python/smc/control/optimal_control/path_following_croco_ocp.py
+++ b/python/smc/control/optimal_control/path_following_croco_ocp.py
@@ -194,7 +194,7 @@ class BaseAndDualArmEEPathFollowingOCP(CrocoOCP):
         super().__init__(args, robot, x0, goal)
 
     def constructTaskObjectiveFunction(self, goal) -> None:
-        T_w_e_left, T_w_e_right = self.robot.getT_w_e
+        T_w_e_left, T_w_e_right = self.robot.getLeftRightT_w_e()
         path_ee_left = [T_w_e_left] * self.args.n_knots
         path_ee_right = [T_w_e_right] * self.args.n_knots
         path_base = [np.append(self.x0[:2], 0.0)] * self.args.n_knots
@@ -202,7 +202,7 @@ class BaseAndDualArmEEPathFollowingOCP(CrocoOCP):
         for i in range(self.args.n_knots):
             l_eePoseResidual = crocoddyl.ResidualModelFramePlacement(
                 self.state,
-                self.robot.model.l_ee_frame_id,
+                self.robot.l_ee_frame_id,
                 path_ee_left[i],
                 self.state.nv,
             )
@@ -212,7 +212,7 @@ class BaseAndDualArmEEPathFollowingOCP(CrocoOCP):
             )
             r_eePoseResidual = crocoddyl.ResidualModelFramePlacement(
                 self.state,
-                self.robot.model.r_ee_frame_id,
+                self.robot.r_ee_frame_id,
                 path_ee_right[i],
                 self.state.nv,
             )
@@ -221,7 +221,7 @@ class BaseAndDualArmEEPathFollowingOCP(CrocoOCP):
                 "r_ee_pose" + str(i), r_eeTrackingCost, self.args.ee_pose_cost
             )
             baseTranslationResidual = crocoddyl.ResidualModelFrameTranslation(
-                self.state, self.robot.model.base_frame_id, path_base[i], self.state.nv
+                self.state, self.robot.base_frame_id, path_base[i], self.state.nv
             )
             baseTrackingCost = crocoddyl.CostModelResidual(
                 self.state, baseTranslationResidual
diff --git a/python/smc/control/optimal_control/point_to_point_croco_ocp.py b/python/smc/control/optimal_control/point_to_point_croco_ocp.py
index 1b9625dc447bb6e835c8919bf14357e8e0fb0216..519458f91f113b5a3ca333abfdb42752716f83c5 100644
--- a/python/smc/control/optimal_control/point_to_point_croco_ocp.py
+++ b/python/smc/control/optimal_control/point_to_point_croco_ocp.py
@@ -214,8 +214,6 @@ class BaseAndDualArmIKOCP(DualArmIKOCP):
         robot: DualArmWholeBodyInterface,
         x0: np.ndarray,
         goal,
-        #        T_w_eegoal: pin.SE3,
-        #        p_basegoal: np.ndarray,
     ):
         # T_w_eegoal, p_basegoal = goal
         super().__init__(args, robot, x0, goal)