diff --git a/python/ur_simple_control/__pycache__/managers.cpython-312.pyc b/python/ur_simple_control/__pycache__/managers.cpython-312.pyc
index b59fcce0499fab94884093e38ecf948933a2717a..eebd716670a2552ef6b2850384768b390ae8cf3d 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/basics/__pycache__/basics.cpython-312.pyc b/python/ur_simple_control/basics/__pycache__/basics.cpython-312.pyc
index e1ec0d0b99d4fb96948b9ab5c2c7409a97300de5..1faaaed581604ec4087a48d6b79a04fa2e648a92 100644
Binary files a/python/ur_simple_control/basics/__pycache__/basics.cpython-312.pyc and b/python/ur_simple_control/basics/__pycache__/basics.cpython-312.pyc differ
diff --git a/python/ur_simple_control/basics/basics.py b/python/ur_simple_control/basics/basics.py
index aeb95345ce0ab563d438da95c9d10e2bc1e0b472..c8f48d08b458601ee23f36b92d2cfcec641dd81a 100644
--- a/python/ur_simple_control/basics/basics.py
+++ b/python/ur_simple_control/basics/basics.py
@@ -120,14 +120,17 @@ def jointTrajFollowingPDControlLoop(stop_at_final : bool, robot: RobotManager, r
 
 
 
-    error_q = q_ref - q
+    if robot.robot_name == "ur5e":
+        error_q = q_ref - q
+    if robot.robot_name == "heron":
+        error_q = pin.difference(robot.model, q, q_ref) #/ robot.dt
     error_v = v_ref - v
     Kp = 1.0
     Kd = 0.5
 
     #          feedforward                      feedback 
     v_cmd = v_ref + Kp * error_q #+ Kd * error_v
-    qd_cmd = v_cmd[:6]
+    #qd_cmd = v_cmd[:6]
     robot.sendQd(v_cmd)
 
     log_item['error_qs'] = error_q
diff --git a/python/ur_simple_control/managers.py b/python/ur_simple_control/managers.py
index dfffff597f4062b3daf7d7f7ffaec21bea628a1c..a23fba31207a3e68da78bce780f81d1f34983734 100644
--- a/python/ur_simple_control/managers.py
+++ b/python/ur_simple_control/managers.py
@@ -1,12 +1,5 @@
 # TODO rename all private variables to start with '_'
-# TODO: neither visualization works without printing from manager, 
-#       make this make sense dude (but it does work with that, so this aint prio)
-# TODO: just read the q and update everything every timestep, don't deepcopy,
-# TODO: rewrite all getSomething functions to updateSomething functions,
-#       and then make the getSomething function just be return self.something.copy()
-# --> just create a RobotManager.step() function, update everything there
-# don't do forwardKinematics 2 extra times for no good reason. make that the libraries
-# responsibility, not the users
+# TODO: make importing nicer with __init__.py files
 import pinocchio as pin
 import numpy as np
 import time
@@ -17,12 +10,13 @@ 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
+from ur_simple_control.util.get_model import get_model, heron_approximation
 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
 import argparse
+from sys import exc_info
 
 """
 general notes
@@ -75,6 +69,9 @@ def getMinimalArgParser():
     #################################################
     #  general arguments: connection, plotting etc  #
     #################################################
+    parser.add_argument('--robot', type=str, \
+            help="which robot you're running or simulating", default="ur5e", \
+            choices=['ur5e', 'heron'])
     parser.add_argument('--simulation', action=argparse.BooleanOptionalAction, \
             help="whether you are running the UR simulator", default=False)
     parser.add_argument('--robot-ip', type=str, 
@@ -273,7 +270,8 @@ class ControlLoopManager:
                 # don't send what wasn't ready
                 if self.args.visualize_manipulator:
                     if self.robot_manager.manipulator_visualizer_queue.qsize() < 5:
-                        self.robot_manager.manipulator_visualizer_queue.put_nowait(self.robot_manager.q)
+                        self.robot_manager.manipulator_visualizer_queue.put_nowait({"q" : self.robot_manager.q,
+                                                                                    "T_w_e" : self.robot_manager.getT_w_e()})
 #                    if self.args.debug_prints:
 #                        print("manipulator_visualizer_queue size status:", self.robot_manager.manipulator_visualizer_queue.qsize())
                 if self.args.real_time_plotting:
@@ -334,7 +332,7 @@ class ControlLoopManager:
         """
         print('sending 300 speedjs full of zeros and exiting')
         for i in range(300):
-            vel_cmd = np.zeros(6)
+            vel_cmd = np.zeros(self.robot_manager.model.nv)
             #self.robot_manager.rtde_control.speedJ(vel_cmd, 0.1, 1.0 / 500)
             self.robot_manager.sendQd(vel_cmd)
         # hopefully this actually stops it
@@ -363,7 +361,7 @@ class ControlLoopManager:
         if self.args.visualize_manipulator:
             if self.args.debug_prints:
                 print("i am putting befree in manipulator to stop the manipulator visualizer")
-            self.robot_manager.manipulator_visualizer_queue.put_nowait("befree")
+            self.robot_manager.manipulator_visualizer_queue.put_nowait({"befree" : "befree"})
             #time.sleep(1)
             #self.robot_manager.manipulator_visualizer_process.join()
             self.robot_manager.manipulator_visualizer_process.terminate()
@@ -421,8 +419,14 @@ class RobotManager:
         self.simulation = args.simulation
         # load model
         # collision and visual models are none if args.visualize == False
-        self.model, self.collision_model, self.visual_model, self.data = \
-             get_model()
+        self.robot_name = args.robot
+        if self.robot_name == "ur5e":
+            self.model, self.collision_model, self.visual_model, self.data = \
+                 get_model()
+        if self.robot_name == "heron":
+            self.model, self.collision_model, self.visual_model, self.data = \
+                 heron_approximation()
+
         # start visualize manipulator process if selected.
         # has to be started here because it lives throughout the whole run
         if args.visualize_manipulator:
@@ -529,7 +533,7 @@ class RobotManager:
             q = np.array(q)
             self.q = q
             if args.visualize_manipulator:
-                self.manipulator_visualizer_queue.put(q)
+                self.manipulator_visualizer_queue.put({"q" : q})
 
 
         # do it once to get T_w_e
@@ -816,24 +820,31 @@ class RobotManager:
         """
         # we're hiding the extra 2 prismatic joint shenanigans from the control writer
         # because there you shouldn't need to know this anyway
-        qd_cmd = qd[:6]
-        # np.clip is ok with bounds being scalar, it does what it should
-        # (but you can also give it an array)
-        qd_cmd = np.clip(qd_cmd, -1 * self.max_qd, self.max_qd)
-        if not self.pinocchio_only:
-            # speedj(qd, scalar_lead_axis_acc, hangup_time_on_command)
-            self.rtde_control.speedJ(qd_cmd, self.acceleration, self.dt)
-        else:
-            # this one takes all 8 elements of qd since we're still in pinocchio
-            # this is ugly, todo: fix
-            qd = qd[:6]
-            qd = qd_cmd.reshape((6,))
-            qd = list(qd)
-            qd.append(0.0)
-            qd.append(0.0)
-            qd = np.array(qd)
-            self.v_q = qd
-            self.q = pin.integrate(self.model, self.q, qd * self.dt)
+        if self.robot_name == "ur5e":
+            qd_cmd = qd[:6]
+            # np.clip is ok with bounds being scalar, it does what it should
+            # (but you can also give it an array)
+            qd_cmd = np.clip(qd_cmd, -1 * self.max_qd, self.max_qd)
+            if not self.pinocchio_only:
+                # speedj(qd, scalar_lead_axis_acc, hangup_time_on_command)
+                self.rtde_control.speedJ(qd_cmd, self.acceleration, self.dt)
+            else:
+                # this one takes all 8 elements of qd since we're still in pinocchio
+                # this is ugly, todo: fix
+                qd = qd[:6]
+                qd = qd_cmd.reshape((6,))
+                qd = list(qd)
+                qd.append(0.0)
+                qd.append(0.0)
+                qd = np.array(qd)
+                self.v_q = qd
+                self.q = pin.integrate(self.model, self.q, qd * self.dt)
+
+        # TODO: implement real thing
+        if self.robot_name == "heron":
+                self.v_q = qd
+                self.q = pin.integrate(self.model, self.q, qd * self.dt)
+
 
     def openGripper(self):
         if self.gripper is None:
@@ -901,7 +912,7 @@ class RobotManager:
                 for i in range(len(goal_list)):
                    goal_list[i] = float(goal_list[i])
             except:
-                e = sys.exc_info()
+                e = exc_info()
                 print("The input is not in the expected format. Try again.")
                 print(e)
             if e == "ok":
@@ -912,6 +923,10 @@ class RobotManager:
         # NOTE i'm not deepcopying this on purpose
         # but that might be the preferred thing, we'll see
         self.Mgoal = Mgoal
+        if self.args.visualize_manipulator:
+            # TODO document this somewhere
+            self.manipulator_visualizer_queue.put(
+                    {"Mgoal" : Mgoal})
         return Mgoal
 
     def killManipulatorVisualizer(self):
@@ -929,7 +944,7 @@ class RobotManager:
         if self.args.debug_prints:
             print("i am putting befree in plotter_queue to stop the manipulator visualizer")
         # putting this command tells our process to kill the meshcat zmq server process
-        self.manipulator_visualizer_queue.put_nowait("befree")
+        self.manipulator_visualizer_queue.put_nowait({"befree" : "befree"})
         time.sleep(0.1)
         self.manipulator_visualizer_process.terminate()
         if self.args.debug_prints:
diff --git a/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py b/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py
index 152e0f9218da9b6edb1464dd7027143c9dad7415..8faf38ad94f7a9e28f5c3a89ff729de346cd8c1d 100644
--- a/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py
+++ b/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py
@@ -20,8 +20,6 @@ def get_OCP_args(parser : argparse.ArgumentParser):
     return parser
                         
 
-# TODO: use fddp and incorporate torque (i.e. velocity constraints)
-#   --> those will correspond to max_qd and acceleration attributes in robotmanager 
 def CrocoIKOCP(args, robot : RobotManager, goal : pin.SE3):
     # create torque bounds which correspond to percentage
     # of maximum allowed acceleration 
@@ -87,9 +85,14 @@ def CrocoIKOCP(args, robot : RobotManager, goal : pin.SE3):
     bounds = crocoddyl.ActivationBounds(xlb, xub, 1.0)
     xLimitResidual = crocoddyl.ResidualModelState(state, x0, actuation.nu)
     xLimitActivation = crocoddyl.ActivationModelQuadraticBarrier(bounds)
-    limitCost = crocoddyl.CostModelResidual(state, xLimitActivation, xLimitResidual)
-    runningCostModel.addCost("limitCost", limitCost, 1e3)
-    terminalCostModel.addCost("limitCost", limitCost, 1e3)
+
+    # TOOODOOO ADD THE SAME FOR WHOLE HERON
+    if robot.robot_name == "ur5e":
+        limitCost = crocoddyl.CostModelResidual(state, xLimitActivation, xLimitResidual)
+        runningCostModel.addCost("limitCost", limitCost, 1e3)
+        terminalCostModel.addCost("limitCost", limitCost, 1e3)
+
+
     # NOTE: i have no idea how to incorporate this into anything
     # we need to add state constraints for things to make sense
     # NOTE!!!: there are no hard inequality constraints on states
@@ -158,6 +161,9 @@ if __name__ == "__main__":
     robot.max_qd = 3.14 
     print("velocity limits", robot.model.velocityLimit)
     robot.q = pin.randomConfiguration(robot.model)
+    robot.q[0] = 0.1
+    robot.q[1] = 0.1
+    print(robot.q)
     goal = pin.SE3.Random()
     goal.translation = np.random.random(3) * 0.4
     reference, solver = CrocoIKOCP(args, robot, goal)
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 4f3cd82c660184ddd923df726a18a1539e1dba3b..7fb311856ef9a0ccfa2de4ba05ba86e1b0356785 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 2d624babbace54bee57892bd25692f37c833fd5f..a5737ee1084517f9f6c1a1550e1d6d8680df75c6 100644
--- a/python/ur_simple_control/util/get_model.py
+++ b/python/ur_simple_control/util/get_model.py
@@ -9,6 +9,7 @@ import numpy as np
 import sys
 import os
 from importlib.resources import files
+import hppfcl as fcl
 
 # can't get the urdf reading with these functions to save my life, idk what or why
 
@@ -83,3 +84,80 @@ def get_model():
 
     return model, collision_model, visual_model, data
 
+# this gives me a flying joint for the camera,
+# and a million joints for wheels -> it's unusable
+# TODO: look what's done in pink, see if this can be usable
+# after you've removed camera joint and similar.
+def get_heron_model():
+    
+    #urdf_path_relative = files('ur_simple_control.robot_descriptions.urdf').joinpath('ur5e_with_robotiq_hande_FIXED_PATHS.urdf')
+    urdf_path_absolute = "/home/gospodar/home2/gospodar/lund/praxis/software/ros/ros-containers/home/model.urdf"
+    #mesh_dir = files('ur_simple_control')
+    #mesh_dir_absolute = os.path.abspath(mesh_dir)
+    mesh_dir_absolute = "/home/gospodar/lund/praxis/software/ros/ros-containers/home/heron_description/MIR_robot"
+
+    model = None
+    collision_model = None
+    visual_model = None
+    # this command just calls the ones below it. both are kept here
+    # in case pinocchio people decide to change their api.
+    #model, collision_model, visual_model = pin.buildModelsFromUrdf(urdf_path_absolute, mesh_dir_absolute)
+    model = pin.buildModelFromUrdf(urdf_path_absolute)
+    visual_model = pin.buildGeomFromUrdf(model, urdf_path_absolute, pin.GeometryType.VISUAL, None, mesh_dir_absolute)
+    collision_model = pin.buildGeomFromUrdf(model, urdf_path_absolute, pin.GeometryType.COLLISION, None, mesh_dir_absolute)
+
+    data = pin.Data(model)
+
+    return model, collision_model, visual_model, data
+
+def heron_approximation():
+    # 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()
+    joint_name = "mobile_base_planar_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_JOINT_ID = model_mobile_base.addJoint(parent_id, pin.JointModelPlanar(), 
+            joint_placement.copy(), joint_name)
+    #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_JOINT_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_JOINT_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_JOINT_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
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 eaf4ae636a0f3e0494bc8ae3166c0c46fe1bf886..7039720b6b1d9858d08e7c3911c57c5324a85ff2 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 8c787350b82b417d6e13112fcd3f0d18e666fea6..0215e599f385bc8268641ecaa48baa462c7760d8 100644
--- a/python/ur_simple_control/visualize/visualize.py
+++ b/python/ur_simple_control/visualize/visualize.py
@@ -4,11 +4,12 @@ from collections import deque, namedtuple
 import time
 import copy
 from pinocchio.visualize import MeshcatVisualizer
+import meshcat_shapes
 
-# tkinter stuff
-from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
-from tkinter import *
-from tkinter.ttk import *
+# tkinter stuff for later reference
+#from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
+#from tkinter import *
+#from tkinter.ttk import *
 
 # rows and cols are flipped lel
 def getNRowsMColumnsFromTotalNumber(n_plots):
@@ -162,6 +163,10 @@ def manipulatorVisualizer(args, model, collision_model, visual_model, queue):
     # for whatever reason the hand-e files don't have/
     # meshcat can't read scaling information.
     # so we scale manually
+    init_target_frame = False
+    init_ee_frame = False
+    # if we go for manipulator end-effector
+    #meshcat_shapes.frame(viewer["end_effector_target"], opacity=0.5)
     for geom in visual_model.geometryObjects:
         if "hand" in geom.name:
             s = geom.meshScale
@@ -180,16 +185,31 @@ def manipulatorVisualizer(args, model, collision_model, visual_model, queue):
     print("MANIPULATORVISUALIZER: FULLY ONLINE")
     try:
         while True:
-            q = queue.get()
-            if type(q) == str:
-                print("got str q")
-                if q == "befree":
+            cmd = queue.get()
+            for key in cmd:
+                if key == "befree":
                     if args.debug_prints:
                         print("MANIPULATORVISUALIZER: got befree, manipulatorVisualizer out")
                     viz.viewer.window.server_proc.kill()
                     viz.viewer.window.server_proc.wait()
                     break
-            viz.display(q)
+                if key == "Mgoal":
+                    # having this via flag is stupid, but i can't
+                    # be bothered with something else rn
+                    if init_target_frame == False:
+                        meshcat_shapes.frame(viz.viewer["Mgoal"], opacity=0.5)
+                        init_target_frame = True
+                    viz.viewer["Mgoal"].set_transform(cmd["Mgoal"].homogeneous)
+                if key == "T_w_e":
+                    # having this via flag is stupid, but i can't
+                    # be bothered with something else rn
+                    if init_ee_frame == False:
+                        meshcat_shapes.frame(viz.viewer["T_w_e"], opacity=0.5)
+                        init_ee_frame = True
+                    viz.viewer["T_w_e"].set_transform(cmd["T_w_e"].homogeneous)
+                if key == "q":
+                    viz.display(cmd["q"])
+
     except KeyboardInterrupt:
         if args.debug_prints:
             print("MANIPULATORVISUALIZER: caught KeyboardInterrupt, i'm out")