dexcontrol 0.2.12__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 (59) hide show
  1. dexcontrol/__init__.py +18 -8
  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/core/chassis.py +9 -4
  8. dexcontrol/config/core/hand.py +1 -0
  9. dexcontrol/config/sensors/cameras/__init__.py +1 -2
  10. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  11. dexcontrol/config/sensors/vega_sensors.py +12 -18
  12. dexcontrol/config/vega.py +4 -1
  13. dexcontrol/core/arm.py +61 -37
  14. dexcontrol/core/chassis.py +141 -119
  15. dexcontrol/core/component.py +110 -59
  16. dexcontrol/core/hand.py +118 -85
  17. dexcontrol/core/head.py +18 -29
  18. dexcontrol/core/misc.py +327 -155
  19. dexcontrol/core/robot_query_interface.py +463 -0
  20. dexcontrol/core/torso.py +4 -8
  21. dexcontrol/proto/dexcontrol_msg_pb2.py +27 -39
  22. dexcontrol/proto/dexcontrol_msg_pb2.pyi +75 -118
  23. dexcontrol/proto/dexcontrol_query_pb2.py +39 -39
  24. dexcontrol/proto/dexcontrol_query_pb2.pyi +17 -4
  25. dexcontrol/robot.py +245 -574
  26. dexcontrol/sensors/__init__.py +1 -2
  27. dexcontrol/sensors/camera/__init__.py +0 -2
  28. dexcontrol/sensors/camera/base_camera.py +144 -0
  29. dexcontrol/sensors/camera/rgb_camera.py +67 -63
  30. dexcontrol/sensors/camera/zed_camera.py +89 -147
  31. dexcontrol/sensors/imu/chassis_imu.py +76 -56
  32. dexcontrol/sensors/imu/zed_imu.py +54 -43
  33. dexcontrol/sensors/lidar/rplidar.py +16 -20
  34. dexcontrol/sensors/manager.py +4 -11
  35. dexcontrol/sensors/ultrasonic.py +14 -27
  36. dexcontrol/utils/__init__.py +0 -11
  37. dexcontrol/utils/comm_helper.py +111 -0
  38. dexcontrol/utils/constants.py +1 -1
  39. dexcontrol/utils/os_utils.py +169 -1
  40. dexcontrol/utils/pb_utils.py +0 -22
  41. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.dist-info}/METADATA +13 -1
  42. dexcontrol-0.3.1.dist-info/RECORD +68 -0
  43. dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
  44. dexcontrol/sensors/camera/luxonis_camera.py +0 -169
  45. dexcontrol/utils/rate_limiter.py +0 -172
  46. dexcontrol/utils/rtc_utils.py +0 -144
  47. dexcontrol/utils/subscribers/__init__.py +0 -52
  48. dexcontrol/utils/subscribers/base.py +0 -281
  49. dexcontrol/utils/subscribers/camera.py +0 -332
  50. dexcontrol/utils/subscribers/decoders.py +0 -88
  51. dexcontrol/utils/subscribers/generic.py +0 -110
  52. dexcontrol/utils/subscribers/imu.py +0 -175
  53. dexcontrol/utils/subscribers/lidar.py +0 -172
  54. dexcontrol/utils/subscribers/protobuf.py +0 -111
  55. dexcontrol/utils/subscribers/rtc.py +0 -316
  56. dexcontrol/utils/zenoh_utils.py +0 -122
  57. dexcontrol-0.2.12.dist-info/RECORD +0 -75
  58. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.dist-info}/WHEEL +0 -0
  59. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,172 +0,0 @@
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
- """LIDAR Zenoh subscriber for scan data.
12
-
13
- This module provides a specialized subscriber for LIDAR scan data,
14
- using the serialization format from dexsensor.
15
- """
16
-
17
- from typing import Any
18
-
19
- import numpy as np
20
- import zenoh
21
- from loguru import logger
22
-
23
- from .base import BaseZenohSubscriber, CustomDataHandler
24
-
25
- # Import lidar serialization functions from dexsensor
26
- try:
27
- from dexsensor.serialization.lidar import decode_scan_data
28
- except ImportError:
29
- logger.error(
30
- "Failed to import dexsensor lidar serialization functions. Please install dexsensor."
31
- )
32
- decode_scan_data = None
33
-
34
-
35
- class LidarSubscriber(BaseZenohSubscriber):
36
- """Zenoh subscriber for LIDAR scan data.
37
-
38
- This subscriber handles LIDAR scan data encoded using the dexsensor
39
- lidar serialization format. Uses lazy decoding - data is only decoded
40
- when requested.
41
- """
42
-
43
- def __init__(
44
- self,
45
- topic: str,
46
- zenoh_session: zenoh.Session,
47
- name: str = "lidar_subscriber",
48
- enable_fps_tracking: bool = True,
49
- fps_log_interval: int = 50,
50
- custom_data_handler: CustomDataHandler | None = None,
51
- ) -> None:
52
- """Initialize the LIDAR subscriber.
53
-
54
- Args:
55
- topic: Zenoh topic to subscribe to for LIDAR data.
56
- zenoh_session: Active Zenoh session for communication.
57
- name: Name for logging purposes.
58
- enable_fps_tracking: Whether to track and log FPS metrics.
59
- fps_log_interval: Number of frames between FPS calculations.
60
- custom_data_handler: Optional custom function to handle incoming data.
61
- If provided, this will replace the default data
62
- handling logic entirely.
63
- """
64
- super().__init__(
65
- topic,
66
- zenoh_session,
67
- name,
68
- enable_fps_tracking,
69
- fps_log_interval,
70
- custom_data_handler,
71
- )
72
- self._latest_raw_data: bytes | None = None
73
-
74
- def _data_handler(self, sample: zenoh.Sample) -> None:
75
- """Handle incoming LIDAR scan data.
76
-
77
- Args:
78
- sample: Zenoh sample containing encoded LIDAR scan data.
79
- """
80
- with self._data_lock:
81
- self._latest_raw_data = sample.payload.to_bytes()
82
- self._active = True
83
-
84
- self._update_fps_metrics()
85
-
86
- def get_latest_data(self) -> dict[str, Any] | None:
87
- """Get the latest LIDAR scan data.
88
-
89
- Returns:
90
- Latest scan data dictionary if available, None otherwise.
91
- Dictionary contains:
92
- - ranges: Array of range measurements in meters
93
- - angles: Array of corresponding angles in radians
94
- - qualities: Array of quality values (0-255) if available, None otherwise
95
- - timestamp: Timestamp in nanoseconds (int)
96
- """
97
- with self._data_lock:
98
- if self._latest_raw_data is None:
99
- return None
100
-
101
- if decode_scan_data is None:
102
- logger.error(
103
- f"Cannot decode LIDAR scan for {self._name}: dexsensor not available"
104
- )
105
- return None
106
-
107
- try:
108
- # Decode the LIDAR scan data
109
- scan_data = decode_scan_data(self._latest_raw_data)
110
- # Return a copy to avoid external modifications
111
- return {
112
- key: value.copy() if isinstance(value, np.ndarray) else value
113
- for key, value in scan_data.items()
114
- }
115
- except Exception as e:
116
- logger.error(f"Failed to decode LIDAR scan for {self._name}: {e}")
117
- return None
118
-
119
- def get_latest_scan(self) -> dict[str, Any] | None:
120
- """Get the latest LIDAR scan data.
121
-
122
- Alias for get_latest_data() for clarity.
123
-
124
- Returns:
125
- Latest scan data dictionary if available, None otherwise.
126
- """
127
- return self.get_latest_data()
128
-
129
- def get_ranges(self) -> np.ndarray | None:
130
- """Get the latest range measurements.
131
-
132
- Returns:
133
- Array of range measurements in meters if available, None otherwise.
134
- """
135
- scan_data = self.get_latest_data()
136
- if scan_data is not None:
137
- return scan_data["ranges"]
138
- return None
139
-
140
- def get_angles(self) -> np.ndarray | None:
141
- """Get the latest angle measurements.
142
-
143
- Returns:
144
- Array of angle measurements in radians if available, None otherwise.
145
- """
146
- scan_data = self.get_latest_data()
147
- if scan_data is not None:
148
- return scan_data["angles"]
149
- return None
150
-
151
- def get_qualities(self) -> np.ndarray | None:
152
- """Get the latest quality measurements.
153
-
154
- Returns:
155
- Array of quality values (0-255) if available, None otherwise.
156
- """
157
- scan_data = self.get_latest_data()
158
- if scan_data is not None:
159
- return scan_data.get("qualities")
160
- return None
161
-
162
- def has_qualities(self) -> bool:
163
- """Check if the latest scan data includes quality information.
164
-
165
- Returns:
166
- True if quality data is available, False otherwise.
167
- """
168
- scan_data = self.get_latest_data()
169
- if scan_data is not None:
170
- qualities = scan_data.get("qualities")
171
- return qualities is not None and len(qualities) > 0
172
- return False
@@ -1,111 +0,0 @@
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
- """Protobuf-specific Zenoh subscriber.
12
-
13
- This module provides a subscriber specifically designed for handling protobuf messages
14
- with automatic parsing and type safety. Uses lazy decoding for efficiency.
15
- """
16
-
17
- from typing import TypeVar, cast
18
-
19
- import zenoh
20
- from google.protobuf.message import Message
21
- from loguru import logger
22
-
23
- from .base import BaseZenohSubscriber, CustomDataHandler
24
-
25
- M = TypeVar("M", bound=Message)
26
-
27
-
28
- class ProtobufZenohSubscriber(BaseZenohSubscriber):
29
- """Zenoh subscriber specifically for protobuf messages.
30
-
31
- This subscriber automatically handles protobuf message parsing and provides
32
- type-safe access to the latest message data. Uses lazy decoding - protobuf
33
- messages are only parsed when requested.
34
- """
35
-
36
- def __init__(
37
- self,
38
- topic: str,
39
- zenoh_session: zenoh.Session,
40
- message_type: type[M],
41
- name: str = "protobuf_subscriber",
42
- enable_fps_tracking: bool = False,
43
- fps_log_interval: int = 100,
44
- custom_data_handler: CustomDataHandler | None = None,
45
- ) -> None:
46
- """Initialize the protobuf Zenoh subscriber.
47
-
48
- Args:
49
- topic: Zenoh topic to subscribe to for protobuf messages.
50
- zenoh_session: Active Zenoh session for communication.
51
- message_type: Protobuf message class to parse incoming data.
52
- name: Name for logging purposes.
53
- enable_fps_tracking: Whether to track and log FPS metrics.
54
- fps_log_interval: Number of frames between FPS calculations.
55
- custom_data_handler: Optional custom function to handle incoming data.
56
- If provided, this will replace the default data
57
- handling logic entirely.
58
- """
59
- super().__init__(
60
- topic,
61
- zenoh_session,
62
- name,
63
- enable_fps_tracking,
64
- fps_log_interval,
65
- custom_data_handler,
66
- )
67
- self._message_type = message_type
68
- self._latest_raw_data: zenoh.ZBytes = zenoh.ZBytes("")
69
-
70
- def _data_handler(self, sample: zenoh.Sample) -> None:
71
- """Handle incoming protobuf data.
72
-
73
- Args:
74
- sample: Zenoh sample containing protobuf data.
75
- """
76
- with self._data_lock:
77
- self._latest_raw_data = sample.payload
78
- self._active = True
79
-
80
- self._update_fps_metrics()
81
-
82
- def get_latest_data(self) -> M | None:
83
- """Get the latest protobuf message.
84
-
85
- Returns:
86
- Latest parsed protobuf message if available and parsing succeeded,
87
- None otherwise.
88
- """
89
- with self._data_lock:
90
- if not self._active:
91
- return None
92
-
93
- # Parse protobuf message on demand
94
- try:
95
- message = self._message_type()
96
- message.ParseFromString(self._latest_raw_data.to_bytes())
97
- return cast(M, message)
98
- except Exception as e:
99
- logger.error(f"Failed to parse protobuf message for {self._name}: {e}")
100
- return None
101
-
102
- def get_latest_raw_data(self) -> bytes | None:
103
- """Get the latest raw protobuf data bytes.
104
-
105
- Returns:
106
- Latest raw protobuf data bytes if available, None otherwise.
107
- """
108
- with self._data_lock:
109
- if not self._active:
110
- return None
111
- return self._latest_raw_data.to_bytes()
@@ -1,316 +0,0 @@
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
- import asyncio
12
- import json
13
- import threading
14
- import time
15
-
16
- import numpy as np
17
- import websockets
18
- from aiortc import RTCPeerConnection, RTCSessionDescription
19
- from loguru import logger
20
-
21
- # Try to import uvloop for better performance
22
- try:
23
- import uvloop # type: ignore[import-untyped]
24
-
25
- UVLOOP_AVAILABLE = True
26
- except ImportError:
27
- uvloop = None # type: ignore[assignment]
28
- UVLOOP_AVAILABLE = False
29
-
30
-
31
- class RTCSubscriber:
32
- """
33
- Subscriber for receiving video data via RTC.
34
-
35
- This class connects to a RTC peer through a signaling server,
36
- receives a video stream, and makes the latest frame available.
37
- """
38
-
39
- def __init__(
40
- self,
41
- url: str,
42
- name: str = "rtc_subscriber",
43
- enable_fps_tracking: bool = True,
44
- fps_log_interval: int = 100,
45
- ):
46
- """
47
- Initialize the RTC subscriber.
48
-
49
- Args:
50
- url: WebSocket URL of the signaling server.
51
- name: Name for logging purposes.
52
- enable_fps_tracking: Whether to track and log FPS metrics.
53
- fps_log_interval: Number of frames between FPS calculations.
54
- """
55
- self._url = url
56
- self._name = name
57
- self._pc = RTCPeerConnection()
58
- self._latest_frame: np.ndarray | None = None
59
- self._active = False
60
- self._data_lock = threading.Lock()
61
- self._stop_event = (
62
- threading.Event()
63
- ) # Use threading.Event for cross-thread communication
64
- self._async_stop_event = None # Will be created in the async context
65
- self._websocket = None # Store websocket reference for clean shutdown
66
-
67
- # FPS tracking
68
- self._enable_fps_tracking = enable_fps_tracking
69
- self._fps_log_interval = fps_log_interval
70
- self._frame_count = 0
71
- self._fps = 0.0
72
- self._last_fps_time = time.time()
73
-
74
- self._thread = threading.Thread(target=self._run_event_loop, daemon=True)
75
- self._thread.start()
76
-
77
- def _run_event_loop(self):
78
- """Run the asyncio event loop in a separate thread."""
79
- try:
80
- # Use uvloop if available for better performance
81
- if UVLOOP_AVAILABLE and uvloop is not None:
82
- # Create a new uvloop event loop for this thread
83
- loop = uvloop.new_event_loop()
84
- asyncio.set_event_loop(loop)
85
- logger.debug(f"Using uvloop for {self._name}")
86
-
87
- try:
88
- loop.run_until_complete(self._run())
89
- finally:
90
- loop.close()
91
- else:
92
- # Use default asyncio event loop
93
- asyncio.run(self._run())
94
-
95
- except Exception as e:
96
- logger.error(f"Event loop error for {self._name}: {e}")
97
- finally:
98
- with self._data_lock:
99
- self._active = False
100
-
101
- async def _run(self):
102
- """
103
- Connects to a RTC peer, receives video, and saves frames to disk.
104
- """
105
- # Create async stop event in the async context
106
- self._async_stop_event = asyncio.Event()
107
-
108
- # Start a task to monitor the threading stop event
109
- monitor_task = asyncio.create_task(self._monitor_stop_event())
110
-
111
- @self._pc.on("track")
112
- async def on_track(track):
113
- if track.kind == "video":
114
- while (
115
- self._async_stop_event is not None
116
- and not self._async_stop_event.is_set()
117
- ):
118
- try:
119
- frame = await asyncio.wait_for(
120
- track.recv(), timeout=1.0
121
- ) # Reduced timeout for faster shutdown response
122
- img = frame.to_ndarray(format="rgb24")
123
- with self._data_lock:
124
- self._latest_frame = img
125
- if not self._active:
126
- self._active = True
127
- self._update_fps_metrics()
128
- except asyncio.TimeoutError:
129
- # Check if we should stop before logging error
130
- if (
131
- self._async_stop_event is not None
132
- and not self._async_stop_event.is_set()
133
- ):
134
- logger.warning(
135
- f"Timeout: No frame received in 1 second from {self._url}"
136
- )
137
- continue
138
- except Exception as e:
139
- if (
140
- self._async_stop_event is not None
141
- and not self._async_stop_event.is_set()
142
- ):
143
- logger.error(f"Error receiving frame from {self._url}: {e}")
144
- break
145
-
146
- @self._pc.on("connectionstatechange")
147
- async def on_connectionstatechange():
148
- if self._pc.connectionState == "failed":
149
- logger.warning(f"RTC connection failed for {self._url}")
150
- await self._pc.close()
151
- if self._async_stop_event is not None:
152
- self._async_stop_event.set()
153
-
154
- try:
155
- async with websockets.connect(self._url) as websocket:
156
- self._websocket = websocket
157
-
158
- # Create an offer. The server's assertive codec control makes
159
- # client-side preferences redundant and potentially conflicting.
160
- self._pc.addTransceiver("video", direction="recvonly")
161
- offer = await self._pc.createOffer()
162
- await self._pc.setLocalDescription(offer)
163
-
164
- # Send the offer to the server
165
- await websocket.send(
166
- json.dumps(
167
- {
168
- "sdp": self._pc.localDescription.sdp,
169
- "type": self._pc.localDescription.type,
170
- }
171
- )
172
- )
173
-
174
- # Wait for the answer
175
- response = json.loads(await websocket.recv())
176
- if response["type"] == "answer":
177
- await self._pc.setRemoteDescription(
178
- RTCSessionDescription(
179
- sdp=response["sdp"], type=response["type"]
180
- )
181
- )
182
- else:
183
- logger.error(
184
- f"Received unexpected message type: {response['type']} from {self._url}"
185
- )
186
- if self._async_stop_event is not None:
187
- self._async_stop_event.set()
188
-
189
- # Wait until the stop event is set
190
- if self._async_stop_event is not None:
191
- await self._async_stop_event.wait()
192
-
193
- except websockets.exceptions.ConnectionClosed:
194
- logger.info(f"WebSocket connection closed for {self._url}")
195
- except Exception as e:
196
- if not self._async_stop_event.is_set():
197
- logger.error(f"Operation failed for {self._url}: {e}")
198
- finally:
199
- # Cancel the monitor task
200
- monitor_task.cancel()
201
- try:
202
- await monitor_task
203
- except asyncio.CancelledError:
204
- pass
205
-
206
- # Close websocket if still open
207
- if self._websocket:
208
- try:
209
- await self._websocket.close()
210
- except Exception as e:
211
- logger.debug(f"Error closing websocket for {self._url}: {e}")
212
-
213
- # Close peer connection if not already closed
214
- if self._pc.connectionState != "closed":
215
- try:
216
- await self._pc.close()
217
- except Exception as e:
218
- logger.debug(f"Error closing peer connection for {self._url}: {e}")
219
-
220
- with self._data_lock:
221
- self._active = False
222
-
223
- async def _monitor_stop_event(self):
224
- """Monitor the threading stop event and set the async stop event when needed."""
225
- while not self._stop_event.is_set():
226
- await asyncio.sleep(0.1) # Check every 100ms
227
- if self._async_stop_event is not None:
228
- self._async_stop_event.set()
229
-
230
- def _update_fps_metrics(self) -> None:
231
- """Update FPS tracking metrics.
232
-
233
- Increments frame counter and recalculates FPS at specified intervals.
234
- Only has an effect if fps_tracking was enabled during initialization.
235
- """
236
- if not self._enable_fps_tracking:
237
- return
238
-
239
- self._frame_count += 1
240
- if self._frame_count >= self._fps_log_interval:
241
- current_time = time.time()
242
- elapsed = current_time - self._last_fps_time
243
- self._fps = self._frame_count / elapsed
244
- logger.info(f"{self._name} frequency: {self._fps:.2f} Hz")
245
- self._frame_count = 0
246
- self._last_fps_time = current_time
247
-
248
- def get_latest_data(self) -> np.ndarray | None:
249
- """
250
- Get the latest video frame.
251
-
252
- Returns:
253
- Latest video frame as a numpy array (HxWxC RGB) if available, None otherwise.
254
- """
255
- with self._data_lock:
256
- return self._latest_frame.copy() if self._latest_frame is not None else None
257
-
258
- def is_active(self) -> bool:
259
- """Check if the subscriber is actively receiving data."""
260
- with self._data_lock:
261
- return self._active
262
-
263
- def wait_for_active(self, timeout: float = 5.0) -> bool:
264
- """
265
- Wait for the subscriber to start receiving data.
266
-
267
- Args:
268
- timeout: Maximum time to wait in seconds.
269
-
270
- Returns:
271
- True if subscriber becomes active, False if timeout is reached.
272
- """
273
- start_time = time.time()
274
- while not self.is_active():
275
- if time.time() - start_time > timeout:
276
- logger.error(
277
- f"No data received from {self._name} at {self._url} after {timeout}s"
278
- )
279
- return False
280
- time.sleep(0.1)
281
- return True
282
-
283
- def shutdown(self):
284
- """Stop the subscriber and release resources."""
285
-
286
- # Signal the async loop to stop
287
- self._stop_event.set()
288
-
289
- # Wait for the thread to finish with a reasonable timeout
290
- if self._thread.is_alive():
291
- self._thread.join(
292
- timeout=10.0
293
- ) # Increased timeout for more graceful shutdown
294
-
295
- if self._thread.is_alive():
296
- logger.warning(
297
- f"{self._name} thread did not shut down gracefully within timeout."
298
- )
299
-
300
- # Ensure active state is set to False
301
- with self._data_lock:
302
- self._active = False
303
-
304
- @property
305
- def name(self) -> str:
306
- """Get the subscriber name."""
307
- return self._name
308
-
309
- @property
310
- def fps(self) -> float:
311
- """Get the current FPS measurement.
312
-
313
- Returns:
314
- Current frames per second measurement.
315
- """
316
- return self._fps