From 2d7c2bc35ff444d57256eecf84b9c9e45c18ab4e Mon Sep 17 00:00:00 2001
From: gospar
Date: Tue, 14 Oct 2025 15:22:59 +0200
Subject: [PATCH 01/15] robust invese kinematics
---
.../kinematics/analytical_kinematics.py | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/src/reachy_mini/kinematics/analytical_kinematics.py b/src/reachy_mini/kinematics/analytical_kinematics.py
index 0691b7d8..2422f2cd 100644
--- a/src/reachy_mini/kinematics/analytical_kinematics.py
+++ b/src/reachy_mini/kinematics/analytical_kinematics.py
@@ -67,10 +67,17 @@ def ik(
"""
_pose = pose.copy()
_pose[:3, 3][2] += self.head_z_offset
-
- stewart_joints = self.kin.inverse_kinematics(_pose, body_yaw) # type: ignore[arg-type]
-
- return np.array([body_yaw] + stewart_joints)
+
+ # inverse kinematics solution that modulates the body yaw to
+ # stay within the mechanical limits (max_body_yaw)
+ # additionally it makes sure the the relative yaw between the body and the head
+ # stays within the mechanical limits (max_relative_yaw)
+ reachy_joints = self.kin.inverse_kinematics_safe(_pose,
+ body_yaw = body_yaw,
+ max_relative_yaw = np.deg2rad(65),
+ max_body_yaw = np.deg2rad(160)) # type: ignore[arg-type]
+
+ return np.array(reachy_joints) # type: ignore[arg-type]
def fk(
self,
From f8229803f7a7aca7237ff965ead0152331453b45 Mon Sep 17 00:00:00 2001
From: gospar
Date: Thu, 16 Oct 2025 09:08:52 +0200
Subject: [PATCH 02/15] remove elapsed time dumping
---
src/reachy_mini/kinematics/analytical_kinematics.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/reachy_mini/kinematics/analytical_kinematics.py b/src/reachy_mini/kinematics/analytical_kinematics.py
index 2422f2cd..62cd5cb3 100644
--- a/src/reachy_mini/kinematics/analytical_kinematics.py
+++ b/src/reachy_mini/kinematics/analytical_kinematics.py
@@ -65,6 +65,8 @@ def ik(
check_collision and no_iterations are not used by AnalyticalKinematics. We keep them for compatibility with the other kinematics engines
"""
+ # import time
+ # k = time.time()
_pose = pose.copy()
_pose[:3, 3][2] += self.head_z_offset
@@ -76,7 +78,7 @@ def ik(
body_yaw = body_yaw,
max_relative_yaw = np.deg2rad(65),
max_body_yaw = np.deg2rad(160)) # type: ignore[arg-type]
-
+ # print("IK time {0:.3f}us".format((time.time() - k)*1e6))
return np.array(reachy_joints) # type: ignore[arg-type]
def fk(
From 47a108616fd807528834e10f65edd8802e9c8b2f Mon Sep 17 00:00:00 2001
From: gospar
Date: Wed, 22 Oct 2025 10:15:10 +0200
Subject: [PATCH 03/15] bump the version of the rust_kinematics
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index a66a0efa..48853f59 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,7 +24,7 @@ dependencies = [
"huggingface-hub==0.34.4",
"sounddevice==0.5.1",
"soundfile==0.13.1",
- "reachy-mini-rust-kinematics>=1.0.1",
+ "reachy-mini-rust-kinematics>=1.0.2",
"asgiref",
"aiohttp",
]
From 3f7209829ce6bd0cc1131853f2eadfbe725eb8fb Mon Sep 17 00:00:00 2001
From: gospar
Date: Wed, 22 Oct 2025 10:45:22 +0200
Subject: [PATCH 04/15] ci launch
---
src/reachy_mini/kinematics/analytical_kinematics.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/reachy_mini/kinematics/analytical_kinematics.py b/src/reachy_mini/kinematics/analytical_kinematics.py
index 62cd5cb3..65ec2b5c 100644
--- a/src/reachy_mini/kinematics/analytical_kinematics.py
+++ b/src/reachy_mini/kinematics/analytical_kinematics.py
@@ -97,7 +97,7 @@ def fk(
if no_iterations < 1:
raise ValueError("no_iterations must be at least 1")
-
+
T_world_platform = None
for _ in range(no_iterations):
T_world_platform = np.array(
From 723652d44b756d279624b41266120d06104435c6 Mon Sep 17 00:00:00 2001
From: gospar
Date: Wed, 22 Oct 2025 10:53:20 +0200
Subject: [PATCH 05/15] update for mypy
---
.../kinematics/analytical_kinematics.py | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/src/reachy_mini/kinematics/analytical_kinematics.py b/src/reachy_mini/kinematics/analytical_kinematics.py
index 65ec2b5c..5ea41b9f 100644
--- a/src/reachy_mini/kinematics/analytical_kinematics.py
+++ b/src/reachy_mini/kinematics/analytical_kinematics.py
@@ -46,13 +46,13 @@ def __init__(self) -> None:
for motor in self.motors:
self.kin.add_branch(
motor["branch_position"],
- np.linalg.inv(motor["T_motor_world"]), # type: ignore[arg-type]
+ np.linalg.inv(motor["T_motor_world"]),
1 if motor["solution"] else -1,
)
sleep_head_pose = SLEEP_HEAD_POSE.copy()
sleep_head_pose[:3, 3][2] += self.head_z_offset
- self.kin.reset_forward_kinematics(sleep_head_pose) # type: ignore[arg-type]
+ self.kin.reset_forward_kinematics(sleep_head_pose)
def ik(
self,
@@ -65,8 +65,6 @@ def ik(
check_collision and no_iterations are not used by AnalyticalKinematics. We keep them for compatibility with the other kinematics engines
"""
- # import time
- # k = time.time()
_pose = pose.copy()
_pose[:3, 3][2] += self.head_z_offset
@@ -77,9 +75,9 @@ def ik(
reachy_joints = self.kin.inverse_kinematics_safe(_pose,
body_yaw = body_yaw,
max_relative_yaw = np.deg2rad(65),
- max_body_yaw = np.deg2rad(160)) # type: ignore[arg-type]
- # print("IK time {0:.3f}us".format((time.time() - k)*1e6))
- return np.array(reachy_joints) # type: ignore[arg-type]
+ max_body_yaw = np.deg2rad(160))
+
+ return np.array(reachy_joints)
def fk(
self,
@@ -97,7 +95,7 @@ def fk(
if no_iterations < 1:
raise ValueError("no_iterations must be at least 1")
-
+
T_world_platform = None
for _ in range(no_iterations):
T_world_platform = np.array(
From 617f8cdab17c239cde0c8667fbc821d2d30662f3 Mon Sep 17 00:00:00 2001
From: gospar
Date: Wed, 22 Oct 2025 10:57:34 +0200
Subject: [PATCH 06/15] ignore arg type
---
src/reachy_mini/kinematics/analytical_kinematics.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/reachy_mini/kinematics/analytical_kinematics.py b/src/reachy_mini/kinematics/analytical_kinematics.py
index 5ea41b9f..43c4fcde 100644
--- a/src/reachy_mini/kinematics/analytical_kinematics.py
+++ b/src/reachy_mini/kinematics/analytical_kinematics.py
@@ -46,13 +46,13 @@ def __init__(self) -> None:
for motor in self.motors:
self.kin.add_branch(
motor["branch_position"],
- np.linalg.inv(motor["T_motor_world"]),
+ np.linalg.inv(motor["T_motor_world"]), # type: ignore[arg-type]
1 if motor["solution"] else -1,
)
sleep_head_pose = SLEEP_HEAD_POSE.copy()
sleep_head_pose[:3, 3][2] += self.head_z_offset
- self.kin.reset_forward_kinematics(sleep_head_pose)
+ self.kin.reset_forward_kinematics(sleep_head_pose) # type: ignore[arg-type]
def ik(
self,
@@ -72,7 +72,7 @@ def ik(
# stay within the mechanical limits (max_body_yaw)
# additionally it makes sure the the relative yaw between the body and the head
# stays within the mechanical limits (max_relative_yaw)
- reachy_joints = self.kin.inverse_kinematics_safe(_pose,
+ reachy_joints = self.kin.inverse_kinematics_safe(_pose, # type: ignore[arg-type]
body_yaw = body_yaw,
max_relative_yaw = np.deg2rad(65),
max_body_yaw = np.deg2rad(160))
From 4ef630a97f15234006c121cd48b20da4c5fdd497 Mon Sep 17 00:00:00 2001
From: gospar
Date: Thu, 23 Oct 2025 19:01:27 +0200
Subject: [PATCH 07/15] inital support for collision!
---
.../daemon/backend/mujoco/backend copy.py | 312 ++++++++++++
.../daemon/backend/mujoco/backend.py | 22 +
.../reachy_mini/mjcf/reachy_mini copy 2.xml | 450 ++++++++++++++++++
.../reachy_mini/mjcf/reachy_mini copy.xml | 448 +++++++++++++++++
.../reachy_mini/mjcf/reachy_mini.xml | 12 +-
.../reachy_mini/mjcf/reachy_mini.xml.old | 440 +++++++++++++++++
6 files changed, 1683 insertions(+), 1 deletion(-)
create mode 100644 src/reachy_mini/daemon/backend/mujoco/backend copy.py
create mode 100644 src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy 2.xml
create mode 100644 src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy.xml
create mode 100644 src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml.old
diff --git a/src/reachy_mini/daemon/backend/mujoco/backend copy.py b/src/reachy_mini/daemon/backend/mujoco/backend copy.py
new file mode 100644
index 00000000..881ce03b
--- /dev/null
+++ b/src/reachy_mini/daemon/backend/mujoco/backend copy.py
@@ -0,0 +1,312 @@
+"""Mujoco Backend for Reachy Mini.
+
+This module provides the MujocoBackend class for simulating the Reachy Mini robot using the MuJoCo physics engine.
+
+It includes methods for running the simulation, getting joint positions, and controlling the robot's joints.
+
+"""
+
+import json
+import time
+from dataclasses import dataclass
+from importlib.resources import files
+from threading import Thread
+from typing import Optional
+
+import mujoco
+import mujoco.viewer
+import numpy as np
+
+import reachy_mini
+from reachy_mini.daemon.backend.abstract import Backend
+from reachy_mini.daemon.backend.mujoco.utils import (
+ get_actuator_names,
+ get_joint_addr_from_name,
+ get_joint_id_from_name,
+)
+from reachy_mini.daemon.backend.mujoco.video_udp import UDPJPEGFrameSender
+
+
+class MujocoBackend(Backend):
+ """Simulated Reachy Mini using MuJoCo."""
+
+ def __init__(self, scene="empty", check_collision: bool = False):
+ """Initialize the MujocoBackend with a specified scene.
+
+ Args:
+ scene (str): The name of the scene to load. Default is "empty".
+ check_collision (bool): If True, enable collision checking. Default is False.
+
+ """
+ super().__init__(check_collision=check_collision)
+
+ from reachy_mini.reachy_mini import (
+ SLEEP_ANTENNAS_JOINT_POSITIONS,
+ SLEEP_HEAD_JOINT_POSITIONS,
+ )
+
+ self._SLEEP_ANTENNAS_JOINT_POSITIONS = SLEEP_ANTENNAS_JOINT_POSITIONS
+ self._SLEEP_HEAD_JOINT_POSITIONS = SLEEP_HEAD_JOINT_POSITIONS
+
+ mjcf_root_path = str(
+ files(reachy_mini).joinpath("descriptions/reachy_mini/mjcf/")
+ )
+ self.model = mujoco.MjModel.from_xml_path( # type: ignore
+ f"{mjcf_root_path}/scenes/{scene}.xml"
+ )
+ self.data = mujoco.MjData(self.model) # type: ignore
+ self.model.opt.timestep = 0.002 # s, simulation timestep, 500hz
+ self.decimation = 10 # -> 50hz control loop
+ self.rendering_timestep = 0.04 # s, rendering loop # 25Hz
+
+ self.camera_id = mujoco.mj_name2id( # type: ignore
+ self.model,
+ mujoco.mjtObj.mjOBJ_CAMERA, # type: ignore
+ "eye_camera",
+ )
+
+ self.head_id = mujoco.mj_name2id( # type: ignore
+ self.model,
+ mujoco.mjtObj.mjOBJ_BODY, # type: ignore
+ "pp01063_stewart_plateform",
+ )
+
+ self.platform_to_head_transform = np.array(
+ [
+ [8.66025292e-01, 5.00000194e-01, -1.83660327e-06, -1.34282000e-02],
+ [5.55111512e-16, -3.67320510e-06, -1.00000000e00, -1.20000000e-03],
+ [-5.00000194e-01, 8.66025292e-01, -3.18108852e-06, 3.65883000e-02],
+ [0, 0, 0, 1.00000000e00],
+ ]
+ )
+ # remove_z_offset = np.eye(4)
+ # remove_z_offset[2, 3] = -0.177
+ # self.platform_to_head_transform = self.platform_to_head_transform @ remove_z_offset
+
+ self.current_head_pose = np.eye(4)
+
+ # print("Joints in the model:")
+ # for i in range(self.model.njoint):
+ # name = mujoco.mj_id2joint(self.model, i)
+ # print(f" {i}: {name}")
+
+ self.joint_names = get_actuator_names(self.model)
+
+ self.joint_ids = [
+ get_joint_id_from_name(self.model, n) for n in self.joint_names
+ ]
+ self.joint_qpos_addr = [
+ get_joint_addr_from_name(self.model, n) for n in self.joint_names
+ ]
+
+ # disable the collisions by default
+ self.model.geom_contype[13] = 0 # head capsule
+ self.model.geom_conaffinity[13] = 0
+ self.model.geom_contype[80] = 0 # top of the torso
+ self.model.geom_conaffinity[80] = 0
+
+ def rendering_loop(self):
+ """Offline Rendering loop for the Mujoco simulation.
+
+ Capture the image from the virtual Reachy's camera and send it over UDP.
+ """
+ streamer_udp = UDPJPEGFrameSender()
+ camera_size = (1280, 720)
+ offscreen_renderer = mujoco.Renderer(
+ self.model, height=camera_size[1], width=camera_size[0]
+ )
+ while not self.should_stop.is_set():
+ start_t = time.time()
+ offscreen_renderer.update_scene(self.data, self.camera_id)
+ im = offscreen_renderer.render()
+ streamer_udp.send_frame(im)
+
+ took = time.time() - start_t
+ time.sleep(max(0, self.rendering_timestep - took))
+
+
+
+ def run(self):
+ """Run the Mujoco simulation with a viewer.
+
+ This method initializes the viewer and enters the main simulation loop.
+ It updates the joint positions at a rate and publishes the joint positions.
+ """
+ step = 1
+ with mujoco.viewer.launch_passive(
+ self.model, self.data, show_left_ui=False, show_right_ui=False
+ ) as viewer:
+ with viewer.lock():
+ viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FREE # type: ignore
+ viewer.cam.distance = 0.8 # ≃ ||pos - lookat||
+ viewer.cam.azimuth = 160 # degrees
+ viewer.cam.elevation = -20 # degrees
+ viewer.cam.lookat[:] = [0, 0, 0.15]
+
+ # force one render with your new camera
+ mujoco.mj_step(self.model, self.data) # type: ignore
+ viewer.sync()
+
+ # im = self.get_camera()
+ # self.streamer_udp.send_frame(im)
+ with viewer.lock():
+ self.data.qpos[self.joint_qpos_addr] = np.array(
+ self._SLEEP_HEAD_JOINT_POSITIONS
+ + self._SLEEP_ANTENNAS_JOINT_POSITIONS
+ ).reshape(-1, 1)
+ self.data.ctrl[:] = np.array(
+ self._SLEEP_HEAD_JOINT_POSITIONS
+ + self._SLEEP_ANTENNAS_JOINT_POSITIONS
+ )
+
+ # recompute all kinematics, collisions, etc.
+ mujoco.mj_forward(self.model, self.data) # type: ignore
+
+ for i in range(20):
+ # one more frame so the viewer shows your startup pose
+ mujoco.mj_step(self.model, self.data) # type: ignore
+ viewer.sync()
+
+ rendering_thread = Thread(target=self.rendering_loop, daemon=True)
+ rendering_thread.start()
+
+
+ #if self.check_collision:
+ # enable collisions
+ self.model.geom_contype[13] = 1 # head capsule
+ self.model.geom_conaffinity[13] = 1
+ self.model.geom_contype[80] = 1 # top of the torso
+ self.model.geom_conaffinity[80] = 1
+
+ mujoco.mj_step(self.model, self.data)
+
+
+
+ # 3) now enter your normal loop
+ while not self.should_stop.is_set():
+ start_t = time.time()
+
+ if step % self.decimation == 0:
+ # update the current states
+ self.current_head_joint_positions = (
+ self.get_present_head_joint_positions()
+ )
+ self.current_antenna_joint_positions = (
+ self.get_present_antenna_joint_positions()
+ )
+ self.current_head_pose = self.get_mj_present_head_pose()
+
+ # Update the target head joint positions from IK if necessary
+ # - does nothing if the targets did not change
+ if self.ik_required:
+ self.update_target_head_joints_from_ik(
+ self.target_head_pose, self.target_body_yaw
+ )
+
+ if self.target_head_joint_positions is not None:
+ self.data.ctrl[:7] = self.target_head_joint_positions
+ if self.target_antenna_joint_positions is not None:
+ self.data.ctrl[-2:] = self.target_antenna_joint_positions
+
+ if (
+ self.joint_positions_publisher is not None
+ and self.pose_publisher is not None
+ ):
+ self.joint_positions_publisher.put(
+ json.dumps(
+ {
+ "head_joint_positions": self.current_head_joint_positions,
+ "antennas_joint_positions": self.current_antenna_joint_positions,
+ }
+ ).encode("utf-8")
+ )
+ self.pose_publisher.put(
+ json.dumps(
+ {
+ "head_pose": self.get_present_head_pose().tolist(),
+ }
+ ).encode("utf-8")
+ )
+ viewer.sync()
+
+ mujoco.mj_step(self.model, self.data) # type: ignore
+
+ took = time.time() - start_t
+ time.sleep(max(0, self.model.opt.timestep - took))
+ # print(f"Step {step}: took {took*1000:.1f}ms")
+ step += 1
+ self.ready.set()
+
+ def get_mj_present_head_pose(self) -> np.ndarray:
+ """Get the current head pose from the Mujoco simulation.
+
+ Returns:
+ np.ndarray: The current head pose as a 4x4 transformation matrix.
+
+ """
+ mj_current_head_pose = np.eye(4)
+ mj_current_head_pose[:3, :3] = self.data.xmat[self.head_id].reshape(3, 3)
+ mj_current_head_pose[:3, 3] = self.data.xpos[self.head_id]
+ mj_current_head_pose = mj_current_head_pose @ self.platform_to_head_transform
+ mj_current_head_pose[2, 3] -= 0.177
+ return mj_current_head_pose
+
+ def close(self) -> None:
+ """Close the Mujoco backend."""
+ # TODO Do something in mujoco here ?
+ pass
+
+ def get_status(self) -> "MujocoBackendStatus":
+ """Get the status of the Mujoco backend.
+
+ Returns:
+ dict: An empty dictionary as the Mujoco backend does not have a specific status to report.
+
+ """
+ return MujocoBackendStatus()
+
+ def get_present_head_joint_positions(self):
+ """Get the current joint positions of the head."""
+ return self.data.qpos[self.joint_qpos_addr[:7]].flatten().tolist()
+
+ def get_present_antenna_joint_positions(self):
+ """Get the current joint positions of the antennas."""
+ return self.data.qpos[self.joint_qpos_addr[-2:]].flatten().tolist()
+
+ def enable_motors(self) -> None:
+ """Enable the motors.
+
+ Does nothing in the Mujoco backend as it does not have a concept of enabling/disabling motors.
+ """
+ pass
+
+ def disable_motors(self) -> None:
+ """Disable the motors.
+
+ Does nothing in the Mujoco backend as it does not have a concept of enabling/disabling motors.
+ """
+ pass
+
+ def set_head_operation_mode(self, mode: int) -> None:
+ """Set mode of operation for the head.
+
+ This does nothing in the Mujoco backend as it does not have a concept of operation modes.
+ """
+ pass
+
+ def set_antennas_operation_mode(self, mode: int) -> None:
+ """Set mode of operation for the antennas.
+
+ This does nothing in the Mujoco backend as it does not have a concept of operation modes.
+ """
+ pass
+
+
+@dataclass
+class MujocoBackendStatus:
+ """Dataclass to represent the status of the Mujoco backend.
+
+ Empty for now, as the Mujoco backend does not have a specific status to report.
+ """
+
+ error: Optional[str] = None
diff --git a/src/reachy_mini/daemon/backend/mujoco/backend.py b/src/reachy_mini/daemon/backend/mujoco/backend.py
index aba69eaa..1b855aa2 100644
--- a/src/reachy_mini/daemon/backend/mujoco/backend.py
+++ b/src/reachy_mini/daemon/backend/mujoco/backend.py
@@ -105,6 +105,20 @@ def __init__(
self.joint_qpos_addr = [
get_joint_addr_from_name(self.model, n) for n in self.joint_names
]
+
+ self.col_inds = []
+ for i,type in enumerate(self.model.geom_contype):
+ if type != 0:
+ print(f"Geom {i} : contype={self.model.geom_contype[i]}, conaffinity={self.model.geom_conaffinity[i]}")
+ self.col_inds.append(i)
+ self.model.geom_contype[i] = 0
+ self.model.geom_conaffinity[i] = 0
+
+ # # disable the collisions by default
+ # self.model.geom_contype[13] = 0 # head capsule
+ # self.model.geom_conaffinity[13] = 0
+ # self.model.geom_contype[80] = 0 # top of the torso
+ # self.model.geom_conaffinity[80] = 0
def rendering_loop(self) -> None:
"""Offline Rendering loop for the Mujoco simulation.
@@ -162,6 +176,14 @@ def run(self) -> None:
# recompute all kinematics, collisions, etc.
mujoco.mj_forward(self.model, self.data)
+ for i in range(100):
+ mujoco.mj_step(self.model, self.data)
+
+ # enable collisions
+ for i in self.col_inds:
+ self.model.geom_contype[i] = 1
+ self.model.geom_conaffinity[i] = 1
+
# one more frame so the viewer shows your startup pose
mujoco.mj_step(self.model, self.data)
if not self.headless:
diff --git a/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy 2.xml b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy 2.xml
new file mode 100644
index 00000000..8f8ccd2c
--- /dev/null
+++ b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy 2.xml
@@ -0,0 +1,450 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy.xml b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy.xml
new file mode 100644
index 00000000..a97dbc09
--- /dev/null
+++ b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy.xml
@@ -0,0 +1,448 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml
index bf5dafa5..83ee966a 100644
--- a/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml
+++ b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml
@@ -11,7 +11,7 @@
-
+
@@ -75,6 +75,11 @@
+
+
+
+
+
@@ -471,9 +476,14 @@
resolution="1280 720"
fovy="80"
/>
+
+
diff --git a/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml.old b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml.old
new file mode 100644
index 00000000..529ebb57
--- /dev/null
+++ b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml.old
@@ -0,0 +1,440 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From c20106309637c7ded9dc1e0c25304630031482ab Mon Sep 17 00:00:00 2001
From: gospar
Date: Fri, 24 Oct 2025 10:26:25 +0200
Subject: [PATCH 08/15] relatively good for now
---
examples/gui_demos/mini_head_lookat_gui.py | 100 ++++
.../{ => gui_demos}/mini_head_position_gui.py | 5 -
.../daemon/backend/mujoco/backend copy.py | 312 ------------
.../daemon/backend/mujoco/backend.py | 16 +-
.../reachy_mini/mjcf/joints_properties.xml | 4 +-
.../reachy_mini/mjcf/reachy_mini copy 2.xml | 450 ------------------
.../reachy_mini/mjcf/reachy_mini copy.xml | 448 -----------------
.../reachy_mini/mjcf/reachy_mini.xml | 19 +-
.../reachy_mini/mjcf/reachy_mini.xml.old | 440 -----------------
.../kinematics/placo_kinematics.py | 10 +-
10 files changed, 124 insertions(+), 1680 deletions(-)
create mode 100644 examples/gui_demos/mini_head_lookat_gui.py
rename examples/{ => gui_demos}/mini_head_position_gui.py (93%)
delete mode 100644 src/reachy_mini/daemon/backend/mujoco/backend copy.py
delete mode 100644 src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy 2.xml
delete mode 100644 src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy.xml
delete mode 100644 src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml.old
diff --git a/examples/gui_demos/mini_head_lookat_gui.py b/examples/gui_demos/mini_head_lookat_gui.py
new file mode 100644
index 00000000..035c5148
--- /dev/null
+++ b/examples/gui_demos/mini_head_lookat_gui.py
@@ -0,0 +1,100 @@
+"""Reachy Mini Head Position GUI Example."""
+
+import time
+import tkinter as tk
+
+import numpy as np
+from scipy.spatial.transform import Rotation as R
+
+from reachy_mini import ReachyMini
+from reachy_mini.utils import create_head_pose
+
+def main():
+ """Run a GUI to set the head position and orientation of Reachy Mini."""
+ with ReachyMini(media_backend="no_media") as mini:
+ t0 = time.time()
+
+ root = tk.Tk()
+ root.title("Set Look At XYZ Position")
+
+ # Add sliders for X, Y, Z position
+ x_var = tk.DoubleVar(value=0.0)
+ y_var = tk.DoubleVar(value=0.0)
+ z_var = tk.DoubleVar(value=0.0)
+
+ tk.Label(root, text="X (m):").grid(row=0, column=0)
+ tk.Scale(
+ root,
+ variable=x_var,
+ from_=-0.2,
+ to=0.2,
+ resolution=0.001,
+ orient=tk.HORIZONTAL,
+ length=200,
+ ).grid(row=0, column=1)
+ tk.Label(root, text="Y (m):").grid(row=1, column=0)
+ tk.Scale(
+ root,
+ variable=y_var,
+ from_=-0.2,
+ to=0.2,
+ resolution=0.001,
+ orient=tk.HORIZONTAL,
+ length=200,
+ ).grid(row=1, column=1)
+ tk.Label(root, text="Z (m):").grid(row=2, column=0)
+ tk.Scale(
+ root,
+ variable=z_var,
+ from_=-0.2,
+ to=0.2,
+ resolution=0.001,
+ orient=tk.HORIZONTAL,
+ length=200,
+ ).grid(row=2, column=1)
+
+ tk.Label(root, text="Body Yaw (deg):").grid(row=3, column=0)
+ body_yaw_var = tk.DoubleVar(value=0.0)
+ tk.Scale(
+ root,
+ variable=body_yaw_var,
+ from_=-180,
+ to=180,
+ resolution=1.0,
+ orient=tk.HORIZONTAL,
+ length=200,
+ ).grid(row=3, column=1)
+
+ mini.goto_target(create_head_pose(), antennas=[0.0, 0.0], duration=1.0)
+
+ # Run the GUI in a non-blocking way
+ root.update()
+
+ try:
+ while True:
+ t = time.time() - t0
+ target = np.deg2rad(30) * np.sin(2 * np.pi * 0.5 * t)
+
+ head = np.eye(4)
+ head[:3, 3] = [0, 0, 0.0]
+
+ head = mini.look_at_world(x_var.get(), y_var.get(), z_var.get(), duration=0.3, perform_movement=False)
+
+ root.update()
+
+ mini.set_target(
+ head=head,
+ body_yaw=np.deg2rad(body_yaw_var.get()),
+ antennas=np.array([target, -target]),
+ )
+
+
+
+ except KeyboardInterrupt:
+ pass
+ finally:
+ root.destroy()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/mini_head_position_gui.py b/examples/gui_demos/mini_head_position_gui.py
similarity index 93%
rename from examples/mini_head_position_gui.py
rename to examples/gui_demos/mini_head_position_gui.py
index 7b0f732d..d6df8dbd 100644
--- a/examples/mini_head_position_gui.py
+++ b/examples/gui_demos/mini_head_position_gui.py
@@ -83,11 +83,6 @@ def main():
length=200,
).grid(row=6, column=1)
- # add a checkbox to enable/disable collision checking
- collision_check_var = tk.BooleanVar(value=False)
- tk.Checkbutton(root, text="Check Collision", variable=collision_check_var).grid(
- row=7, column=1
- )
mini.goto_target(create_head_pose(), antennas=[0.0, 0.0], duration=1.0)
diff --git a/src/reachy_mini/daemon/backend/mujoco/backend copy.py b/src/reachy_mini/daemon/backend/mujoco/backend copy.py
deleted file mode 100644
index 881ce03b..00000000
--- a/src/reachy_mini/daemon/backend/mujoco/backend copy.py
+++ /dev/null
@@ -1,312 +0,0 @@
-"""Mujoco Backend for Reachy Mini.
-
-This module provides the MujocoBackend class for simulating the Reachy Mini robot using the MuJoCo physics engine.
-
-It includes methods for running the simulation, getting joint positions, and controlling the robot's joints.
-
-"""
-
-import json
-import time
-from dataclasses import dataclass
-from importlib.resources import files
-from threading import Thread
-from typing import Optional
-
-import mujoco
-import mujoco.viewer
-import numpy as np
-
-import reachy_mini
-from reachy_mini.daemon.backend.abstract import Backend
-from reachy_mini.daemon.backend.mujoco.utils import (
- get_actuator_names,
- get_joint_addr_from_name,
- get_joint_id_from_name,
-)
-from reachy_mini.daemon.backend.mujoco.video_udp import UDPJPEGFrameSender
-
-
-class MujocoBackend(Backend):
- """Simulated Reachy Mini using MuJoCo."""
-
- def __init__(self, scene="empty", check_collision: bool = False):
- """Initialize the MujocoBackend with a specified scene.
-
- Args:
- scene (str): The name of the scene to load. Default is "empty".
- check_collision (bool): If True, enable collision checking. Default is False.
-
- """
- super().__init__(check_collision=check_collision)
-
- from reachy_mini.reachy_mini import (
- SLEEP_ANTENNAS_JOINT_POSITIONS,
- SLEEP_HEAD_JOINT_POSITIONS,
- )
-
- self._SLEEP_ANTENNAS_JOINT_POSITIONS = SLEEP_ANTENNAS_JOINT_POSITIONS
- self._SLEEP_HEAD_JOINT_POSITIONS = SLEEP_HEAD_JOINT_POSITIONS
-
- mjcf_root_path = str(
- files(reachy_mini).joinpath("descriptions/reachy_mini/mjcf/")
- )
- self.model = mujoco.MjModel.from_xml_path( # type: ignore
- f"{mjcf_root_path}/scenes/{scene}.xml"
- )
- self.data = mujoco.MjData(self.model) # type: ignore
- self.model.opt.timestep = 0.002 # s, simulation timestep, 500hz
- self.decimation = 10 # -> 50hz control loop
- self.rendering_timestep = 0.04 # s, rendering loop # 25Hz
-
- self.camera_id = mujoco.mj_name2id( # type: ignore
- self.model,
- mujoco.mjtObj.mjOBJ_CAMERA, # type: ignore
- "eye_camera",
- )
-
- self.head_id = mujoco.mj_name2id( # type: ignore
- self.model,
- mujoco.mjtObj.mjOBJ_BODY, # type: ignore
- "pp01063_stewart_plateform",
- )
-
- self.platform_to_head_transform = np.array(
- [
- [8.66025292e-01, 5.00000194e-01, -1.83660327e-06, -1.34282000e-02],
- [5.55111512e-16, -3.67320510e-06, -1.00000000e00, -1.20000000e-03],
- [-5.00000194e-01, 8.66025292e-01, -3.18108852e-06, 3.65883000e-02],
- [0, 0, 0, 1.00000000e00],
- ]
- )
- # remove_z_offset = np.eye(4)
- # remove_z_offset[2, 3] = -0.177
- # self.platform_to_head_transform = self.platform_to_head_transform @ remove_z_offset
-
- self.current_head_pose = np.eye(4)
-
- # print("Joints in the model:")
- # for i in range(self.model.njoint):
- # name = mujoco.mj_id2joint(self.model, i)
- # print(f" {i}: {name}")
-
- self.joint_names = get_actuator_names(self.model)
-
- self.joint_ids = [
- get_joint_id_from_name(self.model, n) for n in self.joint_names
- ]
- self.joint_qpos_addr = [
- get_joint_addr_from_name(self.model, n) for n in self.joint_names
- ]
-
- # disable the collisions by default
- self.model.geom_contype[13] = 0 # head capsule
- self.model.geom_conaffinity[13] = 0
- self.model.geom_contype[80] = 0 # top of the torso
- self.model.geom_conaffinity[80] = 0
-
- def rendering_loop(self):
- """Offline Rendering loop for the Mujoco simulation.
-
- Capture the image from the virtual Reachy's camera and send it over UDP.
- """
- streamer_udp = UDPJPEGFrameSender()
- camera_size = (1280, 720)
- offscreen_renderer = mujoco.Renderer(
- self.model, height=camera_size[1], width=camera_size[0]
- )
- while not self.should_stop.is_set():
- start_t = time.time()
- offscreen_renderer.update_scene(self.data, self.camera_id)
- im = offscreen_renderer.render()
- streamer_udp.send_frame(im)
-
- took = time.time() - start_t
- time.sleep(max(0, self.rendering_timestep - took))
-
-
-
- def run(self):
- """Run the Mujoco simulation with a viewer.
-
- This method initializes the viewer and enters the main simulation loop.
- It updates the joint positions at a rate and publishes the joint positions.
- """
- step = 1
- with mujoco.viewer.launch_passive(
- self.model, self.data, show_left_ui=False, show_right_ui=False
- ) as viewer:
- with viewer.lock():
- viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FREE # type: ignore
- viewer.cam.distance = 0.8 # ≃ ||pos - lookat||
- viewer.cam.azimuth = 160 # degrees
- viewer.cam.elevation = -20 # degrees
- viewer.cam.lookat[:] = [0, 0, 0.15]
-
- # force one render with your new camera
- mujoco.mj_step(self.model, self.data) # type: ignore
- viewer.sync()
-
- # im = self.get_camera()
- # self.streamer_udp.send_frame(im)
- with viewer.lock():
- self.data.qpos[self.joint_qpos_addr] = np.array(
- self._SLEEP_HEAD_JOINT_POSITIONS
- + self._SLEEP_ANTENNAS_JOINT_POSITIONS
- ).reshape(-1, 1)
- self.data.ctrl[:] = np.array(
- self._SLEEP_HEAD_JOINT_POSITIONS
- + self._SLEEP_ANTENNAS_JOINT_POSITIONS
- )
-
- # recompute all kinematics, collisions, etc.
- mujoco.mj_forward(self.model, self.data) # type: ignore
-
- for i in range(20):
- # one more frame so the viewer shows your startup pose
- mujoco.mj_step(self.model, self.data) # type: ignore
- viewer.sync()
-
- rendering_thread = Thread(target=self.rendering_loop, daemon=True)
- rendering_thread.start()
-
-
- #if self.check_collision:
- # enable collisions
- self.model.geom_contype[13] = 1 # head capsule
- self.model.geom_conaffinity[13] = 1
- self.model.geom_contype[80] = 1 # top of the torso
- self.model.geom_conaffinity[80] = 1
-
- mujoco.mj_step(self.model, self.data)
-
-
-
- # 3) now enter your normal loop
- while not self.should_stop.is_set():
- start_t = time.time()
-
- if step % self.decimation == 0:
- # update the current states
- self.current_head_joint_positions = (
- self.get_present_head_joint_positions()
- )
- self.current_antenna_joint_positions = (
- self.get_present_antenna_joint_positions()
- )
- self.current_head_pose = self.get_mj_present_head_pose()
-
- # Update the target head joint positions from IK if necessary
- # - does nothing if the targets did not change
- if self.ik_required:
- self.update_target_head_joints_from_ik(
- self.target_head_pose, self.target_body_yaw
- )
-
- if self.target_head_joint_positions is not None:
- self.data.ctrl[:7] = self.target_head_joint_positions
- if self.target_antenna_joint_positions is not None:
- self.data.ctrl[-2:] = self.target_antenna_joint_positions
-
- if (
- self.joint_positions_publisher is not None
- and self.pose_publisher is not None
- ):
- self.joint_positions_publisher.put(
- json.dumps(
- {
- "head_joint_positions": self.current_head_joint_positions,
- "antennas_joint_positions": self.current_antenna_joint_positions,
- }
- ).encode("utf-8")
- )
- self.pose_publisher.put(
- json.dumps(
- {
- "head_pose": self.get_present_head_pose().tolist(),
- }
- ).encode("utf-8")
- )
- viewer.sync()
-
- mujoco.mj_step(self.model, self.data) # type: ignore
-
- took = time.time() - start_t
- time.sleep(max(0, self.model.opt.timestep - took))
- # print(f"Step {step}: took {took*1000:.1f}ms")
- step += 1
- self.ready.set()
-
- def get_mj_present_head_pose(self) -> np.ndarray:
- """Get the current head pose from the Mujoco simulation.
-
- Returns:
- np.ndarray: The current head pose as a 4x4 transformation matrix.
-
- """
- mj_current_head_pose = np.eye(4)
- mj_current_head_pose[:3, :3] = self.data.xmat[self.head_id].reshape(3, 3)
- mj_current_head_pose[:3, 3] = self.data.xpos[self.head_id]
- mj_current_head_pose = mj_current_head_pose @ self.platform_to_head_transform
- mj_current_head_pose[2, 3] -= 0.177
- return mj_current_head_pose
-
- def close(self) -> None:
- """Close the Mujoco backend."""
- # TODO Do something in mujoco here ?
- pass
-
- def get_status(self) -> "MujocoBackendStatus":
- """Get the status of the Mujoco backend.
-
- Returns:
- dict: An empty dictionary as the Mujoco backend does not have a specific status to report.
-
- """
- return MujocoBackendStatus()
-
- def get_present_head_joint_positions(self):
- """Get the current joint positions of the head."""
- return self.data.qpos[self.joint_qpos_addr[:7]].flatten().tolist()
-
- def get_present_antenna_joint_positions(self):
- """Get the current joint positions of the antennas."""
- return self.data.qpos[self.joint_qpos_addr[-2:]].flatten().tolist()
-
- def enable_motors(self) -> None:
- """Enable the motors.
-
- Does nothing in the Mujoco backend as it does not have a concept of enabling/disabling motors.
- """
- pass
-
- def disable_motors(self) -> None:
- """Disable the motors.
-
- Does nothing in the Mujoco backend as it does not have a concept of enabling/disabling motors.
- """
- pass
-
- def set_head_operation_mode(self, mode: int) -> None:
- """Set mode of operation for the head.
-
- This does nothing in the Mujoco backend as it does not have a concept of operation modes.
- """
- pass
-
- def set_antennas_operation_mode(self, mode: int) -> None:
- """Set mode of operation for the antennas.
-
- This does nothing in the Mujoco backend as it does not have a concept of operation modes.
- """
- pass
-
-
-@dataclass
-class MujocoBackendStatus:
- """Dataclass to represent the status of the Mujoco backend.
-
- Empty for now, as the Mujoco backend does not have a specific status to report.
- """
-
- error: Optional[str] = None
diff --git a/src/reachy_mini/daemon/backend/mujoco/backend.py b/src/reachy_mini/daemon/backend/mujoco/backend.py
index 1b855aa2..9f023866 100644
--- a/src/reachy_mini/daemon/backend/mujoco/backend.py
+++ b/src/reachy_mini/daemon/backend/mujoco/backend.py
@@ -92,11 +92,6 @@ def __init__(
self.current_head_pose = np.eye(4)
- # print("Joints in the model:")
- # for i in range(self.model.njoint):
- # name = mujoco.mj_id2joint(self.model, i)
- # print(f" {i}: {name}")
-
self.joint_names = get_actuator_names(self.model)
self.joint_ids = [
@@ -114,11 +109,6 @@ def __init__(
self.model.geom_contype[i] = 0
self.model.geom_conaffinity[i] = 0
- # # disable the collisions by default
- # self.model.geom_contype[13] = 0 # head capsule
- # self.model.geom_conaffinity[13] = 0
- # self.model.geom_contype[80] = 0 # top of the torso
- # self.model.geom_conaffinity[80] = 0
def rendering_loop(self) -> None:
"""Offline Rendering loop for the Mujoco simulation.
@@ -183,6 +173,10 @@ def run(self) -> None:
for i in self.col_inds:
self.model.geom_contype[i] = 1
self.model.geom_conaffinity[i] = 1
+
+
+ for i in range(100):
+ mujoco.mj_step(self.model, self.data)
# one more frame so the viewer shows your startup pose
mujoco.mj_step(self.model, self.data)
@@ -252,7 +246,7 @@ def run(self) -> None:
took = time.time() - start_t
time.sleep(max(0, self.model.opt.timestep - took))
- # print(f"Step {step}: took {took*1000:.1f}ms")
+ #print(f"Step {step}: took {took*1e6:.1f}us")
step += 1
if not self.headless:
diff --git a/src/reachy_mini/descriptions/reachy_mini/mjcf/joints_properties.xml b/src/reachy_mini/descriptions/reachy_mini/mjcf/joints_properties.xml
index 0d7260b4..7a166408 100644
--- a/src/reachy_mini/descriptions/reachy_mini/mjcf/joints_properties.xml
+++ b/src/reachy_mini/descriptions/reachy_mini/mjcf/joints_properties.xml
@@ -2,8 +2,8 @@
-
-
+
+
diff --git a/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy 2.xml b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy 2.xml
deleted file mode 100644
index 8f8ccd2c..00000000
--- a/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy 2.xml
+++ /dev/null
@@ -1,450 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy.xml b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy.xml
deleted file mode 100644
index a97dbc09..00000000
--- a/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini copy.xml
+++ /dev/null
@@ -1,448 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml
index 83ee966a..dc66a337 100644
--- a/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml
+++ b/src/reachy_mini/descriptions/reachy_mini/mjcf/reachy_mini.xml
@@ -19,8 +19,8 @@
-
-
+
+
@@ -75,10 +75,15 @@
-
-
-
-
+
+
+
+
+
+
+
+
@@ -476,7 +481,7 @@
resolution="1280 720"
fovy="80"
/>
-
+