reachy-mini 1.2.5rc1__py3-none-any.whl → 1.2.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. reachy_mini/apps/app.py +24 -21
  2. reachy_mini/apps/manager.py +17 -3
  3. reachy_mini/apps/sources/hf_auth.py +92 -0
  4. reachy_mini/apps/sources/hf_space.py +1 -1
  5. reachy_mini/apps/sources/local_common_venv.py +199 -24
  6. reachy_mini/apps/templates/main.py.j2 +4 -3
  7. reachy_mini/daemon/app/dashboard/static/js/apps.js +9 -1
  8. reachy_mini/daemon/app/dashboard/static/js/appstore.js +228 -0
  9. reachy_mini/daemon/app/dashboard/static/js/logs.js +148 -0
  10. reachy_mini/daemon/app/dashboard/templates/logs.html +37 -0
  11. reachy_mini/daemon/app/dashboard/templates/sections/appstore.html +92 -0
  12. reachy_mini/daemon/app/dashboard/templates/sections/cache.html +82 -0
  13. reachy_mini/daemon/app/dashboard/templates/sections/daemon.html +5 -0
  14. reachy_mini/daemon/app/dashboard/templates/settings.html +1 -0
  15. reachy_mini/daemon/app/main.py +172 -7
  16. reachy_mini/daemon/app/models.py +8 -0
  17. reachy_mini/daemon/app/routers/apps.py +56 -0
  18. reachy_mini/daemon/app/routers/cache.py +58 -0
  19. reachy_mini/daemon/app/routers/hf_auth.py +57 -0
  20. reachy_mini/daemon/app/routers/logs.py +124 -0
  21. reachy_mini/daemon/app/routers/state.py +25 -1
  22. reachy_mini/daemon/app/routers/wifi_config.py +75 -0
  23. reachy_mini/daemon/app/services/bluetooth/bluetooth_service.py +1 -1
  24. reachy_mini/daemon/app/services/bluetooth/commands/WIFI_RESET.sh +8 -0
  25. reachy_mini/daemon/app/services/wireless/launcher.sh +8 -2
  26. reachy_mini/daemon/app/services/wireless/reachy-mini-daemon.service +13 -0
  27. reachy_mini/daemon/backend/abstract.py +29 -9
  28. reachy_mini/daemon/backend/mockup_sim/__init__.py +12 -0
  29. reachy_mini/daemon/backend/mockup_sim/backend.py +176 -0
  30. reachy_mini/daemon/backend/mujoco/backend.py +0 -5
  31. reachy_mini/daemon/backend/robot/backend.py +78 -5
  32. reachy_mini/daemon/daemon.py +46 -7
  33. reachy_mini/daemon/utils.py +71 -15
  34. reachy_mini/io/zenoh_client.py +26 -0
  35. reachy_mini/io/zenoh_server.py +10 -6
  36. reachy_mini/kinematics/nn_kinematics.py +2 -2
  37. reachy_mini/kinematics/placo_kinematics.py +15 -15
  38. reachy_mini/media/__init__.py +55 -1
  39. reachy_mini/media/audio_base.py +185 -13
  40. reachy_mini/media/audio_control_utils.py +60 -5
  41. reachy_mini/media/audio_gstreamer.py +97 -16
  42. reachy_mini/media/audio_sounddevice.py +120 -19
  43. reachy_mini/media/audio_utils.py +110 -5
  44. reachy_mini/media/camera_base.py +182 -11
  45. reachy_mini/media/camera_constants.py +132 -4
  46. reachy_mini/media/camera_gstreamer.py +42 -2
  47. reachy_mini/media/camera_opencv.py +83 -5
  48. reachy_mini/media/camera_utils.py +95 -7
  49. reachy_mini/media/media_manager.py +139 -6
  50. reachy_mini/media/webrtc_client_gstreamer.py +142 -13
  51. reachy_mini/media/webrtc_daemon.py +72 -7
  52. reachy_mini/motion/recorded_move.py +76 -2
  53. reachy_mini/reachy_mini.py +196 -40
  54. reachy_mini/tools/reflash_motors.py +1 -1
  55. reachy_mini/tools/scan_motors.py +86 -0
  56. reachy_mini/tools/setup_motor.py +49 -31
  57. reachy_mini/utils/interpolation.py +1 -1
  58. reachy_mini/utils/wireless_version/startup_check.py +278 -21
  59. reachy_mini/utils/wireless_version/update.py +44 -1
  60. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/METADATA +7 -6
  61. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/RECORD +65 -53
  62. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/WHEEL +0 -0
  63. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/entry_points.txt +0 -0
  64. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/licenses/LICENSE +0 -0
  65. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/top_level.txt +0 -0
@@ -30,6 +30,7 @@ from reachy_mini.io import (
30
30
  from reachy_mini.media.media_manager import MediaManager
31
31
  from reachy_mini.tools.reflash_motors import reflash_motors
32
32
 
33
+ from .backend.mockup_sim import MockupSimBackend, MockupSimBackendStatus
33
34
  from .backend.mujoco import MujocoBackend, MujocoBackendStatus
34
35
  from .backend.robot import RobotBackend, RobotBackendStatus
35
36
 
@@ -57,7 +58,7 @@ class Daemon:
57
58
  self.wireless_version = wireless_version
58
59
  self.desktop_app_daemon = desktop_app_daemon
59
60
 
60
- self.backend: "RobotBackend | MujocoBackend | None" = None
61
+ self.backend: "RobotBackend | MujocoBackend | MockupSimBackend | None" = None
61
62
  # Get package version
62
63
  try:
63
64
  package_version = version("reachy_mini")
@@ -72,6 +73,7 @@ class Daemon:
72
73
  wireless_version=wireless_version,
73
74
  desktop_app_daemon=desktop_app_daemon,
74
75
  simulation_enabled=None,
76
+ mockup_sim_enabled=None,
75
77
  backend_status=None,
76
78
  error=None,
77
79
  wlan_ip=None,
@@ -91,9 +93,18 @@ class Daemon:
91
93
  self.logger.error(f"Failed to initialize WebRTC: {e}")
92
94
  self._webrtc = None
93
95
 
96
+ def __del__(self) -> None:
97
+ """Destructor to ensure proper cleanup."""
98
+ self.logger.debug("Cleaning up Daemon resources...")
99
+ if self._webrtc is not None:
100
+ self._webrtc.stop()
101
+ self._webrtc.__del__()
102
+ self._webrtc = None
103
+
94
104
  async def start(
95
105
  self,
96
106
  sim: bool = False,
107
+ mockup_sim: bool = False,
97
108
  serialport: str = "auto",
98
109
  scene: str = "empty",
99
110
  localhost_only: bool = True,
@@ -110,6 +121,7 @@ class Daemon:
110
121
 
111
122
  Args:
112
123
  sim (bool): If True, run in simulation mode using Mujoco. Defaults to False.
124
+ mockup_sim (bool): If True, run in lightweight simulation mode (no MuJoCo). Defaults to False.
113
125
  serialport (str): Serial port for real motors. Defaults to "auto", which will try to find the port automatically.
114
126
  scene (str): Name of the scene to load in simulation mode ("empty" or "minimal"). Defaults to "empty".
115
127
  localhost_only (bool): If True, restrict the server to localhost only clients. Defaults to True.
@@ -131,16 +143,20 @@ class Daemon:
131
143
  return self._status.state
132
144
 
133
145
  self.logger.info(
134
- f"Daemon start parameters: sim={sim}, serialport={serialport}, scene={scene}, localhost_only={localhost_only}, wake_up_on_start={wake_up_on_start}, check_collision={check_collision}, kinematics_engine={kinematics_engine}, headless={headless}, hardware_config_filepath={hardware_config_filepath}"
146
+ f"Daemon start parameters: sim={sim}, mockup_sim={mockup_sim}, serialport={serialport}, scene={scene}, localhost_only={localhost_only}, wake_up_on_start={wake_up_on_start}, check_collision={check_collision}, kinematics_engine={kinematics_engine}, headless={headless}, hardware_config_filepath={hardware_config_filepath}"
135
147
  )
136
148
 
149
+ # mockup-sim behaves exactly like a real robot for apps (they open webcam directly)
150
+ # Only MuJoCo (--sim) sets simulation_enabled=True (streams video via UDP)
137
151
  self._status.simulation_enabled = sim
152
+ self._status.mockup_sim_enabled = mockup_sim
138
153
 
139
154
  if not localhost_only:
140
155
  self._status.wlan_ip = get_ip_address()
141
156
 
142
157
  self._start_params = {
143
158
  "sim": sim,
159
+ "mockup_sim": mockup_sim,
144
160
  "serialport": serialport,
145
161
  "headless": headless,
146
162
  "websocket_uri": websocket_uri,
@@ -157,6 +173,7 @@ class Daemon:
157
173
  self.backend = self._setup_backend(
158
174
  wireless_version=self.wireless_version,
159
175
  sim=sim,
176
+ mockup_sim=mockup_sim,
160
177
  serialport=serialport,
161
178
  scene=scene,
162
179
  check_collision=check_collision,
@@ -348,12 +365,13 @@ class Daemon:
348
365
  self._status.state = DaemonState.STOPPING
349
366
  self.backend.is_shutting_down = True
350
367
  self._thread_event_publish_status.set()
351
- self.zenoh_server.stop()
368
+
352
369
  if self.websocket_server is not None:
353
370
  self.websocket_server.stop()
354
371
 
355
372
  if self._webrtc:
356
- self._webrtc.stop()
373
+ # We use pause() instead of stop() to keep the signalling server running and the producer registered, allowing proper restart.
374
+ self._webrtc.pause()
357
375
 
358
376
  if goto_sleep_on_stop:
359
377
  try:
@@ -378,6 +396,9 @@ class Daemon:
378
396
  self.backend.close()
379
397
  self.backend.ready.clear()
380
398
 
399
+ # zenoh server must be closed after backend finishes to publish all data
400
+ self.zenoh_server.stop()
401
+
381
402
  if self._status.state != DaemonState.ERROR:
382
403
  self.logger.info("Daemon stopped successfully.")
383
404
  self._status.state = DaemonState.STOPPED
@@ -400,6 +421,7 @@ class Daemon:
400
421
  async def restart(
401
422
  self,
402
423
  sim: Optional[bool] = None,
424
+ mockup_sim: Optional[bool] = None,
403
425
  serialport: Optional[str] = None,
404
426
  scene: Optional[str] = None,
405
427
  headless: Optional[bool] = None,
@@ -414,6 +436,7 @@ class Daemon:
414
436
 
415
437
  Args:
416
438
  sim (bool): If True, run in simulation mode using Mujoco. Defaults to None (uses the previous value).
439
+ mockup_sim (bool): If True, run in lightweight simulation mode (no MuJoCo). Defaults to None (uses the previous value).
417
440
  serialport (str): Serial port for real motors. Defaults to None (uses the previous value).
418
441
  scene (str): Name of the scene to load in simulation mode ("empty" or "minimal"). Defaults to None (uses the previous value).
419
442
  headless (bool): If True, run Mujoco in headless mode (no GUI). Defaults to None (uses the previous value).
@@ -442,6 +465,9 @@ class Daemon:
442
465
  )
443
466
  params: dict[str, Any] = {
444
467
  "sim": sim if sim is not None else self._start_params["sim"],
468
+ "mockup_sim": mockup_sim
469
+ if mockup_sim is not None
470
+ else self._start_params["mockup_sim"],
445
471
  "serialport": serialport
446
472
  if serialport is not None
447
473
  else self._start_params["serialport"],
@@ -501,6 +527,7 @@ class Daemon:
501
527
  async def run4ever(
502
528
  self,
503
529
  sim: bool = False,
530
+ mockup_sim: bool = False,
504
531
  serialport: str = "auto",
505
532
  scene: str = "empty",
506
533
  localhost_only: bool = True,
@@ -519,6 +546,7 @@ class Daemon:
519
546
 
520
547
  Args:
521
548
  sim (bool): If True, run in simulation mode using Mujoco. Defaults to False.
549
+ mockup_sim (bool): If True, run in lightweight simulation mode (no MuJoCo). Defaults to False.
522
550
  serialport (str): Serial port for real motors. Defaults to "auto", which will try to find the port automatically.
523
551
  scene (str): Name of the scene to load in simulation mode ("empty" or "minimal"). Defaults to "empty".
524
552
  localhost_only (bool): If True, restrict the server to localhost only clients. Defaults to True.
@@ -534,6 +562,7 @@ class Daemon:
534
562
  """
535
563
  await self.start(
536
564
  sim=sim,
565
+ mockup_sim=mockup_sim,
537
566
  serialport=serialport,
538
567
  scene=scene,
539
568
  localhost_only=localhost_only,
@@ -569,6 +598,7 @@ class Daemon:
569
598
  self,
570
599
  wireless_version: bool,
571
600
  sim: bool,
601
+ mockup_sim: bool,
572
602
  serialport: str,
573
603
  scene: str,
574
604
  check_collision: bool,
@@ -578,8 +608,14 @@ class Daemon:
578
608
  websocket_uri: Optional[str],
579
609
  hardware_config_filepath: str | None = None,
580
610
  reflash_motors_on_start: bool = True,
581
- ) -> "RobotBackend | MujocoBackend":
582
- if sim:
611
+ ) -> "RobotBackend | MujocoBackend | MockupSimBackend":
612
+ if mockup_sim:
613
+ return MockupSimBackend(
614
+ check_collision=check_collision,
615
+ kinematics_engine=kinematics_engine,
616
+ use_audio=use_audio,
617
+ )
618
+ elif sim:
583
619
  return MujocoBackend(
584
620
  scene=scene,
585
621
  check_collision=check_collision,
@@ -645,7 +681,10 @@ class DaemonStatus:
645
681
  wireless_version: bool
646
682
  desktop_app_daemon: bool
647
683
  simulation_enabled: Optional[bool]
648
- backend_status: Optional[RobotBackendStatus | MujocoBackendStatus]
684
+ mockup_sim_enabled: Optional[bool]
685
+ backend_status: Optional[
686
+ RobotBackendStatus | MujocoBackendStatus | MockupSimBackendStatus
687
+ ]
649
688
  error: Optional[str] = None
650
689
  wlan_ip: Optional[str] = None
651
690
  version: Optional[str] = None
@@ -1,7 +1,6 @@
1
1
  """Utilities for managing the Reachy Mini daemon."""
2
2
 
3
3
  import os
4
- import socket
5
4
  import struct
6
5
  import subprocess
7
6
  import time
@@ -11,6 +10,46 @@ from typing import Any, List
11
10
  import psutil
12
11
  import serial.tools.list_ports
13
12
 
13
+ # Path to the unix socket created by WebRTC daemon for local camera access
14
+ CAMERA_SOCKET_PATH = "/tmp/reachymini_camera_socket"
15
+
16
+
17
+ def is_localhost(ip: str | None) -> bool:
18
+ """Check if an IP address corresponds to localhost.
19
+
20
+ Args:
21
+ ip: The IP address to check. Can be None.
22
+
23
+ Returns:
24
+ True if the IP is a localhost address, False otherwise.
25
+
26
+ """
27
+ if ip is None:
28
+ return False
29
+
30
+ localhost_addresses = {
31
+ "127.0.0.1",
32
+ "::1",
33
+ "localhost",
34
+ "0.0.0.0",
35
+ }
36
+ return ip in localhost_addresses or ip.startswith("127.")
37
+
38
+
39
+ def is_local_camera_available() -> bool:
40
+ """Check if local camera access is available via the unix socket.
41
+
42
+ On wireless Reachy Mini, the WebRTC daemon exposes raw camera frames
43
+ via a unix socket at /tmp/reachymini_camera_socket. Local clients
44
+ (running on the CM4) can access this socket directly without going
45
+ through WebRTC encoding/decoding, which saves CPU and reduces latency.
46
+
47
+ Returns:
48
+ True if the local camera socket exists and is accessible.
49
+
50
+ """
51
+ return os.path.exists(CAMERA_SOCKET_PATH)
52
+
14
53
 
15
54
  def daemon_check(spawn_daemon: bool, use_sim: bool) -> None:
16
55
  """Check if the Reachy Mini daemon is running and spawn it if necessary."""
@@ -88,20 +127,37 @@ def find_serial_port(
88
127
 
89
128
 
90
129
  def get_ip_address(ifname: str = "wlan0") -> str | None:
91
- """Get the IP address of a specific network interface (Linux Only)."""
92
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
93
- try:
94
- import fcntl
95
-
96
- return socket.inet_ntoa(
97
- fcntl.ioctl(
98
- s.fileno(),
99
- 0x8915, # SIOCGIFADDR
100
- struct.pack("256s", ifname[:15].encode("utf-8")),
101
- )[20:24]
102
- )
103
- except OSError:
104
- print(f"Could not get IP address for interface {ifname}.")
130
+ """Get the IP address of a specific network interface (Linux and Windows)."""
131
+ import platform
132
+ import socket
133
+
134
+ if platform.system() == "Linux":
135
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
136
+ try:
137
+ import fcntl
138
+
139
+ return socket.inet_ntoa(
140
+ fcntl.ioctl(
141
+ s.fileno(),
142
+ 0x8915, # SIOCGIFADDR
143
+ struct.pack("256s", ifname[:15].encode("utf-8")),
144
+ )[20:24]
145
+ )
146
+ except OSError:
147
+ print(f"Could not get IP address for interface {ifname}.")
148
+ return None
149
+ elif platform.system() == "Windows":
150
+ import psutil
151
+
152
+ addrs = psutil.net_if_addrs()
153
+ if ifname in addrs:
154
+ for snic in addrs[ifname]:
155
+ if snic.family == socket.AF_INET:
156
+ return str(snic.address)
157
+ print(f"Could not get IP address for interface {ifname} on Windows.")
158
+ return None
159
+ else:
160
+ print(f"Platform {platform.system()} not supported for get_ip_address.")
105
161
  return None
106
162
 
107
163
 
@@ -59,6 +59,7 @@ class ZenohClient(AbstractClient):
59
59
  self.joint_position_received = threading.Event()
60
60
  self.head_pose_received = threading.Event()
61
61
  self.status_received = threading.Event()
62
+ self.imu_data_received = threading.Event()
62
63
 
63
64
  self.session = zenoh.open(c)
64
65
  self.cmd_pub = self.session.declare_publisher(f"{self.prefix}/command")
@@ -83,6 +84,11 @@ class ZenohClient(AbstractClient):
83
84
  self._handle_status,
84
85
  )
85
86
 
87
+ self.imu_sub = self.session.declare_subscriber(
88
+ f"{self.prefix}/imu_data",
89
+ self._handle_imu_data,
90
+ )
91
+
86
92
  self._last_head_joint_positions = None
87
93
  self._last_antennas_joint_positions = None
88
94
  self._last_head_pose: Optional[npt.NDArray[np.float64]] = None
@@ -92,6 +98,7 @@ class ZenohClient(AbstractClient):
92
98
  self._recorded_data_ready = threading.Event()
93
99
  self._is_alive = False
94
100
  self._last_status: Dict[str, Any] = {} # contains a DaemonStatus
101
+ self._last_imu_data: Optional[Dict[str, List[float] | float]] = None
95
102
 
96
103
  self.tasks: dict[UUID, TaskState] = {}
97
104
  self.task_request_pub = self.session.declare_publisher(f"{self.prefix}/task")
@@ -178,6 +185,13 @@ class ZenohClient(AbstractClient):
178
185
  self._last_status = status
179
186
  self.status_received.set()
180
187
 
188
+ def _handle_imu_data(self, sample: zenoh.Sample) -> None:
189
+ """Handle incoming IMU data."""
190
+ if sample.payload:
191
+ imu_data = json.loads(sample.payload.to_string())
192
+ self._last_imu_data = imu_data
193
+ self.imu_data_received.set()
194
+
181
195
  def get_current_joints(self) -> tuple[list[float], list[float]]:
182
196
  """Get the current joint positions."""
183
197
  assert (
@@ -214,6 +228,18 @@ class ZenohClient(AbstractClient):
214
228
  self.status_received.clear() # ready for next run
215
229
  return self._last_status
216
230
 
231
+ def get_current_imu_data(self) -> Optional[Dict[str, List[float] | float]]:
232
+ """Get the current IMU data.
233
+
234
+ Returns:
235
+ dict with 'accelerometer', 'gyroscope', 'quaternion', and 'temperature' keys,
236
+ or None if no data has been received yet or IMU is not available.
237
+
238
+ """
239
+ if self._last_imu_data is None:
240
+ return None
241
+ return self._last_imu_data.copy()
242
+
217
243
  def _handle_head_pose(self, sample: zenoh.Sample) -> None:
218
244
  """Handle incoming head pose."""
219
245
  if sample.payload:
@@ -72,13 +72,11 @@ class ZenohServer(AbstractServer):
72
72
  },
73
73
  # Allow standard discovery
74
74
  "scouting": {
75
- "multicast": { "enabled": True },
76
- "gossip": { "enabled": True },
75
+ "multicast": {"enabled": True},
76
+ "gossip": {"enabled": True},
77
77
  },
78
78
  # No forced connect target; router will accept incoming sessions
79
- "connect": {
80
- "endpoints": []
81
- }
79
+ "connect": {"endpoints": []},
82
80
  }
83
81
  )
84
82
  )
@@ -96,6 +94,10 @@ class ZenohServer(AbstractServer):
96
94
  self.pub_pose = self.session.declare_publisher(f"{self.prefix}/head_pose")
97
95
  self.backend.set_pose_publisher(self.pub_pose)
98
96
 
97
+ # Declare IMU data publisher
98
+ self.pub_imu = self.session.declare_publisher(f"{self.prefix}/imu_data")
99
+ self.backend.set_imu_publisher(self.pub_imu)
100
+
99
101
  self.task_req_sub = self.session.declare_subscriber(
100
102
  f"{self.prefix}/task",
101
103
  self._handle_task_request,
@@ -118,7 +120,9 @@ class ZenohServer(AbstractServer):
118
120
  data = sample.payload.to_string()
119
121
  command = json.loads(data)
120
122
  with self._lock:
121
- block_targets = self.backend.is_move_running # Prevent concurrent target updates from different clients
123
+ block_targets = (
124
+ self.backend.is_move_running
125
+ ) # Prevent concurrent target updates from different clients
122
126
 
123
127
  def _maybe_ignore(field: str) -> bool:
124
128
  """Return True if the command should be ignored while a move runs."""
@@ -13,13 +13,13 @@ class NNKinematics:
13
13
  """Neural Network based FK/IK. Fitted from PlacoKinematics data."""
14
14
 
15
15
  def __init__(self, models_root_path: str):
16
- """Intialize."""
16
+ """Initialize."""
17
17
  self.fk_model_path = f"{models_root_path}/fknetwork.onnx"
18
18
  self.ik_model_path = f"{models_root_path}/iknetwork.onnx"
19
19
  self.fk_infer = OnnxInfer(self.fk_model_path)
20
20
  self.ik_infer = OnnxInfer(self.ik_model_path)
21
21
 
22
- self.automatic_body_yaw = False # No used, kept for canaompatibility
22
+ self.automatic_body_yaw = False # Not used, kept for compatibility
23
23
 
24
24
  def ik(
25
25
  self,
@@ -70,7 +70,7 @@ class PlacoKinematics:
70
70
  # we could go to soft limits to avoid over-constraining the IK
71
71
  # but the current implementation works robustly with hard limits
72
72
  # so we keep the hard limits for now
73
- constrant_type = "hard" # "hard" or "soft"
73
+ constraint_type = "hard" # "hard" or "soft"
74
74
 
75
75
  # IK closing tasks
76
76
  ik_closing_tasks = []
@@ -78,7 +78,7 @@ class PlacoKinematics:
78
78
  ik_closing_task = self.ik_solver.add_relative_position_task(
79
79
  f"closing_{i}_1", f"closing_{i}_2", np.zeros(3)
80
80
  )
81
- ik_closing_task.configure(f"closing_{i}", constrant_type, 1.0)
81
+ ik_closing_task.configure(f"closing_{i}", constraint_type, 1.0)
82
82
  ik_closing_tasks.append(ik_closing_task)
83
83
 
84
84
  # FK closing tasks
@@ -87,7 +87,7 @@ class PlacoKinematics:
87
87
  fk_closing_task = self.fk_solver.add_relative_position_task(
88
88
  f"closing_{i}_1", f"closing_{i}_2", np.zeros(3)
89
89
  )
90
- fk_closing_task.configure(f"closing_{i}", constrant_type, 1.0)
90
+ fk_closing_task.configure(f"closing_{i}", constraint_type, 1.0)
91
91
  fk_closing_tasks.append(fk_closing_task)
92
92
 
93
93
  # Add the constraint between the rotated torso and the head
@@ -227,9 +227,9 @@ class PlacoKinematics:
227
227
  self.robot_ik.set_joint_limits("yaw_body", -2.8, 2.8)
228
228
 
229
229
  # initial state
230
- self._inital_q = self.robot.state.q.copy()
231
- self._inital_qd = np.zeros_like(self.robot.state.qd)
232
- self._inital_qdd = np.zeros_like(self.robot.state.qdd)
230
+ self._initial_q = self.robot.state.q.copy()
231
+ self._initial_qd = np.zeros_like(self.robot.state.qd)
232
+ self._initial_qdd = np.zeros_like(self.robot.state.qdd)
233
233
 
234
234
  # initial FK to set the head pose
235
235
  for _ in range(10):
@@ -237,11 +237,11 @@ class PlacoKinematics:
237
237
  self.robot_ik.update_kinematics()
238
238
 
239
239
  # last good q to revert to in case of collision
240
- self._inital_q = self.robot_ik.state.q.copy()
240
+ self._initial_q = self.robot_ik.state.q.copy()
241
241
  self._last_good_q = self.robot_ik.state.q.copy()
242
242
 
243
243
  # update the robot state to the initial state
244
- self._update_state_to_initial(self.robot) # revert to the inital state
244
+ self._update_state_to_initial(self.robot) # revert to the initial state
245
245
  self.robot.update_kinematics()
246
246
 
247
247
  if self.check_collision:
@@ -262,9 +262,9 @@ class PlacoKinematics:
262
262
  robot (placo.RobotWrapper): The robot wrapper instance to update.
263
263
 
264
264
  """
265
- robot.state.q = self._inital_q
266
- robot.state.qd = self._inital_qd
267
- robot.state.qdd = self._inital_qdd
265
+ robot.state.q = self._initial_q
266
+ robot.state.qd = self._initial_qd
267
+ robot.state.qdd = self._initial_qdd
268
268
 
269
269
  def _pose_distance(
270
270
  self, pose1: npt.NDArray[np.float64], pose2: npt.NDArray[np.float64]
@@ -374,7 +374,7 @@ class PlacoKinematics:
374
374
  self._logger.debug("IK: Poses too far, starting from initial configuration")
375
375
 
376
376
  done = True
377
- # do the inital ik
377
+ # do the initial ik
378
378
  for i in range(no_iterations):
379
379
  try:
380
380
  self.ik_solver.solve(True) # False to not update the kinematics
@@ -398,7 +398,7 @@ class PlacoKinematics:
398
398
  self.robot_ik.update_kinematics()
399
399
 
400
400
  no_iterations += 2 # add a few more iterations
401
- # do the inital ik with 10 iterations
401
+ # do the initial ik with 10 iterations
402
402
  for i in range(no_iterations):
403
403
  try:
404
404
  self.ik_solver.solve(True) # False to not update the kinematics
@@ -455,7 +455,7 @@ class PlacoKinematics:
455
455
  )
456
456
 
457
457
  done = True
458
- # do the inital ik with 2 iterations
458
+ # do the initial ik with 2 iterations
459
459
  for i in range(no_iterations):
460
460
  try:
461
461
  self.fk_solver.solve(True) # False to not update the kinematics
@@ -476,7 +476,7 @@ class PlacoKinematics:
476
476
  self.robot.update_kinematics()
477
477
 
478
478
  no_iterations += 2 # add a few more iterations
479
- # do the inital ik with 10 iterations
479
+ # do the initial ik with 10 iterations
480
480
  for i in range(no_iterations):
481
481
  try:
482
482
  self.fk_solver.solve(True) # False to not update the kinematics
@@ -1 +1,55 @@
1
- """Media module."""
1
+ """Media module for Reachy Mini robot.
2
+
3
+ This module provides comprehensive audio and video capabilities for the Reachy Mini robot,
4
+ supporting multiple backends and offering a unified interface for media operations.
5
+
6
+ The media module includes:
7
+ - Camera capture and video streaming
8
+ - Microphone input and audio recording
9
+ - Speaker output and sound playback
10
+ - Direction of Arrival (DoA) estimation with ReSpeaker microphone array
11
+ - Multiple backend support (OpenCV, GStreamer, SoundDevice, WebRTC)
12
+ - Camera calibration and intrinsic parameter access
13
+ - Cross-platform compatibility
14
+
15
+ Key Components:
16
+ - MediaManager: Unified interface for managing audio and video devices
17
+ - CameraBase: Abstract base class for camera implementations
18
+ - AudioBase: Abstract base class for audio implementations
19
+ - Multiple backend implementations for different use cases
20
+
21
+ Example usage:
22
+ >>> from reachy_mini.media.media_manager import MediaManager, MediaBackend
23
+ >>>
24
+ >>> # Create media manager with default backend
25
+ >>> media = MediaManager(backend=MediaBackend.DEFAULT)
26
+ >>>
27
+ >>> # Capture video frames
28
+ >>> frame = media.get_frame()
29
+ >>> if frame is not None:
30
+ ... cv2.imshow("Camera", frame)
31
+ ... cv2.waitKey(1)
32
+ >>>
33
+ >>> # Record audio
34
+ >>> media.start_recording()
35
+ >>> samples = media.get_audio_sample()
36
+ >>>
37
+ >>> # Play sound
38
+ >>> media.play_sound("/path/to/sound.wav")
39
+ >>>
40
+ >>> # Clean up
41
+ >>> media.close()
42
+
43
+ Available backends:
44
+ - MediaBackend.DEFAULT: OpenCV + SoundDevice (cross-platform default)
45
+ - MediaBackend.GSTREAMER: GStreamer-based media (advanced features)
46
+ - MediaBackend.WEBRTC: WebRTC for real-time communication
47
+ - MediaBackend.NO_MEDIA: No media devices (headless operation)
48
+
49
+ For more information on specific components, see:
50
+ - media_manager.py: Media management and backend selection
51
+ - camera_base.py: Camera interface definition
52
+ - audio_base.py: Audio interface definition
53
+ - camera_opencv.py: OpenCV camera implementation
54
+ - audio_sounddevice.py: SoundDevice audio implementation
55
+ """