diff --git a/examples/graz/clik_client.py b/examples/graz/clik_client.py
index aca40dc8f90f809159bfb3804b72cce1f7e3edeb..079e1a68892b405eeb7853626c2bcfd3ef15f702 100644
--- a/examples/graz/clik_client.py
+++ b/examples/graz/clik_client.py
@@ -41,7 +41,7 @@ def controlLoopClikExternalGoal(
     ik_solver,
     i,
     past_data,
-):
+) -> tuple[bool, dict[str, np.ndarray], dict[str, np.ndarray]]:
     """
     controlLoop
     -----------------------------
@@ -61,11 +61,12 @@ def controlLoopClikExternalGoal(
     SEerror = T_w_e.actInv(T_goal)
     err_vector = pin.log6(SEerror).vector
     J = pin.computeFrameJacobian(robot.model, robot.data, q, robot.ee_frame_id)
+    K = np.diag([30] * 3 + [15] * 3)
 
     qd_cmd = (
         J.T
         @ np.linalg.inv(J @ J.T + np.eye(J.shape[0], J.shape[0]) * args.tikhonov_damp)
-        @ (err_vector + data["v"])
+        @ (K @ err_vector + data["v"])
     )
     if qd_cmd is None:
         print("the controller you chose didn't work, using dampedPseudoinverse instead")
diff --git a/python/smc/control/controller_templates/generic_control_loop.py b/python/smc/control/controller_templates/generic_control_loop.py
new file mode 100644
index 0000000000000000000000000000000000000000..f9b5838124dface1e6e89cfac18f9937c36866ce
--- /dev/null
+++ b/python/smc/control/controller_templates/generic_control_loop.py
@@ -0,0 +1,28 @@
+from argparse import Namespace
+from typing import Any, Callable
+import numpy as np
+
+from smc.robots.robotmanager_abstract import AbstractRobotManager
+
+global control_loop_return
+control_loop_return = tuple[bool, dict[str, np.ndarray], dict[str, np.ndarray]]
+
+
+def GenericControlLoop(
+    X: Any,
+    control_loop: Callable[
+        [Any, Namespace, AbstractRobotManager, dict[str, np.ndarray], float],
+        control_loop_return,
+    ],
+    args: Namespace,
+    robot: AbstractRobotManager,
+    past_data: dict[str, np.ndarray],
+    t: int,  # will be float eventually
+) -> control_loop_return:
+    breakFlag = False
+    log_item = {}
+    save_past_item = {}
+
+    control_loop(X, args, robot, past_data, t)
+
+    return breakFlag, log_item, save_past_item
diff --git a/python/smc/control/controller_templates/path_following_template.py b/python/smc/control/controller_templates/path_following_template.py
new file mode 100644
index 0000000000000000000000000000000000000000..4dd851c1f036606f5d57215d1989132a325ca01d
--- /dev/null
+++ b/python/smc/control/controller_templates/path_following_template.py
@@ -0,0 +1,62 @@
+from smc.robots.robotmanager_abstract import AbstractRobotManager
+from smc.multiprocessing.process_manager import ProcessManager
+
+from pinocchio import SE3, log6
+from argparse import Namespace
+from typing import Any, Callable
+import numpy as np
+
+global control_loop_return
+control_loop_return = tuple[bool, dict[str, np.ndarray], dict[str, np.ndarray]]
+
+
+def PathFollowingFromPlannerControlLoop(
+    path_planner: ProcessManager,
+    get_position: Callable[[AbstractRobotManager], np.ndarray],
+    SOLVER: Callable,
+    control_loop: Callable[
+        [Any, np.ndarray, Namespace, AbstractRobotManager, dict[str, np.ndarray], int],
+        np.ndarray,
+    ],
+    args: Namespace,
+    robot: AbstractRobotManager,
+    past_data: dict[str, np.ndarray],
+    t: int,  # will be float eventually
+) -> control_loop_return:
+    """
+    Point2PointControlLoop
+    ---------------
+    generic control loop for point to point motion
+    (handling error to final point etc).
+    """
+    breakFlag = False
+    log_item = {}
+    save_past_item = {}
+
+    p = get_position(robot)
+
+    path_planner.sendCommand(p)
+    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"] = robot.q
+        log_item["dqs"] = robot.v
+        return breakFlag, save_past_item, log_item
+
+    _, path2D_untimed = data
+    path2D_untimed = np.array(path2D_untimed).reshape((-1, 2))
+
+    v_cmd, log_item_inner = control_loop(
+        SOLVER, path2D_untimed, args, robot, past_data, t
+    )
+    robot.sendVelocityCommand(v_cmd)
+    log_item.update(log_item_inner)
+
+    log_item["qs"] = robot.q
+    log_item["dqs"] = robot.v
+    log_item["dqs_cmd"] = v_cmd.reshape((robot.model.nv,))
+    return breakFlag, save_past_item, log_item
diff --git a/python/smc/control/controller_templates/point_to_point.py b/python/smc/control/controller_templates/point_to_point.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f2a61ee780585294fb1d510474f5fd694b8fb60
--- /dev/null
+++ b/python/smc/control/controller_templates/point_to_point.py
@@ -0,0 +1,46 @@
+from smc.robots.interfaces.single_arm_interface import SingleArmInterface
+
+from pinocchio import SE3, log6
+from argparse import Namespace
+from typing import Any, Callable
+import numpy as np
+
+global control_loop_return
+control_loop_return = tuple[bool, dict[str, np.ndarray], dict[str, np.ndarray]]
+
+
+def Point2PointControlLoop(
+    SOLVER: Callable,
+    T_w_goal: SE3,
+    control_loop: Callable[
+        [Any, SE3, Namespace, SingleArmInterface, dict[str, np.ndarray], int],
+        np.ndarray,
+    ],
+    args: Namespace,
+    robot: SingleArmInterface,
+    past_data: dict[str, np.ndarray],
+    t: int,  # will be float eventually
+) -> control_loop_return:
+    """
+    Point2PointControlLoop
+    ---------------
+    generic control loop for point to point motion
+    (handling error to final point etc).
+    """
+    breakFlag = False
+    log_item = {}
+    save_past_item = {}
+
+    v_cmd = control_loop(SOLVER, T_w_goal, args, robot, past_data, t)
+    robot.sendVelocityCommand(v_cmd)
+
+    T_w_e = robot.T_w_e
+    SEerror = T_w_e.actInv(T_w_goal)
+    err_vector = log6(SEerror).vector
+    if np.linalg.norm(err_vector) < robot.args.goal_error:
+        breakFlag = True
+    log_item["qs"] = robot.q
+    log_item["err_norm"] = np.linalg.norm(err_vector).reshape((1,))
+    log_item["dqs"] = robot.v
+    log_item["dqs_cmd"] = v_cmd.reshape((robot.model.nv,))
+    return breakFlag, save_past_item, 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 bd1ec8913f3221895e32b6924751640e5138a172..e6500f3f409034b0ad17062e7dc30b2b2d1d1843 100644
--- a/python/smc/control/optimal_control/croco_mpc_path_following.py
+++ b/python/smc/control/optimal_control/croco_mpc_path_following.py
@@ -301,28 +301,30 @@ def BaseAndEEPathFollowingFromPlannerMPCControlLoop(
     log_item["err_norm_ee"] = np.linalg.norm(err_vector_ee).reshape((1,))
     log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,))
     log_item["qs"] = q.reshape((robot.model.nq,))
-    #log_item["dqs"] = robot.v.reshape((robot.model.nv,))
+    # log_item["dqs"] = robot.v.reshape((robot.model.nv,))
     return breakFlag, save_past_dict, log_item
 
 
-def initializePastData(args: Namespace, T_w_e: SE3, p_base: np.ndarray, max_base_v: float) -> np.ndarray:
+def initializePastData(
+    args: Namespace, T_w_e: SE3, p_base: np.ndarray, max_base_v: float
+) -> np.ndarray:
     # prepopulate past data to make base and cart be on the same path in the past
     # (which didn't actually happen because this just started)
     p_ee = T_w_e.translation[:2]
     straight_line_path = np.linspace(p_ee, p_base, args.past_window_size)
     straight_line_path_timed = path2D_timed(args, straight_line_path, max_base_v)
     # time it the same way the base path is timed
-#    time_past = np.linspace(
-#        0.0, args.past_window_size * (1 / args.ctrl_freq), args.past_window_size
-#    )
-#    s = np.linspace(0.0, args.n_knots * args.ocp_dt, args.past_window_size)
-#    path2D_handlebar = np.hstack(
-#        (
-#            np.interp(s, time_past, straight_line_path[:, 0]).reshape((-1, 1)),
-#            np.interp(s, time_past, straight_line_path[:, 1]).reshape((-1, 1)),
-#        )
-#    )
-#    return path2D_handlebar
+    #    time_past = np.linspace(
+    #        0.0, args.past_window_size * (1 / args.ctrl_freq), args.past_window_size
+    #    )
+    #    s = np.linspace(0.0, args.n_knots * args.ocp_dt, args.past_window_size)
+    #    path2D_handlebar = np.hstack(
+    #        (
+    #            np.interp(s, time_past, straight_line_path[:, 0]).reshape((-1, 1)),
+    #            np.interp(s, time_past, straight_line_path[:, 1]).reshape((-1, 1)),
+    #        )
+    #    )
+    #    return path2D_handlebar
     return straight_line_path
 
 
@@ -363,7 +365,7 @@ def BaseAndEEPathFollowingMPC(
         )
     log_item = {
         "qs": np.zeros(robot.model.nq),
-    #    "dqs": np.zeros(robot.model.nv),
+        #    "dqs": np.zeros(robot.model.nv),
         "err_vec_ee": np.zeros((6,)),
         "err_norm_ee": np.zeros((1,)),
         "err_norm_base": np.zeros((1,)),
diff --git a/python/smc/multiprocessing/networking/client.py b/python/smc/multiprocessing/networking/client.py
index 1d617d261e81013308a472da618554153eb9f620..74806870ac82cc3dd3c7744a1d2126ff18f5fcc4 100644
--- a/python/smc/multiprocessing/networking/client.py
+++ b/python/smc/multiprocessing/networking/client.py
@@ -44,7 +44,14 @@ def client_receiver(args: Namespace, init_command, shm_name: str, lock):
         msg_in_bytes = b""
         len_size_offset = 0
         while True:
-            next_pos, pos = _DecodeVarint32(buffer, pos)
+            try:
+                next_pos, pos = _DecodeVarint32(buffer, pos)
+            except IndexError:
+                print(
+                    "NETWORKING CLIENT_RECEIVER: got IndexError while decoding message length. \
+                        this only happens if the sender died. i'll exit as well"
+                )
+                return None, -1
             # TODO: either save the message chunk, or save how many initial bytes to ignore in the next message
             if pos + next_pos > buffer_len:
                 # print("NETWORKING CLIENT: BUFFER OVERFLOW, DROPPING MSG!")
@@ -81,6 +88,9 @@ def client_receiver(args: Namespace, init_command, shm_name: str, lock):
             msg_raw = s.recv(1024)
             buffer += msg_raw
             msg_in_bytes, pos = parse_message(buffer)
+            # -1 means an error happened
+            if pos == -1:
+                break
             buffer = buffer[pos:]
             dict_msg = encoder_decoder.serializedPb2MsgToDict(msg_in_bytes, msg_code)