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.
- dexcontrol/__init__.py +14 -3
- dexcontrol/apps/dualsense_teleop_base.py +16 -11
- dexcontrol/config/__init__.py +10 -5
- dexcontrol/config/core/__init__.py +8 -3
- dexcontrol/config/core/arm.py +8 -3
- dexcontrol/config/core/chassis.py +10 -5
- dexcontrol/config/core/hand.py +14 -9
- dexcontrol/config/core/head.py +8 -3
- dexcontrol/config/core/misc.py +8 -3
- dexcontrol/config/core/torso.py +8 -3
- dexcontrol/config/sensors/__init__.py +8 -3
- dexcontrol/config/sensors/cameras/__init__.py +9 -4
- dexcontrol/config/sensors/cameras/rgb_camera.py +18 -5
- dexcontrol/config/sensors/cameras/zed_camera.py +36 -0
- dexcontrol/config/sensors/imu/__init__.py +10 -5
- dexcontrol/config/sensors/imu/chassis_imu.py +21 -0
- dexcontrol/config/sensors/imu/zed_imu.py +21 -0
- dexcontrol/config/sensors/lidar/__init__.py +8 -3
- dexcontrol/config/sensors/lidar/rplidar.py +9 -3
- dexcontrol/config/sensors/ultrasonic/__init__.py +8 -3
- dexcontrol/config/sensors/ultrasonic/ultrasonic.py +9 -3
- dexcontrol/config/sensors/vega_sensors.py +34 -21
- dexcontrol/config/vega.py +14 -6
- dexcontrol/core/__init__.py +9 -0
- dexcontrol/core/arm.py +21 -6
- dexcontrol/core/chassis.py +8 -3
- dexcontrol/core/component.py +26 -6
- dexcontrol/core/hand.py +8 -3
- dexcontrol/core/head.py +18 -3
- dexcontrol/core/misc.py +94 -16
- dexcontrol/core/torso.py +8 -3
- dexcontrol/proto/dexcontrol_msg_pb2.py +17 -15
- dexcontrol/proto/dexcontrol_msg_pb2.pyi +24 -0
- dexcontrol/robot.py +82 -28
- dexcontrol/sensors/__init__.py +13 -8
- dexcontrol/sensors/camera/__init__.py +11 -6
- dexcontrol/sensors/camera/rgb_camera.py +33 -24
- dexcontrol/sensors/camera/zed_camera.py +364 -0
- dexcontrol/sensors/imu/__init__.py +13 -8
- dexcontrol/sensors/imu/chassis_imu.py +155 -0
- dexcontrol/sensors/imu/{nine_axis_imu.py → zed_imu.py} +41 -26
- dexcontrol/sensors/lidar/__init__.py +11 -1
- dexcontrol/sensors/lidar/rplidar.py +8 -3
- dexcontrol/sensors/manager.py +22 -9
- dexcontrol/sensors/ultrasonic.py +8 -3
- dexcontrol/utils/__init__.py +8 -3
- dexcontrol/utils/constants.py +10 -0
- dexcontrol/utils/io_utils.py +8 -3
- dexcontrol/utils/motion_utils.py +8 -3
- dexcontrol/utils/os_utils.py +23 -4
- dexcontrol/utils/pb_utils.py +8 -3
- dexcontrol/utils/rate_limiter.py +8 -3
- dexcontrol/utils/rtc_utils.py +144 -0
- dexcontrol/utils/subscribers/__init__.py +11 -3
- dexcontrol/utils/subscribers/base.py +26 -5
- dexcontrol/utils/subscribers/camera.py +10 -6
- dexcontrol/utils/subscribers/decoders.py +8 -3
- dexcontrol/utils/subscribers/generic.py +8 -3
- dexcontrol/utils/subscribers/imu.py +8 -3
- dexcontrol/utils/subscribers/lidar.py +8 -3
- dexcontrol/utils/subscribers/protobuf.py +8 -3
- dexcontrol/utils/subscribers/rtc.py +315 -0
- dexcontrol/utils/timer.py +8 -3
- dexcontrol/utils/trajectory_utils.py +8 -3
- dexcontrol/utils/viz_utils.py +8 -3
- dexcontrol/utils/zenoh_utils.py +83 -0
- dexcontrol-0.2.3.dist-info/METADATA +265 -0
- dexcontrol-0.2.3.dist-info/RECORD +72 -0
- {dexcontrol-0.2.1.dist-info → dexcontrol-0.2.3.dist-info}/WHEEL +1 -2
- dexcontrol-0.2.3.dist-info/licenses/LICENSE +184 -0
- dexcontrol/config/sensors/cameras/gemini_camera.py +0 -16
- dexcontrol/config/sensors/imu/gemini_imu.py +0 -15
- dexcontrol/config/sensors/imu/nine_axis_imu.py +0 -15
- dexcontrol/sensors/camera/gemini_camera.py +0 -139
- dexcontrol/sensors/imu/gemini_imu.py +0 -139
- dexcontrol/utils/reset_orbbec_camera_usb.py +0 -98
- dexcontrol-0.2.1.dist-info/METADATA +0 -369
- dexcontrol-0.2.1.dist-info/RECORD +0 -72
- dexcontrol-0.2.1.dist-info/licenses/LICENSE +0 -188
- dexcontrol-0.2.1.dist-info/licenses/NOTICE +0 -13
- 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 (
|
|
1
|
+
# Copyright (C) 2025 Dexmate Inc.
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
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
|
-
-
|
|
18
|
+
- ZedIMUSensor: IMU specific to Zed hardware (6-axis: accelerometer + gyroscope)
|
|
14
19
|
"""
|
|
15
20
|
|
|
16
|
-
from .
|
|
17
|
-
from .
|
|
21
|
+
from .chassis_imu import ChassisIMUSensor
|
|
22
|
+
from .zed_imu import ZedIMUSensor
|
|
18
23
|
|
|
19
24
|
__all__ = [
|
|
20
|
-
"
|
|
21
|
-
"
|
|
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
|