dexcontrol 0.2.12__py3-none-any.whl → 0.3.4__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.
Files changed (60) hide show
  1. dexcontrol/__init__.py +17 -8
  2. dexcontrol/apps/dualsense_teleop_base.py +1 -1
  3. dexcontrol/comm/__init__.py +51 -0
  4. dexcontrol/comm/rtc.py +401 -0
  5. dexcontrol/comm/subscribers.py +329 -0
  6. dexcontrol/config/core/chassis.py +9 -4
  7. dexcontrol/config/core/hand.py +1 -0
  8. dexcontrol/config/sensors/cameras/__init__.py +1 -2
  9. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  10. dexcontrol/config/sensors/vega_sensors.py +12 -18
  11. dexcontrol/config/vega.py +4 -1
  12. dexcontrol/core/arm.py +66 -42
  13. dexcontrol/core/chassis.py +142 -120
  14. dexcontrol/core/component.py +107 -58
  15. dexcontrol/core/hand.py +119 -86
  16. dexcontrol/core/head.py +22 -33
  17. dexcontrol/core/misc.py +331 -158
  18. dexcontrol/core/robot_query_interface.py +467 -0
  19. dexcontrol/core/torso.py +5 -9
  20. dexcontrol/robot.py +245 -574
  21. dexcontrol/sensors/__init__.py +1 -2
  22. dexcontrol/sensors/camera/__init__.py +0 -2
  23. dexcontrol/sensors/camera/base_camera.py +150 -0
  24. dexcontrol/sensors/camera/rgb_camera.py +68 -64
  25. dexcontrol/sensors/camera/zed_camera.py +140 -164
  26. dexcontrol/sensors/imu/chassis_imu.py +81 -62
  27. dexcontrol/sensors/imu/zed_imu.py +54 -43
  28. dexcontrol/sensors/lidar/rplidar.py +16 -20
  29. dexcontrol/sensors/manager.py +4 -14
  30. dexcontrol/sensors/ultrasonic.py +15 -28
  31. dexcontrol/utils/__init__.py +0 -11
  32. dexcontrol/utils/comm_helper.py +110 -0
  33. dexcontrol/utils/constants.py +1 -1
  34. dexcontrol/utils/error_code.py +2 -4
  35. dexcontrol/utils/os_utils.py +172 -4
  36. dexcontrol/utils/pb_utils.py +6 -28
  37. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/METADATA +16 -3
  38. dexcontrol-0.3.4.dist-info/RECORD +62 -0
  39. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/WHEEL +1 -1
  40. dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
  41. dexcontrol/proto/dexcontrol_msg_pb2.py +0 -73
  42. dexcontrol/proto/dexcontrol_msg_pb2.pyi +0 -220
  43. dexcontrol/proto/dexcontrol_query_pb2.py +0 -77
  44. dexcontrol/proto/dexcontrol_query_pb2.pyi +0 -162
  45. dexcontrol/sensors/camera/luxonis_camera.py +0 -169
  46. dexcontrol/utils/motion_utils.py +0 -199
  47. dexcontrol/utils/rate_limiter.py +0 -172
  48. dexcontrol/utils/rtc_utils.py +0 -144
  49. dexcontrol/utils/subscribers/__init__.py +0 -52
  50. dexcontrol/utils/subscribers/base.py +0 -281
  51. dexcontrol/utils/subscribers/camera.py +0 -332
  52. dexcontrol/utils/subscribers/decoders.py +0 -88
  53. dexcontrol/utils/subscribers/generic.py +0 -110
  54. dexcontrol/utils/subscribers/imu.py +0 -175
  55. dexcontrol/utils/subscribers/lidar.py +0 -172
  56. dexcontrol/utils/subscribers/protobuf.py +0 -111
  57. dexcontrol/utils/subscribers/rtc.py +0 -316
  58. dexcontrol/utils/zenoh_utils.py +0 -122
  59. dexcontrol-0.2.12.dist-info/RECORD +0 -75
  60. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,467 @@
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
+ """Query utilities for robot communication using DexComm.
12
+
13
+ This module provides the RobotQueryInterface class that encapsulates all communication
14
+ queries with the robot server using DexComm's service pattern. It handles various query
15
+ types including hand type detection, version information, status queries, and control
16
+ operations.
17
+ """
18
+
19
+ import json
20
+ import time
21
+ from typing import TYPE_CHECKING, Any, Literal, cast
22
+
23
+ # Use DexComm for all communication
24
+ from dexcomm import call_service
25
+ from dexcomm.serialization.protobuf import control_query_pb2
26
+ from loguru import logger
27
+
28
+ from dexcontrol.config.vega import VegaConfig, get_vega_config
29
+ from dexcontrol.core.hand import HandType
30
+ from dexcontrol.utils.comm_helper import get_zenoh_config_path
31
+ from dexcontrol.utils.os_utils import resolve_key_name
32
+ from dexcontrol.utils.pb_utils import (
33
+ ComponentStatus,
34
+ status_to_dict,
35
+ )
36
+ from dexcontrol.utils.viz_utils import show_component_status
37
+
38
+ if TYPE_CHECKING:
39
+ from dexcontrol.config.vega import VegaConfig
40
+
41
+
42
+ class RobotQueryInterface:
43
+ """Base class for zenoh query operations.
44
+
45
+ This class provides a clean interface for all zenoh-based queries and
46
+ communication operations. It maintains references to the zenoh session
47
+ and configuration needed for queries.
48
+
49
+ Can be used as a context manager for automatic resource cleanup:
50
+ >>> with RobotQueryInterface.create() as interface:
51
+ ... version_info = interface.get_version_info()
52
+ """
53
+
54
+ def __init__(self, configs: "VegaConfig"):
55
+ """Initialize the RobotQueryInterface.
56
+
57
+ Args:
58
+ configs: Robot configuration containing query names.
59
+ """
60
+ # Session parameter kept for compatibility but not used
61
+ self._configs = configs
62
+ self._owns_session = False
63
+
64
+ @classmethod
65
+ def create(cls) -> "RobotQueryInterface":
66
+ """Create a standalone RobotQueryInterface.
67
+
68
+ This class method provides a convenient way to create a RobotQueryInterface
69
+ without requiring the full Robot class. DexComm handles all session
70
+ management internally.
71
+
72
+ Returns:
73
+ RobotQueryInterface instance ready for use.
74
+
75
+ Example:
76
+ >>> query_interface = RobotQueryInterface.create()
77
+ >>> version_info = query_interface.get_version_info()
78
+ >>> query_interface.close()
79
+ """
80
+ # DexComm handles session internally, we just need config
81
+ config: VegaConfig = get_vega_config()
82
+ instance = cls(config)
83
+ instance._owns_session = True
84
+ return instance
85
+
86
+ def close(self) -> None:
87
+ """Close the communication session if owned by this instance.
88
+
89
+ This method should be called when done using a standalone
90
+ RobotQueryInterface to properly clean up resources.
91
+ """
92
+ if self._owns_session:
93
+ # DexComm cleanup is handled automatically
94
+ logger.debug("DexComm session cleanup handled automatically")
95
+
96
+ def __enter__(self) -> "RobotQueryInterface":
97
+ """Enter context manager."""
98
+ return self
99
+
100
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
101
+ """Exit context manager and clean up resources."""
102
+ self.close()
103
+
104
+ def query_hand_type(self) -> dict[str, HandType]:
105
+ """Query the hand type information from the server.
106
+
107
+ Returns:
108
+ Dictionary containing hand type information for left and right hands.
109
+ Format: {"left": hand_type_name, "right": hand_type_name}
110
+ Possible hand types: "UNKNOWN", "HandF5D6_V1", "HandF5D6_V2"
111
+ UNKNOWN means not connected or unknown end effector connected.
112
+
113
+ Raises:
114
+ RuntimeError: If hand type information cannot be retrieved after 3 attempts.
115
+ """
116
+ full_topic = resolve_key_name(self._configs.hand_info_query_name)
117
+ max_attempts = 3
118
+ last_error = None
119
+
120
+ for _ in range(max_attempts):
121
+ try:
122
+ # Query hand type using DexComm service
123
+ response = call_service(
124
+ full_topic,
125
+ timeout=10.0,
126
+ config=get_zenoh_config_path(),
127
+ request_serializer=None,
128
+ response_deserializer=None,
129
+ )
130
+
131
+ if response:
132
+ payload_str = response.decode("utf-8")
133
+ hand_info = json.loads(payload_str)
134
+
135
+ # Validate the expected format
136
+ if (
137
+ isinstance(hand_info, dict)
138
+ and "left" in hand_info
139
+ and "right" in hand_info
140
+ ):
141
+ logger.info(f"End effector hand types: {hand_info}")
142
+ return {
143
+ "left": HandType(hand_info["left"]),
144
+ "right": HandType(hand_info["right"]),
145
+ }
146
+ else:
147
+ last_error = f"Invalid response format: {hand_info}"
148
+ else:
149
+ last_error = "No response received from server"
150
+
151
+ except Exception as e:
152
+ last_error = str(e)
153
+
154
+ # All attempts failed, raise error
155
+ error_msg = f"Failed to query hand type after {max_attempts} attempts"
156
+ if last_error:
157
+ error_msg += f": {last_error}"
158
+ raise RuntimeError(error_msg)
159
+
160
+ def query_ntp(
161
+ self,
162
+ sample_count: int = 30,
163
+ show: bool = False,
164
+ timeout: float = 1.0,
165
+ device: Literal["soc", "jetson"] = "soc",
166
+ ) -> dict[Literal["success", "offset", "rtt"], bool | float]:
167
+ """Query the NTP server via zenoh for time synchronization and compute robust statistics.
168
+
169
+ Args:
170
+ sample_count: Number of NTP samples to request (default: 50).
171
+ show: Whether to print summary statistics using a rich table.
172
+ timeout: Timeout for the zenoh querier in seconds (default: 2.0).
173
+ device: Which device to query for NTP ("soc" or "jetson").
174
+
175
+ Returns:
176
+ Dictionary with keys:
177
+ - "success": True if any replies were received, False otherwise.
178
+ - "offset": Mean offset (in seconds) after removing RTT outliers.
179
+ - "rtt": Mean round-trip time (in seconds) after removing RTT outliers.
180
+ """
181
+ if device == "soc":
182
+ ntp_key = resolve_key_name(self._configs.soc_ntp_query_name)
183
+ elif device == "jetson":
184
+ raise NotImplementedError("Jetson NTP query is not implemented yet")
185
+
186
+ time_offset = []
187
+ time_rtt = []
188
+
189
+ reply_count = 0
190
+ for i in range(sample_count):
191
+ request = control_query_pb2.NTPRequest()
192
+ request.client_send_time_ns = time.time_ns()
193
+ request.sample_count = sample_count
194
+ request.sample_index = i
195
+
196
+ # Use call_service for NTP query
197
+ try:
198
+ response_data = call_service(
199
+ ntp_key,
200
+ request=request,
201
+ timeout=timeout,
202
+ config=get_zenoh_config_path(),
203
+ request_serializer=lambda x: x.SerializeToString(),
204
+ response_deserializer=None,
205
+ )
206
+
207
+ if response_data:
208
+ reply_count += 1
209
+ client_receive_time_ns = time.time_ns()
210
+ response = control_query_pb2.NTPResponse()
211
+ response.ParseFromString(response_data)
212
+ t0 = request.client_send_time_ns
213
+ t1 = response.server_receive_time_ns
214
+ t2 = response.server_send_time_ns
215
+ t3 = client_receive_time_ns
216
+ offset = ((t1 - t0) + (t2 - t3)) // 2 / 1e9
217
+ rtt = (t3 - t0) / 1e9
218
+ time_offset.append(offset)
219
+ time_rtt.append(rtt)
220
+ except Exception as e:
221
+ logger.debug(f"NTP query {i} failed: {e}")
222
+
223
+ if i < sample_count - 1:
224
+ time.sleep(0.01)
225
+ if reply_count == 0:
226
+ return {"success": False, "offset": 0.0, "rtt": 0.0}
227
+
228
+ # Compute simple NTP statistics
229
+ import numpy as np
230
+
231
+ stats = {
232
+ "offset (mean)": float(np.mean(time_offset)) if time_offset else 0.0,
233
+ "round_trip_time (mean)": float(np.mean(time_rtt)) if time_rtt else 0.0,
234
+ "offset (std)": float(np.std(time_offset)) if time_offset else 0.0,
235
+ "round_trip_time (std)": float(np.std(time_rtt)) if time_rtt else 0.0,
236
+ }
237
+ offset = float(stats["offset (mean)"])
238
+ rtt = float(stats["round_trip_time (mean)"])
239
+ if show:
240
+ from dexcontrol.utils.viz_utils import show_ntp_stats
241
+
242
+ show_ntp_stats(stats)
243
+
244
+ return {"success": True, "offset": offset, "rtt": rtt}
245
+
246
+ def get_version_info(self, show: bool = True) -> dict[str, Any]:
247
+ """Retrieve comprehensive version information using JSON interface.
248
+
249
+ This method queries the new JSON-based version endpoint that provides:
250
+ - Server component versions (hardware, software, compile_time, hashes)
251
+ - Minimum required client version
252
+ - Version compatibility information
253
+
254
+ Args:
255
+ show: Whether to display the version information.
256
+
257
+ Returns:
258
+ Dictionary containing comprehensive version information with structure:
259
+ {
260
+ "server": {
261
+ "component_name": {
262
+ "hardware_version": int,
263
+ "software_version": int,
264
+ "compile_time": str,
265
+ "main_hash": str,
266
+ "sub_hash": str
267
+ }
268
+ },
269
+ "client": {
270
+ "minimal_version": str
271
+ }
272
+ }
273
+
274
+ Raises:
275
+ RuntimeError: If version information cannot be retrieved.
276
+ """
277
+ try:
278
+ response = call_service(
279
+ resolve_key_name(self._configs.version_info_name),
280
+ timeout=5.0,
281
+ config=get_zenoh_config_path(),
282
+ request_serializer=None,
283
+ response_deserializer=None,
284
+ )
285
+
286
+ if response:
287
+ try:
288
+ # Parse JSON response directly
289
+ if isinstance(response, bytes):
290
+ payload_str = response.decode("utf-8")
291
+ else:
292
+ payload_str = response
293
+ version_info = json.loads(payload_str)
294
+
295
+ # Validate expected structure
296
+ if (
297
+ isinstance(version_info, dict)
298
+ and "server" in version_info
299
+ and "client" in version_info
300
+ ):
301
+ if show:
302
+ self._show_version_info(version_info)
303
+ return version_info
304
+ else:
305
+ logger.warning(
306
+ f"Invalid version info format received: {version_info}"
307
+ )
308
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
309
+ logger.warning(f"Failed to parse version info response: {e}")
310
+
311
+ raise RuntimeError("No valid version information received from server")
312
+
313
+ except Exception as e:
314
+ raise RuntimeError(f"Failed to retrieve version information: {e}") from e
315
+
316
+ def get_component_status(
317
+ self, show: bool = True
318
+ ) -> dict[str, dict[str, bool | ComponentStatus]]:
319
+ """Retrieve status information for all components.
320
+
321
+ Args:
322
+ show: Whether to display the status information.
323
+
324
+ Returns:
325
+ Dictionary containing status information for all components.
326
+
327
+ Raises:
328
+ RuntimeError: If status information cannot be retrieved.
329
+ """
330
+ try:
331
+ response = call_service(
332
+ resolve_key_name(self._configs.status_info_name),
333
+ timeout=2.0,
334
+ config=get_zenoh_config_path(),
335
+ request_serializer=None,
336
+ response_deserializer=None,
337
+ )
338
+
339
+ status_dict = {}
340
+ if response:
341
+ # Parse protobuf response directly
342
+ status_msg = cast(
343
+ control_query_pb2.ComponentStates,
344
+ control_query_pb2.ComponentStates.FromString(response),
345
+ )
346
+ status_dict = status_to_dict(status_msg)
347
+
348
+ if show:
349
+ show_component_status(status_dict)
350
+ return status_dict
351
+ except Exception as e:
352
+ raise RuntimeError(f"Failed to retrieve component status: {e}") from e
353
+
354
+ def reboot_component(
355
+ self,
356
+ part: Literal["arm", "chassis", "torso"],
357
+ ) -> None:
358
+ """Reboot a specific robot component.
359
+
360
+ Args:
361
+ part: Component to reboot ("arm", "chassis", or "torso").
362
+
363
+ Raises:
364
+ ValueError: If the specified component is invalid.
365
+ RuntimeError: If the reboot operation fails.
366
+ """
367
+ component_map = {
368
+ "arm": control_query_pb2.RebootComponent.Component.ARM,
369
+ "chassis": control_query_pb2.RebootComponent.Component.CHASSIS,
370
+ "torso": control_query_pb2.RebootComponent.Component.TORSO,
371
+ }
372
+
373
+ if part not in component_map:
374
+ raise ValueError(f"Invalid component: {part}")
375
+
376
+ try:
377
+ query_msg = control_query_pb2.RebootComponent(component=component_map[part])
378
+
379
+ call_service(
380
+ resolve_key_name(self._configs.reboot_query_name),
381
+ request=query_msg,
382
+ timeout=30.0,
383
+ config=get_zenoh_config_path(),
384
+ request_serializer=lambda x: x.SerializeToString(),
385
+ response_deserializer=None,
386
+ )
387
+ logger.info(f"Rebooting component: {part}")
388
+ except Exception as e:
389
+ raise RuntimeError(f"Failed to reboot component {part}: {e}") from e
390
+
391
+ def clear_error(
392
+ self,
393
+ part: Literal["left_arm", "right_arm", "chassis", "head"] | str,
394
+ ) -> None:
395
+ """Clear error state for a specific component.
396
+
397
+ Args:
398
+ part: Component to clear error state for.
399
+
400
+ Raises:
401
+ ValueError: If the specified component is invalid.
402
+ RuntimeError: If the error clearing operation fails.
403
+ """
404
+ component_map = {
405
+ "left_arm": control_query_pb2.ClearError.Component.LEFT_ARM,
406
+ "right_arm": control_query_pb2.ClearError.Component.RIGHT_ARM,
407
+ "chassis": control_query_pb2.ClearError.Component.CHASSIS,
408
+ "head": control_query_pb2.ClearError.Component.HEAD,
409
+ }
410
+
411
+ if part not in component_map:
412
+ raise ValueError(f"Invalid component: {part}")
413
+
414
+ try:
415
+ query_msg = control_query_pb2.ClearError(component=component_map[part])
416
+
417
+ call_service(
418
+ resolve_key_name(self._configs.clear_error_query_name),
419
+ request=query_msg,
420
+ timeout=2.0,
421
+ config=get_zenoh_config_path(),
422
+ request_serializer=lambda x: x.SerializeToString(),
423
+ response_deserializer=None,
424
+ )
425
+ logger.info(f"Cleared error of {part}")
426
+ except Exception as e:
427
+ raise RuntimeError(
428
+ f"Failed to clear error for component {part}: {e}"
429
+ ) from e
430
+
431
+ def _show_version_info(self, version_info: dict[str, Any]) -> None:
432
+ """Display comprehensive version information in a formatted table.
433
+
434
+ Args:
435
+ version_info: Dictionary containing server and client version information.
436
+ """
437
+ from rich.console import Console
438
+ from rich.table import Table
439
+
440
+ console = Console()
441
+ table = Table(title="🤖 Robot System Version Information")
442
+
443
+ table.add_column("Component", justify="left", style="cyan", no_wrap=True)
444
+ table.add_column("Hardware Ver.", justify="center", style="magenta")
445
+ table.add_column("Software Ver.", justify="center", style="green")
446
+ table.add_column("Compile Time", justify="center", style="yellow")
447
+ table.add_column("Main Hash", justify="center", style="blue")
448
+ table.add_column("Sub Hash", justify="center", style="red")
449
+
450
+ # Display server components
451
+ server_info = version_info.get("server", {})
452
+ for component, info in server_info.items():
453
+ if isinstance(info, dict):
454
+ table.add_row(
455
+ component,
456
+ str(info.get("hardware_version", "N/A")),
457
+ str(info.get("software_version", "N/A")),
458
+ str(info.get("compile_time", "N/A")),
459
+ str(info.get("main_hash", "N/A")[:8])
460
+ if info.get("main_hash")
461
+ else "N/A",
462
+ str(info.get("sub_hash", "N/A")[:8])
463
+ if info.get("sub_hash")
464
+ else "N/A",
465
+ )
466
+
467
+ console.print(table)
dexcontrol/core/torso.py CHANGED
@@ -15,12 +15,11 @@ communication. It handles joint position and velocity control and state monitori
15
15
  """
16
16
 
17
17
  import numpy as np
18
- import zenoh
18
+ from dexcomm.serialization.protobuf import control_msg_pb2
19
19
  from jaxtyping import Float
20
20
 
21
21
  from dexcontrol.config.core import TorsoConfig
22
22
  from dexcontrol.core.component import RobotJointComponent
23
- from dexcontrol.proto import dexcontrol_msg_pb2
24
23
 
25
24
 
26
25
  class Torso(RobotJointComponent):
@@ -37,20 +36,17 @@ class Torso(RobotJointComponent):
37
36
  def __init__(
38
37
  self,
39
38
  configs: TorsoConfig,
40
- zenoh_session: zenoh.Session,
41
39
  ) -> None:
42
40
  """Initialize the torso controller.
43
41
 
44
42
  Args:
45
43
  configs: Torso configuration parameters containing communication topics
46
44
  and default velocity settings.
47
- zenoh_session: Active Zenoh communication session for message passing.
48
45
  """
49
46
  super().__init__(
50
47
  state_sub_topic=configs.state_sub_topic,
51
48
  control_pub_topic=configs.control_pub_topic,
52
- state_message_type=dexcontrol_msg_pb2.TorsoState,
53
- zenoh_session=zenoh_session,
49
+ state_message_type=control_msg_pb2.MotorStateWithCurrent,
54
50
  joint_name=configs.joint_name,
55
51
  joint_limit=configs.joint_limit
56
52
  if hasattr(configs, "joint_limit")
@@ -119,9 +115,9 @@ class Torso(RobotJointComponent):
119
115
  )
120
116
 
121
117
  # Create and send control message
122
- control_msg = dexcontrol_msg_pb2.TorsoCommand()
123
- control_msg.joint_pos.extend(joint_pos.tolist())
124
- control_msg.joint_vel.extend(joint_vel.tolist())
118
+ control_msg = control_msg_pb2.MotorPosVelCommand()
119
+ control_msg.pos.extend(joint_pos.tolist())
120
+ control_msg.vel.extend(joint_vel.tolist())
125
121
  self._publish_control(control_msg)
126
122
 
127
123
  # Wait if specified