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.
- dexcontrol/__init__.py +16 -7
- dexcontrol/apps/dualsense_teleop_base.py +1 -1
- dexcontrol/comm/__init__.py +51 -0
- dexcontrol/comm/base.py +421 -0
- dexcontrol/comm/rtc.py +400 -0
- dexcontrol/comm/subscribers.py +329 -0
- dexcontrol/config/sensors/cameras/__init__.py +1 -2
- dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
- dexcontrol/config/sensors/vega_sensors.py +12 -18
- dexcontrol/core/arm.py +29 -25
- dexcontrol/core/chassis.py +3 -12
- dexcontrol/core/component.py +68 -43
- dexcontrol/core/hand.py +50 -52
- dexcontrol/core/head.py +14 -26
- dexcontrol/core/misc.py +188 -166
- dexcontrol/core/robot_query_interface.py +137 -114
- dexcontrol/core/torso.py +0 -4
- dexcontrol/robot.py +15 -37
- dexcontrol/sensors/__init__.py +1 -2
- dexcontrol/sensors/camera/__init__.py +0 -2
- dexcontrol/sensors/camera/base_camera.py +144 -0
- dexcontrol/sensors/camera/rgb_camera.py +67 -63
- dexcontrol/sensors/camera/zed_camera.py +89 -147
- dexcontrol/sensors/imu/chassis_imu.py +76 -56
- dexcontrol/sensors/imu/zed_imu.py +54 -43
- dexcontrol/sensors/lidar/rplidar.py +16 -20
- dexcontrol/sensors/manager.py +4 -11
- dexcontrol/sensors/ultrasonic.py +14 -27
- dexcontrol/utils/__init__.py +0 -11
- dexcontrol/utils/comm_helper.py +111 -0
- dexcontrol/utils/constants.py +1 -1
- dexcontrol/utils/os_utils.py +8 -22
- {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.1.dist-info}/METADATA +2 -1
- dexcontrol-0.3.1.dist-info/RECORD +68 -0
- dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
- dexcontrol/sensors/camera/luxonis_camera.py +0 -169
- dexcontrol/utils/rate_limiter.py +0 -172
- dexcontrol/utils/rtc_utils.py +0 -144
- dexcontrol/utils/subscribers/__init__.py +0 -52
- dexcontrol/utils/subscribers/base.py +0 -281
- dexcontrol/utils/subscribers/camera.py +0 -332
- dexcontrol/utils/subscribers/decoders.py +0 -88
- dexcontrol/utils/subscribers/generic.py +0 -110
- dexcontrol/utils/subscribers/imu.py +0 -175
- dexcontrol/utils/subscribers/lidar.py +0 -172
- dexcontrol/utils/subscribers/protobuf.py +0 -111
- dexcontrol/utils/subscribers/rtc.py +0 -316
- dexcontrol/utils/zenoh_utils.py +0 -369
- dexcontrol-0.3.0.dist-info/RECORD +0 -76
- {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.1.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
|
14
|
+
from typing import Any, Dict, Optional
|
|
16
15
|
|
|
17
16
|
import numpy as np
|
|
18
|
-
import
|
|
17
|
+
from loguru import logger
|
|
19
18
|
|
|
20
|
-
from dexcontrol.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
28
|
-
from dexcontrol.
|
|
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
|
|
48
|
-
|
|
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
|
-
|
|
65
|
-
self._zenoh_session = zenoh_session
|
|
46
|
+
super().__init__(configs.name)
|
|
66
47
|
self._configs = configs
|
|
67
|
-
self._subscribers: Dict[str, Optional[
|
|
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[
|
|
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
|
|
89
|
-
|
|
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
|
|
73
|
+
# Create RGB subscriber (RTC or DexComm)
|
|
98
74
|
if self._configs.use_rtc:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
117
|
-
|
|
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
|
|
142
|
-
"""
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
241
|
+
data = subscriber.get_latest() if subscriber else None
|
|
271
242
|
|
|
272
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
"""
|
|
11
|
+
"""Chassis IMU sensor implementation using DexComm subscribers.
|
|
12
12
|
|
|
13
|
-
This module provides
|
|
14
|
-
|
|
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
|
|
17
|
+
from typing import Dict, List, Optional
|
|
18
18
|
|
|
19
19
|
import numpy as np
|
|
20
|
-
import zenoh
|
|
21
20
|
|
|
22
|
-
from dexcontrol.
|
|
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
|
|
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
|
|
34
|
+
"""Initialize the chassis IMU sensor.
|
|
38
35
|
|
|
39
36
|
Args:
|
|
40
|
-
configs: Configuration for the
|
|
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
|
|
46
|
-
self._subscriber =
|
|
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
|
|
48
|
+
"""Shutdown the chassis IMU sensor."""
|
|
58
49
|
self._subscriber.shutdown()
|
|
59
50
|
|
|
60
51
|
def is_active(self) -> bool:
|
|
61
|
-
"""Check if the
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
69
|
+
msg = self._subscriber.wait_for_message(timeout)
|
|
70
|
+
return msg is not None
|
|
78
71
|
|
|
79
|
-
def get_obs(self, obs_keys:
|
|
80
|
-
"""Get observation data for the
|
|
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 '
|
|
90
|
-
- 'acc': Linear acceleration from '
|
|
91
|
-
- 'quat': Orientation quaternion from '
|
|
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.
|
|
98
|
-
data = cast(dexcontrol_msg_pb2.IMUState, data)
|
|
91
|
+
data = self._subscriber.get_latest()
|
|
99
92
|
if data is None:
|
|
100
|
-
|
|
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] =
|
|
103
|
+
obs_out[key] = data.get('gyro', np.zeros(3))
|
|
107
104
|
elif key == 'acc':
|
|
108
|
-
obs_out[key] =
|
|
105
|
+
obs_out[key] = data.get('acc', np.zeros(3))
|
|
109
106
|
elif key == 'quat':
|
|
110
|
-
obs_out[key] = np.array([
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
120
|
-
"""Get the latest linear acceleration from
|
|
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
|
-
|
|
121
|
+
data = self._subscriber.get_latest()
|
|
122
|
+
return data.get('acc') if data else None
|
|
126
123
|
|
|
127
|
-
def
|
|
128
|
-
"""Get the latest angular velocity from
|
|
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
|
-
|
|
130
|
+
data = self._subscriber.get_latest()
|
|
131
|
+
return data.get('gyro') if data else None
|
|
134
132
|
|
|
135
|
-
def
|
|
136
|
-
"""Get the latest orientation quaternion from
|
|
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
|
|
137
|
+
Orientation quaternion [w, x, y, z] if available, None otherwise.
|
|
138
|
+
Note: dexcomm uses [w, x, y, z] quaternion format.
|
|
140
139
|
"""
|
|
141
|
-
|
|
140
|
+
data = self._subscriber.get_latest()
|
|
141
|
+
return data.get('quat') if data else None
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
158
|
+
True if magnetometer data is available, False otherwise.
|
|
149
159
|
"""
|
|
150
|
-
|
|
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:
|