dexcontrol 0.2.10__py3-none-any.whl → 0.3.0__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 (47) hide show
  1. dexcontrol/__init__.py +2 -0
  2. dexcontrol/config/core/arm.py +5 -1
  3. dexcontrol/config/core/chassis.py +9 -4
  4. dexcontrol/config/core/hand.py +2 -1
  5. dexcontrol/config/core/head.py +7 -8
  6. dexcontrol/config/core/misc.py +14 -1
  7. dexcontrol/config/core/torso.py +8 -4
  8. dexcontrol/config/sensors/cameras/__init__.py +2 -1
  9. dexcontrol/config/sensors/cameras/luxonis_camera.py +51 -0
  10. dexcontrol/config/sensors/cameras/rgb_camera.py +1 -1
  11. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  12. dexcontrol/config/sensors/vega_sensors.py +9 -1
  13. dexcontrol/config/vega.py +34 -3
  14. dexcontrol/core/arm.py +103 -58
  15. dexcontrol/core/chassis.py +146 -115
  16. dexcontrol/core/component.py +83 -20
  17. dexcontrol/core/hand.py +74 -39
  18. dexcontrol/core/head.py +41 -28
  19. dexcontrol/core/misc.py +256 -25
  20. dexcontrol/core/robot_query_interface.py +440 -0
  21. dexcontrol/core/torso.py +28 -10
  22. dexcontrol/proto/dexcontrol_msg_pb2.py +27 -37
  23. dexcontrol/proto/dexcontrol_msg_pb2.pyi +111 -126
  24. dexcontrol/proto/dexcontrol_query_pb2.py +39 -35
  25. dexcontrol/proto/dexcontrol_query_pb2.pyi +41 -4
  26. dexcontrol/robot.py +266 -409
  27. dexcontrol/sensors/__init__.py +2 -1
  28. dexcontrol/sensors/camera/__init__.py +2 -0
  29. dexcontrol/sensors/camera/luxonis_camera.py +169 -0
  30. dexcontrol/sensors/camera/zed_camera.py +17 -8
  31. dexcontrol/sensors/imu/chassis_imu.py +5 -1
  32. dexcontrol/sensors/imu/zed_imu.py +3 -2
  33. dexcontrol/sensors/lidar/rplidar.py +1 -0
  34. dexcontrol/sensors/manager.py +3 -0
  35. dexcontrol/utils/constants.py +3 -0
  36. dexcontrol/utils/error_code.py +236 -0
  37. dexcontrol/utils/os_utils.py +183 -1
  38. dexcontrol/utils/pb_utils.py +0 -22
  39. dexcontrol/utils/subscribers/lidar.py +1 -0
  40. dexcontrol/utils/trajectory_utils.py +17 -5
  41. dexcontrol/utils/viz_utils.py +86 -11
  42. dexcontrol/utils/zenoh_utils.py +288 -2
  43. {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/METADATA +15 -2
  44. dexcontrol-0.3.0.dist-info/RECORD +76 -0
  45. dexcontrol-0.2.10.dist-info/RECORD +0 -72
  46. {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/WHEEL +0 -0
  47. {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -12,8 +12,11 @@
12
12
 
13
13
  import os
14
14
  import re
15
- from typing import Final
15
+ from typing import Any, Final
16
16
 
17
+ from loguru import logger
18
+
19
+ import dexcontrol
17
20
  from dexcontrol.utils.constants import ROBOT_NAME_ENV_VAR
18
21
 
19
22
 
@@ -56,3 +59,182 @@ def get_robot_model() -> str:
56
59
  raise ValueError(f"Unknown robot model: {robot_model_abb}")
57
60
  model = robot_model_abb_mapping[robot_model_abb] + "-" + robot_name.split("-")[-1]
58
61
  return model
62
+
63
+
64
+ def check_version_compatibility(
65
+ version_info: dict[str, Any], show_warnings: bool = True
66
+ ) -> None:
67
+ """Check version compatibility between client and server.
68
+
69
+ This function uses the new JSON-based version interface to:
70
+ 1. Compare client library version with server's minimum required version
71
+ 2. Check server component versions for compatibility
72
+ 3. Provide clear guidance for version mismatches
73
+
74
+ Args:
75
+ version_info: Dictionary containing version information from get_version_info()
76
+ show_warnings: Whether to show warning messages (default: True)
77
+ """
78
+ validate_client_version(version_info)
79
+ validate_server_version(version_info)
80
+
81
+
82
+ def validate_client_version(version_info: dict[str, Any]) -> None:
83
+ """Validate client library version against server requirements.
84
+
85
+ Args:
86
+ version_info: Dictionary containing server and client version information.
87
+ """
88
+
89
+ client_info = version_info.get("client", {})
90
+ min_required_version = client_info.get("minimal_version")
91
+
92
+ if not min_required_version:
93
+ logger.debug("No minimum version requirement from server")
94
+ return
95
+
96
+ # Get current library version
97
+ current_version = getattr(dexcontrol, "__version__", "unknown")
98
+
99
+ if current_version == "unknown":
100
+ logger.warning("Could not determine current library version")
101
+ return
102
+
103
+ # Compare versions
104
+ comparison = compare_versions(current_version, min_required_version)
105
+
106
+ if comparison < 0:
107
+ show_version_upgrade_warning(current_version, min_required_version)
108
+
109
+
110
+ def show_version_upgrade_warning(current: str, required: str) -> None:
111
+ """Display version upgrade warning to user.
112
+
113
+ Args:
114
+ current: Current library version
115
+ required: Required minimum version
116
+ """
117
+ logger.error(
118
+ f"🚨 CLIENT VERSION TOO OLD! 🚨\n"
119
+ f"Current library version: {current}\n"
120
+ f"Minimum required version: {required}\n"
121
+ f"\n"
122
+ f"⚠️ Your dexcontrol library is outdated and may not work correctly!\n"
123
+ f"📦 Please update your library using: pip install --upgrade dexcontrol\n"
124
+ )
125
+
126
+
127
+ def validate_server_version(version_info: dict[str, Any]) -> None:
128
+ """Validate server software versions against minimum requirements.
129
+
130
+ Args:
131
+ version_info: Dictionary containing server and client version information.
132
+ """
133
+ server_info = version_info.get("server", {})
134
+
135
+ if not server_info:
136
+ logger.debug("No server version information available")
137
+ return
138
+
139
+ # Check each component's software version
140
+ components_below_min = []
141
+ for component_name, component_info in server_info.items():
142
+ if isinstance(component_info, dict):
143
+ software_version = component_info.get("software_version")
144
+ if software_version is not None:
145
+ try:
146
+ # Convert to int if it's a string
147
+ software_version_int = int(software_version)
148
+ if software_version_int < dexcontrol.MIN_SOC_SOFTWARE_VERSION:
149
+ components_below_min.append(
150
+ (component_name, software_version_int)
151
+ )
152
+ except (ValueError, TypeError) as e:
153
+ logger.debug(
154
+ f"Could not parse software version for {component_name}: {e}"
155
+ )
156
+
157
+ # If any components are below minimum version, show warning
158
+ if components_below_min:
159
+ show_server_version_warning(
160
+ components_below_min, dexcontrol.MIN_SOC_SOFTWARE_VERSION
161
+ )
162
+
163
+
164
+ def show_server_version_warning(
165
+ components: list[tuple[str, int]], min_version: int
166
+ ) -> None:
167
+ """Display server version warning to user.
168
+
169
+ Args:
170
+ components: List of (component_name, version) tuples for components below minimum.
171
+ min_version: Minimum required server software version.
172
+ """
173
+ components_str = "\n".join(
174
+ f" - {name}: version {version}" for name, version in components
175
+ )
176
+
177
+ logger.error(
178
+ f"🚨 SERVER VERSION TOO OLD! 🚨\n"
179
+ f"The following server components are below minimum version {min_version}:\n"
180
+ f"{components_str}\n"
181
+ f"\n"
182
+ f"⚠️ Your robot's firmware may be outdated and some features may not work correctly!\n"
183
+ f"📦 Please contact your robot admin or check https://github.com/dexmate-ai/vega-firmware.\n"
184
+ )
185
+
186
+
187
+ def compare_versions(version1: str, version2: str) -> int:
188
+ """Compare two semantic version strings.
189
+
190
+ Args:
191
+ version1: First version string (e.g., "1.2.3")
192
+ version2: Second version string (e.g., "1.1.0")
193
+
194
+ Returns:
195
+ -1 if version1 < version2
196
+ 0 if version1 == version2
197
+ 1 if version1 > version2
198
+ """
199
+ try:
200
+ # Clean version strings (remove 'v' prefix, handle pre-release suffixes)
201
+ def clean_version(v: str) -> list[int]:
202
+ v = v.strip().lower()
203
+ if v.startswith("v"):
204
+ v = v[1:]
205
+ # Split by dots and take only numeric parts
206
+ parts = v.split(".")
207
+ numeric_parts = []
208
+ for part in parts:
209
+ # Remove any non-numeric suffixes (like -alpha, -rc1, etc.)
210
+ numeric_part = ""
211
+ for char in part:
212
+ if char.isdigit():
213
+ numeric_part += char
214
+ else:
215
+ break
216
+ if numeric_part:
217
+ numeric_parts.append(int(numeric_part))
218
+ return numeric_parts
219
+
220
+ parts1 = clean_version(version1)
221
+ parts2 = clean_version(version2)
222
+
223
+ # Pad shorter version with zeros
224
+ max_len = max(len(parts1), len(parts2))
225
+ parts1.extend([0] * (max_len - len(parts1)))
226
+ parts2.extend([0] * (max_len - len(parts2)))
227
+
228
+ # Compare part by part
229
+ for p1, p2 in zip(parts1, parts2):
230
+ if p1 < p2:
231
+ return -1
232
+ elif p1 > p2:
233
+ return 1
234
+
235
+ return 0
236
+
237
+ except Exception as e:
238
+ logger.debug(f"Version comparison error: {e}")
239
+ # Fallback to string comparison
240
+ return -1 if version1 < version2 else (1 if version1 > version2 else 0)
@@ -20,28 +20,6 @@ TYPE_SOFTWARE_VERSION = dict[
20
20
  ]
21
21
 
22
22
 
23
- def software_version_to_dict(
24
- version_msg: dexcontrol_query_pb2.SoftwareVersion,
25
- ) -> dict[str, TYPE_SOFTWARE_VERSION]:
26
- """Convert a SoftwareVersion protobuf message to a dictionary.
27
-
28
- Args:
29
- version_msg: SoftwareVersion protobuf message.
30
-
31
- Returns:
32
- Dictionary containing version information with component names as keys.
33
- """
34
- return {
35
- key: {
36
- "hardware_version": value.hardware_version,
37
- "software_version": value.software_version,
38
- "main_hash": value.main_hash,
39
- "compile_time": value.compile_time,
40
- }
41
- for key, value in version_msg.firmware_version.items()
42
- }
43
-
44
-
45
23
  class ComponentStatus(Enum):
46
24
  """Enum representing the status of a component."""
47
25
 
@@ -92,6 +92,7 @@ class LidarSubscriber(BaseZenohSubscriber):
92
92
  - ranges: Array of range measurements in meters
93
93
  - angles: Array of corresponding angles in radians
94
94
  - qualities: Array of quality values (0-255) if available, None otherwise
95
+ - timestamp: Timestamp in nanoseconds (int)
95
96
  """
96
97
  with self._data_lock:
97
98
  if self._latest_raw_data is None:
@@ -16,7 +16,7 @@ import numpy as np
16
16
  def generate_linear_trajectory(
17
17
  current_pos: np.ndarray,
18
18
  target_pos: np.ndarray,
19
- max_vel: float = 0.5,
19
+ max_vel: float | np.ndarray = 0.5,
20
20
  control_hz: float = 100,
21
21
  ) -> tuple[np.ndarray, int]:
22
22
  """Generate a linear trajectory between current and target positions.
@@ -24,7 +24,9 @@ def generate_linear_trajectory(
24
24
  Args:
25
25
  current_pos: Current position array.
26
26
  target_pos: Target position array.
27
- max_vel: Maximum velocity in units per second.
27
+ max_vel: Maximum velocity in units per second. Can be:
28
+ - float: Same velocity limit for all dimensions
29
+ - numpy array: Per-dimension velocity limits (same length as current_pos)
28
30
  control_hz: Control frequency in Hz.
29
31
 
30
32
  Returns:
@@ -32,9 +34,19 @@ def generate_linear_trajectory(
32
34
  - trajectory: Array of waypoints from current to target position.
33
35
  - num_steps: Number of steps in the trajectory.
34
36
  """
35
- # Calculate linear interpolation between current and target positions
36
- max_diff = np.max(np.abs(target_pos - current_pos))
37
- num_steps = int(max_diff / max_vel * control_hz)
37
+ # Calculate time needed for each dimension
38
+ pos_diff = np.abs(target_pos - current_pos)
39
+
40
+ if isinstance(max_vel, np.ndarray):
41
+ # Per-dimension velocity limits - find the dimension that takes longest
42
+ time_needed = pos_diff / max_vel
43
+ max_time = np.max(time_needed)
44
+ else:
45
+ # Single velocity limit for all dimensions
46
+ max_diff = np.max(pos_diff)
47
+ max_time = max_diff / max_vel
48
+
49
+ num_steps = int(max_time * control_hz)
38
50
 
39
51
  # Ensure at least one step
40
52
  num_steps = max(1, num_steps)
@@ -10,13 +10,14 @@
10
10
 
11
11
  """Utility functions for displaying information in a Rich table format."""
12
12
 
13
+ from loguru import logger
13
14
  from rich.console import Console
14
15
  from rich.table import Table
15
16
 
16
17
  from dexcontrol.utils.pb_utils import TYPE_SOFTWARE_VERSION
17
18
 
18
19
 
19
- def show_software_version(version_info: dict[str, TYPE_SOFTWARE_VERSION]) -> None:
20
+ def show_software_version(version_info: dict[str, TYPE_SOFTWARE_VERSION]):
20
21
  """Create a Rich table for displaying firmware version information.
21
22
 
22
23
  Args:
@@ -42,25 +43,26 @@ def show_software_version(version_info: dict[str, TYPE_SOFTWARE_VERSION]) -> Non
42
43
  console.print(table)
43
44
 
44
45
 
45
- def show_component_status(status_info: dict[str, dict]) -> None:
46
+ def show_component_status(status_info: dict[str, dict]):
46
47
  """Create a Rich table for displaying component status information.
47
48
 
48
49
  Args:
49
50
  status_info: Dictionary containing status info for each component.
50
51
  """
52
+ from dexcontrol.utils.error_code import get_error_description
51
53
  from dexcontrol.utils.pb_utils import ComponentStatus
52
54
 
53
55
  table = Table(title="Component Status")
54
56
  table.add_column("Component", style="cyan")
55
57
  table.add_column("Connected", justify="center")
56
58
  table.add_column("Enabled", justify="center")
57
- table.add_column("Error", justify="center")
59
+ table.add_column("Error", justify="left")
58
60
 
59
61
  status_icons = {
60
- True: ":white_check_mark:",
61
- False: ":x:",
62
- ComponentStatus.NORMAL: ":white_check_mark:",
63
- ComponentStatus.NA: "N/A",
62
+ True: "[green]:white_check_mark:[/green]",
63
+ False: "[red]:x:[/red]",
64
+ ComponentStatus.NORMAL: "[green]:white_check_mark:[/green]",
65
+ ComponentStatus.NA: "[dim]N/A[/dim]",
64
66
  }
65
67
 
66
68
  # Sort components by name to ensure consistent order
@@ -70,15 +72,51 @@ def show_component_status(status_info: dict[str, dict]) -> None:
70
72
  connected = status_icons[status["connected"]]
71
73
 
72
74
  # Format enabled status
73
- enabled = status_icons.get(status["enabled"], ":x:")
75
+ enabled = status_icons.get(status["enabled"], "[red]:x:[/red]")
74
76
 
75
77
  # Format error status
76
78
  if status["error_state"] == ComponentStatus.NORMAL:
77
- error = ":white_check_mark:"
79
+ error = "[green]:white_check_mark:[/green]"
78
80
  elif status["error_state"] == ComponentStatus.NA:
79
- error = "N/A"
81
+ error = "[dim]N/A[/dim]"
80
82
  else:
81
- error = status["error_code"]
83
+ # Convert error code to human-readable text
84
+ error_code = status["error_code"]
85
+ if isinstance(error_code, int):
86
+ error_desc = get_error_description(component, error_code)
87
+ # Show both raw code and description with formatting
88
+ if error_code == 0:
89
+ error = "[green]:white_check_mark:[/green]"
90
+ else:
91
+ # Check if this is an unknown error
92
+ if "Unknown" in error_desc:
93
+ # For unknown errors, show the hex code prominently
94
+ error = f"[bold red]:warning: 0x{error_code:08X}[/bold red]\n[dim italic]Unknown error code[/dim italic]"
95
+ else:
96
+ # For known errors, show both code and description
97
+ error = f"[bold red]:warning: 0x{error_code:08X}[/bold red]\n[yellow]{error_desc}[/yellow]"
98
+ else:
99
+ # Handle hex string format if provided
100
+ try:
101
+ error_code_int = (
102
+ int(error_code, 16)
103
+ if isinstance(error_code, str)
104
+ else error_code
105
+ )
106
+ error_desc = get_error_description(component, error_code_int)
107
+ # Show both raw code and description with formatting
108
+ if error_code_int == 0:
109
+ error = "[green]:white_check_mark:[/green]"
110
+ else:
111
+ # Check if this is an unknown error
112
+ if "Unknown" in error_desc:
113
+ # For unknown errors, show the hex code prominently
114
+ error = f"[bold red]:warning: 0x{error_code_int:08X}[/bold red]\n[dim italic]Unknown error code[/dim italic]"
115
+ else:
116
+ # For known errors, show both code and description
117
+ error = f"[bold red]:warning: 0x{error_code_int:08X}[/bold red]\n[yellow]{error_desc}[/yellow]"
118
+ except (ValueError, TypeError):
119
+ error = f"[red]{str(error_code)}[/red]"
82
120
 
83
121
  table.add_row(
84
122
  component,
@@ -89,3 +127,40 @@ def show_component_status(status_info: dict[str, dict]) -> None:
89
127
 
90
128
  console = Console()
91
129
  console.print(table)
130
+
131
+
132
+ def show_ntp_stats(stats: dict[str, float]):
133
+ """Display NTP statistics in a Rich table format.
134
+
135
+ Args:
136
+ stats: Dictionary containing NTP statistics (e.g., mean_offset, mean_rtt, etc.).
137
+ """
138
+ table = Table()
139
+ table.add_column("Time Statistic", style="cyan")
140
+ table.add_column("Value (Unit: second)", justify="right")
141
+
142
+ for key, value in stats.items():
143
+ # Format floats to 6 decimal places, lists as comma-separated, others as str
144
+ if isinstance(value, float):
145
+ value_str = f"{value:.6f}"
146
+ elif isinstance(value, list):
147
+ value_str = ", ".join(
148
+ f"{v:.6f}" if isinstance(v, float) else str(v) for v in value
149
+ )
150
+ else:
151
+ value_str = str(value)
152
+ table.add_row(key, value_str)
153
+
154
+ console = Console()
155
+ console.print(table)
156
+
157
+ if "offset (mean)" in stats:
158
+ offset = stats["offset (mean)"]
159
+ if offset > 0:
160
+ logger.info(
161
+ f"To synchronize: server_time ≈ local_time + {offset:.3f} second"
162
+ )
163
+ else:
164
+ logger.info(
165
+ f"To synchronize: server_time ≈ local_time - {abs(offset):.3f} second"
166
+ )