dexcontrol 0.2.1__py3-none-any.whl → 0.2.3__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.

Potentially problematic release.


This version of dexcontrol might be problematic. Click here for more details.

Files changed (81) hide show
  1. dexcontrol/__init__.py +14 -3
  2. dexcontrol/apps/dualsense_teleop_base.py +16 -11
  3. dexcontrol/config/__init__.py +10 -5
  4. dexcontrol/config/core/__init__.py +8 -3
  5. dexcontrol/config/core/arm.py +8 -3
  6. dexcontrol/config/core/chassis.py +10 -5
  7. dexcontrol/config/core/hand.py +14 -9
  8. dexcontrol/config/core/head.py +8 -3
  9. dexcontrol/config/core/misc.py +8 -3
  10. dexcontrol/config/core/torso.py +8 -3
  11. dexcontrol/config/sensors/__init__.py +8 -3
  12. dexcontrol/config/sensors/cameras/__init__.py +9 -4
  13. dexcontrol/config/sensors/cameras/rgb_camera.py +18 -5
  14. dexcontrol/config/sensors/cameras/zed_camera.py +36 -0
  15. dexcontrol/config/sensors/imu/__init__.py +10 -5
  16. dexcontrol/config/sensors/imu/chassis_imu.py +21 -0
  17. dexcontrol/config/sensors/imu/zed_imu.py +21 -0
  18. dexcontrol/config/sensors/lidar/__init__.py +8 -3
  19. dexcontrol/config/sensors/lidar/rplidar.py +9 -3
  20. dexcontrol/config/sensors/ultrasonic/__init__.py +8 -3
  21. dexcontrol/config/sensors/ultrasonic/ultrasonic.py +9 -3
  22. dexcontrol/config/sensors/vega_sensors.py +34 -21
  23. dexcontrol/config/vega.py +14 -6
  24. dexcontrol/core/__init__.py +9 -0
  25. dexcontrol/core/arm.py +21 -6
  26. dexcontrol/core/chassis.py +8 -3
  27. dexcontrol/core/component.py +26 -6
  28. dexcontrol/core/hand.py +8 -3
  29. dexcontrol/core/head.py +18 -3
  30. dexcontrol/core/misc.py +94 -16
  31. dexcontrol/core/torso.py +8 -3
  32. dexcontrol/proto/dexcontrol_msg_pb2.py +17 -15
  33. dexcontrol/proto/dexcontrol_msg_pb2.pyi +24 -0
  34. dexcontrol/robot.py +82 -28
  35. dexcontrol/sensors/__init__.py +13 -8
  36. dexcontrol/sensors/camera/__init__.py +11 -6
  37. dexcontrol/sensors/camera/rgb_camera.py +33 -24
  38. dexcontrol/sensors/camera/zed_camera.py +364 -0
  39. dexcontrol/sensors/imu/__init__.py +13 -8
  40. dexcontrol/sensors/imu/chassis_imu.py +155 -0
  41. dexcontrol/sensors/imu/{nine_axis_imu.py → zed_imu.py} +41 -26
  42. dexcontrol/sensors/lidar/__init__.py +11 -1
  43. dexcontrol/sensors/lidar/rplidar.py +8 -3
  44. dexcontrol/sensors/manager.py +22 -9
  45. dexcontrol/sensors/ultrasonic.py +8 -3
  46. dexcontrol/utils/__init__.py +8 -3
  47. dexcontrol/utils/constants.py +10 -0
  48. dexcontrol/utils/io_utils.py +8 -3
  49. dexcontrol/utils/motion_utils.py +8 -3
  50. dexcontrol/utils/os_utils.py +23 -4
  51. dexcontrol/utils/pb_utils.py +8 -3
  52. dexcontrol/utils/rate_limiter.py +8 -3
  53. dexcontrol/utils/rtc_utils.py +144 -0
  54. dexcontrol/utils/subscribers/__init__.py +11 -3
  55. dexcontrol/utils/subscribers/base.py +26 -5
  56. dexcontrol/utils/subscribers/camera.py +10 -6
  57. dexcontrol/utils/subscribers/decoders.py +8 -3
  58. dexcontrol/utils/subscribers/generic.py +8 -3
  59. dexcontrol/utils/subscribers/imu.py +8 -3
  60. dexcontrol/utils/subscribers/lidar.py +8 -3
  61. dexcontrol/utils/subscribers/protobuf.py +8 -3
  62. dexcontrol/utils/subscribers/rtc.py +315 -0
  63. dexcontrol/utils/timer.py +8 -3
  64. dexcontrol/utils/trajectory_utils.py +8 -3
  65. dexcontrol/utils/viz_utils.py +8 -3
  66. dexcontrol/utils/zenoh_utils.py +83 -0
  67. dexcontrol-0.2.3.dist-info/METADATA +265 -0
  68. dexcontrol-0.2.3.dist-info/RECORD +72 -0
  69. {dexcontrol-0.2.1.dist-info → dexcontrol-0.2.3.dist-info}/WHEEL +1 -2
  70. dexcontrol-0.2.3.dist-info/licenses/LICENSE +184 -0
  71. dexcontrol/config/sensors/cameras/gemini_camera.py +0 -16
  72. dexcontrol/config/sensors/imu/gemini_imu.py +0 -15
  73. dexcontrol/config/sensors/imu/nine_axis_imu.py +0 -15
  74. dexcontrol/sensors/camera/gemini_camera.py +0 -139
  75. dexcontrol/sensors/imu/gemini_imu.py +0 -139
  76. dexcontrol/utils/reset_orbbec_camera_usb.py +0 -98
  77. dexcontrol-0.2.1.dist-info/METADATA +0 -369
  78. dexcontrol-0.2.1.dist-info/RECORD +0 -72
  79. dexcontrol-0.2.1.dist-info/licenses/LICENSE +0 -188
  80. dexcontrol-0.2.1.dist-info/licenses/NOTICE +0 -13
  81. dexcontrol-0.2.1.dist-info/top_level.txt +0 -1
@@ -0,0 +1,364 @@
1
+ # Copyright (C) 2025 Dexmate Inc.
2
+ #
3
+ # This software is dual-licensed:
4
+ #
5
+ # 1. GNU Affero General Public License v3.0 (AGPL-3.0)
6
+ # See LICENSE-AGPL for details
7
+ #
8
+ # 2. Commercial License
9
+ # For commercial licensing terms, contact: contact@dexmate.ai
10
+
11
+ """ZED camera sensor implementation using RTC subscribers for RGB and Zenoh subscriber for depth."""
12
+
13
+ import logging
14
+
15
+ import numpy as np
16
+ import zenoh
17
+
18
+ from dexcontrol.utils.os_utils import resolve_key_name
19
+ from dexcontrol.utils.rtc_utils import create_rtc_subscriber_from_zenoh
20
+ from dexcontrol.utils.subscribers.camera import DepthCameraSubscriber
21
+ from dexcontrol.utils.subscribers.rtc import RTCSubscriber
22
+ from dexcontrol.utils.zenoh_utils import query_zenoh_json
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Optional import for depth processing
27
+ try:
28
+ from dexsensor.serialization.camera import decode_depth
29
+ DEXSENSOR_AVAILABLE = True
30
+ except ImportError:
31
+ logger.warning("dexsensor not available. Depth data will be returned without decoding.")
32
+ decode_depth = None
33
+ DEXSENSOR_AVAILABLE = False
34
+
35
+
36
+ class ZedCameraSensor:
37
+ """ZED camera sensor using RTC subscribers for RGB and Zenoh subscriber for depth.
38
+
39
+ This sensor provides left RGB, right RGB, and depth image data from a ZED camera.
40
+ RGB streams use RTC subscribers for efficient data handling, while depth uses
41
+ regular Zenoh subscriber.
42
+
43
+ Note: For depth data decoding, dexsensor package is required.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ configs,
49
+ zenoh_session: zenoh.Session,
50
+ *args,
51
+ **kwargs,
52
+ ) -> None:
53
+ """Initialize the ZED camera sensor.
54
+
55
+ Args:
56
+ configs: Configuration for the ZED camera sensor.
57
+ zenoh_session: Active Zenoh session for communication.
58
+ """
59
+ self._name = configs.name
60
+ self._zenoh_session = zenoh_session
61
+ self._configs = configs
62
+
63
+ # Initialize subscribers dictionary - RGB uses RTC, depth uses Zenoh
64
+ self._subscribers: dict[str, RTCSubscriber | DepthCameraSubscriber | None] = {}
65
+
66
+ # Create subscribers for each enabled stream
67
+ self._create_subscribers()
68
+
69
+ def _create_subscribers(self) -> None:
70
+ """Create subscribers for each enabled stream - RTC for RGB, Zenoh for depth."""
71
+ subscriber_config = self._configs.subscriber_config
72
+
73
+ # Define stream types and their configurations
74
+ streams = {
75
+ 'left_rgb': subscriber_config.get('left_rgb', {}),
76
+ 'right_rgb': subscriber_config.get('right_rgb', {}),
77
+ 'depth': subscriber_config.get('depth', {})
78
+ }
79
+
80
+ for stream_name, stream_config in streams.items():
81
+ if stream_config.get('enable', False):
82
+ try:
83
+ if stream_name == 'depth':
84
+ # Use regular Zenoh subscriber for depth
85
+ topic = stream_config.get('topic')
86
+ if topic:
87
+ subscriber = DepthCameraSubscriber(
88
+ topic=topic,
89
+ zenoh_session=self._zenoh_session,
90
+ name=f"{self._name}_{stream_name}_subscriber",
91
+ enable_fps_tracking=self._configs.enable_fps_tracking,
92
+ fps_log_interval=self._configs.fps_log_interval,
93
+ )
94
+ logger.info(f"Created Zenoh depth subscriber for {self._name} {stream_name}")
95
+ self._subscribers[stream_name] = subscriber
96
+ else:
97
+ logger.warning(f"No topic found for {self._name} {stream_name}")
98
+ self._subscribers[stream_name] = None
99
+ else:
100
+ # Use RTC subscriber for RGB streams
101
+ info_key = stream_config.get('info_key')
102
+ if info_key:
103
+ subscriber = create_rtc_subscriber_from_zenoh(
104
+ zenoh_session=self._zenoh_session,
105
+ info_topic=info_key,
106
+ name=f"{self._name}_{stream_name}_subscriber",
107
+ enable_fps_tracking=self._configs.enable_fps_tracking,
108
+ fps_log_interval=self._configs.fps_log_interval,
109
+ )
110
+
111
+ if subscriber is None:
112
+ logger.warning(f"Failed to create RTC subscriber for {self._name} {stream_name}")
113
+ else:
114
+ logger.info(f"Created RTC subscriber for {self._name} {stream_name}")
115
+
116
+ self._subscribers[stream_name] = subscriber
117
+ else:
118
+ logger.warning(f"No info_key found for {self._name} {stream_name}")
119
+ self._subscribers[stream_name] = None
120
+ except Exception as e:
121
+ logger.error(f"Error creating subscriber for {self._name} {stream_name}: {e}")
122
+ self._subscribers[stream_name] = None
123
+ else:
124
+ logger.info(f"Stream {stream_name} disabled for {self._name}")
125
+ self._subscribers[stream_name] = None
126
+
127
+ # Query for camera info - use info_key from one of the RGB streams
128
+ enabled_rgb_configs = [config for config in [subscriber_config.get('left_rgb'), subscriber_config.get('right_rgb')] if config and config.get('enable')]
129
+ if enabled_rgb_configs:
130
+ info_key = resolve_key_name(enabled_rgb_configs[0].get('info_key')).rstrip('/')
131
+ info_key_root = '/'.join(info_key.split('/')[:-2])
132
+ info_key = f"{info_key_root}/info"
133
+ info = query_zenoh_json(self._zenoh_session, info_key)
134
+ self._camera_info = info
135
+ if info is not None:
136
+ self._depth_min = info.get('depth_min')
137
+ self._depth_max = info.get('depth_max')
138
+ else:
139
+ logger.warning(f"No camera info found for {self._name}")
140
+ self._depth_min = None
141
+ self._depth_max = None
142
+ else:
143
+ logger.warning(f"No enabled RGB streams found for camera info query for {self._name}")
144
+ self._camera_info = None
145
+ self._depth_min = None
146
+ self._depth_max = None
147
+
148
+ def _decode_depth_data(self, encoded_depth_data: bytes | None) -> np.ndarray | None:
149
+ """Decode depth data from encoded bytes to actual depth values.
150
+
151
+ Args:
152
+ encoded_depth_data: Raw depth data as bytes.
153
+
154
+ Returns:
155
+ Decoded depth data as numpy array (HxW).
156
+
157
+ Raises:
158
+ RuntimeError: If dexsensor is not available for depth decoding.
159
+ """
160
+ if encoded_depth_data is None:
161
+ return None
162
+
163
+ if not DEXSENSOR_AVAILABLE or decode_depth is None:
164
+ raise RuntimeError(
165
+ f"dexsensor is required for depth decoding in {self._name}. "
166
+ "Please install dexsensor: pip install dexsensor"
167
+ )
168
+
169
+ try:
170
+ # Decode the depth data from bytes - this returns (depth, depth_min, depth_max)
171
+ depth_decoded = decode_depth(encoded_depth_data)
172
+ return depth_decoded
173
+ except Exception as e:
174
+ raise RuntimeError(f"Failed to decode depth data for {self._name}: {e}")
175
+
176
+ def shutdown(self) -> None:
177
+ """Shutdown the camera sensor."""
178
+ for stream_name, subscriber in self._subscribers.items():
179
+ if subscriber:
180
+ try:
181
+ subscriber.shutdown()
182
+ logger.info(f"Shut down {stream_name} subscriber for {self._name}")
183
+ except Exception as e:
184
+ logger.error(f"Error shutting down {stream_name} subscriber for {self._name}: {e}")
185
+
186
+ def is_active(self) -> bool:
187
+ """Check if any camera stream is actively receiving data.
188
+
189
+ Returns:
190
+ True if at least one stream is receiving data, False otherwise.
191
+ """
192
+ for subscriber in self._subscribers.values():
193
+ if subscriber and subscriber.is_active():
194
+ return True
195
+ return False
196
+
197
+ def is_stream_active(self, stream_name: str) -> bool:
198
+ """Check if a specific stream is actively receiving data.
199
+
200
+ Args:
201
+ stream_name: Name of the stream ('left_rgb', 'right_rgb', 'depth').
202
+
203
+ Returns:
204
+ True if the stream is receiving data, False otherwise.
205
+ """
206
+ subscriber = self._subscribers.get(stream_name)
207
+ return subscriber.is_active() if subscriber else False
208
+
209
+ def wait_for_active(self, timeout: float = 5.0, require_all: bool = False) -> bool:
210
+ """Wait for camera streams to start receiving data.
211
+
212
+ Args:
213
+ timeout: Maximum time to wait in seconds.
214
+ require_all: If True, wait for all enabled streams. If False, wait for any stream.
215
+
216
+ Returns:
217
+ True if condition is met, False if timeout is reached.
218
+ """
219
+ active_subscribers = [sub for sub in self._subscribers.values() if sub is not None]
220
+
221
+ if not active_subscribers:
222
+ logger.warning(f"No active subscribers for {self._name}")
223
+ return False
224
+
225
+ if require_all:
226
+ # Wait for all subscribers to become active
227
+ for subscriber in active_subscribers:
228
+ if not subscriber.wait_for_active(timeout):
229
+ return False
230
+ return True
231
+ else:
232
+ # Wait for any subscriber to become active
233
+ import time
234
+ start_time = time.time()
235
+ while time.time() - start_time < timeout:
236
+ if self.is_active():
237
+ return True
238
+ time.sleep(0.1)
239
+ return False
240
+
241
+ def get_obs(self, obs_keys: list[str] | None = None) -> dict[str, np.ndarray]:
242
+ """Get the latest image data from specified streams.
243
+
244
+ Args:
245
+ obs_keys: List of stream names to get data from.
246
+ If None, gets data from all enabled streams.
247
+
248
+ Returns:
249
+ Dictionary mapping stream names to image arrays (HxWxC for RGB, HxW for depth)
250
+ if available, None otherwise.
251
+
252
+ Raises:
253
+ RuntimeError: If depth data is requested but dexsensor is not available.
254
+ """
255
+ if obs_keys is None:
256
+ obs_keys = list(self._subscribers.keys())
257
+
258
+ obs_out = {}
259
+ for key in obs_keys:
260
+ if key in self._subscribers:
261
+ subscriber = self._subscribers[key]
262
+ if subscriber:
263
+ raw_data = subscriber.get_latest_data()
264
+ obs_out[key] = raw_data
265
+ else:
266
+ obs_out[key] = None
267
+ else:
268
+ logger.warning(f"Unknown stream key: {key} for {self._name}")
269
+
270
+ return obs_out
271
+
272
+ def get_left_rgb(self) -> np.ndarray | None:
273
+ """Get the latest left RGB image.
274
+
275
+ Returns:
276
+ Latest left RGB image as numpy array (HxWxC) if available, None otherwise.
277
+ """
278
+ subscriber = self._subscribers.get('left_rgb')
279
+ return subscriber.get_latest_data() if subscriber else None
280
+
281
+ def get_right_rgb(self) -> np.ndarray | None:
282
+ """Get the latest right RGB image.
283
+
284
+ Returns:
285
+ Latest right RGB image as numpy array (HxWxC) if available, None otherwise.
286
+ """
287
+ subscriber = self._subscribers.get('right_rgb')
288
+ return subscriber.get_latest_data() if subscriber else None
289
+
290
+ def get_depth(self) -> np.ndarray | None:
291
+ """Get the latest depth image.
292
+
293
+ Returns:
294
+ Latest depth image as numpy array (HxW) if available, None otherwise.
295
+
296
+ Raises:
297
+ RuntimeError: If dexsensor is not available for depth decoding.
298
+ """
299
+ subscriber = self._subscribers.get('depth')
300
+ if not subscriber:
301
+ return None
302
+
303
+ # DepthCameraSubscriber already handles decoding
304
+ return subscriber.get_latest_data()
305
+
306
+ @property
307
+ def fps(self) -> dict[str, float]:
308
+ """Get the current FPS measurement for each stream.
309
+
310
+ Returns:
311
+ Dictionary mapping stream names to their FPS measurements.
312
+ """
313
+ fps_dict = {}
314
+ for stream_name, subscriber in self._subscribers.items():
315
+ if subscriber:
316
+ fps_dict[stream_name] = subscriber.fps
317
+ else:
318
+ fps_dict[stream_name] = 0.0
319
+ return fps_dict
320
+
321
+ @property
322
+ def name(self) -> str:
323
+ """Get the sensor name.
324
+
325
+ Returns:
326
+ Sensor name string.
327
+ """
328
+ return self._name
329
+
330
+ @property
331
+ def available_streams(self) -> list:
332
+ """Get list of available stream names.
333
+
334
+ Returns:
335
+ List of stream names that have active subscribers.
336
+ """
337
+ return [name for name, sub in self._subscribers.items() if sub is not None]
338
+
339
+ @property
340
+ def active_streams(self) -> list:
341
+ """Get list of currently active stream names.
342
+
343
+ Returns:
344
+ List of stream names that are currently receiving data.
345
+ """
346
+ return [name for name, sub in self._subscribers.items() if sub and sub.is_active()]
347
+
348
+ @property
349
+ def dexsensor_available(self) -> bool:
350
+ """Check if dexsensor is available for depth decoding.
351
+
352
+ Returns:
353
+ True if dexsensor is available, False otherwise.
354
+ """
355
+ return DEXSENSOR_AVAILABLE
356
+
357
+ @property
358
+ def camera_info(self) -> dict | None:
359
+ """Get the camera info.
360
+
361
+ Returns:
362
+ Camera info dictionary if available, None otherwise.
363
+ """
364
+ return self._camera_info
@@ -1,7 +1,12 @@
1
- # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
1
+ # Copyright (C) 2025 Dexmate Inc.
2
2
  #
3
- # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
- # Condition v1.0 [see LICENSE for details].
3
+ # This software is dual-licensed:
4
+ #
5
+ # 1. GNU Affero General Public License v3.0 (AGPL-3.0)
6
+ # See LICENSE-AGPL for details
7
+ #
8
+ # 2. Commercial License
9
+ # For commercial licensing terms, contact: contact@dexmate.ai
5
10
 
6
11
  """IMU sensors package for dexcontrol.
7
12
 
@@ -10,13 +15,13 @@ sensors using Zenoh subscribers for data communication.
10
15
 
11
16
  Available sensors:
12
17
  - NineAxisIMUSensor: Standard 9-axis IMU with accelerometer, gyroscope, and magnetometer
13
- - GeminiIMUSensor: IMU specific to Gemini hardware (6-axis: accelerometer + gyroscope)
18
+ - ZedIMUSensor: IMU specific to Zed hardware (6-axis: accelerometer + gyroscope)
14
19
  """
15
20
 
16
- from .gemini_imu import GeminiIMUSensor
17
- from .nine_axis_imu import NineAxisIMUSensor
21
+ from .chassis_imu import ChassisIMUSensor
22
+ from .zed_imu import ZedIMUSensor
18
23
 
19
24
  __all__ = [
20
- "NineAxisIMUSensor",
21
- "GeminiIMUSensor",
25
+ "ChassisIMUSensor",
26
+ "ZedIMUSensor",
22
27
  ]
@@ -0,0 +1,155 @@
1
+ # Copyright (C) 2025 Dexmate Inc.
2
+ #
3
+ # This software is dual-licensed:
4
+ #
5
+ # 1. GNU Affero General Public License v3.0 (AGPL-3.0)
6
+ # See LICENSE-AGPL for details
7
+ #
8
+ # 2. Commercial License
9
+ # For commercial licensing terms, contact: contact@dexmate.ai
10
+
11
+ """Ultrasonic sensor implementations using Zenoh subscribers.
12
+
13
+ This module provides ultrasonic sensor classes that use the generic
14
+ subscriber for distance measurements.
15
+ """
16
+
17
+ from typing import Literal, cast
18
+
19
+ import numpy as np
20
+ import zenoh
21
+
22
+ from dexcontrol.proto import dexcontrol_msg_pb2
23
+ from dexcontrol.utils.subscribers import ProtobufZenohSubscriber
24
+
25
+
26
+ class ChassisIMUSensor:
27
+ """Chassis IMU sensor using Zenoh subscriber.
28
+
29
+ This sensor provides IMU data from the chassis
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ configs,
35
+ zenoh_session: zenoh.Session,
36
+ ) -> None:
37
+ """Initialize the ultrasonic sensor.
38
+
39
+ Args:
40
+ configs: Configuration for the ultrasonic sensor.
41
+ zenoh_session: Active Zenoh session for communication.
42
+ """
43
+ self._name = configs.name
44
+
45
+ # Create the generic subscriber with JSON decoder
46
+ self._subscriber = ProtobufZenohSubscriber(
47
+ topic=configs.topic,
48
+ zenoh_session=zenoh_session,
49
+ message_type=dexcontrol_msg_pb2.IMUState,
50
+ name=f"{self._name}_subscriber",
51
+ enable_fps_tracking=configs.enable_fps_tracking,
52
+ fps_log_interval=configs.fps_log_interval,
53
+ )
54
+
55
+
56
+ def shutdown(self) -> None:
57
+ """Shutdown the ultrasonic sensor."""
58
+ self._subscriber.shutdown()
59
+
60
+ def is_active(self) -> bool:
61
+ """Check if the ultrasonic sensor is actively receiving data.
62
+
63
+ Returns:
64
+ True if receiving data, False otherwise.
65
+ """
66
+ return self._subscriber.is_active()
67
+
68
+ def wait_for_active(self, timeout: float = 5.0) -> bool:
69
+ """Wait for the ultrasonic sensor to start receiving data.
70
+
71
+ Args:
72
+ timeout: Maximum time to wait in seconds.
73
+
74
+ Returns:
75
+ True if sensor becomes active, False if timeout is reached.
76
+ """
77
+ return self._subscriber.wait_for_active(timeout)
78
+
79
+ def get_obs(self, obs_keys: list[Literal['ang_vel', 'acc', 'quat']] | None = None) -> dict[str, np.ndarray]:
80
+ """Get observation data for the ZED IMU sensor.
81
+
82
+ Args:
83
+ obs_keys: List of observation keys to retrieve. If None, returns all available data.
84
+ Valid keys: ['ang_vel', 'acc', 'quat']
85
+
86
+ Returns:
87
+ Dictionary with observation data including all IMU measurements.
88
+ Keys are mapped as follows:
89
+ - 'ang_vel': Angular velocity from 'angular_velocity'
90
+ - 'acc': Linear acceleration from 'acceleration'
91
+ - 'quat': Orientation quaternion from 'orientation', in wxyz convention
92
+ """
93
+ if obs_keys is None:
94
+ obs_keys = ['ang_vel', 'acc', 'quat']
95
+
96
+ data = self._subscriber.get_latest_data()
97
+ data = cast(dexcontrol_msg_pb2.IMUState, data)
98
+ if data is None:
99
+ raise RuntimeError("No IMU data available")
100
+
101
+ obs_out = {}
102
+
103
+ for key in obs_keys:
104
+ if key == 'ang_vel':
105
+ obs_out[key] = np.array([data.gyro_x, data.gyro_y, data.gyro_z])
106
+ elif key == 'acc':
107
+ obs_out[key] = np.array([data.acc_x, data.acc_y, data.acc_z])
108
+ elif key == 'quat':
109
+ obs_out[key] = np.array([data.quat_w, data.quat_x, data.quat_y, data.quat_z])
110
+ else:
111
+ raise ValueError(f"Invalid observation key: {key}")
112
+
113
+ return obs_out
114
+
115
+ def get_acceleration(self) -> np.ndarray:
116
+ """Get the latest linear acceleration from ZED IMU.
117
+
118
+ Returns:
119
+ Linear acceleration [x, y, z] in m/s² if available, None otherwise.
120
+ """
121
+ return self.get_obs(obs_keys=['acc'])['acc']
122
+
123
+ def get_angular_velocity(self) -> np.ndarray:
124
+ """Get the latest angular velocity from ZED IMU.
125
+
126
+ Returns:
127
+ Angular velocity [x, y, z] in rad/s if available, None otherwise.
128
+ """
129
+ return self.get_obs(obs_keys=['ang_vel'])['ang_vel']
130
+
131
+ def get_orientation(self) -> np.ndarray:
132
+ """Get the latest orientation quaternion from ZED IMU.
133
+
134
+ Returns:
135
+ Orientation quaternion [x, y, z, w] if available, None otherwise.
136
+ """
137
+ return self.get_obs(obs_keys=['quat'])['quat']
138
+
139
+ @property
140
+ def fps(self) -> float:
141
+ """Get the current FPS measurement.
142
+
143
+ Returns:
144
+ Current frames per second measurement.
145
+ """
146
+ return self._subscriber.fps
147
+
148
+ @property
149
+ def name(self) -> str:
150
+ """Get the sensor name.
151
+
152
+ Returns:
153
+ Sensor name string.
154
+ """
155
+ return self._name