diff --git a/python/examples/clik.py b/python/examples/clik.py
index cd07a47a12b4aceccabd3e4c071cb4b41f90fe5c..be65bc01e0deb01b0186eb9f0ad67c13c69c99cb 100644
--- a/python/examples/clik.py
+++ b/python/examples/clik.py
@@ -23,6 +23,7 @@ if __name__ == "__main__":
     print(robot.getT_w_e())
     Mgoal = robot.defineGoalPointCLI()
     compliantMoveL(args, robot, Mgoal)
+    #moveL(args, robot, Mgoal)
     robot.closeGripper()
     robot.openGripper()
     if not args.pinocchio_only:
diff --git a/python/examples/path_following_mpc.py b/python/examples/path_following_mpc.py
index 19dd8032ea65be3012ede34278e8c40cce9aac83..8afaf5703d3ee43b8cc1d1bcdd647c990ee26a53 100644
--- a/python/examples/path_following_mpc.py
+++ b/python/examples/path_following_mpc.py
@@ -30,6 +30,7 @@ if __name__ == "__main__":
     if importlib.util.find_spec('mim_solvers'):
         import mim_solvers
     robot = RobotManager(args)
+    time.sleep(5)
     # TODO: put this back for nicer demos
     #Mgoal = robot.defineGoalPointCLI()
     #Mgoal = pin.SE3.Random()
diff --git a/python/ur_simple_control/__pycache__/managers.cpython-312.pyc b/python/ur_simple_control/__pycache__/managers.cpython-312.pyc
index 8aac4dcf219d27bcd32fee4d77ef29b50de1926c..0fbef10f71ac938cc681cd484d7d87e0a2dd77e8 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/clik.py b/python/ur_simple_control/clik/clik.py
index d402bf8a3603cb0f71e52f26ef2d71ebe89e279c..3f39f815e75df421ff5d4690fd7c7078ef2f8530 100644
--- a/python/ur_simple_control/clik/clik.py
+++ b/python/ur_simple_control/clik/clik.py
@@ -160,7 +160,7 @@ def controlLoopClik(robot : RobotManager, clik_controller, i, past_data):
     robot.sendQd(qd)
     
     log_item['qs'] = q.reshape((robot.model.nq,))
-    log_item['dqs'] = robot.getQd().reshape((robot.model.nq,))
+    log_item['dqs'] = robot.getQd().reshape((robot.model.nv,))
     # we're not saving here, but need to respect the API, 
     # hence the empty dict
     return breakFlag, {}, log_item
@@ -237,7 +237,7 @@ def moveL(args, robot : RobotManager, goal_point):
     # 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),
+            'dqs' : np.zeros(robot.model.nv),
         }
     loop_manager = ControlLoopManager(robot, controlLoop, args, {}, log_item)
     loop_manager.run()
diff --git a/python/ur_simple_control/managers.py b/python/ur_simple_control/managers.py
index df84f3b78a6b1eed963d92ba0d36b78d61c7811b..b6be29c9f186d8f31cc1a4f9c033947a1c8edfab 100644
--- a/python/ur_simple_control/managers.py
+++ b/python/ur_simple_control/managers.py
@@ -13,7 +13,7 @@ from ur_simple_control.util.grippers.robotiq.robotiq_gripper import RobotiqGripp
 from ur_simple_control.util.grippers.on_robot.twofg import TWOFG
 import copy
 import signal
-from ur_simple_control.util.get_model import get_model, heron_approximation, getGripperlessUR5e
+from ur_simple_control.util.get_model import get_model, heron_approximation, heron_approximationDD, getGripperlessUR5e
 from collections import deque
 from ur_simple_control.visualize.visualize import plotFromDict, realTimePlotter, manipulatorVisualizer
 from ur_simple_control.util.logging_utils import LogManager
@@ -79,7 +79,7 @@ def getMinimalArgParser():
     #################################################
     parser.add_argument('--robot', type=str, \
             help="which robot you're running or simulating", default="ur5e", \
-            choices=['ur5e', 'heron', 'gripperlessur5e'])
+            choices=['ur5e', 'heron', 'herondd', 'gripperlessur5e'])
     parser.add_argument('--simulation', action=argparse.BooleanOptionalAction, \
             help="whether you are running the UR simulator", default=False)
     parser.add_argument('--robot-ip', type=str, 
@@ -406,6 +406,9 @@ class RobotManager:
         if self.robot_name == "heron":
             self.model, self.collision_model, self.visual_model, self.data = \
                  heron_approximation()
+        if self.robot_name == "herondd":
+            self.model, self.collision_model, self.visual_model, self.data = \
+                 heron_approximationDD()
         if self.robot_name == "gripperlessur5e":
             self.model, self.collision_model, self.visual_model, self.data = \
                  getGripperlessUR5e()
@@ -817,6 +820,9 @@ class RobotManager:
         if self.robot_name == "heron":
                 self.v_q = qd
                 self.q = pin.integrate(self.model, self.q, qd * self.dt)
+        if self.robot_name == "herondd":
+                self.v_q = qd
+                self.q = pin.integrate(self.model, self.q, qd * self.dt)
 
         if self.robot_name == "gripperlessur5e":
             qd_cmd = qd[:6]
diff --git a/python/ur_simple_control/optimal_control/crocoddyl_mpc.py b/python/ur_simple_control/optimal_control/crocoddyl_mpc.py
index 2d36c726052bb28e40821619279bedc6d3ca060f..940acd6e580c4e344c5f6b8b81d5421f88d3a7b3 100644
--- a/python/ur_simple_control/optimal_control/crocoddyl_mpc.py
+++ b/python/ur_simple_control/optimal_control/crocoddyl_mpc.py
@@ -111,15 +111,23 @@ def CrocoPathFollowingMPCControlLoop(args, robot : RobotManager, solver : crocod
     # TODO: implement
     #path = robot.getPath()
     
+    # 1) make 6D path out of [[x0,y0],...] 
+    # that represents the center of the cart
+    # 2) do T_mobile_base_ee_pos to get 
+    # end-effector reference frame(s)
+
     # generate bullshit just to see it works
     T_w_e = robot.getT_w_e()
-    translation = np.array([0.01, 0.0, 0.0])
     path = []
+    t = i * robot.dt
     for i in range(args.n_knots):
+        t += 0.01
         new = T_w_e.copy()
-        new.translation = new.translation + i * translation
+        translation = 2 * np.array([np.cos(t/20), np.sin(t/20), 0.3])
+        new.translation = translation
         path.append(new)
 
+
     x0 = np.concatenate([robot.getQ(), robot.getQd()])
     solver.problem.x0 = x0
     # warmstart solver with previous solution
diff --git a/python/ur_simple_control/util/__pycache__/get_model.cpython-312.pyc b/python/ur_simple_control/util/__pycache__/get_model.cpython-312.pyc
index 05d82360395cf608e6ad774a2bfb5c43c0ebd23f..361d94f551ed5213c11d66ae7a950361e4d7f57f 100644
Binary files a/python/ur_simple_control/util/__pycache__/get_model.cpython-312.pyc and b/python/ur_simple_control/util/__pycache__/get_model.cpython-312.pyc differ
diff --git a/python/ur_simple_control/util/get_model.py b/python/ur_simple_control/util/get_model.py
index afe4dcc7af341645d0e5c74c0bfae4d1cb394dc7..0a2f07581c7c4f6639fc45bcc5ec05a22fb9b7e8 100644
--- a/python/ur_simple_control/util/get_model.py
+++ b/python/ur_simple_control/util/get_model.py
@@ -234,3 +234,78 @@ def heron_approximation():
             geom.meshScale = s
 
     return model, visual_model.copy(), visual_model, data
+
+# TODO:
+# try building the mobile base as a rotational joint,
+# on top of which is a prismatic joint.
+# this way you get the differential-drive basis that you want,
+# and you don't have to manually correct the additional 
+# degree of freedom everywhere (jacobian, control etc)
+def heron_approximationDD():
+    # arm + gripper 
+    model_arm, collision_model_arm, visual_model_arm, data_arm = get_model()
+
+    # mobile base as planar joint (there's probably a better
+    # option but whatever right now)
+    model_mobile_base = pin.Model()
+    model_mobile_base.name = "mobile_base"
+    geom_model_mobile_base = pin.GeometryModel()
+    revolute_joint_name = "mobile_base_rotational_joint"
+    prismatic_joint_name = "mobile_base_prismatic_joint"
+    parent_id = 0
+    # TEST
+    joint_placement = pin.SE3.Identity()
+    #joint_placement.rotation = pin.rpy.rpyToMatrix(0, -np.pi/2, 0) 
+    #joint_placement.translation[2] = 0.2
+    MOBILE_BASE_REVOLUTE_ID = model_mobile_base.addJoint(parent_id, 
+            pin.JointModelRZ(), 
+            joint_placement.copy(), revolute_joint_name)
+    MOBILE_BASE_PRISMATIC_ID = model_mobile_base.addJoint(
+            MOBILE_BASE_REVOLUTE_ID, 
+            pin.JointModelPY(), 
+            joint_placement.copy(), prismatic_joint_name)
+    # we should immediately set velocity limits.
+    # there are no position limit by default and that is what we want.
+    # TODO: put in heron's values
+    model_mobile_base.velocityLimit[0] = 2
+    model_mobile_base.velocityLimit[1] = 2
+#    model_mobile_base.velocityLimit[2] = 2
+    # TODO: i have literally no idea what reasonable numbers are here
+    model_mobile_base.effortLimit[0] = 200
+    model_mobile_base.effortLimit[1] = 200
+#    model_mobile_base.effortLimit[2] = 200
+    #print("OBJECT_JOINT_ID",OBJECT_JOINT_ID)
+    #body_inertia = pin.Inertia.FromBox(args.box_mass, box_dimensions[0], 
+    #        box_dimensions[1], box_dimensions[2])
+
+    # pretty much random numbers
+    # TODO: find heron (mir) numbers
+    body_inertia = pin.Inertia.FromBox(30, 0.5, 
+            0.3, 0.4)
+    # maybe change placement to sth else depending on where its grasped
+    model_mobile_base.appendBodyToJoint(MOBILE_BASE_PRISMATIC_ID, 
+                                        body_inertia, pin.SE3.Identity()) 
+    box_shape = fcl.Box(0.5, 0.3, 0.4) 
+    body_placement = pin.SE3.Identity()
+    geometry_mobile_base = pin.GeometryObject("box_shape", MOBILE_BASE_PRISMATIC_ID, box_shape, body_placement.copy())
+
+    geometry_mobile_base.meshColor = np.array([1., 0.1, 0.1, 1.])
+    geom_model_mobile_base.addGeometryObject(geometry_mobile_base)
+
+    # have to add the frame manually
+    model_mobile_base.addFrame(pin.Frame('base',MOBILE_BASE_PRISMATIC_ID,0,joint_placement.copy(),pin.FrameType.JOINT) )
+
+    # frame-index should be 1
+    model, visual_model = pin.appendModel(model_mobile_base, model_arm, geom_model_mobile_base, visual_model_arm, 1, pin.SE3.Identity())
+    data = model.createData()
+    
+    # fix gripper
+    for geom in visual_model.geometryObjects:
+        if "hand" in geom.name:
+            s = geom.meshScale
+            geom.meshcolor = np.array([1.0, 0.1, 0.1, 1.0])
+            # this looks exactly correct lmao
+            s *= 0.001
+            geom.meshScale = s
+
+    return model, visual_model.copy(), visual_model, data