diff --git a/python/examples/cart_pulling.py b/python/examples/cart_pulling.py
index 9df36a683218250dd71f4f754e4f3340147f1cf2..103355a4a0ad6ca8d169a4e785360471b3cb379b 100644
--- a/python/examples/cart_pulling.py
+++ b/python/examples/cart_pulling.py
@@ -17,6 +17,9 @@ import importlib.util
 from ur_simple_control.path_generation.planner import starPlanner, getPlanningArgs, createMap
 import yaml
 
+# TODO:
+# - make reference step size in path_generator an argument here
+#   because we use that for path interpolation later on as well
 
 def get_args():
     parser = getMinimalArgParser()
@@ -24,6 +27,8 @@ def get_args():
     parser = getClikArgs(parser) # literally just for goal error
     parser = getPlanningArgs(parser)
     parser.add_argument('--handlebar-height', type=float, default=0.5)
+    parser.add_argument('--base-to-handlebar-preferred-distance', type=float, default=0.7, \
+            help="prefered path arclength from mobile base position to handlebar")
     argcomplete.autocomplete(parser)
     args = parser.parse_args()
     # TODO TODO TODO: REMOVE PRESET HACKS
@@ -73,7 +78,8 @@ if __name__ == "__main__":
 
     planning_function = partial(starPlanner, goal)
     # TODO: ensure alignment in orientation between planner and actual robot
-    path_planner = ProcessManager(args, planning_function, None, 2, None)
+    path_planner = ProcessManager(args, planning_function, robot.q[:2], 3, None)
+    #time.sleep(5)
     # clik version
     #cartesianPathFollowingWithPlanner(args, robot, path_planner)
     # end-effector tracking version
diff --git a/python/ur_simple_control/__pycache__/managers.cpython-312.pyc b/python/ur_simple_control/__pycache__/managers.cpython-312.pyc
index 8b2e1e5ebef5450829084854b558d13de09d65bb..c2815be629cbe704bc81afe0a7c810441fb93951 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/managers.py b/python/ur_simple_control/managers.py
index 79b63f59d192ad5fe83657c536d722a50d3c3851..c64cd2666bf3ac1fc382e94118990a47ea8f2da4 100644
--- a/python/ur_simple_control/managers.py
+++ b/python/ur_simple_control/managers.py
@@ -17,7 +17,7 @@ from ur_simple_control.util.get_model import get_model, heron_approximation, her
 from collections import deque
 from ur_simple_control.visualize.visualize import plotFromDict, realTimePlotter, manipulatorVisualizer
 from ur_simple_control.util.logging_utils import LogManager
-from multiprocessing import Process, Queue
+from multiprocessing import Process, Queue, Lock, shared_memory
 # argcomplete is an external package which creates tab completion in shell
 # argparse is argument parsing from the standard library
 import argcomplete, argparse
@@ -277,6 +277,10 @@ class ControlLoopManager:
                 if self.args.visualize_manipulator:
                     self.robot_manager.visualizer_manager.sendCommand({"q" : self.robot_manager.q,
                                                           "T_w_e" : self.robot_manager.getT_w_e()})
+                    if self.robot_manager.robot_name == "heron":
+                        T_base = self.robot_manager.data.oMi[1]
+                        self.robot_manager.visualizer_manager.sendCommand({"T_base" : T_base})
+
                             #if self.robot_manager.manipulator_visualizer_queue.qsize() < 5:
                             #    self.robot_manager.updateViz({"q" : self.robot_manager.q,
                             #                              "T_w_e" : self.robot_manager.getT_w_e()})
@@ -997,6 +1001,7 @@ class ProcessManager:
     # need to create new ones, but i won't optimize in advance
     def __init__(self, args, side_function, init_command, comm_direction, init_value=None):
         self.args = args
+        self.comm_direction = comm_direction
 
         if comm_direction == 0:
             self.command_queue = Queue()
@@ -1011,17 +1016,28 @@ class ProcessManager:
             self.data_queue = Queue()
             self.side_process = Process(target=side_function, 
                                                      args=(args, init_command, self.command_queue, self.data_queue,))
+        # both ways, with shared memory for commands
+        if comm_direction == 3:
+            self.data_queue = Queue()
+            # "sending" the command via shared memory
+            shm_name = "command"
+            self.shm = shared_memory.SharedMemory(shm_name, create=True, size=init_command.nbytes)
+            self.shared_command = np.ndarray(init_command.shape, dtype=init_command.dtype, buffer=self.shm.buf)
+            self.shared_command[:] = init_command[:]
+            self.shared_command_lock = Lock()
+            # the process has to create its shared memory
+            self.side_process = Process(target=side_function, 
+                                         args=(args, init_command, shm_name, self.shared_command_lock, self.data_queue,))
         if type(side_function) == partial:
             self.side_process.name = side_function.func.__name__
         else:
             self.side_process.name = side_function.__name__ + "_process"
         self.lastest_data = init_value
-        if self.args.debug_prints:
-            print(f"PROCESS_MANAGER: i created the command queue for {self.side_process.name}", self.command_queue)
 
         self.side_process.start()
         if self.args.debug_prints:
-            print("PROCESS_MANAGER: i am starting {self.side_process.name}")
+            print(f"PROCESS_MANAGER: i am starting {self.side_process.name}")
+
 
     # TODO: enforce that
     # the key should be a string containing the command,
@@ -1074,6 +1090,23 @@ class ProcessManager:
             #self.command_queue.get_nowait()
             self.command_queue.put_nowait(command)
 
+    # TODO: implement
+    def sendCommandViaSharedMemory(self, command):
+        """ 
+        sendCommandViaSharedMemory
+        instead of having a queue for the commands, have a shared memory variable.
+        this makes sense if you want to send the latest command only,
+        instead of stacking up old commands in a queue.
+        the locking and unlocking of the shared memory happens here 
+        and you don't have to think about it in the control loop nor
+        do you need to pollute half of robotmanager or whatever else
+        to deal with this.
+        """
+        assert type(command) == np.ndarray
+        assert command.shape == self.shared_command.shape
+        self.shared_command_lock.acquire()
+        self.shared_command[:] = command[:]
+        self.shared_command_lock.release()
 
     def getData(self):
         if not self.data_queue.empty():
@@ -1081,6 +1114,9 @@ class ProcessManager:
         return copy.deepcopy(self.lastest_data)
 
     def terminateProcess(self):
+        if self.comm_direction == 3:
+            self.shm.close()
+            self.shm.unlink()
         if self.args.debug_prints:
             print(f"i am putting befree in {self.side_process.name}'s command queue to stop it")
         self.command_queue.put_nowait("befree")
diff --git a/python/ur_simple_control/optimal_control/crocoddyl_mpc.py b/python/ur_simple_control/optimal_control/crocoddyl_mpc.py
index 143ca68edced3eb0942f98f3e38267d5248aa288..078883ce4c0cb408dd1754e43c241ffae5e1dad8 100644
--- a/python/ur_simple_control/optimal_control/crocoddyl_mpc.py
+++ b/python/ur_simple_control/optimal_control/crocoddyl_mpc.py
@@ -221,13 +221,19 @@ def BaseAndEEPathFollowingMPCControlLoop(args, robot : RobotManager, solver : cr
     T_w_e = robot.getT_w_e()
     #p = T_w_e.translation[:2]
     p = q[:2]
+    print("CTRL:", p)
+    # 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
 
     if not (type(path_planner) == types.FunctionType):
-        path_planner.sendFreshestCommand({'p' : p})
+        #path_planner.sendFreshestCommand({'p' : p})
+        path_planner.sendCommandViaSharedMemory(p)
 
     # 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
     if type(path_planner) == types.FunctionType:
         pathSE3 = path_planner(T_w_e, i)
     else:
@@ -242,19 +248,40 @@ def BaseAndEEPathFollowingMPCControlLoop(args, robot : RobotManager, solver : cr
         if data == "done":
             breakFlag = True
 
-        path_pol, path2D_untimed = data
-        path2D_untimed = np.array(path2D_untimed).reshape((-1,2))
+        path_pol_base, path2D_untimed_base = data
+        #path2D_untimed_base.insert(0, p[1])
+        #path2D_untimed_base.insert(0, p[0])
+        path2D_untimed_base = np.array(path2D_untimed_base).reshape((-1,2))
         # should be precomputed somewhere but this is nowhere near the biggest problem
         max_base_v = np.linalg.norm(robot.model.velocityLimit[:2])
 
         # 1) make 6D path out of [[x0,y0],...] 
         # that represents the center of the cart
-        pathSE3 = path2D_to_timed_SE3_base_and_ee(args, path_pol, path2D_untimed, max_base_v)
+        # TODO: USE THIS ONCE IMPLEMENTED
+        #pathSE3 = path2D_to_timed_SE3_base_and_ee(args, path_pol_base, path2D_untimed_base, max_base_v)
+        # TODO: separate out timing construction so that it can be used for the handlebar path as well
+        pathSE3_base = path2D_to_timed_SE3(args, path_pol_base, path2D_untimed_base, max_base_v, 0.0)
     # TODO: EVIL AND HAS TO BE REMOVED FROM HERE
     if args.visualize_manipulator:
         if i % 20 == 0:
-            robot.visualizer_manager.sendCommand({"path": pathSE3})
+            robot.visualizer_manager.sendCommand({"path": pathSE3_base})
     
+    # now find the previous path point of arclength base_to_handlebar_preferred_distance
+    # 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
+    arra = np.array(past_data['path2D_untimed'])
+    path2D_handlebar = np.vstack((arra[i:], path2D_untimed_base))
+    # now we need to construct timing for this
+
+
+
     x0 = np.concatenate([robot.getQ(), robot.getQd()])
     solver.problem.x0 = x0
     # warmstart solver with previous solution
@@ -263,10 +290,10 @@ def BaseAndEEPathFollowingMPCControlLoop(args, robot : RobotManager, solver : cr
     us_init = list(solver.us[1:]) + [solver.us[-1]]
 
     for i, runningModel in enumerate(solver.problem.runningModels):
-        runningModel.differential.costs.costs['gripperPose' + str(i)].cost.residual.reference = pathSE3[i]
+        runningModel.differential.costs.costs['gripperPose' + str(i)].cost.residual.reference = pathSE3_base[i]
 
     # idk if that's necessary
-    solver.problem.terminalModel.differential.costs.costs['gripperPose'+str(args.n_knots)].cost.residual.reference = pathSE3[-1]
+    solver.problem.terminalModel.differential.costs.costs['gripperPose'+str(args.n_knots)].cost.residual.reference = pathSE3_base[-1]
 
     solver.solve(xs_init, us_init, args.max_solver_iter)
     xs = np.array(solver.xs)
@@ -308,7 +335,12 @@ def BaseAndEEPathFollowingMPC(args, robot, x0, path_planner):
             'qs' : np.zeros(robot.model.nq),
             'dqs' : np.zeros(robot.model.nv)
         }
-    save_past_dict = {}
+    # 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.getT_w_e()
+    save_past_dict = {'path2D_untimed' : T_w_e.translation[:2]}
     loop_manager = ControlLoopManager(robot, controlLoop, args, save_past_dict, log_item)
     loop_manager.run()
 
diff --git a/python/ur_simple_control/path_generation/planner.py b/python/ur_simple_control/path_generation/planner.py
index 49658b0825e85ed0d25d8844dcd544a0f1b4e285..38eeaf2a88450b36b2a2e53248091e51a1725ea7 100644
--- a/python/ur_simple_control/path_generation/planner.py
+++ b/python/ur_simple_control/path_generation/planner.py
@@ -13,7 +13,7 @@ import pinocchio as pin
 
 import matplotlib.pyplot as plt
 import matplotlib.collections as plt_col
-from multiprocessing import Queue
+from multiprocessing import Queue, Lock, shared_memory
 
 def getPlanningArgs(parser):
     parser.add_argument('--planning-robot-params-file', type=str,
@@ -462,7 +462,7 @@ def pathVisualizer(x0, goal, map_as_list, positions):
 def pathPointFromPathParam(n_pol, path_dim, path_pol, s):
     return [np.polyval(path_pol[j*(n_pol+1):(j+1)*(n_pol+1)], s) for j in range(path_dim)]
 
-def path2D_to_timed_SE3(args, path_pol, path2D_untimed, max_base_v):
+def path2D_to_timed_SE3(args, path_pol, path2D_untimed, max_base_v, path_height):
     """
     path2D_to_timed_SE3
     ---------------------
@@ -514,18 +514,26 @@ def path2D_to_timed_SE3(args, path_pol, path2D_untimed, max_base_v):
     total_time = path_length / base_v
     # 2) we find the correspondence between s and time
     # TODO: read from where it should be, but seba checked that it's 5 for some reason
+    # TODO THIS IS N IN PATH PLANNING, MAKE THIS A SHARED ARGUMENT
     max_s = 5 
     t_to_s = max_s / total_time
     # 3) construct the ocp-timed 2D path 
+    # TODO MAKE REFERENCE_STEP_SIZE A SHARED ARGUMENT
+    # TODO: we should know max s a priori
+    reference_step_size = 0.5
+    s_vec = np.arange(0, len(path2D_untimed)) / reference_step_size
+
     path2D = []
     # time is i * args.ocp_dt
     for i in range(args.n_knots + 1):
-        s = (i * args.ocp_dt) * t_to_s
-        path2D.append(pathPointFromPathParam(args.n_pol, args.np, path_pol, s))
+        # what it should be but gets stuck if we're not yet on path
+        #s = (i * args.ocp_dt) * t_to_s
+        # full path
+        s = i * (max_s / args.n_knots)
+        #path2D.append(pathPointFromPathParam(args.n_pol, args.np, path_pol, s))
+        path2D.append(np.array([np.interp(s, s_vec, path2D_untimed[:,0]), 
+                                np.interp(s, s_vec, path2D_untimed[:,1])]))
     path2D = np.array(path2D)
-    #print("=" * 15)
-    #print(path2D_untimed)
-    #print('----------------------------------------')
 
     ####################################################
     #  constructing the SE3 reference from [x,y] path  #
@@ -571,10 +579,8 @@ def path2D_to_timed_SE3(args, path_pol, path2D_untimed, max_base_v):
         rotation = pin.rpy.rpyToMatrix(0.0, 0.0, thetas[i])
         rotation = pin.rpy.rpyToMatrix(np.pi/2, np.pi/2,0.0) @ rotation
         #rotation = np.eye(3)
-        translation = np.array([path2D[i][0], path2D[i][1], 
-                                args.handlebar_height])
+        translation = np.array([path2D[i][0], path2D[i][1], path_height])
         path.append(pin.SE3(rotation, translation))
-        #print(path[-1].translation)
 
     return path
 
@@ -633,7 +639,6 @@ def path2D_to_timed_SE3_base_and_ee(args, path_pol, path2D_untimed, max_base_v):
     y_diff = y_i_plus_1 - y_i
     y_diff = y_diff.reshape((-1,1))
     path_length = np.sum(np.linalg.norm(np.hstack((x_diff, y_diff)), axis=1))
-    print(path_length)
     total_time = path_length / base_v
     # 2) we find the correspondence between s (path parameter) and time
     # TODO: read from where max_s should be 5, but seba checked that it's 5 for some reason
@@ -644,14 +649,12 @@ def path2D_to_timed_SE3_base_and_ee(args, path_pol, path2D_untimed, max_base_v):
     # TODO: tune this value!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1
     # TODO: tune this value!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1
     # TODO make this an argument
-    base_to_handlebar_prefered_distance = 0.7
     # TODO: we need a function that takes time as input and spits
-    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!11
-    need to have set interpolation to implement that
+#    need to have set interpolation to implement that
     # out a path point of distance base_to_handlebar_prefered_distance from time 0.
     # (it's the classic integral for curve length) 
     #      cart_to_base_prefered_distance as percentage of path length * size of s (again why the fuck isn't s=1????)
-    handlebar_to_base_as_s = (cart_to_base_prefered_distance / path_length) * max_s
+    handlebar_to_base_as_s = (args.base_to_handlebar_preferred_distance / path_length) * max_s
     path2D_mobile_base = []
     path2D_cart = []
     # time is i * args.ocp_dt
@@ -711,13 +714,14 @@ def path2D_to_timed_SE3_base_and_ee(args, path_pol, path2D_untimed, max_base_v):
         translation = np.array([path2D_cart[i][0], path2D_cart[i][1], 
                                 args.handlebar_height])
         path.append(pin.SE3(rotation, translation))
-        #print(path[-1].translation)
 
     return path
 
 # function to be put into ProcessManager,
 # spitting out path points
-def starPlanner(goal, args, init_cmd, cmd_queue : Queue, data_queue : Queue):
+#def starPlanner(goal, args, init_cmd, cmd_queue : Queue, data_queue : Queue):
+# let's try shared memory
+def starPlanner(goal, args, init_cmd, shm_name, lock : Lock, data_queue : Queue):
     """
     starPlanner
     ------------
@@ -726,6 +730,11 @@ def starPlanner(goal, args, init_cmd, cmd_queue : Queue, data_queue : Queue):
     out path points and just the path points.
     goal and path are [x,y]
     """
+    # shm stuff
+    shm = shared_memory.SharedMemory(name=shm_name)
+    # dtype is default, but i have to pass it
+    p_shared = np.ndarray((2,), dtype=np.float64, buffer=shm.buf)
+    p = np.zeros(2)
     # environment
     obstacles, _ = createMap()    
 
@@ -752,8 +761,11 @@ def starPlanner(goal, args, init_cmd, cmd_queue : Queue, data_queue : Queue):
     try:
         while True:
             # has to be a blocking call
-            cmd = cmd_queue.get()
-            p = cmd['p']
+            #cmd = cmd_queue.get()
+            #p = cmd['p']
+            lock.acquire()
+            p[:] = p_shared[:] 
+            lock.release()
 
             if np.linalg.norm(p - goal) < convergence_threshold:
                 data_queue.put("done")
@@ -765,9 +777,13 @@ def starPlanner(goal, args, init_cmd, cmd_queue : Queue, data_queue : Queue):
             r0, rg, rho, obstacles_star = scene_updater.update(p, goal, obstacles)
             # compute the path
             path_pol, epsilon = path_gen.update(p, r0, rg, rho, obstacles_star)
-            data_queue.put((path_pol, path_gen.target_path))
+            # TODO: this is stupid, just used shared memory bro
+            if data_queue.qsize() < 1:
+                data_queue.put((path_pol, path_gen.target_path))
 
     except KeyboardInterrupt:
+        shm.close()
+        shm.unlink()
         if args.debug_prints:
             print("PLANNER: caught KeyboardInterrupt, i'm out")
 
diff --git a/python/ur_simple_control/path_generation/tunnel_mpc_params.yaml b/python/ur_simple_control/path_generation/tunnel_mpc_params.yaml
index 1f1ca862f725ed5a3d033dd093dfb0a5238bd9ac..5568e99abeff2546dc3ae8cae9f829f0998bbfc3 100644
--- a/python/ur_simple_control/path_generation/tunnel_mpc_params.yaml
+++ b/python/ur_simple_control/path_generation/tunnel_mpc_params.yaml
@@ -13,7 +13,7 @@ tunnel_mpc:
   max_compute_time: 10
   crep: 1.
   reactivity: 1.
-  buffer: 1
+  buffer: 0
 
   # MPC
   ce: 100.
@@ -29,6 +29,7 @@ tunnel_mpc:
   integration_method: 'euler'
   np: 2
   n_pol: 10
+  # TODO: test if this is the length of the path
   N: 5
   dt: 0.2
   solver_tol: 1.e-5
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 5f9c8d3e8db06097c501d6e568f9c82e342ef74f..0b9be5219556e6bda86aa8b5060d344b697ac177 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/visualize/__pycache__/visualize.cpython-312.pyc b/python/ur_simple_control/visualize/__pycache__/visualize.cpython-312.pyc
index 5e23ff210ab7394dfd4a3855298def2a26a4fc03..2095b13bf2b1b332271a3dd4614c22b5bfcd9ccb 100644
Binary files a/python/ur_simple_control/visualize/__pycache__/visualize.cpython-312.pyc and b/python/ur_simple_control/visualize/__pycache__/visualize.cpython-312.pyc differ
diff --git a/python/ur_simple_control/visualize/visualize.py b/python/ur_simple_control/visualize/visualize.py
index 3c039a0449183cb6c4384a5fa6c869325003d8ba..233245482caf3365bedab4585dc8ee958d6782c8 100644
--- a/python/ur_simple_control/visualize/visualize.py
+++ b/python/ur_simple_control/visualize/visualize.py
@@ -172,6 +172,7 @@ def manipulatorVisualizer(model, collision_model, visual_model, args, cmd, queue
     # set shapes we know we'll use
     meshcat_shapes.frame(viz.viewer["Mgoal"], opacity=0.5)
     meshcat_shapes.frame(viz.viewer["T_w_e"], opacity=0.5)
+    meshcat_shapes.frame(viz.viewer["T_base"], opacity=0.5)
     print("MANIPULATORVISUALIZER: FULLY ONLINE")
     try:
         while True:
@@ -187,6 +188,8 @@ def manipulatorVisualizer(model, collision_model, visual_model, args, cmd, queue
                     viz.viewer["Mgoal"].set_transform(cmd["Mgoal"].homogeneous)
                 if key == "T_w_e":
                     viz.viewer["T_w_e"].set_transform(cmd["T_w_e"].homogeneous)
+                if key == "T_base":
+                    viz.viewer["T_base"].set_transform(cmd["T_base"].homogeneous)
                 if key == "q":
                     viz.display(cmd["q"])
                 if key == "point":