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.
- reachy_mini/apps/app.py +24 -21
- reachy_mini/apps/manager.py +17 -3
- reachy_mini/apps/sources/hf_auth.py +92 -0
- reachy_mini/apps/sources/hf_space.py +1 -1
- reachy_mini/apps/sources/local_common_venv.py +199 -24
- reachy_mini/apps/templates/main.py.j2 +4 -3
- reachy_mini/daemon/app/dashboard/static/js/apps.js +9 -1
- reachy_mini/daemon/app/dashboard/static/js/appstore.js +228 -0
- reachy_mini/daemon/app/dashboard/static/js/logs.js +148 -0
- reachy_mini/daemon/app/dashboard/templates/logs.html +37 -0
- reachy_mini/daemon/app/dashboard/templates/sections/appstore.html +92 -0
- reachy_mini/daemon/app/dashboard/templates/sections/cache.html +82 -0
- reachy_mini/daemon/app/dashboard/templates/sections/daemon.html +5 -0
- reachy_mini/daemon/app/dashboard/templates/settings.html +1 -0
- reachy_mini/daemon/app/main.py +172 -7
- reachy_mini/daemon/app/models.py +8 -0
- reachy_mini/daemon/app/routers/apps.py +56 -0
- reachy_mini/daemon/app/routers/cache.py +58 -0
- reachy_mini/daemon/app/routers/hf_auth.py +57 -0
- reachy_mini/daemon/app/routers/logs.py +124 -0
- reachy_mini/daemon/app/routers/state.py +25 -1
- reachy_mini/daemon/app/routers/wifi_config.py +75 -0
- reachy_mini/daemon/app/services/bluetooth/bluetooth_service.py +1 -1
- reachy_mini/daemon/app/services/bluetooth/commands/WIFI_RESET.sh +8 -0
- reachy_mini/daemon/app/services/wireless/launcher.sh +8 -2
- reachy_mini/daemon/app/services/wireless/reachy-mini-daemon.service +13 -0
- reachy_mini/daemon/backend/abstract.py +29 -9
- reachy_mini/daemon/backend/mockup_sim/__init__.py +12 -0
- reachy_mini/daemon/backend/mockup_sim/backend.py +176 -0
- reachy_mini/daemon/backend/mujoco/backend.py +0 -5
- reachy_mini/daemon/backend/robot/backend.py +78 -5
- reachy_mini/daemon/daemon.py +46 -7
- reachy_mini/daemon/utils.py +71 -15
- reachy_mini/io/zenoh_client.py +26 -0
- reachy_mini/io/zenoh_server.py +10 -6
- reachy_mini/kinematics/nn_kinematics.py +2 -2
- reachy_mini/kinematics/placo_kinematics.py +15 -15
- reachy_mini/media/__init__.py +55 -1
- reachy_mini/media/audio_base.py +185 -13
- reachy_mini/media/audio_control_utils.py +60 -5
- reachy_mini/media/audio_gstreamer.py +97 -16
- reachy_mini/media/audio_sounddevice.py +120 -19
- reachy_mini/media/audio_utils.py +110 -5
- reachy_mini/media/camera_base.py +182 -11
- reachy_mini/media/camera_constants.py +132 -4
- reachy_mini/media/camera_gstreamer.py +42 -2
- reachy_mini/media/camera_opencv.py +83 -5
- reachy_mini/media/camera_utils.py +95 -7
- reachy_mini/media/media_manager.py +139 -6
- reachy_mini/media/webrtc_client_gstreamer.py +142 -13
- reachy_mini/media/webrtc_daemon.py +72 -7
- reachy_mini/motion/recorded_move.py +76 -2
- reachy_mini/reachy_mini.py +196 -40
- reachy_mini/tools/reflash_motors.py +1 -1
- reachy_mini/tools/scan_motors.py +86 -0
- reachy_mini/tools/setup_motor.py +49 -31
- reachy_mini/utils/interpolation.py +1 -1
- reachy_mini/utils/wireless_version/startup_check.py +278 -21
- reachy_mini/utils/wireless_version/update.py +44 -1
- {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/METADATA +7 -6
- {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/RECORD +65 -53
- {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/WHEEL +0 -0
- {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/entry_points.txt +0 -0
- {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/licenses/LICENSE +0 -0
- {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/top_level.txt +0 -0
reachy_mini/daemon/daemon.py
CHANGED
|
@@ -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
|
-
|
|
368
|
+
|
|
352
369
|
if self.websocket_server is not None:
|
|
353
370
|
self.websocket_server.stop()
|
|
354
371
|
|
|
355
372
|
if self._webrtc:
|
|
356
|
-
|
|
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
|
|
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
|
-
|
|
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
|
reachy_mini/daemon/utils.py
CHANGED
|
@@ -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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
reachy_mini/io/zenoh_client.py
CHANGED
|
@@ -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:
|
reachy_mini/io/zenoh_server.py
CHANGED
|
@@ -72,13 +72,11 @@ class ZenohServer(AbstractServer):
|
|
|
72
72
|
},
|
|
73
73
|
# Allow standard discovery
|
|
74
74
|
"scouting": {
|
|
75
|
-
"multicast": {
|
|
76
|
-
"gossip": {
|
|
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 =
|
|
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
|
-
"""
|
|
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 #
|
|
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
|
-
|
|
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}",
|
|
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}",
|
|
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.
|
|
231
|
-
self.
|
|
232
|
-
self.
|
|
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.
|
|
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
|
|
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.
|
|
266
|
-
robot.state.qd = self.
|
|
267
|
-
robot.state.qdd = self.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
reachy_mini/media/__init__.py
CHANGED
|
@@ -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
|
+
"""
|