dexcontrol 0.3.0__py3-none-any.whl → 0.3.1__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 (51) hide show
  1. dexcontrol/__init__.py +16 -7
  2. dexcontrol/apps/dualsense_teleop_base.py +1 -1
  3. dexcontrol/comm/__init__.py +51 -0
  4. dexcontrol/comm/base.py +421 -0
  5. dexcontrol/comm/rtc.py +400 -0
  6. dexcontrol/comm/subscribers.py +329 -0
  7. dexcontrol/config/sensors/cameras/__init__.py +1 -2
  8. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  9. dexcontrol/config/sensors/vega_sensors.py +12 -18
  10. dexcontrol/core/arm.py +29 -25
  11. dexcontrol/core/chassis.py +3 -12
  12. dexcontrol/core/component.py +68 -43
  13. dexcontrol/core/hand.py +50 -52
  14. dexcontrol/core/head.py +14 -26
  15. dexcontrol/core/misc.py +188 -166
  16. dexcontrol/core/robot_query_interface.py +137 -114
  17. dexcontrol/core/torso.py +0 -4
  18. dexcontrol/robot.py +15 -37
  19. dexcontrol/sensors/__init__.py +1 -2
  20. dexcontrol/sensors/camera/__init__.py +0 -2
  21. dexcontrol/sensors/camera/base_camera.py +144 -0
  22. dexcontrol/sensors/camera/rgb_camera.py +67 -63
  23. dexcontrol/sensors/camera/zed_camera.py +89 -147
  24. dexcontrol/sensors/imu/chassis_imu.py +76 -56
  25. dexcontrol/sensors/imu/zed_imu.py +54 -43
  26. dexcontrol/sensors/lidar/rplidar.py +16 -20
  27. dexcontrol/sensors/manager.py +4 -11
  28. dexcontrol/sensors/ultrasonic.py +14 -27
  29. dexcontrol/utils/__init__.py +0 -11
  30. dexcontrol/utils/comm_helper.py +111 -0
  31. dexcontrol/utils/constants.py +1 -1
  32. dexcontrol/utils/os_utils.py +8 -22
  33. {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.1.dist-info}/METADATA +2 -1
  34. dexcontrol-0.3.1.dist-info/RECORD +68 -0
  35. dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
  36. dexcontrol/sensors/camera/luxonis_camera.py +0 -169
  37. dexcontrol/utils/rate_limiter.py +0 -172
  38. dexcontrol/utils/rtc_utils.py +0 -144
  39. dexcontrol/utils/subscribers/__init__.py +0 -52
  40. dexcontrol/utils/subscribers/base.py +0 -281
  41. dexcontrol/utils/subscribers/camera.py +0 -332
  42. dexcontrol/utils/subscribers/decoders.py +0 -88
  43. dexcontrol/utils/subscribers/generic.py +0 -110
  44. dexcontrol/utils/subscribers/imu.py +0 -175
  45. dexcontrol/utils/subscribers/lidar.py +0 -172
  46. dexcontrol/utils/subscribers/protobuf.py +0 -111
  47. dexcontrol/utils/subscribers/rtc.py +0 -316
  48. dexcontrol/utils/zenoh_utils.py +0 -369
  49. dexcontrol-0.3.0.dist-info/RECORD +0 -76
  50. {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.1.dist-info}/WHEEL +0 -0
  51. {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -8,71 +8,50 @@
8
8
  # 2. Commercial License
9
9
  # For commercial licensing terms, contact: contact@dexmate.ai
10
10
 
11
- """ZED camera sensor implementation using RTC subscribers for RGB and Zenoh subscriber for depth."""
11
+ """ZED camera sensor implementation using RTC or DexComm subscribers for RGB and depth."""
12
12
 
13
- import logging
14
13
  import time
15
- from typing import Any, Dict, Optional, Union
14
+ from typing import Any, Dict, Optional
16
15
 
17
16
  import numpy as np
18
- import zenoh
17
+ from loguru import logger
19
18
 
20
- from dexcontrol.config.sensors.cameras import ZedCameraConfig
21
- from dexcontrol.utils.os_utils import resolve_key_name
22
- from dexcontrol.utils.rtc_utils import create_rtc_subscriber_from_zenoh
23
- from dexcontrol.utils.subscribers.camera import (
24
- DepthCameraSubscriber,
25
- RGBCameraSubscriber,
19
+ from dexcontrol.comm import (
20
+ create_camera_subscriber,
21
+ create_depth_subscriber,
22
+ create_rtc_camera_subscriber,
26
23
  )
27
- from dexcontrol.utils.subscribers.rtc import RTCSubscriber
28
- from dexcontrol.utils.zenoh_utils import query_zenoh_json
29
-
30
- logger = logging.getLogger(__name__)
31
-
32
- # Optional import for depth processing
33
- try:
34
- from dexsensor.serialization.camera import decode_depth
35
- DEXSENSOR_AVAILABLE = True
36
- except ImportError:
37
- logger.warning("dexsensor not available. Depth data will be returned without decoding.")
38
- decode_depth = None
39
- DEXSENSOR_AVAILABLE = False
24
+ from dexcontrol.config.sensors.cameras import ZedCameraConfig
25
+ from dexcontrol.sensors.camera.base_camera import BaseCameraSensor
40
26
 
41
27
 
42
- class ZedCameraSensor:
28
+ class ZedCameraSensor(BaseCameraSensor):
43
29
  """ZED camera sensor for multi-stream (RGB, Depth) data acquisition.
44
30
 
45
31
  This sensor manages left RGB, right RGB, and depth data streams from a ZED
46
32
  camera. It can be configured to use high-performance RTC subscribers for RGB
47
- streams (`use_rtc=True`) or fall back to standard Zenoh subscribers
48
- (`use_rtc=False`). The depth stream always uses a standard Zenoh subscriber.
33
+ streams (`use_rtc=True`) or standard DexComm subscribers. Both types provide
34
+ the same API interface, making them interchangeable.
49
35
  """
50
36
 
51
- SubscriberType = Union[RTCSubscriber, DepthCameraSubscriber, RGBCameraSubscriber]
52
-
53
37
  def __init__(
54
38
  self,
55
39
  configs: ZedCameraConfig,
56
- zenoh_session: zenoh.Session,
57
40
  ) -> None:
58
41
  """Initialize the ZED camera sensor and its subscribers.
59
42
 
60
43
  Args:
61
44
  configs: Configuration object for the ZED camera.
62
- zenoh_session: Active Zenoh session for communication.
63
45
  """
64
- self._name = configs.name
65
- self._zenoh_session = zenoh_session
46
+ super().__init__(configs.name)
66
47
  self._configs = configs
67
- self._subscribers: Dict[str, Optional[ZedCameraSensor.SubscriberType]] = {}
68
- self._camera_info: Optional[Dict[str, Any]] = None
48
+ self._subscribers: Dict[str, Optional[Any]] = {} # Will hold either RTCSubscriberWrapper or Subscriber
69
49
 
70
50
  self._create_subscribers()
71
- self._query_camera_info()
72
51
 
73
52
  def _create_subscriber(
74
53
  self, stream_name: str, stream_config: Dict[str, Any]
75
- ) -> Optional[SubscriberType]:
54
+ ) -> Optional[Any]:
76
55
  """Factory method to create a subscriber based on stream type and config."""
77
56
  try:
78
57
  if not stream_config.get("enable", False):
@@ -85,41 +64,40 @@ class ZedCameraSensor:
85
64
  if not topic:
86
65
  logger.warning(f"'{self._name}': No 'topic' for depth stream.")
87
66
  return None
88
- logger.info(f"'{self._name}': Creating Zenoh depth subscriber.")
89
- return DepthCameraSubscriber(
67
+ logger.info(f"'{self._name}': Creating depth subscriber.")
68
+ # Use new DexComm integration
69
+ return create_depth_subscriber(
90
70
  topic=topic,
91
- zenoh_session=self._zenoh_session,
92
- name=f"{self._name}_{stream_name}_subscriber",
93
- enable_fps_tracking=self._configs.enable_fps_tracking,
94
- fps_log_interval=self._configs.fps_log_interval,
95
71
  )
96
72
 
97
- # Create RGB subscriber (RTC or Zenoh)
73
+ # Create RGB subscriber (RTC or DexComm)
98
74
  if self._configs.use_rtc:
99
- info_key = stream_config.get("info_key")
100
- if not info_key:
101
- logger.warning(f"'{self._name}': No 'info_key' for RTC stream '{stream_name}'.")
102
- return None
103
- logger.info(f"'{self._name}': Creating RTC subscriber for '{stream_name}'.")
104
- return create_rtc_subscriber_from_zenoh(
105
- zenoh_session=self._zenoh_session,
106
- info_topic=info_key,
107
- name=f"{self._name}_{stream_name}_subscriber",
108
- enable_fps_tracking=self._configs.enable_fps_tracking,
109
- fps_log_interval=self._configs.fps_log_interval,
110
- )
75
+ # Check if we have RTC info from camera_info
76
+ rtc_url = self._get_rtc_signaling_url(stream_name)
77
+ if rtc_url:
78
+ logger.info(f"'{self._name}': Creating RTC subscriber for '{stream_name}' with direct URL.")
79
+ return create_rtc_camera_subscriber(
80
+ signaling_url=rtc_url,
81
+ )
82
+ else:
83
+ # Fallback to querying via info_key
84
+ info_key = stream_config.get("info_key")
85
+ if not info_key:
86
+ logger.warning(f"'{self._name}': No RTC URL or info_key for stream '{stream_name}'.")
87
+ return None
88
+ logger.info(f"'{self._name}': Creating RTC subscriber for '{stream_name}' with info_key.")
89
+ return create_rtc_camera_subscriber(
90
+ info_topic=info_key,
91
+ )
111
92
  else:
112
93
  topic = stream_config.get("topic")
113
94
  if not topic:
114
95
  logger.warning(f"'{self._name}': No 'topic' for Zenoh stream '{stream_name}'.")
115
96
  return None
116
- logger.info(f"'{self._name}': Creating Zenoh RGB subscriber for '{stream_name}'.")
117
- return RGBCameraSubscriber(
97
+ logger.info(f"'{self._name}': Creating RGB subscriber for '{stream_name}'.")
98
+ # Use new DexComm integration
99
+ return create_camera_subscriber(
118
100
  topic=topic,
119
- zenoh_session=self._zenoh_session,
120
- name=f"{self._name}_{stream_name}_subscriber",
121
- enable_fps_tracking=self._configs.enable_fps_tracking,
122
- fps_log_interval=self._configs.fps_log_interval,
123
101
  )
124
102
 
125
103
  except Exception as e:
@@ -129,6 +107,13 @@ class ZedCameraSensor:
129
107
  def _create_subscribers(self) -> None:
130
108
  """Create subscribers for all configured camera streams."""
131
109
  subscriber_config = self._configs.subscriber_config
110
+
111
+ # Determine info endpoint for camera metadata
112
+ info_endpoint = self._determine_info_endpoint(subscriber_config)
113
+
114
+ # Query camera info first to potentially get RTC URLs
115
+ self._query_camera_info(info_endpoint)
116
+
132
117
  stream_definitions = {
133
118
  "left_rgb": subscriber_config.get("left_rgb", {}),
134
119
  "right_rgb": subscriber_config.get("right_rgb", {}),
@@ -138,42 +123,28 @@ class ZedCameraSensor:
138
123
  for name, config in stream_definitions.items():
139
124
  self._subscribers[name] = self._create_subscriber(name, config)
140
125
 
141
- def _query_camera_info(self) -> None:
142
- """Query Zenoh for camera metadata if using RTC."""
143
-
144
- enabled_rgb_streams = [
145
- s
146
- for s_name, s in self._subscribers.items()
147
- if "rgb" in s_name and s is not None
148
- ]
149
-
150
- if not enabled_rgb_streams:
151
- logger.warning(f"'{self._name}': No enabled RGB streams to query for camera info.")
152
- return
126
+ def _determine_info_endpoint(self, subscriber_config: dict) -> Optional[str]:
127
+ """Determine the info endpoint for querying camera metadata.
153
128
 
154
- # Use the info_key from the first available RGB subscriber's config
155
- first_stream_name = "left_rgb" if self._subscribers.get("left_rgb") else "right_rgb"
156
- stream_config = self._configs.subscriber_config.get(first_stream_name, {})
157
- info_key = stream_config.get("info_key")
158
-
159
- if not info_key:
160
- logger.warning(f"'{self._name}': Could not find info_key for camera info query.")
161
- return
129
+ Args:
130
+ subscriber_config: Subscriber configuration dict
162
131
 
163
- try:
164
- # Construct the root info key (e.g., 'camera/head/info')
165
- resolved_key = resolve_key_name(info_key).rstrip("/")
166
- info_key_root = "/".join(resolved_key.split("/")[:-2])
167
- final_info_key = f"{info_key_root}/info"
168
-
169
- logger.info(f"'{self._name}': Querying for camera info at '{final_info_key}'.")
170
- self._camera_info = query_zenoh_json(self._zenoh_session, final_info_key)
171
- if self._camera_info:
172
- logger.info(f"'{self._name}': Successfully received camera info.")
173
- else:
174
- logger.warning(f"'{self._name}': No camera info found at '{final_info_key}'.")
175
- except Exception as e:
176
- logger.error(f"'{self._name}': Failed to query camera info: {e}")
132
+ Returns:
133
+ Info endpoint or None
134
+ """
135
+ # Try to find the first enabled RGB stream to get the base info endpoint
136
+ for stream_name in ["left_rgb", "right_rgb"]:
137
+ stream_config = subscriber_config.get(stream_name, {})
138
+ if stream_config.get("enable", False):
139
+ if self._configs.use_rtc:
140
+ info_key = stream_config.get("info_key")
141
+ if info_key:
142
+ return info_key
143
+ else:
144
+ topic = stream_config.get("topic")
145
+ if topic:
146
+ return self._derive_info_endpoint_from_topic(topic)
147
+ return None
177
148
 
178
149
  def shutdown(self) -> None:
179
150
  """Shutdown all active subscribers for the camera sensor."""
@@ -196,7 +167,7 @@ class ZedCameraSensor:
196
167
  True if at least one subscriber is active, False otherwise.
197
168
  """
198
169
  return any(
199
- sub.is_active() for sub in self._subscribers.values() if sub is not None
170
+ sub.get_latest() is not None for sub in self._subscribers.values() if sub is not None
200
171
  )
201
172
 
202
173
  def is_stream_active(self, stream_name: str) -> bool:
@@ -209,7 +180,7 @@ class ZedCameraSensor:
209
180
  True if the specified stream's subscriber is active, False otherwise.
210
181
  """
211
182
  subscriber = self._subscribers.get(stream_name)
212
- return subscriber.is_active() if subscriber else False
183
+ return subscriber.get_latest() is not None if subscriber else False
213
184
 
214
185
  def wait_for_active(self, timeout: float = 5.0, require_all: bool = False) -> bool:
215
186
  """Wait for camera streams to become active.
@@ -229,7 +200,7 @@ class ZedCameraSensor:
229
200
 
230
201
  if require_all:
231
202
  for sub in enabled_subscribers:
232
- if not sub.wait_for_active(timeout):
203
+ if not sub.wait_for_message(timeout):
233
204
  logger.warning(f"'{self._name}': Timed out waiting for subscriber '{sub.name}'.")
234
205
  return False
235
206
  logger.info(f"'{self._name}': All enabled streams are active.")
@@ -267,16 +238,26 @@ class ZedCameraSensor:
267
238
  obs_out = {}
268
239
  for key in keys_to_fetch:
269
240
  subscriber = self._subscribers.get(key)
270
- data = subscriber.get_latest_data() if subscriber else None
241
+ data = subscriber.get_latest() if subscriber else None
271
242
 
272
- is_tuple_or_list = isinstance(data, (tuple, list))
243
+ # DexComm returns dict with 'data' and 'timestamp' keys when timestamp is present
244
+ has_timestamp = isinstance(data, dict) and 'timestamp' in data
273
245
 
274
246
  if include_timestamp:
275
- if not is_tuple_or_list:
276
- logger.warning(f"Timestamp is not available yet for {key} stream.")
277
- obs_out[key] = data
247
+ # Always return a consistent structure
248
+ if has_timestamp:
249
+ obs_out[key] = {
250
+ 'data': data.get('data') if isinstance(data, dict) else None,
251
+ 'timestamp': data.get('timestamp') if isinstance(data, dict) else None,
252
+ }
253
+ else:
254
+ obs_out[key] = {'data': data, 'timestamp': None}
278
255
  else:
279
- obs_out[key] = data[0] if is_tuple_or_list else data
256
+ if has_timestamp:
257
+ # Extract payload when timestamp wrapper is present
258
+ obs_out[key] = data.get('data') if isinstance(data, dict) else None
259
+ else:
260
+ obs_out[key] = data
280
261
  return obs_out
281
262
 
282
263
  def get_left_rgb(self) -> Optional[np.ndarray]:
@@ -286,7 +267,7 @@ class ZedCameraSensor:
286
267
  The latest left RGB image as a numpy array, or None if not available.
287
268
  """
288
269
  subscriber = self._subscribers.get("left_rgb")
289
- return subscriber.get_latest_data() if subscriber else None
270
+ return subscriber.get_latest() if subscriber else None
290
271
 
291
272
  def get_right_rgb(self) -> Optional[np.ndarray]:
292
273
  """Get the latest image from the right RGB stream.
@@ -295,7 +276,7 @@ class ZedCameraSensor:
295
276
  The latest right RGB image as a numpy array, or None if not available.
296
277
  """
297
278
  subscriber = self._subscribers.get("right_rgb")
298
- return subscriber.get_latest_data() if subscriber else None
279
+ return subscriber.get_latest() if subscriber else None
299
280
 
300
281
  def get_depth(self) -> Optional[np.ndarray]:
301
282
  """Get the latest image from the depth stream.
@@ -306,29 +287,8 @@ class ZedCameraSensor:
306
287
  The latest depth image as a numpy array, or None if not available.
307
288
  """
308
289
  subscriber = self._subscribers.get("depth")
309
- return subscriber.get_latest_data() if subscriber else None
290
+ return subscriber.get_latest() if subscriber else None
310
291
 
311
- @property
312
- def fps(self) -> Dict[str, float]:
313
- """Get the current FPS measurement for each active stream.
314
-
315
- Returns:
316
- A dictionary mapping stream names to their FPS measurements.
317
- """
318
- return {
319
- name: sub.fps
320
- for name, sub in self._subscribers.items()
321
- if sub is not None
322
- }
323
-
324
- @property
325
- def name(self) -> str:
326
- """Get the sensor name.
327
-
328
- Returns:
329
- Sensor name string.
330
- """
331
- return self._name
332
292
 
333
293
  @property
334
294
  def available_streams(self) -> list:
@@ -346,25 +306,7 @@ class ZedCameraSensor:
346
306
  Returns:
347
307
  List of stream names that are currently receiving data.
348
308
  """
349
- return [name for name, sub in self._subscribers.items() if sub and sub.is_active()]
350
-
351
- @property
352
- def dexsensor_available(self) -> bool:
353
- """Check if dexsensor is available for depth decoding.
354
-
355
- Returns:
356
- True if dexsensor is available, False otherwise.
357
- """
358
- return DEXSENSOR_AVAILABLE
359
-
360
- @property
361
- def camera_info(self) -> dict | None:
362
- """Get the camera info.
363
-
364
- Returns:
365
- Camera info dictionary if available, None otherwise.
366
- """
367
- return self._camera_info
309
+ return [name for name, sub in self._subscribers.items() if sub and sub.get_latest() is not None]
368
310
 
369
311
  @property
370
312
  def height(self) -> dict[str, int]:
@@ -8,23 +8,21 @@
8
8
  # 2. Commercial License
9
9
  # For commercial licensing terms, contact: contact@dexmate.ai
10
10
 
11
- """Ultrasonic sensor implementations using Zenoh subscribers.
11
+ """Chassis IMU sensor implementation using DexComm subscribers.
12
12
 
13
- This module provides ultrasonic sensor classes that use the generic
14
- subscriber for distance measurements.
13
+ This module provides IMU sensor class for chassis IMU data
14
+ using DexComm's Raw API.
15
15
  """
16
16
 
17
- from typing import Literal, cast
17
+ from typing import Dict, List, Optional
18
18
 
19
19
  import numpy as np
20
- import zenoh
21
20
 
22
- from dexcontrol.proto import dexcontrol_msg_pb2
23
- from dexcontrol.utils.subscribers import ProtobufZenohSubscriber
21
+ from dexcontrol.comm import create_imu_subscriber
24
22
 
25
23
 
26
24
  class ChassisIMUSensor:
27
- """Chassis IMU sensor using Zenoh subscriber.
25
+ """Chassis IMU sensor using DexComm subscriber.
28
26
 
29
27
  This sensor provides IMU data from the chassis
30
28
  """
@@ -32,41 +30,35 @@ class ChassisIMUSensor:
32
30
  def __init__(
33
31
  self,
34
32
  configs,
35
- zenoh_session: zenoh.Session,
36
33
  ) -> None:
37
- """Initialize the ultrasonic sensor.
34
+ """Initialize the chassis IMU sensor.
38
35
 
39
36
  Args:
40
- configs: Configuration for the ultrasonic sensor.
41
- zenoh_session: Active Zenoh session for communication.
37
+ configs: Configuration for the chassis IMU sensor.
42
38
  """
43
39
  self._name = configs.name
44
40
 
45
- # Create the generic subscriber with JSON decoder
46
- self._subscriber = ProtobufZenohSubscriber(
41
+ # Create IMU subscriber using DexComm integration
42
+ self._subscriber = create_imu_subscriber(
47
43
  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
44
  )
54
45
 
55
46
 
56
47
  def shutdown(self) -> None:
57
- """Shutdown the ultrasonic sensor."""
48
+ """Shutdown the chassis IMU sensor."""
58
49
  self._subscriber.shutdown()
59
50
 
60
51
  def is_active(self) -> bool:
61
- """Check if the ultrasonic sensor is actively receiving data.
52
+ """Check if the chassis IMU sensor is actively receiving data.
62
53
 
63
54
  Returns:
64
55
  True if receiving data, False otherwise.
65
56
  """
66
- return self._subscriber.is_active()
57
+ data = self._subscriber.get_latest()
58
+ return data is not None
67
59
 
68
60
  def wait_for_active(self, timeout: float = 5.0) -> bool:
69
- """Wait for the ultrasonic sensor to start receiving data.
61
+ """Wait for the chassis IMU sensor to start receiving data.
70
62
 
71
63
  Args:
72
64
  timeout: Maximum time to wait in seconds.
@@ -74,80 +66,108 @@ class ChassisIMUSensor:
74
66
  Returns:
75
67
  True if sensor becomes active, False if timeout is reached.
76
68
  """
77
- return self._subscriber.wait_for_active(timeout)
69
+ msg = self._subscriber.wait_for_message(timeout)
70
+ return msg is not None
78
71
 
79
- def get_obs(self, obs_keys: list[Literal['ang_vel', 'acc', 'quat']] | None = None) -> dict[Literal['ang_vel', 'acc', 'quat', 'timestamp_ns'], np.ndarray]:
80
- """Get observation data for the ZED IMU sensor.
72
+ def get_obs(self, obs_keys: Optional[List[str]] = None) -> Optional[Dict[str, np.ndarray]]:
73
+ """Get observation data for the chassis IMU sensor.
81
74
 
82
75
  Args:
83
76
  obs_keys: List of observation keys to retrieve. If None, returns all available data.
84
- Valid keys: ['ang_vel', 'acc', 'quat']
77
+ Valid keys: ['ang_vel', 'acc', 'quat', 'mag', 'timestamp']
85
78
 
86
79
  Returns:
87
80
  Dictionary with observation data including all IMU measurements.
88
81
  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
82
+ - 'ang_vel': Angular velocity from 'gyro'
83
+ - 'acc': Linear acceleration from 'acc'
84
+ - 'quat': Orientation quaternion from 'quat' [w, x, y, z]
85
+ - 'mag': Magnetometer from 'mag' (if available)
92
86
  - 'timestamp_ns': Timestamp in nanoseconds
93
87
  """
94
88
  if obs_keys is None:
95
89
  obs_keys = ['ang_vel', 'acc', 'quat']
96
90
 
97
- data = self._subscriber.get_latest_data()
98
- data = cast(dexcontrol_msg_pb2.IMUState, data)
91
+ data = self._subscriber.get_latest()
99
92
  if data is None:
100
- raise RuntimeError("No IMU data available")
93
+ return None
101
94
 
102
95
  obs_out = {}
103
96
 
97
+ # Add timestamp if available
98
+ if 'timestamp' in data:
99
+ obs_out['timestamp_ns'] = data['timestamp']
100
+
104
101
  for key in obs_keys:
105
102
  if key == 'ang_vel':
106
- obs_out[key] = np.array([data.gyro_x, data.gyro_y, data.gyro_z])
103
+ obs_out[key] = data.get('gyro', np.zeros(3))
107
104
  elif key == 'acc':
108
- obs_out[key] = np.array([data.acc_x, data.acc_y, data.acc_z])
105
+ obs_out[key] = data.get('acc', np.zeros(3))
109
106
  elif key == 'quat':
110
- obs_out[key] = np.array([data.quat_w, data.quat_x, data.quat_y, data.quat_z])
111
- else:
112
- raise ValueError(f"Invalid observation key: {key}")
113
-
114
- if hasattr(data, 'timestamp_ns'):
115
- obs_out['timestamp_ns'] = data.timestamp_ns
107
+ obs_out[key] = data.get('quat', np.array([1.0, 0.0, 0.0, 0.0]))
108
+ elif key == 'mag' and 'mag' in data:
109
+ obs_out[key] = data['mag']
110
+ elif key == 'timestamp' and 'timestamp' in data:
111
+ obs_out['timestamp_ns'] = data['timestamp']
116
112
 
117
113
  return obs_out
118
114
 
119
- def get_acceleration(self) -> np.ndarray:
120
- """Get the latest linear acceleration from ZED IMU.
115
+ def get_acc(self) -> Optional[np.ndarray]:
116
+ """Get the latest linear acceleration from chassis IMU.
121
117
 
122
118
  Returns:
123
119
  Linear acceleration [x, y, z] in m/s² if available, None otherwise.
124
120
  """
125
- return self.get_obs(obs_keys=['acc'])['acc']
121
+ data = self._subscriber.get_latest()
122
+ return data.get('acc') if data else None
126
123
 
127
- def get_angular_velocity(self) -> np.ndarray:
128
- """Get the latest angular velocity from ZED IMU.
124
+ def get_gyro(self) -> Optional[np.ndarray]:
125
+ """Get the latest angular velocity from chassis IMU.
129
126
 
130
127
  Returns:
131
128
  Angular velocity [x, y, z] in rad/s if available, None otherwise.
132
129
  """
133
- return self.get_obs(obs_keys=['ang_vel'])['ang_vel']
130
+ data = self._subscriber.get_latest()
131
+ return data.get('gyro') if data else None
134
132
 
135
- def get_orientation(self) -> np.ndarray:
136
- """Get the latest orientation quaternion from ZED IMU.
133
+ def get_quat(self) -> Optional[np.ndarray]:
134
+ """Get the latest orientation quaternion from chassis IMU.
137
135
 
138
136
  Returns:
139
- Orientation quaternion [x, y, z, w] if available, None otherwise.
137
+ Orientation quaternion [w, x, y, z] if available, None otherwise.
138
+ Note: dexcomm uses [w, x, y, z] quaternion format.
140
139
  """
141
- return self.get_obs(obs_keys=['quat'])['quat']
140
+ data = self._subscriber.get_latest()
141
+ return data.get('quat') if data else None
142
142
 
143
- @property
144
- def fps(self) -> float:
145
- """Get the current FPS measurement.
143
+ def get_mag(self) -> Optional[np.ndarray]:
144
+ """Get the latest magnetometer reading from chassis IMU.
145
+
146
+ Returns:
147
+ Magnetic field [x, y, z] in µT if available, None otherwise.
148
+ """
149
+ data = self._subscriber.get_latest()
150
+ if not data or not isinstance(data, dict):
151
+ return None
152
+ return data.get('mag', None)
153
+
154
+ def has_mag(self) -> bool:
155
+ """Check if the chassis IMU has magnetometer data available.
146
156
 
147
157
  Returns:
148
- Current frames per second measurement.
158
+ True if magnetometer data is available, False otherwise.
149
159
  """
150
- return self._subscriber.fps
160
+ data = self._subscriber.get_latest()
161
+ if not data or not isinstance(data, dict):
162
+ return False
163
+ return 'mag' in data and data['mag'] is not None
164
+
165
+ # Backward compatibility aliases
166
+ get_acceleration = get_acc
167
+ get_angular_velocity = get_gyro
168
+ get_orientation = get_quat
169
+ get_magnetometer = get_mag
170
+ has_magnetometer = has_mag
151
171
 
152
172
  @property
153
173
  def name(self) -> str: