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
|
@@ -1,4 +1,43 @@
|
|
|
1
|
-
"""Audio implementation using sounddevice backend.
|
|
1
|
+
"""Audio implementation using sounddevice backend.
|
|
2
|
+
|
|
3
|
+
This module provides a cross-platform audio implementation using the sounddevice
|
|
4
|
+
library. It supports microphone input, speaker output, and sound file playback
|
|
5
|
+
across different operating systems (Windows, macOS, Linux).
|
|
6
|
+
|
|
7
|
+
The sounddevice backend features:
|
|
8
|
+
- Cross-platform compatibility
|
|
9
|
+
- Low-latency audio processing
|
|
10
|
+
- Support for multiple audio devices
|
|
11
|
+
- Sound file playback (WAV, OGG, FLAC, etc.)
|
|
12
|
+
- Automatic sample rate and channel conversion
|
|
13
|
+
- Thread-safe audio buffer management
|
|
14
|
+
|
|
15
|
+
Note:
|
|
16
|
+
This class is typically used internally by the MediaManager when the DEFAULT
|
|
17
|
+
backend is selected. Direct usage is possible but usually not necessary.
|
|
18
|
+
|
|
19
|
+
Example usage via MediaManager:
|
|
20
|
+
>>> from reachy_mini.media.media_manager import MediaManager, MediaBackend
|
|
21
|
+
>>>
|
|
22
|
+
>>> # Create media manager with sounddevice backend (default)
|
|
23
|
+
>>> media = MediaManager(backend=MediaBackend.DEFAULT, log_level="INFO")
|
|
24
|
+
>>>
|
|
25
|
+
>>> # Start audio recording
|
|
26
|
+
>>> media.start_recording()
|
|
27
|
+
>>>
|
|
28
|
+
>>> # Get audio samples
|
|
29
|
+
>>> samples = media.get_audio_sample()
|
|
30
|
+
>>> if samples is not None:
|
|
31
|
+
... print(f"Captured {len(samples)} audio samples")
|
|
32
|
+
>>>
|
|
33
|
+
>>> # Play a sound file
|
|
34
|
+
>>> media.play_sound("/path/to/sound.wav")
|
|
35
|
+
>>>
|
|
36
|
+
>>> # Clean up
|
|
37
|
+
>>> media.stop_recording()
|
|
38
|
+
>>> media.close()
|
|
39
|
+
|
|
40
|
+
"""
|
|
2
41
|
|
|
3
42
|
import os
|
|
4
43
|
import threading
|
|
@@ -20,13 +59,32 @@ MAX_INPUT_QUEUE_SECONDS = 60.0
|
|
|
20
59
|
|
|
21
60
|
|
|
22
61
|
class SoundDeviceAudio(AudioBase):
|
|
23
|
-
"""Audio device implementation using sounddevice.
|
|
62
|
+
"""Audio device implementation using sounddevice.
|
|
63
|
+
|
|
64
|
+
This class implements the AudioBase interface using the sounddevice library,
|
|
65
|
+
providing cross-platform audio capture and playback capabilities.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
Inherits all attributes from AudioBase.
|
|
69
|
+
Additionally manages sounddevice streams and audio buffers.
|
|
70
|
+
|
|
71
|
+
"""
|
|
24
72
|
|
|
25
73
|
def __init__(
|
|
26
74
|
self,
|
|
27
75
|
log_level: str = "INFO",
|
|
28
76
|
) -> None:
|
|
29
|
-
"""Initialize the SoundDevice audio device.
|
|
77
|
+
"""Initialize the SoundDevice audio device.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
log_level (str): Logging level for audio operations.
|
|
81
|
+
Default: 'INFO'.
|
|
82
|
+
|
|
83
|
+
Note:
|
|
84
|
+
This constructor initializes the sounddevice audio system and sets up
|
|
85
|
+
the necessary audio streams for recording and playback.
|
|
86
|
+
|
|
87
|
+
"""
|
|
30
88
|
super().__init__(log_level=log_level)
|
|
31
89
|
self._input_stream = None
|
|
32
90
|
self._output_stream = None
|
|
@@ -58,7 +116,10 @@ class SoundDeviceAudio(AudioBase):
|
|
|
58
116
|
return self._input_stream is not None and self._input_stream.active
|
|
59
117
|
|
|
60
118
|
def start_recording(self) -> None:
|
|
61
|
-
"""Open the audio input stream, using ReSpeaker card if available.
|
|
119
|
+
"""Open the audio input stream, using ReSpeaker card if available.
|
|
120
|
+
|
|
121
|
+
See AudioBase.start_recording() for complete documentation.
|
|
122
|
+
"""
|
|
62
123
|
if self._is_recording:
|
|
63
124
|
self.stop_recording()
|
|
64
125
|
|
|
@@ -109,7 +170,10 @@ class SoundDeviceAudio(AudioBase):
|
|
|
109
170
|
self._input_queued_samples += indata.shape[0]
|
|
110
171
|
|
|
111
172
|
def get_audio_sample(self) -> Optional[npt.NDArray[np.float32]]:
|
|
112
|
-
"""Read audio data from the buffer. Returns numpy array or None if empty.
|
|
173
|
+
"""Read audio data from the buffer. Returns numpy array or None if empty.
|
|
174
|
+
|
|
175
|
+
See AudioBase.get_audio_sample() for complete documentation.
|
|
176
|
+
"""
|
|
113
177
|
with self._input_lock:
|
|
114
178
|
if self._input_buffer and len(self._input_buffer) > 0:
|
|
115
179
|
data: npt.NDArray[np.float32] = np.concatenate(
|
|
@@ -122,32 +186,47 @@ class SoundDeviceAudio(AudioBase):
|
|
|
122
186
|
return None
|
|
123
187
|
|
|
124
188
|
def get_input_audio_samplerate(self) -> int:
|
|
125
|
-
"""Get the input samplerate of the audio device.
|
|
189
|
+
"""Get the input samplerate of the audio device.
|
|
190
|
+
|
|
191
|
+
See AudioBase.get_input_audio_samplerate() for complete documentation.
|
|
192
|
+
"""
|
|
126
193
|
return int(
|
|
127
194
|
sd.query_devices(self._input_device_id, "input")["default_samplerate"]
|
|
128
195
|
)
|
|
129
196
|
|
|
130
197
|
def get_output_audio_samplerate(self) -> int:
|
|
131
|
-
"""Get the output samplerate of the audio device.
|
|
198
|
+
"""Get the output samplerate of the audio device.
|
|
199
|
+
|
|
200
|
+
See AudioBase.get_output_audio_samplerate() for complete documentation.
|
|
201
|
+
"""
|
|
132
202
|
return int(
|
|
133
203
|
sd.query_devices(self._output_device_id, "output")["default_samplerate"]
|
|
134
204
|
)
|
|
135
205
|
|
|
136
206
|
def get_input_channels(self) -> int:
|
|
137
|
-
"""Get the number of input channels of the audio device.
|
|
207
|
+
"""Get the number of input channels of the audio device.
|
|
208
|
+
|
|
209
|
+
See AudioBase.get_input_channels() for complete documentation.
|
|
210
|
+
"""
|
|
138
211
|
return min(
|
|
139
212
|
int(sd.query_devices(self._input_device_id, "input")["max_input_channels"]),
|
|
140
213
|
MAX_INPUT_CHANNELS,
|
|
141
214
|
)
|
|
142
215
|
|
|
143
216
|
def get_output_channels(self) -> int:
|
|
144
|
-
"""Get the number of output channels of the audio device.
|
|
217
|
+
"""Get the number of output channels of the audio device.
|
|
218
|
+
|
|
219
|
+
See AudioBase.get_output_channels() for complete documentation.
|
|
220
|
+
"""
|
|
145
221
|
return int(
|
|
146
222
|
sd.query_devices(self._output_device_id, "output")["max_output_channels"]
|
|
147
223
|
)
|
|
148
224
|
|
|
149
225
|
def stop_recording(self) -> None:
|
|
150
|
-
"""Close the audio stream and release resources.
|
|
226
|
+
"""Close the audio stream and release resources.
|
|
227
|
+
|
|
228
|
+
See AudioBase.stop_recording() for complete documentation.
|
|
229
|
+
"""
|
|
151
230
|
if self._is_recording:
|
|
152
231
|
self._input_stream.stop() # type: ignore[attr-defined]
|
|
153
232
|
self._input_stream.close() # type: ignore[attr-defined]
|
|
@@ -155,7 +234,10 @@ class SoundDeviceAudio(AudioBase):
|
|
|
155
234
|
self.logger.info("SoundDevice audio stream closed.")
|
|
156
235
|
|
|
157
236
|
def push_audio_sample(self, data: npt.NDArray[np.float32]) -> None:
|
|
158
|
-
"""Push audio data to the output device.
|
|
237
|
+
"""Push audio data to the output device.
|
|
238
|
+
|
|
239
|
+
See AudioBase.push_audio_sample() for complete documentation.
|
|
240
|
+
"""
|
|
159
241
|
if self._output_stream is not None:
|
|
160
242
|
with self._output_lock:
|
|
161
243
|
self._output_buffer.append(data.copy())
|
|
@@ -169,8 +251,22 @@ class SoundDeviceAudio(AudioBase):
|
|
|
169
251
|
with self._output_lock:
|
|
170
252
|
self._output_buffer.clear()
|
|
171
253
|
|
|
254
|
+
def set_max_output_buffers(self, max_buffers: int) -> None:
|
|
255
|
+
"""Set the maximum number of output buffers to queue in the player.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
max_buffers (int): Maximum number of buffers to queue.
|
|
259
|
+
|
|
260
|
+
"""
|
|
261
|
+
self.logger.warning(
|
|
262
|
+
"set_max_output_buffers is not implemented for SoundDeviceAudio."
|
|
263
|
+
)
|
|
264
|
+
|
|
172
265
|
def start_playing(self) -> None:
|
|
173
|
-
"""Open the audio output stream.
|
|
266
|
+
"""Open the audio output stream.
|
|
267
|
+
|
|
268
|
+
See AudioBase.start_playing() for complete documentation.
|
|
269
|
+
"""
|
|
174
270
|
self.clear_output_buffer()
|
|
175
271
|
|
|
176
272
|
if self._output_stream is not None:
|
|
@@ -195,26 +291,26 @@ class SoundDeviceAudio(AudioBase):
|
|
|
195
291
|
"""Handle audio output stream callback."""
|
|
196
292
|
if status:
|
|
197
293
|
self.logger.warning(f"SoundDevice output status: {status}")
|
|
198
|
-
|
|
294
|
+
|
|
199
295
|
with self._output_lock:
|
|
200
296
|
filled = 0
|
|
201
297
|
while filled < frames and self._output_buffer:
|
|
202
298
|
chunk = self._output_buffer[0]
|
|
203
|
-
|
|
299
|
+
|
|
204
300
|
needed = frames - filled
|
|
205
301
|
available = len(chunk)
|
|
206
302
|
take = min(needed, available)
|
|
207
|
-
|
|
208
|
-
outdata[filled:filled + take] = chunk[:take]
|
|
303
|
+
|
|
304
|
+
outdata[filled : filled + take] = chunk[:take]
|
|
209
305
|
filled += take
|
|
210
|
-
|
|
306
|
+
|
|
211
307
|
if take < available:
|
|
212
308
|
# Partial consumption, keep remainder
|
|
213
309
|
self._output_buffer[0] = chunk[take:]
|
|
214
310
|
else:
|
|
215
311
|
# Fully consumed this chunk
|
|
216
312
|
self._output_buffer.pop(0)
|
|
217
|
-
|
|
313
|
+
|
|
218
314
|
# Only pad with zeros if buffer is truly empty
|
|
219
315
|
if filled < frames:
|
|
220
316
|
outdata[filled:] = 0
|
|
@@ -237,7 +333,10 @@ class SoundDeviceAudio(AudioBase):
|
|
|
237
333
|
return chunk
|
|
238
334
|
|
|
239
335
|
def stop_playing(self) -> None:
|
|
240
|
-
"""Close the audio output stream.
|
|
336
|
+
"""Close the audio output stream.
|
|
337
|
+
|
|
338
|
+
See AudioBase.stop_playing() for complete documentation.
|
|
339
|
+
"""
|
|
241
340
|
if self._output_stream is not None:
|
|
242
341
|
self._output_stream.stop()
|
|
243
342
|
self._output_stream.close()
|
|
@@ -248,6 +347,8 @@ class SoundDeviceAudio(AudioBase):
|
|
|
248
347
|
def play_sound(self, sound_file: str) -> None:
|
|
249
348
|
"""Play a sound file.
|
|
250
349
|
|
|
350
|
+
See AudioBase.play_sound() for complete documentation.
|
|
351
|
+
|
|
251
352
|
Args:
|
|
252
353
|
sound_file (str): Path to the sound file to play. May be given relative to the assets directory or as an absolute path.
|
|
253
354
|
|
reachy_mini/media/audio_utils.py
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
"""Utility functions for audio handling, specifically for detecting the ReSpeaker sound card.
|
|
1
|
+
"""Utility functions for audio handling, specifically for detecting the ReSpeaker sound card.
|
|
2
|
+
|
|
3
|
+
This module provides helper functions for working with the ReSpeaker microphone array
|
|
4
|
+
and managing audio device configuration on Linux systems. It includes functions for
|
|
5
|
+
detecting sound cards, checking configuration files, and managing ALSA configuration.
|
|
6
|
+
|
|
7
|
+
Example usage:
|
|
8
|
+
>>> from reachy_mini.media.audio_utils import get_respeaker_card_number, has_reachymini_asoundrc
|
|
9
|
+
>>>
|
|
10
|
+
>>> # Get the ReSpeaker card number
|
|
11
|
+
>>> card_num = get_respeaker_card_number()
|
|
12
|
+
>>> print(f"ReSpeaker card number: {card_num}")
|
|
13
|
+
>>>
|
|
14
|
+
>>> # Check if .asoundrc is properly configured
|
|
15
|
+
>>> if has_reachymini_asoundrc():
|
|
16
|
+
... print("Reachy Mini audio configuration is properly set up")
|
|
17
|
+
... else:
|
|
18
|
+
... print("Need to configure audio devices")
|
|
19
|
+
"""
|
|
2
20
|
|
|
3
21
|
import logging
|
|
4
22
|
import subprocess
|
|
@@ -6,7 +24,27 @@ from pathlib import Path
|
|
|
6
24
|
|
|
7
25
|
|
|
8
26
|
def _process_card_number_output(output: str) -> int:
|
|
9
|
-
"""Process the output of 'arecord -l' to find the ReSpeaker or Reachy Mini Audio card number.
|
|
27
|
+
"""Process the output of 'arecord -l' to find the ReSpeaker or Reachy Mini Audio card number.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
output (str): The output string from the 'arecord -l' command containing
|
|
31
|
+
information about available audio devices.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
int: The card number of the detected Reachy Mini Audio or ReSpeaker device,
|
|
35
|
+
or 0 if neither is found (default sound card).
|
|
36
|
+
|
|
37
|
+
Note:
|
|
38
|
+
This function parses the output of 'arecord -l' to identify Reachy Mini
|
|
39
|
+
Audio or ReSpeaker devices. It prefers Reachy Mini Audio devices and
|
|
40
|
+
warns if only a ReSpeaker device is found (indicating firmware update needed).
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
>>> output = "card 1: ReachyMiniAudio [reachy mini audio], device 0: USB Audio [USB Audio]"
|
|
44
|
+
>>> card_num = _process_card_number_output(output)
|
|
45
|
+
>>> print(f"Detected card: {card_num}")
|
|
46
|
+
|
|
47
|
+
"""
|
|
10
48
|
lines = output.split("\n")
|
|
11
49
|
for line in lines:
|
|
12
50
|
if "reachy mini audio" in line.lower():
|
|
@@ -25,7 +63,33 @@ def _process_card_number_output(output: str) -> int:
|
|
|
25
63
|
|
|
26
64
|
|
|
27
65
|
def get_respeaker_card_number() -> int:
|
|
28
|
-
"""Return the card number of the ReSpeaker sound card, or 0 if not found.
|
|
66
|
+
"""Return the card number of the ReSpeaker sound card, or 0 if not found.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
int: The card number of the detected ReSpeaker/Reachy Mini Audio device.
|
|
70
|
+
Returns 0 if no specific device is found (uses default sound card),
|
|
71
|
+
or -1 if there's an error running the detection command.
|
|
72
|
+
|
|
73
|
+
Note:
|
|
74
|
+
This function runs 'arecord -l' to list available audio capture devices
|
|
75
|
+
and processes the output to find Reachy Mini Audio or ReSpeaker devices.
|
|
76
|
+
It's primarily used on Linux systems with ALSA audio configuration.
|
|
77
|
+
|
|
78
|
+
The function returns:
|
|
79
|
+
- Positive integer: Card number of detected Reachy Mini Audio device
|
|
80
|
+
- 0: No Reachy Mini Audio device found, using default sound card
|
|
81
|
+
- -1: Error occurred while trying to detect audio devices
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> card_num = get_respeaker_card_number()
|
|
85
|
+
>>> if card_num > 0:
|
|
86
|
+
... print(f"Using Reachy Mini Audio card {card_num}")
|
|
87
|
+
... elif card_num == 0:
|
|
88
|
+
... print("Using default sound card")
|
|
89
|
+
... else:
|
|
90
|
+
... print("Error detecting audio devices")
|
|
91
|
+
|
|
92
|
+
"""
|
|
29
93
|
try:
|
|
30
94
|
result = subprocess.run(
|
|
31
95
|
["arecord", "-l"], capture_output=True, text=True, check=True
|
|
@@ -40,7 +104,27 @@ def get_respeaker_card_number() -> int:
|
|
|
40
104
|
|
|
41
105
|
|
|
42
106
|
def has_reachymini_asoundrc() -> bool:
|
|
43
|
-
"""Check if ~/.asoundrc exists and contains both reachymini_audio_sink and reachymini_audio_src.
|
|
107
|
+
"""Check if ~/.asoundrc exists and contains both reachymini_audio_sink and reachymini_audio_src.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
bool: True if ~/.asoundrc exists and contains the required Reachy Mini
|
|
111
|
+
audio configuration entries, False otherwise.
|
|
112
|
+
|
|
113
|
+
Note:
|
|
114
|
+
This function checks for the presence of the ALSA configuration file
|
|
115
|
+
~/.asoundrc and verifies that it contains the necessary configuration
|
|
116
|
+
entries for Reachy Mini audio devices (reachymini_audio_sink and
|
|
117
|
+
reachymini_audio_src). These entries are required for proper audio
|
|
118
|
+
routing and device management.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> if has_reachymini_asoundrc():
|
|
122
|
+
... print("Reachy Mini audio configuration is properly set up")
|
|
123
|
+
... else:
|
|
124
|
+
... print("Need to configure Reachy Mini audio devices")
|
|
125
|
+
... write_asoundrc_to_home() # Create the configuration
|
|
126
|
+
|
|
127
|
+
"""
|
|
44
128
|
asoundrc_path = Path.home().joinpath(".asoundrc")
|
|
45
129
|
if not asoundrc_path.exists():
|
|
46
130
|
return False
|
|
@@ -68,7 +152,28 @@ def check_reachymini_asoundrc() -> bool:
|
|
|
68
152
|
|
|
69
153
|
|
|
70
154
|
def write_asoundrc_to_home() -> None:
|
|
71
|
-
"""Write the .asoundrc file with Reachy Mini audio configuration to the user's home directory.
|
|
155
|
+
"""Write the .asoundrc file with Reachy Mini audio configuration to the user's home directory.
|
|
156
|
+
|
|
157
|
+
This function creates an ALSA configuration file (.asoundrc) in the user's home directory
|
|
158
|
+
that configures the ReSpeaker sound card for proper audio routing and multi-client support.
|
|
159
|
+
The configuration enables simultaneous audio input and output access, which is essential
|
|
160
|
+
for the Reachy Mini Wireless version's audio functionality.
|
|
161
|
+
|
|
162
|
+
The generated configuration includes:
|
|
163
|
+
- Default audio device settings pointing to the ReSpeaker sound card
|
|
164
|
+
- dmix plugin for multi-client audio output (reachymini_audio_sink)
|
|
165
|
+
- dsnoop plugin for multi-client audio input (reachymini_audio_src)
|
|
166
|
+
- Proper buffer and sample rate settings for optimal performance
|
|
167
|
+
|
|
168
|
+
Note:
|
|
169
|
+
This function automatically detects the ReSpeaker card number and creates a configuration
|
|
170
|
+
tailored to the detected hardware. It is primarily used for the Reachy Mini Wireless version.
|
|
171
|
+
|
|
172
|
+
The configuration file will be created at ~/.asoundrc and will overwrite any existing file
|
|
173
|
+
with the same name. Existing audio configurations should be backed up before calling this function.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
"""
|
|
72
177
|
card_id = get_respeaker_card_number()
|
|
73
178
|
asoundrc_content = f"""
|
|
74
179
|
pcm.!default {{
|
reachy_mini/media/camera_base.py
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
"""Base classes for camera implementations.
|
|
2
2
|
|
|
3
3
|
The camera implementations support various backends and provide a unified
|
|
4
|
-
interface for capturing images.
|
|
4
|
+
interface for capturing images. This module defines the abstract base class
|
|
5
|
+
that all camera implementations should inherit from, ensuring consistent
|
|
6
|
+
API across different camera backends.
|
|
7
|
+
|
|
8
|
+
Available backends include:
|
|
9
|
+
- OpenCV: Cross-platform camera backend using OpenCV library
|
|
10
|
+
- GStreamer: GStreamer-based camera backend for advanced video processing
|
|
11
|
+
- WebRTC: WebRTC-based camera for real-time video communication
|
|
12
|
+
|
|
13
|
+
Example usage:
|
|
14
|
+
>>> from reachy_mini.media.camera_base import CameraBase
|
|
15
|
+
>>> class MyCamera(CameraBase):
|
|
16
|
+
... def open(self) -> None:
|
|
17
|
+
... pass
|
|
18
|
+
... def read(self) -> Optional[npt.NDArray[np.uint8]]:
|
|
19
|
+
... pass
|
|
20
|
+
... def close(self) -> None:
|
|
21
|
+
... pass
|
|
5
22
|
"""
|
|
6
23
|
|
|
7
24
|
import logging
|
|
@@ -19,13 +36,38 @@ from reachy_mini.media.camera_constants import (
|
|
|
19
36
|
|
|
20
37
|
|
|
21
38
|
class CameraBase(ABC):
|
|
22
|
-
"""Abstract class for opening and managing a camera.
|
|
39
|
+
"""Abstract class for opening and managing a camera.
|
|
40
|
+
|
|
41
|
+
This class defines the interface that all camera implementations must follow.
|
|
42
|
+
It provides common camera parameters and methods for managing camera devices,
|
|
43
|
+
including image capture, resolution management, and camera calibration.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
logger (logging.Logger): Logger instance for camera-related messages.
|
|
47
|
+
_resolution (Optional[CameraResolution]): Current camera resolution setting.
|
|
48
|
+
camera_specs (Optional[CameraSpecs]): Camera specifications including
|
|
49
|
+
supported resolutions and calibration parameters.
|
|
50
|
+
resized_K (Optional[npt.NDArray[np.float64]]): Camera intrinsic matrix
|
|
51
|
+
resized to match the current resolution.
|
|
52
|
+
|
|
53
|
+
"""
|
|
23
54
|
|
|
24
55
|
def __init__(
|
|
25
56
|
self,
|
|
26
57
|
log_level: str = "INFO",
|
|
27
58
|
) -> None:
|
|
28
|
-
"""Initialize the camera.
|
|
59
|
+
"""Initialize the camera.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
log_level (str): Logging level for camera operations.
|
|
63
|
+
Options: 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'.
|
|
64
|
+
Default: 'INFO'.
|
|
65
|
+
|
|
66
|
+
Note:
|
|
67
|
+
This constructor initializes the logging system. Camera specifications
|
|
68
|
+
and resolution should be set before calling open().
|
|
69
|
+
|
|
70
|
+
"""
|
|
29
71
|
self.logger = logging.getLogger(__name__)
|
|
30
72
|
self.logger.setLevel(log_level)
|
|
31
73
|
self._resolution: Optional[CameraResolution] = None
|
|
@@ -34,32 +76,116 @@ class CameraBase(ABC):
|
|
|
34
76
|
|
|
35
77
|
@property
|
|
36
78
|
def resolution(self) -> tuple[int, int]:
|
|
37
|
-
"""Get the current camera resolution as a tuple (width, height).
|
|
79
|
+
"""Get the current camera resolution as a tuple (width, height).
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
tuple[int, int]: A tuple containing (width, height) in pixels.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
RuntimeError: If camera resolution has not been set.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> width, height = camera.resolution
|
|
89
|
+
>>> print(f"Camera resolution: {width}x{height}")
|
|
90
|
+
|
|
91
|
+
"""
|
|
38
92
|
if self._resolution is None:
|
|
39
93
|
raise RuntimeError("Camera resolution is not set.")
|
|
40
94
|
return (self._resolution.value[0], self._resolution.value[1])
|
|
41
95
|
|
|
42
96
|
@property
|
|
43
97
|
def framerate(self) -> int:
|
|
44
|
-
"""Get the current camera frames per second.
|
|
98
|
+
"""Get the current camera frames per second.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
int: The current frame rate in frames per second (fps).
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
RuntimeError: If camera resolution has not been set.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> fps = camera.framerate
|
|
108
|
+
>>> print(f"Camera frame rate: {fps} fps")
|
|
109
|
+
|
|
110
|
+
"""
|
|
45
111
|
if self._resolution is None:
|
|
46
112
|
raise RuntimeError("Camera resolution is not set.")
|
|
47
113
|
return int(self._resolution.value[2])
|
|
48
114
|
|
|
49
115
|
@property
|
|
50
116
|
def K(self) -> Optional[npt.NDArray[np.float64]]:
|
|
51
|
-
"""Get the camera intrinsic matrix for the current resolution.
|
|
117
|
+
"""Get the camera intrinsic matrix for the current resolution.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Optional[npt.NDArray[np.float64]]: The 3x3 camera intrinsic matrix
|
|
121
|
+
in the format:
|
|
122
|
+
|
|
123
|
+
[[fx, 0, cx],
|
|
124
|
+
[ 0, fy, cy],
|
|
125
|
+
[ 0, 0, 1]]
|
|
126
|
+
|
|
127
|
+
Where fx, fy are focal lengths in pixels, and cx, cy are the
|
|
128
|
+
principal point coordinates. Returns None if not available.
|
|
129
|
+
|
|
130
|
+
Note:
|
|
131
|
+
The intrinsic matrix is automatically resized to match the current
|
|
132
|
+
camera resolution when set_resolution() is called.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> K = camera.K
|
|
136
|
+
>>> if K is not None:
|
|
137
|
+
... fx, fy = K[0, 0], K[1, 1]
|
|
138
|
+
... cx, cy = K[0, 2], K[1, 2]
|
|
139
|
+
|
|
140
|
+
"""
|
|
52
141
|
return self.resized_K
|
|
53
142
|
|
|
54
143
|
@property
|
|
55
144
|
def D(self) -> Optional[npt.NDArray[np.float64]]:
|
|
56
|
-
"""Get the camera distortion coefficients.
|
|
145
|
+
"""Get the camera distortion coefficients.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Optional[npt.NDArray[np.float64]]: The distortion coefficients
|
|
149
|
+
as a 5-element array [k1, k2, p1, p2, k3] representing radial
|
|
150
|
+
and tangential distortion parameters, or None if not available.
|
|
151
|
+
|
|
152
|
+
Note:
|
|
153
|
+
These coefficients can be used with OpenCV's distortion correction
|
|
154
|
+
functions to undistort captured images.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
>>> D = camera.D
|
|
158
|
+
>>> if D is not None:
|
|
159
|
+
... print(f"Distortion coefficients: {D}")
|
|
160
|
+
|
|
161
|
+
"""
|
|
57
162
|
if self.camera_specs is not None:
|
|
58
163
|
return self.camera_specs.D
|
|
59
164
|
return None
|
|
60
165
|
|
|
61
166
|
def set_resolution(self, resolution: CameraResolution) -> None:
|
|
62
|
-
"""Set the camera resolution.
|
|
167
|
+
"""Set the camera resolution.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
resolution (CameraResolution): The desired camera resolution from
|
|
171
|
+
the CameraResolution enum.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
RuntimeError: If camera specs are not set or if trying to change
|
|
175
|
+
resolution of a Mujoco simulated camera.
|
|
176
|
+
ValueError: If the requested resolution is not supported by the camera.
|
|
177
|
+
|
|
178
|
+
Note:
|
|
179
|
+
This method updates the camera's resolution and automatically rescales
|
|
180
|
+
the camera intrinsic matrix (K) to match the new resolution. The
|
|
181
|
+
rescaling preserves the camera's field of view and principal point
|
|
182
|
+
position relative to the image dimensions.
|
|
183
|
+
|
|
184
|
+
Example:
|
|
185
|
+
>>> from reachy_mini.media.camera_constants import CameraResolution
|
|
186
|
+
>>> camera.set_resolution(CameraResolution.R1280x720at30fps)
|
|
187
|
+
|
|
188
|
+
"""
|
|
63
189
|
if self.camera_specs is None:
|
|
64
190
|
raise RuntimeError(
|
|
65
191
|
"Camera specs not set. Open the camera before setting the resolution."
|
|
@@ -86,15 +212,60 @@ class CameraBase(ABC):
|
|
|
86
212
|
|
|
87
213
|
@abstractmethod
|
|
88
214
|
def open(self) -> None:
|
|
89
|
-
"""Open the camera.
|
|
215
|
+
"""Open the camera.
|
|
216
|
+
|
|
217
|
+
This method should initialize the camera device and prepare it for
|
|
218
|
+
capturing images. After calling this method, read() should be able
|
|
219
|
+
to retrieve camera frames.
|
|
220
|
+
|
|
221
|
+
Note:
|
|
222
|
+
Implementations should handle any necessary resource allocation,
|
|
223
|
+
camera configuration, and error checking. If the camera cannot
|
|
224
|
+
be opened, implementations should log appropriate error messages.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
RuntimeError: If the camera cannot be opened due to hardware
|
|
228
|
+
or configuration issues.
|
|
229
|
+
|
|
230
|
+
"""
|
|
90
231
|
pass
|
|
91
232
|
|
|
92
233
|
@abstractmethod
|
|
93
234
|
def read(self) -> Optional[npt.NDArray[np.uint8]]:
|
|
94
|
-
"""Read an image from the camera. Returns the image or None if error.
|
|
235
|
+
"""Read an image from the camera. Returns the image or None if error.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Optional[npt.NDArray[np.uint8]]: A numpy array containing the
|
|
239
|
+
captured image in BGR format (OpenCV convention), or None if
|
|
240
|
+
no image is available or an error occurred.
|
|
241
|
+
|
|
242
|
+
The array shape is (height, width, 3) where the last dimension
|
|
243
|
+
represents the BGR color channels.
|
|
244
|
+
|
|
245
|
+
Note:
|
|
246
|
+
This method should be called after open() has been called.
|
|
247
|
+
The image resolution can be obtained via the resolution property.
|
|
248
|
+
|
|
249
|
+
Example:
|
|
250
|
+
>>> camera.open()
|
|
251
|
+
>>> frame = camera.read()
|
|
252
|
+
>>> if frame is not None:
|
|
253
|
+
... cv2.imshow("Camera Frame", frame)
|
|
254
|
+
... cv2.waitKey(1)
|
|
255
|
+
|
|
256
|
+
"""
|
|
95
257
|
pass
|
|
96
258
|
|
|
97
259
|
@abstractmethod
|
|
98
260
|
def close(self) -> None:
|
|
99
|
-
"""Close the camera and release resources.
|
|
261
|
+
"""Close the camera and release resources.
|
|
262
|
+
|
|
263
|
+
This method should stop any ongoing image capture and release
|
|
264
|
+
all associated resources. After calling this method, read() should
|
|
265
|
+
return None until open() is called again.
|
|
266
|
+
|
|
267
|
+
Note:
|
|
268
|
+
Implementations should ensure proper cleanup to prevent resource leaks.
|
|
269
|
+
|
|
270
|
+
"""
|
|
100
271
|
pass
|