Repository: pollen-robotics/reachy2-sdk (language: python)

Issue:
58 make one parent class orbita for orbita2d and 3d
Move all orbita related code to a folder
Make an abstract class for Orbita and factorize orbita2d and orbita3d
Make one parent class Orbita for Orbita2d and 3d.
There is a lot of code duplication between actuators, we could probably do something to clean this. 
Rename reachy script.
The [reachy.py](https://github.com/pollen-robotics/reachy2-sdk/blob/develop/src/reachy2_sdk/reachy.py) script's name is too generic. We should use a more specific name, such as reachy_info.py or reachy_config.py. 
Reorganize scripts in reachy2_sdk src folder.
## Context
Currently, the 'reachy2_sdk'  src folder contains a large number of scripts, which can make navigation and maintenance more challenging. To improve code readability and file management, I propose a reorganization by creating thematic subfolders.

## Proposal
I suggest grouping related scripts into subfolders based on their function or module. For example, we could have subfolders like 'parts,' 'actuators', 'examples', etc.

## Expected Benefits
1. **Better Organization:** A clearer folder structure would make it easier to locate relevant files.
2. **Improved Maintainability:** Updates and changes will be more easily manageable with a logical organization.
3. **Increased Clarity for New Contributors:** A well-defined structure will make it easier for new team members to navigate.

## Suggested Action Plan
1. **Creation of Subfolders:** Propose creating the specified subfolders.
2. **Moving Existing Scripts:** Move current scripts into the appropriate subfolders.
3. **Update References:** Update references in code, documentation, etc.

## Associated Tasks
- [ ] Create subfolders according to the proposal.
- [ ] Move existing scripts into the appropriate subfolders.
- [ ] Update references in code, documentation, etc.

I believe this change would enhance code management and facilitate collaborative work.



Discussion / hints:

[Orbita2d](https://github.com/pollen-robotics/reachy2-sdk/blob/5735d27646aaf98035b42332b69085a907d4c171/src/reachy2_sdk/orbita2d.py#L23) and [Orbita3d](https://github.com/pollen-robotics/reachy2-sdk/blob/5735d27646aaf98035b42332b69085a907d4c171/src/reachy2_sdk/orbita3d.py#L24) classes are almost identical.
The only differences are :
-  for each Orbita2d instanciated we need to determine which type the two axis are (either roll, pitch, yaw) whereas each Orbita3d has a roll, pitch and yaw axis
- Orbita2ds and Orbita 3ds use different protos in the api

Having a parent class Orbita for both Orbita2d and Orbita3d implementing the common traits would probably help in maintaining the code. 



Fix patch:
diff --git a/.gitignore b/.gitignore
index 0c198f20..05169bac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,7 +20,6 @@ eggs/
 .eggs/
 lib/
 lib64/
-parts/
 sdist/
 var/
 wheels/
diff --git a/setup.cfg b/setup.cfg
index e0562922..497fe56f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,7 +9,7 @@ long_description = file: README.md
 long_description_content_type = text/markdown
 
 [options]
-packages = reachy2_sdk
+packages = find:
 zip_safe = True
 include_package_data = True
 package_dir=
@@ -21,6 +21,9 @@ install_requires =
     pyquaternion==0.9.9
     mobile-base-sdk@git+https://github.com/pollen-robotics/mobile-base-sdk.git@6-update-mobile-base-with-latest-devs
 
+[options.packages.find]
+where = src
+
 [options.extras_require]
 dev = black==23.12.1
       flake8==7.0.0
diff --git a/src/reachy2_sdk/config/__init__.py b/src/reachy2_sdk/config/__init__.py
new file mode 100644
index 00000000..e8455397
--- /dev/null
+++ b/src/reachy2_sdk/config/__init__.py
@@ -0,0 +1,6 @@
+"""
+# Config module.
+
+This module manages the configuration of the connected robot.
+
+"""
diff --git a/src/reachy2_sdk/reachy.py b/src/reachy2_sdk/config/reachy_info.py
similarity index 100%
rename from src/reachy2_sdk/reachy.py
rename to src/reachy2_sdk/config/reachy_info.py
diff --git a/src/reachy2_sdk/media/__init__.py b/src/reachy2_sdk/media/__init__.py
new file mode 100644
index 00000000..e77ca750
--- /dev/null
+++ b/src/reachy2_sdk/media/__init__.py
@@ -0,0 +1,6 @@
+"""
+# Media module.
+
+This module provides access to Reachy's cameras, microphones and speakers.
+
+"""
diff --git a/src/reachy2_sdk/audio.py b/src/reachy2_sdk/media/audio.py
similarity index 100%
rename from src/reachy2_sdk/audio.py
rename to src/reachy2_sdk/media/audio.py
diff --git a/src/reachy2_sdk/motors/__init__.py b/src/reachy2_sdk/motors/__init__.py
new file mode 100644
index 00000000..931f4517
--- /dev/null
+++ b/src/reachy2_sdk/motors/__init__.py
@@ -0,0 +1,7 @@
+"""
+# Motors module.
+
+This module enable the communication with dynamixels motors and provides the register
+ to exchange between grpc and the actuators.
+
+"""
diff --git a/src/reachy2_sdk/dynamixel_motor.py b/src/reachy2_sdk/motors/dynamixel_motor.py
similarity index 100%
rename from src/reachy2_sdk/dynamixel_motor.py
rename to src/reachy2_sdk/motors/dynamixel_motor.py
diff --git a/src/reachy2_sdk/register.py b/src/reachy2_sdk/motors/register.py
similarity index 100%
rename from src/reachy2_sdk/register.py
rename to src/reachy2_sdk/motors/register.py
diff --git a/src/reachy2_sdk/orbita/__init__.py b/src/reachy2_sdk/orbita/__init__.py
new file mode 100644
index 00000000..cf59ab1c
--- /dev/null
+++ b/src/reachy2_sdk/orbita/__init__.py
@@ -0,0 +1,6 @@
+"""
+# Orbita module.
+
+This module manages the orbita motors.
+
+"""
diff --git a/src/reachy2_sdk/orbita/orbita.py b/src/reachy2_sdk/orbita/orbita.py
new file mode 100644
index 00000000..ef89f8de
--- /dev/null
+++ b/src/reachy2_sdk/orbita/orbita.py
@@ -0,0 +1,212 @@
+import asyncio
+from abc import ABC, abstractmethod
+from typing import Any, Dict, List, Tuple
+
+from google.protobuf.wrappers_pb2 import BoolValue, FloatValue
+from reachy2_sdk_api.component_pb2 import ComponentId
+from reachy2_sdk_api.orbita2d_pb2 import Float2d, Orbita2dState, PID2d, Pose2d
+from reachy2_sdk_api.orbita2d_pb2_grpc import Orbita2dServiceStub
+from reachy2_sdk_api.orbita3d_pb2 import Float3d, Orbita3dState
+from reachy2_sdk_api.orbita3d_pb2_grpc import Orbita3dServiceStub
+
+from ..motors.register import Register
+from .orbita_motor import OrbitaMotor
+from .utils import to_internal_position
+
+
+class Orbita(ABC):
+    """The Orbita class is an abstract class to represent any Orbita actuator and its registers, joints, motors and axis.
+
+    The Orbita class is used to store the up-to-date state of the actuator, especially:
+        - its compliancy
+        - its joints state
+        - its motors state
+        - its axis state
+
+    The only register available at the actuator is the compliancy RW register.
+    You can set the compliance on/off (boolean).
+
+    You can access registers of the motors from the actuators with function that act on all the actuator's motors.
+    Lower registers which can be read/write at actuator level:
+    - speed limit (in degree per second, for all motors of the actuator)
+    - torque limit (in %, for all motors of the actuator)
+    - pid (for all motors of the actuator)
+    Lower registers that are read-only but acessible at actuator level:
+    - temperatures (temperatures of all motors of the actuator)
+
+    This class is meant to be derived by Orbita2d and Orbita3d
+    """
+
+    _compliant = Register(readonly=False, type=BoolValue, label="compliant")
+
+    def __init__(
+        self,
+        uid: int,
+        name: str,
+        orbita_type: str,
+        stub: Orbita2dServiceStub | Orbita3dServiceStub,
+    ):
+        """Initialize the common attributes."""
+        self._name = name
+        self._id = uid
+        self._orbita_type = orbita_type
+        self._stub = stub
+
+        self._state: Dict[str, bool] = {}
+        self._register_needing_sync: List[str] = []
+        self._joints: Dict[str, Any] = {}
+        self._motors: Dict[str, OrbitaMotor] = {}
+
+    @abstractmethod
+    def _create_init_state(self, initial_state: Orbita2dState | Orbita3dState) -> Dict[str, Dict[str, FloatValue]]:
+        pass
+
+    def __repr__(self) -> str:
+        """Clean representation of an Orbita."""
+        s = "\n\t".join([str(joint) for joint in self._joints.values()])
+        return f"""<Orbita{self._orbita_type} compliant={self.compliant} joints=\n\t{
+            s
+        }\n>"""
+
+    def set_speed_limit(self, speed_limit: float) -> None:
+        """Set a speed_limit on all motors of the actuator"""
+        if not isinstance(speed_limit, float | int):
+            raise ValueError(f"Expected one of: float, int for speed_limit, got {type(speed_limit).__name__}")
+        speed_limit = to_internal_position(speed_limit)
+        self._set_motors_fields("speed_limit", speed_limit)
+
+    def set_torque_limit(self, torque_limit: float) -> None:
+        """Set a torque_limit on all motors of the actuator"""
+        if not isinstance(torque_limit, float | int):
+            raise ValueError(f"Expected one of: float, int for torque_limit, got {type(torque_limit).__name__}")
+        self._set_motors_fields("torque_limit", torque_limit)
+
+    def set_pid(self, pid: Tuple[float, float, float]) -> None:
+        """Set a pid value on all motors of the actuator"""
+        if isinstance(pid, tuple) and len(pid) == 3 and all(isinstance(n, float | int) for n in pid):
+            for m in self._motors.values():
+                m._tmp_pid = pid
+            self._update_loop("pid")
+        else:
+            raise ValueError("pid should be of type Tuple[float, float, float]")
+
+    def get_speed_limit(self) -> Dict[str, float]:
+        """Get speed_limit of all motors of the actuator"""
+        return {motor_name: m.speed_limit for motor_name, m in self._motors.items()}
+
+    def get_torque_limit(self) -> Dict[str, float]:
+        """Get torque_limit of all motors of the actuator"""
+        return {motor_name: m.torque_limit for motor_name, m in self._motors.items()}
+
+    def get_pid(self) -> Dict[str, Tuple[float, float, float]]:
+        """Get pid of all motors of the actuator"""
+        return {motor_name: m.pid for motor_name, m in self._motors.items()}
+
+    def turn_on(self) -> None:
+        """Turn all motors of the orbita2d on.
+        All orbita2d's motors will then be stiff.
+        """
+        self._compliant = False
+
+    def turn_off(self) -> None:
+        """Turn all motors of the orbita2d on.
+        All orbita2d's motors will then be stiff.
+        """
+        self._compliant = True
+
+    @property
+    def compliant(self) -> Any:
+        """Get compliancy of the actuator"""
+        return self._compliant
+
+    @property
+    def temperatures(self) -> Dict[str, Register]:
+        """Get temperatures of all the motors of the actuator"""
+        return {motor_name: m.temperature for motor_name, m in self._motors.items()}
+
+    def __setattr__(self, __name: str, __value: Any) -> None:
+        """Set the value of the register."""
+        if __name == "_compliant":
+            if not isinstance(__value, bool):
+                raise ValueError(f"Expected bool for compliant value, got {type(__value).__name__}")
+            self._state["compliant"] = __value
+
+            async def set_in_loop() -> None:
+                self._register_needing_sync.append("compliant")
+                self._need_sync.set()
+
+            fut = asyncio.run_coroutine_threadsafe(set_in_loop(), self._loop)
+            fut.result()
+
+        else:
+            super().__setattr__(__name, __value)
+
+    def _set_motors_fields(self, field: str, value: float) -> None:
+        """Set the value of the register for all motors of the actuator.
+
+        It is used to set pid, speed_limit and torque_limit.
+        """
+        for m in self._motors.values():
+            m._tmp_fields[field] = value
+
+        self._update_loop(field)
+
+    def _setup_sync_loop(self) -> None:
+        """Set up the async synchronisation loop.
+
+        The setup is done separately, as the async Event should be created in the same EventLoop than it will be used.
+
+        The _need_sync Event is used to inform the robot that some data need to be pushed to the real robot.
+        The _register_needing_sync stores a list of the register that need to be synced.
+        """
+        self._need_sync = asyncio.Event()
+        self._loop = asyncio.get_running_loop()
+
+    def _update_loop(self, field: str) -> None:
+        """Update the registers that need to be synced.
+
+        Set a threading event to inform the stream command thread that some data need to be pushed
+        to the robot.
+        """
+
+        async def set_in_loop() -> None:
+            self._register_needing_sync.append(field)
+            self._need_sync.set()
+
+        fut = asyncio.run_coroutine_threadsafe(set_in_loop(), self._loop)
+        fut.result()
+
+    @abstractmethod
+    def _build_grpc_cmd_msg_actuator(self, field: str) -> Float2d | Float3d:
+        pass
+
+    @abstractmethod
+    def _build_grpc_cmd_msg(self, field: str) -> Pose2d | PID2d | Float2d | Float3d:
+        pass
+
+    def _make_command(self) -> Dict[str, Any]:
+        """Create a gRPC command from the registers that need to be synced."""
+        values = {
+            "id": ComponentId(id=self._id),
+        }
+
+        set_reg_to_update = set(self._register_needing_sync)
+        for reg in set_reg_to_update:
+            if reg == "compliant":
+                values["compliant"] = BoolValue(value=self._state["compliant"])
+            else:
+                values[reg] = self._build_grpc_cmd_msg_actuator(reg)
+
+        set_reg_to_update = set()
+        for obj in list(self._joints.values()) + list(self._motors.values()):
+            set_reg_to_update = set_reg_to_update.union(set(obj._register_needing_sync))
+        for reg in set_reg_to_update:
+            values[reg] = self._build_grpc_cmd_msg(reg)
+
+        return values
+
+    def _reset_registers(self) -> None:
+        self._register_needing_sync.clear()
+        for obj in list(self._motors.values()):
+            obj._register_needing_sync.clear()
+        self._need_sync.clear()
diff --git a/src/reachy2_sdk/orbita2d.py b/src/reachy2_sdk/orbita/orbita2d.py
similarity index 55%
rename from src/reachy2_sdk/orbita2d.py
rename to src/reachy2_sdk/orbita/orbita2d.py
index fee9e473..c8ae9fe7 100644
--- a/src/reachy2_sdk/orbita2d.py
+++ b/src/reachy2_sdk/orbita/orbita2d.py
@@ -1,10 +1,9 @@
 """This module defines the Orbita2d class and its registers, joints, motors and axis."""
-import asyncio
-from typing import Any, Dict, List, Tuple
+from typing import Dict
 
-from google.protobuf.wrappers_pb2 import BoolValue, FloatValue
+from google.protobuf.wrappers_pb2 import FloatValue
 from grpc import Channel
-from reachy2_sdk_api.component_pb2 import ComponentId, PIDGains
+from reachy2_sdk_api.component_pb2 import PIDGains
 from reachy2_sdk_api.orbita2d_pb2 import (
     Axis,
     Float2d,
@@ -16,11 +15,13 @@
 )
 from reachy2_sdk_api.orbita2d_pb2_grpc import Orbita2dServiceStub
 
-from .orbita_utils import OrbitaAxis, OrbitaJoint, OrbitaMotor, _to_internal_position
-from .register import Register
+from .orbita import Orbita
+from .orbita_axis import OrbitaAxis
+from .orbita_joint import OrbitaJoint
+from .orbita_motor import OrbitaMotor
 
 
-class Orbita2d:
+class Orbita2d(Orbita):
     """The Orbita2d class represents any Orbita2d actuator and its registers, joints, motors and axis.
 
     The Orbita2d class is used to store the up-to-date state of the actuator, especially:
@@ -41,9 +42,7 @@ class Orbita2d:
     - temperatures (temperatures of all motors of the actuator)
     """
 
-    _compliant = Register(readonly=False, type=BoolValue, label="compliant")
-
-    def __init__(  # noqa: C901
+    def __init__(
         self,
         uid: int,
         name: str,
@@ -53,39 +52,12 @@ def __init__(  # noqa: C901
         grpc_channel: Channel,
     ):
         """Initialize the Orbita2d with its joints, motors and its two axis (either roll, pith or yaw for both)."""
-        self.name = name
-        self.id = uid
-        self._stub = Orbita2dServiceStub(grpc_channel)
+        super().__init__(uid, name, "2d", Orbita2dServiceStub(grpc_channel))
 
         axis1_name = Axis.DESCRIPTOR.values_by_number[axis1].name.lower()
         axis2_name = Axis.DESCRIPTOR.values_by_number[axis2].name.lower()
 
-        self._state: Dict[str, bool] = {}
-        init_state: Dict[str, Dict[str, FloatValue]] = {}
-
-        self._register_needing_sync: List[str] = []
-
-        for field, value in initial_state.ListFields():
-            if field.name == "compliant":
-                self._state[field.name] = value
-                init_state["motor_1"][field.name] = value
-                init_state["motor_2"][field.name] = value
-            else:
-                if isinstance(value, Pose2d):
-                    for axis, val in value.ListFields():
-                        if axis.name not in init_state:
-                            init_state[axis.name] = {}
-                        init_state[axis.name][field.name] = val
-                if isinstance(value, Float2d | PID2d):
-                    for motor, val in value.ListFields():
-                        if motor.name not in init_state:
-                            init_state[motor.name] = {}
-                        init_state[motor.name][field.name] = val
-                if isinstance(value, Vector2d):
-                    for axis, val in value.ListFields():
-                        if axis.name not in init_state:
-                            init_state[axis.name] = {}
-                        init_state[axis.name][field.name] = val
+        init_state: Dict[str, Dict[str, FloatValue]] = self._create_init_state(initial_state)
 
         setattr(
             self,
@@ -110,70 +82,31 @@ def __init__(  # noqa: C901
         self.__y = OrbitaAxis(initial_state=init_state["y"])
         self._axis = {"x": self.__x, "y": self.__y}
 
-    def __repr__(self) -> str:
-        """Clean representation of an Orbita2d."""
-        s = "\n\t".join([str(joint) for joint in self._joints.values()])
-        return f"""<Orbita2d compliant={self.compliant} joints=\n\t{
-            s
-        }\n>"""
-
-    def set_speed_limit(self, speed_limit: float) -> None:
-        """Set a speed_limit on all motors of the actuator"""
-        if not isinstance(speed_limit, float | int):
-            raise ValueError(f"Expected one of: float, int for speed_limit, got {type(speed_limit).__name__}")
-        speed_limit = _to_internal_position(speed_limit)
-        self._set_motors_fields("speed_limit", speed_limit)
-
-    def set_torque_limit(self, torque_limit: float) -> None:
-        """Set a torque_limit on all motors of the actuator"""
-        if not isinstance(torque_limit, float | int):
-            raise ValueError(f"Expected one of: float, int for torque_limit, got {type(torque_limit).__name__}")
-        self._set_motors_fields("torque_limit", torque_limit)
-
-    def set_pid(self, pid: Tuple[float, float, float]) -> None:
-        """Set a pid value on all motors of the actuator"""
-        if isinstance(pid, tuple) and len(pid) == 3 and all(isinstance(n, float | int) for n in pid):
-            for m in self._motors.values():
-                m._tmp_pid = pid
-            self._update_loop("pid")
-        else:
-            raise ValueError("pid should be of type Tuple[float, float, float]")
-
-    def get_speed_limit(self) -> Dict[str, float]:
-        """Get speed_limit of all motors of the actuator"""
-        return {motor_name: m.speed_limit for motor_name, m in self._motors.items()}
-
-    def get_torque_limit(self) -> Dict[str, float]:
-        """Get torque_limit of all motors of the actuator"""
-        return {motor_name: m.torque_limit for motor_name, m in self._motors.items()}
-
-    def get_pid(self) -> Dict[str, Tuple[float, float, float]]:
-        """Get pid of all motors of the actuator"""
-        return {motor_name: m.pid for motor_name, m in self._motors.items()}
-
-    def turn_on(self) -> None:
-        """Turn all motors of the orbita2d on.
-
-        All orbita2d's motors will then be stiff.
-        """
-        self._compliant = False
-
-    def turn_off(self) -> None:
-        """Turn all motors of the orbita2d on.
-
-        All orbita2d's motors will then be stiff.
-        """
-        self._compliant = True
-
-    @property
-    def compliant(self) -> Any:
-        """Get compliancy of the actuator"""
-        return self._compliant
+    def _create_init_state(self, initial_state: Orbita2dState) -> Dict[str, Dict[str, FloatValue]]:  # noqa: C901
+        init_state: Dict[str, Dict[str, FloatValue]] = {}
 
-    @property
-    def temperatures(self) -> Dict[str, Register]:
-        """Get temperatures of all the motors of the actuator"""
-        return {motor_name: m.temperature for motor_name, m in self._motors.items()}
+        for field, value in initial_state.ListFields():
+            if field.name == "compliant":
+                self._state[field.name] = value
+                init_state["motor_1"][field.name] = value
+                init_state["motor_2"][field.name] = value
+            else:
+                if isinstance(value, Pose2d):
+                    for axis, val in value.ListFields():
+                        if axis.name not in init_state:
+                            init_state[axis.name] = {}
+                        init_state[axis.name][field.name] = val
+                if isinstance(value, Float2d | PID2d):
+                    for motor, val in value.ListFields():
+                        if motor.name not in init_state:
+                            init_state[motor.name] = {}
+                        init_state[motor.name][field.name] = val
+                if isinstance(value, Vector2d):
+                    for axis, val in value.ListFields():
+                        if axis.name not in init_state:
+                            init_state[axis.name] = {}
+                        init_state[axis.name][field.name] = val
+        return init_state
 
     def _build_grpc_cmd_msg(self, field: str) -> Pose2d | PID2d | Float2d:
         """Build a gRPC message from the registers that need to be synced at the joints and
@@ -235,83 +168,13 @@ def _build_grpc_cmd_msg_actuator(self, field: str) -> Float2d:
             motor_2=FloatValue(value=motor_2_value),
         )
 
-    def _setup_sync_loop(self) -> None:
-        """Set up the async synchronisation loop.
-
-        The setup is done separately, as the async Event should be created in the same EventLoop than it will be used.
-
-        The _need_sync Event is used to inform the robot that some data need to be pushed to the real robot.
-        The _register_needing_sync stores a list of the register that need to be synced.
-        """
-        self._need_sync = asyncio.Event()
-        self._loop = asyncio.get_running_loop()
-
-    def __setattr__(self, __name: str, __value: Any) -> None:
-        """Set the value of the register."""
-        if __name == "_compliant":
-            if not isinstance(__value, bool):
-                raise ValueError(f"Expected bool for compliant value, got {type(__value).__name__}")
-            self._state["compliant"] = __value
-
-            async def set_in_loop() -> None:
-                self._register_needing_sync.append("compliant")
-                self._need_sync.set()
-
-            fut = asyncio.run_coroutine_threadsafe(set_in_loop(), self._loop)
-            fut.result()
-
-        else:
-            super().__setattr__(__name, __value)
-
-    def _set_motors_fields(self, field: str, value: float) -> None:
-        """Set the value of the register for all motors of the actuator.
-
-        It is used to set pid, speed_limit and torque_limit.
-        """
-        for m in self._motors.values():
-            m._tmp_fields[field] = value
-
-        self._update_loop(field)
-
-    def _update_loop(self, field: str) -> None:
-        """Update the registers that need to be synced.
-
-        Set a threading event to inform the stream command thread that some data need to be pushed
-        to the robot.
-        """
-
-        async def set_in_loop() -> None:
-            self._register_needing_sync.append(field)
-            self._need_sync.set()
-
-        fut = asyncio.run_coroutine_threadsafe(set_in_loop(), self._loop)
-        fut.result()
-
     def _pop_command(self) -> Orbita2dCommand:
         """Create a gRPC command from the registers that need to be synced."""
-        values = {
-            "id": ComponentId(id=self.id),
-        }
-
-        set_reg_to_update = set(self._register_needing_sync)
-        for reg in set_reg_to_update:
-            if reg == "compliant":
-                values["compliant"] = BoolValue(value=self._state["compliant"])
-            else:
-                values[reg] = self._build_grpc_cmd_msg_actuator(reg)
-
-        set_reg_to_update = set()
-        for obj in list(self._joints.values()) + list(self._motors.values()):
-            set_reg_to_update = set_reg_to_update.union(set(obj._register_needing_sync))
-        for reg in set_reg_to_update:
-            values[reg] = self._build_grpc_cmd_msg(reg)
+        values = self._make_command()
 
         command = Orbita2dCommand(**values)
 
-        self._register_needing_sync.clear()
-        for obj in list(self._motors.values()):
-            obj._register_needing_sync.clear()
-        self._need_sync.clear()
+        self._reset_registers()
 
         return command
 
diff --git a/src/reachy2_sdk/orbita3d.py b/src/reachy2_sdk/orbita/orbita3d.py
similarity index 59%
rename from src/reachy2_sdk/orbita3d.py
rename to src/reachy2_sdk/orbita/orbita3d.py
index 0e078696..3715730e 100644
--- a/src/reachy2_sdk/orbita3d.py
+++ b/src/reachy2_sdk/orbita/orbita3d.py
@@ -1,10 +1,9 @@
 """This module defines the Orbita3d class and its registers, joints, motors and axis."""
-import asyncio
-from typing import Any, Dict, List, Tuple
+from typing import Dict, List
 
-from google.protobuf.wrappers_pb2 import BoolValue, FloatValue
+from google.protobuf.wrappers_pb2 import FloatValue
 from grpc import Channel
-from reachy2_sdk_api.component_pb2 import ComponentId, PIDGains
+from reachy2_sdk_api.component_pb2 import PIDGains
 from reachy2_sdk_api.kinematics_pb2 import ExtEulerAngles, Rotation3d
 from reachy2_sdk_api.orbita3d_pb2 import (
     Float3d,
@@ -15,11 +14,13 @@
 )
 from reachy2_sdk_api.orbita3d_pb2_grpc import Orbita3dServiceStub
 
-from .orbita_utils import OrbitaAxis, OrbitaJoint, OrbitaMotor, _to_internal_position
-from .register import Register
+from .orbita import Orbita
+from .orbita_axis import OrbitaAxis
+from .orbita_joint import OrbitaJoint
+from .orbita_motor import OrbitaMotor
 
 
-class Orbita3d:
+class Orbita3d(Orbita):
     """The Orbita3d class represents any Orbita2d actuator and its registers, joints, motors and axis.
 
     The Orbita3d class is used to store the up-to-date state of the actuator, especially:
@@ -40,15 +41,9 @@ class Orbita3d:
     - temperatures (temperatures of all motors of the actuator)
     """
 
-    _compliant = Register(readonly=False, type=BoolValue, label="compliant")
-
     def __init__(self, uid: int, name: str, initial_state: Orbita3dState, grpc_channel: Channel):
         """Initialize the Orbita2d with its joints, motors and axis."""
-        self.name = name
-        self.id = uid
-        self._stub = Orbita3dServiceStub(grpc_channel)
-
-        self._state: Dict[str, bool] = {}
+        super().__init__(uid, name, "3d", Orbita3dServiceStub(grpc_channel))
         init_state: Dict[str, Dict[str, FloatValue]] = self._create_init_state(initial_state)
 
         self._register_needing_sync: List[str] = []
@@ -99,92 +94,6 @@ def _create_init_state(self, initial_state: Orbita3dState) -> Dict[str, Dict[str
                         init_state[axis.name][field.name] = val
         return init_state
 
-    def __repr__(self) -> str:
-        """Clean representation of an Orbita3d."""
-        s = "\n\t".join([str(joint) for _, joint in self._joints.items()])
-        return f"""<Orbita3d compliant={self.compliant} joints=\n\t{
-            s
-        }\n>"""
-
-    def set_speed_limit(self, speed_limit: float) -> None:
-        """Set a speed_limit on all motors of the actuator"""
-        if not isinstance(speed_limit, float | int):
-            raise ValueError(f"Expected one of: float, int for speed_limit, got {type(speed_limit).__name__}")
-        speed_limit = _to_internal_position(speed_limit)
-        self._set_motors_fields("speed_limit", speed_limit)
-
-    def set_torque_limit(self, torque_limit: float) -> None:
-        """Set a torque_limit on all motors of the actuator"""
-        if not isinstance(torque_limit, float | int):
-            raise ValueError(f"Expected one of: float, int for torque_limit, got {type(torque_limit).__name__}")
-        self._set_motors_fields("torque_limit", torque_limit)
-
-    def set_pid(self, pid: Tuple[float, float, float]) -> None:
-        """Set a pid value on all motors of the actuator"""
-        if isinstance(pid, tuple) and len(pid) == 3 and all(isinstance(n, float | int) for n in pid):
-            for m in self._motors.values():
-                m._tmp_pid = pid
-            self._update_loop("pid")
-        else:
-            raise ValueError("pid should be of type Tuple[float, float, float]")
-
-    def get_speed_limit(self) -> Dict[str, float]:
-        """Get speed_limit of all motors of the actuator"""
-        return {motor_name: m.speed_limit for motor_name, m in self._motors.items()}
-
-    def get_torque_limit(self) -> Dict[str, float]:
-        """Get torque_limit of all motors of the actuator"""
-        return {motor_name: m.torque_limit for motor_name, m in self._motors.items()}
-
-    def get_pid(self) -> Dict[str, Tuple[float, float, float]]:
-        """Get pid of all motors of the actuator"""
-        return {motor_name: m.pid for motor_name, m in self._motors.items()}
-
-    def turn_on(self) -> None:
-        """Turn all motors of the orbita3d on.
-
-        All orbita3d's motors will then be stiff.
-        """
-        self._compliant = False
-
-    def turn_off(self) -> None:
-        """Turn all motors of the orbita3d on.
-
-        All orbita3d's motors will then be stiff.
-        """
-        self._compliant = True
-
-    @property
-    def compliant(self) -> Any:
-        """Get complinancy of the actuator"""
-        return self._compliant
-
-    @property
-    def temperatures(self) -> Dict[str, Register]:
-        """Get temperatures of all the motors of the actuator"""
-        return {motor_name: m.temperature for motor_name, m in self._motors.items()}
-
-    def _update_with(self, new_state: Orbita3dState) -> None:  # noqa: C901
-        """Update the orbita with a newly received (partial) state received from the gRPC server."""
-        for field, value in new_state.ListFields():
-            if field.name == "compliant":
-                self._state[field.name] = value
-                for m in self._motors.values():
-                    m._state[field.name] = value
-            else:
-                if isinstance(value, Rotation3d):
-                    self.roll._state[field.name] = value.rpy.roll
-                    self.pitch._state[field.name] = value.rpy.pitch
-                    self.yaw._state[field.name] = value.rpy.yaw
-                if isinstance(value, Float3d):
-                    for motor, val in value.ListFields():
-                        m = self._motors[motor.name]
-                        m._state[field.name] = val
-                if isinstance(value, Vector3d):
-                    for axis, val in value.ListFields():
-                        a = self._axis[axis.name]
-                        a._state[field.name] = val
-
     def _build_grpc_cmd_msg(self, field: str) -> Float3d:
         """Build a gRPC message from the registers that need to be synced at the joints and
         motors level. Registers can either be goal_position, pid or speed_limit/torque_limit.
@@ -262,81 +171,33 @@ def _build_grpc_cmd_msg_actuator(self, field: str) -> Float3d:
             motor_3=FloatValue(value=motor_3_value),
         )
 
-    def __setattr__(self, __name: str, __value: Any) -> None:
-        if __name == "_compliant":
-            if not isinstance(__value, bool):
-                raise ValueError(f"Expected bool for compliant value, got {type(__value).__name__}")
-            self._state["compliant"] = __value
-
-            async def set_in_loop() -> None:
-                self._register_needing_sync.append("compliant")
-                self._need_sync.set()
-
-            fut = asyncio.run_coroutine_threadsafe(set_in_loop(), self._loop)
-            fut.result()
-
-        else:
-            super().__setattr__(__name, __value)
-
-    def _setup_sync_loop(self) -> None:
-        """Set up the async synchronisation loop.
-
-        The setup is done separately, as the async Event should be created in the same EventLoop than it will be used.
-
-        The _need_sync Event is used to inform the robot that some data need to be pushed to the real robot.
-        The _register_needing_sync stores a list of the register that need to be synced.
-        """
-        self._need_sync = asyncio.Event()
-        self._loop = asyncio.get_running_loop()
-
-    def _set_motors_fields(self, field: str, value: float) -> None:
-        """Set the value of the register for all motors of the actuator.
-
-        It is used to set pid, speed_limit and torque_limit.
-        """
-        for m in self._motors.values():
-            m._tmp_fields[field] = value
-
-        self._update_loop(field)
-
-    def _update_loop(self, field: str) -> None:
-        """Update the registers that need to be synced.
-
-        Set a threading event to inform the stream command thread that some data need to be pushed
-        to the robot.
-        """
-
-        async def set_in_loop() -> None:
-            self._register_needing_sync.append(field)
-            self._need_sync.set()
-
-        fut = asyncio.run_coroutine_threadsafe(set_in_loop(), self._loop)
-        fut.result()
-
     def _pop_command(self) -> Orbita3dCommand:
         """Create a gRPC command from the registers that need to be synced."""
-        values = {
-            "id": ComponentId(id=self.id),
-        }
-
-        set_reg_to_update = set(self._register_needing_sync)
-        for reg in set_reg_to_update:
-            if reg == "compliant":
-                values["compliant"] = BoolValue(value=self._state["compliant"])
-            else:
-                values[reg] = self._build_grpc_cmd_msg_actuator(reg)
-
-        set_reg_to_update = set()
-        for obj in list(self._joints.values()) + list(self._motors.values()):
-            set_reg_to_update = set_reg_to_update.union(set(obj._register_needing_sync))
-        for reg in set_reg_to_update:
-            values[reg] = self._build_grpc_cmd_msg(reg)
+        values = self._make_command()
 
         command = Orbita3dCommand(**values)
 
-        self._register_needing_sync.clear()
-        for obj in list(self._motors.values()):
-            obj._register_needing_sync.clear()
-        self._need_sync.clear()
+        self._reset_registers()
 
         return command
+
+    def _update_with(self, new_state: Orbita3dState) -> None:
+        """Update the orbita with a newly received (partial) state received from the gRPC server."""
+        for field, value in new_state.ListFields():
+            if field.name == "compliant":
+                self._state[field.name] = value
+                for m in self._motors.values():
+                    m._state[field.name] = value
+            else:
+                if isinstance(value, Rotation3d):
+                    self.roll._state[field.name] = value.rpy.roll
+                    self.pitch._state[field.name] = value.rpy.pitch
+                    self.yaw._state[field.name] = value.rpy.yaw
+                if isinstance(value, Float3d):
+                    for motor, val in value.ListFields():
+                        m = self._motors[motor.name]
+                        m._state[field.name] = val
+                if isinstance(value, Vector3d):
+                    for axis, val in value.ListFields():
+                        a = self._axis[axis.name]
+                        a._state[field.name] = val
diff --git a/src/reachy2_sdk/orbita/orbita_axis.py b/src/reachy2_sdk/orbita/orbita_axis.py
new file mode 100644
index 00000000..7e45142c
--- /dev/null
+++ b/src/reachy2_sdk/orbita/orbita_axis.py
@@ -0,0 +1,33 @@
+"""This module describes Orbita axes."""
+from typing import Dict
+
+from google.protobuf.wrappers_pb2 import FloatValue
+
+from ..motors.register import Register
+from .utils import to_internal_position, to_position
+
+
+class OrbitaAxis:
+    """The OrbitaAxis class represents any Orbita3d or Orbita2d axis.
+
+    The OrbitaAxis class is used to store the up-to-date state of the axis, especially:
+        - its present speed (RO)
+        - its present load (RO)
+    """
+
+    present_speed = Register(
+        readonly=True,
+        type=FloatValue,
+        label="present_speed",
+        conversion=(to_internal_position, to_position),
+    )
+    present_load = Register(readonly=True, type=FloatValue, label="present_load")
+
+    def __init__(self, initial_state: Dict[str, FloatValue]) -> None:
+        """Initialize the axis with its initial state."""
+        self._state = initial_state
+
+        for field in dir(self):
+            value = getattr(self, field)
+            if isinstance(value, Register):
+                value.label = field
diff --git a/src/reachy2_sdk/orbita/orbita_joint.py b/src/reachy2_sdk/orbita/orbita_joint.py
new file mode 100644
index 00000000..3ceaef46
--- /dev/null
+++ b/src/reachy2_sdk/orbita/orbita_joint.py
@@ -0,0 +1,45 @@
+"""This module describes Orbita2d and Orbita3d joints."""
+from typing import Any, Dict, List
+
+from google.protobuf.wrappers_pb2 import FloatValue
+
+from ..motors.register import Register
+from .utils import to_internal_position, to_position
+
+
+class OrbitaJoint:
+    """The OrbitaJoint class represents any Orbita2d or Orbita 3d joint.
+
+    The OrbitaJoint class is used to store the up-to-date state of the joint, especially:
+        - its present_position (RO)
+        - its goal_position (RW)
+    """
+
+    present_position = Register(
+        readonly=True,
+        type=FloatValue,
+        label="present_position",
+        conversion=(to_internal_position, to_position),
+    )
+    goal_position = Register(
+        readonly=False,
+        type=FloatValue,
+        label="goal_position",
+        conversion=(to_internal_position, to_position),
+    )
+
+    def __init__(self, initial_state: Dict[str, FloatValue], axis_type: str, actuator: Any) -> None:
+        self._actuator = actuator
+        self.axis_type = axis_type
+        self._state = initial_state
+        self._tmp_state = initial_state.copy()
+
+        for field in dir(self):
+            value = getattr(self, field)
+            if isinstance(value, Register):
+                value.label = field
+
+        self._register_needing_sync: List[str] = []
+
+    def __repr__(self) -> str:
+        return f'<OrbitaJoint axis_type="{self.axis_type}" present_position={self.present_position} goal_position={self.goal_position} >'  # noqa: E501
diff --git a/src/reachy2_sdk/orbita/orbita_motor.py b/src/reachy2_sdk/orbita/orbita_motor.py
new file mode 100644
index 00000000..d407fb0c
--- /dev/null
+++ b/src/reachy2_sdk/orbita/orbita_motor.py
@@ -0,0 +1,50 @@
+"""This module describes Orbita2d and Orbita3d motors."""
+from typing import Any, Dict, List, Tuple
+
+from google.protobuf.wrappers_pb2 import BoolValue, FloatValue
+from reachy2_sdk_api.component_pb2 import PIDGains
+
+from ..motors.register import Register
+from .utils import to_internal_position, to_position
+
+
+class OrbitaMotor:
+    """The OrbitaMotor class represents any Orbita3d or Orbita2d motor.
+
+    The OrbitaMotor class is used to store the up-to-date state of the motor, especially:
+        - its temperature (RO)
+        - its compliancy (RO)
+        - its speed limit (RW)
+        - its torque limit (RW)
+        - its pid (RW)
+    """
+
+    temperature = Register(readonly=True, type=FloatValue, label="temperature")
+    speed_limit = Register(
+        readonly=False,
+        type=FloatValue,
+        label="speed_limit",
+        conversion=(to_internal_position, to_position),
+    )
+    torque_limit = Register(readonly=False, type=FloatValue, label="torque_limit")
+    compliant = Register(readonly=True, type=BoolValue, label="compliant")
+
+    pid = Register(readonly=False, type=PIDGains, label="pid")
+
+    def __init__(self, initial_state: Dict[str, Any], actuator: Any) -> None:
+        """Initialize the motor with its initial state."""
+        self._actuator = actuator
+
+        self._state = initial_state
+
+        self._tmp_fields: Dict[str, float | None] = {}
+        self._tmp_pid: Tuple[float, float, float]
+
+        for field in dir(self):
+            value = getattr(self, field)
+            if isinstance(value, Register):
+                value.label = field
+                if not value.readonly:
+                    self._tmp_fields[field] = None
+
+        self._register_needing_sync: List[str] = []
diff --git a/src/reachy2_sdk/orbita/utils.py b/src/reachy2_sdk/orbita/utils.py
new file mode 100644
index 00000000..d521676b
--- /dev/null
+++ b/src/reachy2_sdk/orbita/utils.py
@@ -0,0 +1,22 @@
+"""This module defines the utils class functions for Orbita."""
+
+from typing import Any
+
+import numpy as np
+
+
+def to_position(internal_pos: float) -> Any:
+    """Convert an internal angluar value in radians to a value in degrees."""
+    return round(np.rad2deg(internal_pos), 2)
+
+
+def to_internal_position(pos: float) -> Any:
+    """Convert an angluar value in degrees to a value in radians.
+
+    It is necessary to convert the value from degrees to radians because the
+    server expect values in radians.
+    """
+    try:
+        return np.deg2rad(pos)
+    except TypeError:
+        raise TypeError(f"Excepted one of: int, float, got {type(pos).__name__}")
diff --git a/src/reachy2_sdk/orbita_utils.py b/src/reachy2_sdk/orbita_utils.py
deleted file mode 100644
index 34523a5c..00000000
--- a/src/reachy2_sdk/orbita_utils.py
+++ /dev/null
@@ -1,131 +0,0 @@
-"""This module defines the utils class to describe Orbita2d and Orbita3d joints, motors and axis."""
-from typing import Any, Dict, List, Tuple
-
-import numpy as np
-from google.protobuf.wrappers_pb2 import BoolValue, FloatValue
-from reachy2_sdk_api.component_pb2 import PIDGains
-
-from .register import Register
-
-
-def _to_position(internal_pos: float) -> Any:
-    """Convert an internal angluar value in radians to a value in degrees."""
-    return round(np.rad2deg(internal_pos), 2)
-
-
-def _to_internal_position(pos: float) -> Any:
-    """Convert an angluar value in degrees to a value in radians.
-
-    It is necessary to convert the value from degrees to radians because the
-    server expect values in radians.
-    """
-    try:
-        return np.deg2rad(pos)
-    except TypeError:
-        raise TypeError(f"Excepted one of: int, float, got {type(pos).__name__}")
-
-
-class OrbitaJoint:
-    """The OrbitaJoint class represents any Orbita2d or Orbita 3d joint.
-
-    The OrbitaJoint class is used to store the up-to-date state of the joint, especially:
-        - its present_position (RO)
-        - its goal_position (RW)
-    """
-
-    present_position = Register(
-        readonly=True,
-        type=FloatValue,
-        label="present_position",
-        conversion=(_to_internal_position, _to_position),
-    )
-    goal_position = Register(
-        readonly=False,
-        type=FloatValue,
-        label="goal_position",
-        conversion=(_to_internal_position, _to_position),
-    )
-
-    def __init__(self, initial_state: Dict[str, FloatValue], axis_type: str, actuator: Any) -> None:
-        self._actuator = actuator
-        self.axis_type = axis_type
-        self._state = initial_state
-        self._tmp_state = initial_state.copy()
-
-        for field in dir(self):
-            value = getattr(self, field)
-            if isinstance(value, Register):
-                value.label = field
-
-        self._register_needing_sync: List[str] = []
-
-    def __repr__(self) -> str:
-        return f'<OrbitaJoint axis_type="{self.axis_type}" present_position={self.present_position} goal_position={self.goal_position} >'  # noqa: E501
-
-
-class OrbitaMotor:
-    """The OrbitaMotor class represents any Orbita3d or Orbita2d motor.
-
-    The OrbitaMotor class is used to store the up-to-date state of the motor, especially:
-        - its temperature (RO)
-        - its compliancy (RO)
-        - its speed limit (RW)
-        - its torque limit (RW)
-        - its pid (RW)
-    """
-
-    temperature = Register(readonly=True, type=FloatValue, label="temperature")
-    speed_limit = Register(
-        readonly=False,
-        type=FloatValue,
-        label="speed_limit",
-        conversion=(_to_internal_position, _to_position),
-    )
-    torque_limit = Register(readonly=False, type=FloatValue, label="torque_limit")
-    compliant = Register(readonly=True, type=BoolValue, label="compliant")
-
-    pid = Register(readonly=False, type=PIDGains, label="pid")
-
-    def __init__(self, initial_state: Dict[str, Any], actuator: Any) -> None:
-        """Initialize the motor with its initial state."""
-        self._actuator = actuator
-
-        self._state = initial_state
-
-        self._tmp_fields: Dict[str, float | None] = {}
-        self._tmp_pid: Tuple[float, float, float]
-
-        for field in dir(self):
-            value = getattr(self, field)
-            if isinstance(value, Register):
-                value.label = field
-                if not value.readonly:
-                    self._tmp_fields[field] = None
-
-        self._register_needing_sync: List[str] = []
-
-
-class OrbitaAxis:
-    """The OrbitaAxis class represents any Orbita3d or Orbita2d axis.
-
-    The OrbitaAxis class is used to store the up-to-date state of the axis, especially:
-        - its present speed (RO)
-        - its present load (RO)
-    """
-
-    present_speed = Register(
-        readonly=True,
-        type=FloatValue,
-        label="present_speed",
-        conversion=(_to_internal_position, _to_position),
-    )
-    present_load = Register(readonly=True, type=FloatValue, label="present_load")
-
-    def __init__(self, initial_state: Dict[str, FloatValue]) -> None:
-        """Initialize the axis with its initial state."""
-        self._state = initial_state
-
-        for field in dir(self):
-            value = getattr(self, field)
-            if isinstance(value, Register):
-                value.label = field
diff --git a/src/reachy2_sdk/parts/__init__.py b/src/reachy2_sdk/parts/__init__.py
new file mode 100644
index 00000000..ed0f6167
--- /dev/null
+++ b/src/reachy2_sdk/parts/__init__.py
@@ -0,0 +1,6 @@
+"""
+# Parts module.
+
+This module provides Reachy's parts: Arm, Head and Hands.
+
+"""
diff --git a/src/reachy2_sdk/arm.py b/src/reachy2_sdk/parts/arm.py
similarity index 98%
rename from src/reachy2_sdk/arm.py
rename to src/reachy2_sdk/parts/arm.py
index d017c2ce..029861c9 100644
--- a/src/reachy2_sdk/arm.py
+++ b/src/reachy2_sdk/parts/arm.py
@@ -43,15 +43,15 @@
 )
 from reachy2_sdk_api.part_pb2 import PartId
 
-from .hand import Hand
-from .orbita2d import Orbita2d
-from .orbita3d import Orbita3d
-from .orbita_utils import OrbitaJoint
-from .utils import (
+from ..orbita.orbita2d import Orbita2d
+from ..orbita.orbita3d import Orbita3d
+from ..orbita.orbita_joint import OrbitaJoint
+from ..utils.utils import (
     arm_position_to_list,
     get_grpc_interpolation_mode,
     list_to_arm_position,
 )
+from .hand import Hand
 
 
 class Arm:
diff --git a/src/reachy2_sdk/hand.py b/src/reachy2_sdk/parts/hand.py
similarity index 97%
rename from src/reachy2_sdk/hand.py
rename to src/reachy2_sdk/parts/hand.py
index 24669c12..ee976f47 100644
--- a/src/reachy2_sdk/hand.py
+++ b/src/reachy2_sdk/parts/hand.py
@@ -25,8 +25,6 @@ def __init__(self, hand_msg: Hand_proto, initial_state: HandState, grpc_channel:
 
         self._setup_hand(hand_msg, initial_state)
 
-        self._compliant: bool
-
     def __repr__(self) -> str:
         """Clean representation of a Hand."""
         return f"Hand with part_id {self.part_id} and opening of {self.opening}%"
@@ -40,7 +38,7 @@ def _setup_hand(self, hand_msg: Hand_proto, initial_state: HandState) -> None:
         self._present_position: float = round(np.rad2deg(initial_state.present_position.parallel_gripper.position), 1)
         self._goal_position: float = round(np.rad2deg(initial_state.goal_position.parallel_gripper.position), 1)
         self._opening: float = initial_state.opening.value
-        self._compliant = initial_state.compliant.value
+        self._compliant: bool = initial_state.compliant.value
 
     @property
     def opening(self) -> float:
diff --git a/src/reachy2_sdk/head.py b/src/reachy2_sdk/parts/head.py
similarity index 98%
rename from src/reachy2_sdk/head.py
rename to src/reachy2_sdk/parts/head.py
index 3fd299ea..f3477d7d 100644
--- a/src/reachy2_sdk/head.py
+++ b/src/reachy2_sdk/parts/head.py
@@ -32,9 +32,9 @@
 from reachy2_sdk_api.kinematics_pb2 import ExtEulerAngles, Point, Quaternion, Rotation3d
 from reachy2_sdk_api.part_pb2 import PartId
 
-from .orbita3d import Orbita3d
-from .orbita_utils import OrbitaJoint
-from .utils import get_grpc_interpolation_mode
+from ..orbita.orbita3d import Orbita3d
+from ..orbita.orbita_joint import OrbitaJoint
+from ..utils.utils import get_grpc_interpolation_mode
 
 # from .dynamixel_motor import DynamixelMotor
 
diff --git a/src/reachy2_sdk/reachy_sdk.py b/src/reachy2_sdk/reachy_sdk.py
index 726f23ce..b0660deb 100644
--- a/src/reachy2_sdk/reachy_sdk.py
+++ b/src/reachy2_sdk/reachy_sdk.py
@@ -9,13 +9,12 @@
 
 # from reachy2_sdk_api.dynamixel_motor_pb2_grpc import DynamixelMotorServiceStub
 # from .dynamixel_motor import DynamixelMotor
-from __future__ import annotations
+
 
 import asyncio
 import atexit
 import threading
 import time
-import typing as t
 from collections import namedtuple
 from logging import getLogger
 from typing import Any, Dict, List, Optional
@@ -34,14 +33,15 @@
 from reachy2_sdk_api.orbita3d_pb2 import Orbita3dsCommand
 from reachy2_sdk_api.orbita3d_pb2_grpc import Orbita3dServiceStub
 
-from .arm import Arm
-from .audio import Audio
-from .head import Head
-from .orbita2d import Orbita2d
-from .orbita3d import Orbita3d
-from .orbita_utils import OrbitaJoint
-from .reachy import ReachyInfo, get_config
-from .utils import (
+from .config.reachy_info import ReachyInfo, get_config
+from .media.audio import Audio
+from .orbita.orbita2d import Orbita2d
+from .orbita.orbita3d import Orbita3d
+from .orbita.orbita_joint import OrbitaJoint
+from .parts.arm import Arm
+from .parts.head import Head
+from .utils.singleton import Singleton
+from .utils.utils import (
     arm_position_to_list,
     ext_euler_angles_to_list,
     get_interpolation_mode,
@@ -53,27 +53,6 @@
 GoToHomeId = namedtuple("GoToHomeId", ["head", "r_arm", "l_arm"])
 """Named tuple for easy access to goto request on full body"""
 
-_T = t.TypeVar("_T")
-
-
-class Singleton(type, t.Generic[_T]):
-    """
-    @private.
-    Singleton pattern. Only one robot can be instancied by python kernel.
-    """
-
-    _instances: Dict[Singleton[_T], _T] = {}
-
-    def __call__(cls, *args: t.Any, **kwargs: t.Any) -> _T:
-        if cls not in cls._instances:
-            cls._instances[cls] = super().__call__(*args, **kwargs)
-        else:
-            raise ConnectionError("Cannot open 2 robot connections in the same kernel.")
-        return cls._instances[cls]
-
-    def clear(cls) -> None:
-        del cls._instances[cls]
-
 
 class ReachySDK(metaclass=Singleton):
     """The ReachySDK class handles the connection with your robot.
diff --git a/src/reachy2_sdk/utils/__init__.py b/src/reachy2_sdk/utils/__init__.py
new file mode 100644
index 00000000..49d3952a
--- /dev/null
+++ b/src/reachy2_sdk/utils/__init__.py
@@ -0,0 +1,6 @@
+"""
+# Utils module.
+
+This module contains various useful methods and classes.
+
+"""
diff --git a/src/reachy2_sdk/utils/singleton.py b/src/reachy2_sdk/utils/singleton.py
new file mode 100644
index 00000000..ef984993
--- /dev/null
+++ b/src/reachy2_sdk/utils/singleton.py
@@ -0,0 +1,29 @@
+"""Singleton module.
+
+Defines a singleton pattern. Use by ReachySDK to prevent multiple instance within on python kernel.
+
+"""
+from __future__ import annotations
+
+import typing as t
+from typing import Dict
+
+_T = t.TypeVar("_T")
+
+
+class Singleton(type, t.Generic[_T]):
+    """
+    Singleton pattern. Only one robot can be instancied by python kernel.
+    """
+
+    _instances: Dict[Singleton[_T], _T] = {}
+
+    def __call__(cls, *args: t.Any, **kwargs: t.Any) -> _T:
+        if cls not in cls._instances:
+            cls._instances[cls] = super().__call__(*args, **kwargs)
+        else:
+            raise ConnectionError("Cannot open 2 robot connections in the same kernel.")
+        return cls._instances[cls]
+
+    def clear(cls) -> None:
+        del cls._instances[cls]
diff --git a/src/reachy2_sdk/utils.py b/src/reachy2_sdk/utils/utils.py
similarity index 100%
rename from src/reachy2_sdk/utils.py
rename to src/reachy2_sdk/utils/utils.py


Test patch:
diff --git a/tests/test_arm.py b/tests/test_arm.py
index 92934928..9d8cc895 100644
--- a/tests/test_arm.py
+++ b/tests/test_arm.py
@@ -12,8 +12,7 @@
 from reachy2_sdk_api.orbita3d_pb2 import Float3d, Orbita3dState, PID3d, Vector3d
 from reachy2_sdk_api.part_pb2 import PartId
 
-from src.reachy2_sdk.arm import Arm
-from src.reachy2_sdk.orbita2d import (
+from reachy2_sdk.orbita.orbita2d import (
     Axis,
     Float2d,
     Orbita2dState,
@@ -21,7 +20,8 @@
     Pose2d,
     Vector2d,
 )
-from src.reachy2_sdk.orbita_utils import _to_position
+from reachy2_sdk.orbita.utils import to_position
+from reachy2_sdk.parts.arm import Arm
 
 
 @pytest.mark.offline
@@ -100,67 +100,67 @@ def test_class() -> None:
     assert len(arm.actuators) == 3
     assert isinstance(arm.actuators, dict)
 
-    # use _to_position()  to convert radian to degree
-    assert arm.shoulder.roll.goal_position == _to_position(goal_position.axis_2.value)
-    assert arm.shoulder.roll.present_position == _to_position(present_position.axis_2.value)
-    assert arm.shoulder.pitch.goal_position == _to_position(goal_position.axis_1.value)
-    assert arm.shoulder.pitch.present_position == _to_position(present_position.axis_1.value)
+    # use to_position()  to convert radian to degree
+    assert arm.shoulder.roll.goal_position == to_position(goal_position.axis_2.value)
+    assert arm.shoulder.roll.present_position == to_position(present_position.axis_2.value)
+    assert arm.shoulder.pitch.goal_position == to_position(goal_position.axis_1.value)
+    assert arm.shoulder.pitch.present_position == to_position(present_position.axis_1.value)
 
     with pytest.raises(AttributeError):
         arm.shoulder.yaw
 
     assert arm.elbow.compliant
 
-    assert arm.elbow.yaw.goal_position == _to_position(goal_position.axis_2.value)
-    assert arm.elbow.yaw.present_position == _to_position(present_position.axis_2.value)
-    assert arm.elbow.pitch.goal_position == _to_position(goal_position.axis_1.value)
-    assert arm.elbow.pitch.present_position == _to_position(present_position.axis_1.value)
+    assert arm.elbow.yaw.goal_position == to_position(goal_position.axis_2.value)
+    assert arm.elbow.yaw.present_position == to_position(present_position.axis_2.value)
+    assert arm.elbow.pitch.goal_position == to_position(goal_position.axis_1.value)
+    assert arm.elbow.pitch.present_position == to_position(present_position.axis_1.value)
 
     with pytest.raises(AttributeError):
         arm.elbow.roll
 
     assert arm.wrist.compliant
 
-    assert arm.wrist.roll.goal_position == _to_position(goal_rot.rpy.roll.value)
-    assert arm.wrist.roll.present_position == _to_position(present_rot.rpy.roll.value)
-    assert arm.wrist.pitch.goal_position == _to_position(goal_rot.rpy.pitch.value)
-    assert arm.wrist.pitch.present_position == _to_position(present_rot.rpy.pitch.value)
-    assert arm.wrist.yaw.goal_position == _to_position(goal_rot.rpy.yaw.value)
-    assert arm.wrist.yaw.present_position == _to_position(present_rot.rpy.yaw.value)
+    assert arm.wrist.roll.goal_position == to_position(goal_rot.rpy.roll.value)
+    assert arm.wrist.roll.present_position == to_position(present_rot.rpy.roll.value)
+    assert arm.wrist.pitch.goal_position == to_position(goal_rot.rpy.pitch.value)
+    assert arm.wrist.pitch.present_position == to_position(present_rot.rpy.pitch.value)
+    assert arm.wrist.yaw.goal_position == to_position(goal_rot.rpy.yaw.value)
+    assert arm.wrist.yaw.present_position == to_position(present_rot.rpy.yaw.value)
 
     assert arm.joints["shoulder_pitch"].axis_type == "pitch"
-    assert arm.joints["shoulder_pitch"].goal_position == _to_position(goal_position.axis_1.value)
-    assert arm.joints["shoulder_pitch"].present_position == _to_position(present_position.axis_1.value)
+    assert arm.joints["shoulder_pitch"].goal_position == to_position(goal_position.axis_1.value)
+    assert arm.joints["shoulder_pitch"].present_position == to_position(present_position.axis_1.value)
 
     assert arm.joints["shoulder_roll"].axis_type == "roll"
-    assert arm.joints["shoulder_roll"].goal_position == _to_position(goal_position.axis_2.value)
-    assert arm.joints["shoulder_roll"].present_position == _to_position(present_position.axis_2.value)
+    assert arm.joints["shoulder_roll"].goal_position == to_position(goal_position.axis_2.value)
+    assert arm.joints["shoulder_roll"].present_position == to_position(present_position.axis_2.value)
 
     with pytest.raises(KeyError):
         arm.joints["shoulder_yaw"]
 
     assert arm.joints["elbow_pitch"].axis_type == "pitch"
-    assert arm.joints["elbow_pitch"].goal_position == _to_position(goal_position.axis_1.value)
-    assert arm.joints["elbow_pitch"].present_position == _to_position(present_position.axis_1.value)
+    assert arm.joints["elbow_pitch"].goal_position == to_position(goal_position.axis_1.value)
+    assert arm.joints["elbow_pitch"].present_position == to_position(present_position.axis_1.value)
 
     assert arm.joints["elbow_yaw"].axis_type == "yaw"
-    assert arm.joints["elbow_yaw"].goal_position == _to_position(goal_position.axis_2.value)
-    assert arm.joints["elbow_yaw"].present_position == _to_position(present_position.axis_2.value)
+    assert arm.joints["elbow_yaw"].goal_position == to_position(goal_position.axis_2.value)
+    assert arm.joints["elbow_yaw"].present_position == to_position(present_position.axis_2.value)
 
     with pytest.raises(KeyError):
         arm.joints["elbow_roll"]
 
     assert arm.joints["wrist_pitch"].axis_type == "pitch"
-    assert arm.joints["wrist_pitch"].goal_position == _to_position(goal_rot.rpy.pitch.value)
-    assert arm.joints["wrist_pitch"].present_position == _to_position(present_rot.rpy.pitch.value)
+    assert arm.joints["wrist_pitch"].goal_position == to_position(goal_rot.rpy.pitch.value)
+    assert arm.joints["wrist_pitch"].present_position == to_position(present_rot.rpy.pitch.value)
 
     assert arm.joints["wrist_yaw"].axis_type == "yaw"
-    assert arm.joints["wrist_yaw"].goal_position == _to_position(goal_rot.rpy.yaw.value)
-    assert arm.joints["wrist_yaw"].present_position == _to_position(present_rot.rpy.yaw.value)
+    assert arm.joints["wrist_yaw"].goal_position == to_position(goal_rot.rpy.yaw.value)
+    assert arm.joints["wrist_yaw"].present_position == to_position(present_rot.rpy.yaw.value)
 
     assert arm.joints["wrist_roll"].axis_type == "roll"
-    assert arm.joints["wrist_roll"].goal_position == _to_position(goal_rot.rpy.roll.value)
-    assert arm.joints["wrist_roll"].present_position == _to_position(present_rot.rpy.roll.value)
+    assert arm.joints["wrist_roll"].goal_position == to_position(goal_rot.rpy.roll.value)
+    assert arm.joints["wrist_roll"].present_position == to_position(present_rot.rpy.roll.value)
 
     with pytest.raises(ValueError):
         arm.inverse_kinematics(target=np.zeros((1, 1)))
diff --git a/tests/test_basic_movements.py b/tests/test_basic_movements.py
index d9609b51..ea647e64 100644
--- a/tests/test_basic_movements.py
+++ b/tests/test_basic_movements.py
@@ -171,6 +171,7 @@ def test_head_movements(reachy_sdk_zeroed: ReachySDK) -> None:
 @pytest.mark.online
 def test_basic_get_positions(reachy_sdk_zeroed: ReachySDK) -> None:
     expected_pos1 = [0, 0, 0, 0, 0, 0, 0]
+
     assert np.allclose(reachy_sdk_zeroed.l_arm.get_joints_positions(), expected_pos1, atol=1e-01)
 
     expected_pos2 = [15, 10, 20, -50, 10, 10, 20]
diff --git a/tests/test_hand.py b/tests/test_hand.py
index c3be483b..394601eb 100644
--- a/tests/test_hand.py
+++ b/tests/test_hand.py
@@ -16,7 +16,7 @@
     Temperatures,
 )
 
-from reachy2_sdk.hand import Hand
+from reachy2_sdk.parts.hand import Hand
 from src.reachy2_sdk.reachy_sdk import ReachySDK
 
 
diff --git a/tests/test_head.py b/tests/test_head.py
index a966a41e..e0b29451 100644
--- a/tests/test_head.py
+++ b/tests/test_head.py
@@ -7,8 +7,8 @@
 from reachy2_sdk_api.kinematics_pb2 import ExtEulerAngles, Rotation3d
 from reachy2_sdk_api.orbita3d_pb2 import Float3d, Orbita3dState, PID3d, Vector3d
 
-from src.reachy2_sdk.head import Head
-from src.reachy2_sdk.orbita_utils import _to_position
+from reachy2_sdk.orbita.utils import to_position
+from reachy2_sdk.parts.head import Head
 
 
 @pytest.mark.offline
@@ -57,25 +57,25 @@ def test_class() -> None:
     assert len(head.actuators) == 1
     assert isinstance(head.actuators, dict)
 
-    # use _to_position()  to convert radian to degree
-    assert head.neck.roll.goal_position == _to_position(goal_rot.rpy.roll.value)
-    assert head.neck.roll.present_position == _to_position(present_rot.rpy.roll.value)
-    assert head.neck.pitch.goal_position == _to_position(goal_rot.rpy.pitch.value)
-    assert head.neck.pitch.present_position == _to_position(present_rot.rpy.pitch.value)
-    assert head.neck.yaw.goal_position == _to_position(goal_rot.rpy.yaw.value)
-    assert head.neck.yaw.present_position == _to_position(present_rot.rpy.yaw.value)
+    # use to_position()  to convert radian to degree
+    assert head.neck.roll.goal_position == to_position(goal_rot.rpy.roll.value)
+    assert head.neck.roll.present_position == to_position(present_rot.rpy.roll.value)
+    assert head.neck.pitch.goal_position == to_position(goal_rot.rpy.pitch.value)
+    assert head.neck.pitch.present_position == to_position(present_rot.rpy.pitch.value)
+    assert head.neck.yaw.goal_position == to_position(goal_rot.rpy.yaw.value)
+    assert head.neck.yaw.present_position == to_position(present_rot.rpy.yaw.value)
 
     assert head.joints["pitch"].axis_type == "pitch"
-    assert head.joints["pitch"].goal_position == _to_position(goal_rot.rpy.pitch.value)
-    assert head.joints["pitch"].present_position == _to_position(present_rot.rpy.pitch.value)
+    assert head.joints["pitch"].goal_position == to_position(goal_rot.rpy.pitch.value)
+    assert head.joints["pitch"].present_position == to_position(present_rot.rpy.pitch.value)
 
     assert head.joints["yaw"].axis_type == "yaw"
-    assert head.joints["yaw"].goal_position == _to_position(goal_rot.rpy.yaw.value)
-    assert head.joints["yaw"].present_position == _to_position(present_rot.rpy.yaw.value)
+    assert head.joints["yaw"].goal_position == to_position(goal_rot.rpy.yaw.value)
+    assert head.joints["yaw"].present_position == to_position(present_rot.rpy.yaw.value)
 
     assert head.joints["roll"].axis_type == "roll"
-    assert head.joints["roll"].goal_position == _to_position(goal_rot.rpy.roll.value)
-    assert head.joints["roll"].present_position == _to_position(present_rot.rpy.roll.value)
+    assert head.joints["roll"].goal_position == to_position(goal_rot.rpy.roll.value)
+    assert head.joints["roll"].present_position == to_position(present_rot.rpy.roll.value)
 
     # updating values
     compliance = BoolValue(value=False)
@@ -114,21 +114,21 @@ def test_class() -> None:
     assert len(head.actuators) == 1
     assert isinstance(head.actuators, dict)
 
-    assert head.neck.roll.goal_position == _to_position(goal_rot.rpy.roll.value)
-    assert head.neck.roll.present_position == _to_position(present_rot.rpy.roll.value)
-    assert head.neck.pitch.goal_position == _to_position(goal_rot.rpy.pitch.value)
-    assert head.neck.pitch.present_position == _to_position(present_rot.rpy.pitch.value)
-    assert head.neck.yaw.goal_position == _to_position(goal_rot.rpy.yaw.value)
-    assert head.neck.yaw.present_position == _to_position(present_rot.rpy.yaw.value)
+    assert head.neck.roll.goal_position == to_position(goal_rot.rpy.roll.value)
+    assert head.neck.roll.present_position == to_position(present_rot.rpy.roll.value)
+    assert head.neck.pitch.goal_position == to_position(goal_rot.rpy.pitch.value)
+    assert head.neck.pitch.present_position == to_position(present_rot.rpy.pitch.value)
+    assert head.neck.yaw.goal_position == to_position(goal_rot.rpy.yaw.value)
+    assert head.neck.yaw.present_position == to_position(present_rot.rpy.yaw.value)
 
     assert head.joints["pitch"].axis_type == "pitch"
-    assert head.joints["pitch"].goal_position == _to_position(goal_rot.rpy.pitch.value)
-    assert head.joints["pitch"].present_position == _to_position(present_rot.rpy.pitch.value)
+    assert head.joints["pitch"].goal_position == to_position(goal_rot.rpy.pitch.value)
+    assert head.joints["pitch"].present_position == to_position(present_rot.rpy.pitch.value)
 
     assert head.joints["yaw"].axis_type == "yaw"
-    assert head.joints["yaw"].goal_position == _to_position(goal_rot.rpy.yaw.value)
-    assert head.joints["yaw"].present_position == _to_position(present_rot.rpy.yaw.value)
+    assert head.joints["yaw"].goal_position == to_position(goal_rot.rpy.yaw.value)
+    assert head.joints["yaw"].present_position == to_position(present_rot.rpy.yaw.value)
 
     assert head.joints["roll"].axis_type == "roll"
-    assert head.joints["roll"].goal_position == _to_position(goal_rot.rpy.roll.value)
-    assert head.joints["roll"].present_position == _to_position(present_rot.rpy.roll.value)
+    assert head.joints["roll"].goal_position == to_position(goal_rot.rpy.roll.value)
+    assert head.joints["roll"].present_position == to_position(present_rot.rpy.roll.value)
diff --git a/tests/test_orbita2d.py b/tests/test_orbita2d.py
index 479d5ec0..5a95aa0b 100644
--- a/tests/test_orbita2d.py
+++ b/tests/test_orbita2d.py
@@ -3,7 +3,7 @@
 from google.protobuf.wrappers_pb2 import BoolValue, FloatValue
 from reachy2_sdk_api.component_pb2 import PIDGains
 
-from src.reachy2_sdk.orbita2d import (
+from reachy2_sdk.orbita.orbita2d import (
     Axis,
     Float2d,
     Orbita2d,
@@ -12,7 +12,7 @@
     Pose2d,
     Vector2d,
 )
-from src.reachy2_sdk.orbita_utils import _to_position
+from reachy2_sdk.orbita.utils import to_position
 
 
 @pytest.mark.offline
@@ -48,11 +48,11 @@ def test_class() -> None:
     assert orbita2d.__repr__() != ""
 
     assert orbita2d.compliant
-    # use _to_position()  to convert radian to degree
-    assert orbita2d.roll.goal_position == _to_position(goal_position.axis_2.value)
-    assert orbita2d.roll.present_position == _to_position(present_position.axis_2.value)
-    assert orbita2d.pitch.goal_position == _to_position(goal_position.axis_1.value)
-    assert orbita2d.pitch.present_position == _to_position(present_position.axis_1.value)
+    # use to_position()  to convert radian to degree
+    assert orbita2d.roll.goal_position == to_position(goal_position.axis_2.value)
+    assert orbita2d.roll.present_position == to_position(present_position.axis_2.value)
+    assert orbita2d.pitch.goal_position == to_position(goal_position.axis_1.value)
+    assert orbita2d.pitch.present_position == to_position(present_position.axis_1.value)
 
     with pytest.raises(AttributeError):
         orbita2d.yaw
@@ -71,8 +71,8 @@ def test_class() -> None:
     assert torques_set["motor_2"] == torque_limit.motor_2.value
 
     speed_set = orbita2d.get_speed_limit()
-    assert speed_set["motor_1"] == _to_position(speed_limit.motor_1.value)
-    assert speed_set["motor_2"] == _to_position(speed_limit.motor_2.value)
+    assert speed_set["motor_1"] == to_position(speed_limit.motor_1.value)
+    assert speed_set["motor_2"] == to_position(speed_limit.motor_2.value)
 
     orbita2d.temperatures["motor_1"] == temperature.motor_1.value
     orbita2d.temperatures["motor_2"] == temperature.motor_1.value
@@ -135,10 +135,10 @@ def test_class() -> None:
 
     assert not orbita2d.compliant
 
-    assert orbita2d.roll.goal_position == _to_position(goal_position.axis_2.value)
-    assert orbita2d.roll.present_position == _to_position(present_position.axis_2.value)
-    assert orbita2d.pitch.goal_position == _to_position(goal_position.axis_1.value)
-    assert orbita2d.pitch.present_position == _to_position(present_position.axis_1.value)
+    assert orbita2d.roll.goal_position == to_position(goal_position.axis_2.value)
+    assert orbita2d.roll.present_position == to_position(present_position.axis_2.value)
+    assert orbita2d.pitch.goal_position == to_position(goal_position.axis_1.value)
+    assert orbita2d.pitch.present_position == to_position(present_position.axis_1.value)
 
     # pid not changed. testing against old values
     pid_set = orbita2d.get_pid()
@@ -155,8 +155,8 @@ def test_class() -> None:
     assert torques_set["motor_2"] == torque_limit.motor_2.value
 
     speed_set = orbita2d.get_speed_limit()
-    assert speed_set["motor_1"] == _to_position(speed_limit.motor_1.value)
-    assert speed_set["motor_2"] == _to_position(speed_limit.motor_2.value)
+    assert speed_set["motor_1"] == to_position(speed_limit.motor_1.value)
+    assert speed_set["motor_2"] == to_position(speed_limit.motor_2.value)
 
     orbita2d.temperatures["motor_1"] == temperature.motor_1.value
     orbita2d.temperatures["motor_2"] == temperature.motor_1.value
diff --git a/tests/test_orbita3d.py b/tests/test_orbita3d.py
index e0f6e9b6..f33dbc75 100644
--- a/tests/test_orbita3d.py
+++ b/tests/test_orbita3d.py
@@ -5,8 +5,8 @@
 from reachy2_sdk_api.kinematics_pb2 import ExtEulerAngles, Rotation3d
 from reachy2_sdk_api.orbita3d_pb2 import Float3d, Orbita3dState, PID3d, Vector3d
 
-from src.reachy2_sdk.orbita3d import Orbita3d
-from src.reachy2_sdk.orbita_utils import _to_position
+from reachy2_sdk.orbita.orbita3d import Orbita3d
+from reachy2_sdk.orbita.utils import to_position
 
 
 @pytest.mark.offline
@@ -48,13 +48,13 @@ def test_class() -> None:
     with pytest.raises(AttributeError):
         orbita3d.compliant = "wrong value"
 
-    # use _to_position()  to convert radian to degree
-    assert orbita3d.roll.goal_position == _to_position(goal_rot.rpy.roll.value)
-    assert orbita3d.roll.present_position == _to_position(present_rot.rpy.roll.value)
-    assert orbita3d.pitch.goal_position == _to_position(goal_rot.rpy.pitch.value)
-    assert orbita3d.pitch.present_position == _to_position(present_rot.rpy.pitch.value)
-    assert orbita3d.yaw.goal_position == _to_position(goal_rot.rpy.yaw.value)
-    assert orbita3d.yaw.present_position == _to_position(present_rot.rpy.yaw.value)
+    # use to_position()  to convert radian to degree
+    assert orbita3d.roll.goal_position == to_position(goal_rot.rpy.roll.value)
+    assert orbita3d.roll.present_position == to_position(present_rot.rpy.roll.value)
+    assert orbita3d.pitch.goal_position == to_position(goal_rot.rpy.pitch.value)
+    assert orbita3d.pitch.present_position == to_position(present_rot.rpy.pitch.value)
+    assert orbita3d.yaw.goal_position == to_position(goal_rot.rpy.yaw.value)
+    assert orbita3d.yaw.present_position == to_position(present_rot.rpy.yaw.value)
 
     pid_set = orbita3d.get_pid()
     assert pid_set["motor_1"][0] == pid.motor_1.p.value
@@ -75,9 +75,9 @@ def test_class() -> None:
     assert torques_set["motor_3"] == torque_limit.motor_3.value
 
     speed_set = orbita3d.get_speed_limit()
-    assert speed_set["motor_1"] == _to_position(speed_limit.motor_1.value)
-    assert speed_set["motor_2"] == _to_position(speed_limit.motor_2.value)
-    assert speed_set["motor_3"] == _to_position(speed_limit.motor_3.value)
+    assert speed_set["motor_1"] == to_position(speed_limit.motor_1.value)
+    assert speed_set["motor_2"] == to_position(speed_limit.motor_2.value)
+    assert speed_set["motor_3"] == to_position(speed_limit.motor_3.value)
 
     orbita3d.temperatures["motor_1"] == temperature.motor_1.value
     orbita3d.temperatures["motor_2"] == temperature.motor_1.value
@@ -133,12 +133,12 @@ def test_class() -> None:
 
     assert not orbita3d.compliant
 
-    assert orbita3d.roll.goal_position == _to_position(goal_rot.rpy.roll.value)
-    assert orbita3d.roll.present_position == _to_position(present_rot.rpy.roll.value)
-    assert orbita3d.pitch.goal_position == _to_position(goal_rot.rpy.pitch.value)
-    assert orbita3d.pitch.present_position == _to_position(present_rot.rpy.pitch.value)
-    assert orbita3d.yaw.goal_position == _to_position(goal_rot.rpy.yaw.value)
-    assert orbita3d.yaw.present_position == _to_position(present_rot.rpy.yaw.value)
+    assert orbita3d.roll.goal_position == to_position(goal_rot.rpy.roll.value)
+    assert orbita3d.roll.present_position == to_position(present_rot.rpy.roll.value)
+    assert orbita3d.pitch.goal_position == to_position(goal_rot.rpy.pitch.value)
+    assert orbita3d.pitch.present_position == to_position(present_rot.rpy.pitch.value)
+    assert orbita3d.yaw.goal_position == to_position(goal_rot.rpy.yaw.value)
+    assert orbita3d.yaw.present_position == to_position(present_rot.rpy.yaw.value)
 
     # pid not changed. testing against old values
     pid_set = orbita3d.get_pid()
@@ -160,9 +160,9 @@ def test_class() -> None:
     assert torques_set["motor_3"] == torque_limit.motor_3.value
 
     speed_set = orbita3d.get_speed_limit()
-    assert speed_set["motor_1"] == _to_position(speed_limit.motor_1.value)
-    assert speed_set["motor_2"] == _to_position(speed_limit.motor_2.value)
-    assert speed_set["motor_3"] == _to_position(speed_limit.motor_3.value)
+    assert speed_set["motor_1"] == to_position(speed_limit.motor_1.value)
+    assert speed_set["motor_2"] == to_position(speed_limit.motor_2.value)
+    assert speed_set["motor_3"] == to_position(speed_limit.motor_3.value)
 
     orbita3d.temperatures["motor_1"] == temperature.motor_1.value
     orbita3d.temperatures["motor_2"] == temperature.motor_1.value
diff --git a/tests/test_orbita_utils.py b/tests/test_orbita_utils.py
index c35d2260..b76c2136 100644
--- a/tests/test_orbita_utils.py
+++ b/tests/test_orbita_utils.py
@@ -1,9 +1,9 @@
 import pytest
 
-from src.reachy2_sdk.orbita_utils import _to_internal_position
+from reachy2_sdk.orbita.utils import to_internal_position
 
 
 @pytest.mark.offline
 def test_internal_pos() -> None:
     with pytest.raises(TypeError):
-        _to_internal_position("2.3")
+        to_internal_position("2.3")
diff --git a/tests/test_reachy.py b/tests/test_reachy.py
index 8b0c25dc..ae80becd 100644
--- a/tests/test_reachy.py
+++ b/tests/test_reachy.py
@@ -5,7 +5,7 @@
 from reachy2_sdk_api.reachy_pb2 import Reachy
 from reachy2_sdk_api.reachy_pb2 import ReachyInfo as ReachyInfo_proto
 
-from src.reachy2_sdk.reachy import ReachyInfo, get_config
+from reachy2_sdk.config.reachy_info import ReachyInfo, get_config
 
 
 @pytest.mark.offline
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 41516b22..49765f81 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -3,7 +3,7 @@
 from google.protobuf.wrappers_pb2 import FloatValue
 from reachy2_sdk_api.kinematics_pb2 import ExtEulerAngles
 
-from src.reachy2_sdk.utils import (
+from reachy2_sdk.utils.utils import (
     arm_position_to_list,
     convert_to_degrees,
     convert_to_radians,