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
@@ -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
 
@@ -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 {{
@@ -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