dexcontrol 0.2.12__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.
- dexcontrol/__init__.py +2 -1
- dexcontrol/config/core/chassis.py +9 -4
- dexcontrol/config/core/hand.py +1 -0
- dexcontrol/config/vega.py +4 -1
- dexcontrol/core/arm.py +32 -12
- dexcontrol/core/chassis.py +146 -115
- dexcontrol/core/component.py +42 -16
- dexcontrol/core/hand.py +74 -39
- dexcontrol/core/head.py +6 -5
- dexcontrol/core/misc.py +172 -22
- dexcontrol/core/robot_query_interface.py +440 -0
- dexcontrol/core/torso.py +4 -4
- dexcontrol/proto/dexcontrol_msg_pb2.py +27 -39
- dexcontrol/proto/dexcontrol_msg_pb2.pyi +75 -118
- dexcontrol/proto/dexcontrol_query_pb2.py +39 -39
- dexcontrol/proto/dexcontrol_query_pb2.pyi +17 -4
- dexcontrol/robot.py +259 -566
- dexcontrol/utils/os_utils.py +183 -1
- dexcontrol/utils/pb_utils.py +0 -22
- dexcontrol/utils/zenoh_utils.py +249 -2
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.0.dist-info}/METADATA +12 -1
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.0.dist-info}/RECORD +24 -23
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.0.dist-info}/WHEEL +0 -0
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.0.dist-info}/licenses/LICENSE +0 -0
dexcontrol/robot.py
CHANGED
|
@@ -24,6 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import os
|
|
26
26
|
import signal
|
|
27
|
+
import sys
|
|
27
28
|
import threading
|
|
28
29
|
import time
|
|
29
30
|
import weakref
|
|
@@ -40,24 +41,19 @@ from rich.table import Table
|
|
|
40
41
|
import dexcontrol
|
|
41
42
|
from dexcontrol.config.vega import VegaConfig, get_vega_config
|
|
42
43
|
from dexcontrol.core.component import RobotComponent
|
|
43
|
-
from dexcontrol.
|
|
44
|
+
from dexcontrol.core.hand import HandType
|
|
45
|
+
from dexcontrol.core.misc import ServerLogSubscriber
|
|
46
|
+
from dexcontrol.core.robot_query_interface import RobotQueryInterface
|
|
44
47
|
from dexcontrol.sensors import Sensors
|
|
45
48
|
from dexcontrol.utils.constants import ROBOT_NAME_ENV_VAR
|
|
46
|
-
from dexcontrol.utils.os_utils import
|
|
47
|
-
from dexcontrol.utils.pb_utils import (
|
|
48
|
-
TYPE_SOFTWARE_VERSION,
|
|
49
|
-
ComponentStatus,
|
|
50
|
-
software_version_to_dict,
|
|
51
|
-
status_to_dict,
|
|
52
|
-
)
|
|
49
|
+
from dexcontrol.utils.os_utils import check_version_compatibility, get_robot_model
|
|
53
50
|
from dexcontrol.utils.rate_limiter import RateLimiter
|
|
54
51
|
from dexcontrol.utils.trajectory_utils import generate_linear_trajectory
|
|
55
|
-
from dexcontrol.utils.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
from dexcontrol.utils.zenoh_utils import (
|
|
53
|
+
close_zenoh_session_with_timeout,
|
|
54
|
+
create_zenoh_session,
|
|
55
|
+
wait_for_zenoh_cleanup,
|
|
59
56
|
)
|
|
60
|
-
from dexcontrol.utils.zenoh_utils import compute_ntp_stats
|
|
61
57
|
|
|
62
58
|
if TYPE_CHECKING:
|
|
63
59
|
from dexcontrol.core.arm import Arm
|
|
@@ -73,66 +69,25 @@ _active_robots: weakref.WeakSet[Robot] = weakref.WeakSet()
|
|
|
73
69
|
_signal_handlers_registered: bool = False
|
|
74
70
|
|
|
75
71
|
|
|
76
|
-
def _signal_handler(signum: int, frame: Any) -> None:
|
|
77
|
-
"""Signal handler to shutdown all active Robot instances.
|
|
78
|
-
|
|
79
|
-
Args:
|
|
80
|
-
signum: Signal number received (e.g., SIGINT, SIGTERM).
|
|
81
|
-
frame: Current stack frame (unused).
|
|
82
|
-
"""
|
|
83
|
-
logger.info(f"Received signal {signum}, shutting down all active robots...")
|
|
84
|
-
# Create a list copy to avoid modification during iteration
|
|
85
|
-
robots_to_shutdown = list(_active_robots)
|
|
86
|
-
|
|
87
|
-
# Set a total timeout for all shutdowns
|
|
88
|
-
shutdown_start = time.time()
|
|
89
|
-
max_shutdown_time = 5.0 # Maximum 5 seconds for all shutdowns
|
|
90
|
-
|
|
91
|
-
for robot in robots_to_shutdown:
|
|
92
|
-
if time.time() - shutdown_start > max_shutdown_time:
|
|
93
|
-
logger.warning("Shutdown timeout reached, forcing exit")
|
|
94
|
-
break
|
|
95
|
-
|
|
96
|
-
logger.info(f"Shutting down robot: {robot}")
|
|
97
|
-
try:
|
|
98
|
-
robot.shutdown()
|
|
99
|
-
except Exception as e: # pylint: disable=broad-except
|
|
100
|
-
logger.error(f"Error during robot shutdown: {e}", exc_info=True)
|
|
101
|
-
|
|
102
|
-
logger.info("All robots shutdown complete")
|
|
103
|
-
# Force exit to ensure the process terminates
|
|
104
|
-
os._exit(0)
|
|
105
|
-
|
|
106
|
-
|
|
107
72
|
def _register_signal_handlers() -> None:
|
|
108
|
-
"""Register signal handlers for graceful shutdown.
|
|
109
|
-
|
|
110
|
-
This function ensures signal handlers are registered only once and sets up
|
|
111
|
-
handlers for SIGINT (Ctrl+C), SIGTERM, and SIGHUP (on Unix systems).
|
|
112
|
-
"""
|
|
73
|
+
"""Register signal handlers for graceful shutdown."""
|
|
113
74
|
global _signal_handlers_registered
|
|
114
75
|
if _signal_handlers_registered:
|
|
115
76
|
return
|
|
116
77
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
78
|
+
def signal_handler(signum: int, frame: Any) -> None:
|
|
79
|
+
sys.exit(0)
|
|
80
|
+
|
|
81
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
82
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
120
83
|
|
|
121
|
-
# On Unix systems, also handle SIGHUP
|
|
122
84
|
if hasattr(signal, "SIGHUP"):
|
|
123
|
-
signal.signal(signal.SIGHUP,
|
|
85
|
+
signal.signal(signal.SIGHUP, signal_handler)
|
|
124
86
|
|
|
125
87
|
_signal_handlers_registered = True
|
|
126
88
|
|
|
127
89
|
|
|
128
|
-
class
|
|
129
|
-
"""Type hints for component configuration."""
|
|
130
|
-
|
|
131
|
-
_target_: str
|
|
132
|
-
configs: dict[str, Any]
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
class Robot:
|
|
90
|
+
class Robot(RobotQueryInterface):
|
|
136
91
|
"""Main interface class for robot control and monitoring.
|
|
137
92
|
|
|
138
93
|
This class serves as the primary interface for interacting with a robot system.
|
|
@@ -141,33 +96,32 @@ class Robot:
|
|
|
141
96
|
system-wide operations like status monitoring, trajectory execution, and component
|
|
142
97
|
control.
|
|
143
98
|
|
|
144
|
-
The Robot class supports context manager usage and automatic cleanup on program
|
|
145
|
-
interruption through signal handlers.
|
|
146
|
-
|
|
147
99
|
Example usage:
|
|
148
100
|
# Using context manager (recommended)
|
|
149
101
|
with Robot() as robot:
|
|
150
102
|
robot.set_joint_pos({"left_arm": [0, 0, 0, 0, 0, 0, 0]})
|
|
103
|
+
version_info = robot.get_version_info()
|
|
151
104
|
|
|
152
105
|
# Manual usage with explicit shutdown
|
|
153
106
|
robot = Robot()
|
|
154
107
|
try:
|
|
155
108
|
robot.set_joint_pos({"left_arm": [0, 0, 0, 0, 0, 0, 0]})
|
|
109
|
+
hand_types = robot.query_hand_type()
|
|
156
110
|
finally:
|
|
157
111
|
robot.shutdown()
|
|
158
112
|
|
|
159
113
|
Attributes:
|
|
160
|
-
left_arm: Left arm component interface.
|
|
161
|
-
right_arm: Right arm component interface.
|
|
162
|
-
left_hand: Left hand component interface.
|
|
163
|
-
right_hand: Right hand component interface.
|
|
164
|
-
head: Head component interface.
|
|
165
|
-
chassis: Chassis component interface.
|
|
166
|
-
torso: Torso component interface.
|
|
114
|
+
left_arm: Left arm component interface (7-DOF manipulator).
|
|
115
|
+
right_arm: Right arm component interface (7-DOF manipulator).
|
|
116
|
+
left_hand: Left hand component interface (conditional, based on hardware).
|
|
117
|
+
right_hand: Right hand component interface (conditional, based on hardware).
|
|
118
|
+
head: Head component interface (3-DOF pan-tilt-roll).
|
|
119
|
+
chassis: Chassis component interface (mobile base).
|
|
120
|
+
torso: Torso component interface (1-DOF pitch).
|
|
167
121
|
battery: Battery monitoring interface.
|
|
168
122
|
estop: Emergency stop interface.
|
|
169
123
|
heartbeat: Heartbeat monitoring interface.
|
|
170
|
-
sensors: Sensor systems interface.
|
|
124
|
+
sensors: Sensor systems interface (cameras, IMU, lidar, etc.).
|
|
171
125
|
"""
|
|
172
126
|
|
|
173
127
|
# Type annotations for dynamically created attributes
|
|
@@ -214,25 +168,20 @@ class Robot:
|
|
|
214
168
|
robot_model = get_robot_model()
|
|
215
169
|
self._robot_model: Final[str] = robot_model
|
|
216
170
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
raise ValueError(f"Failed to load robot configuration: {e}") from e
|
|
171
|
+
# Load configuration and initialize zenoh session
|
|
172
|
+
self._configs: Final[VegaConfig] = configs or get_vega_config(robot_model)
|
|
173
|
+
self._zenoh_session: zenoh.Session = create_zenoh_session(zenoh_config_file)
|
|
221
174
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
zenoh_config_file
|
|
225
|
-
)
|
|
226
|
-
except Exception as e:
|
|
227
|
-
raise RuntimeError(f"Failed to initialize zenoh session: {e}") from e
|
|
175
|
+
# Initialize ZenohQueryable parent class
|
|
176
|
+
super().__init__(self._zenoh_session, self._configs)
|
|
228
177
|
|
|
229
178
|
self._robot_name: Final[str] = os.getenv(ROBOT_NAME_ENV_VAR, "robot")
|
|
230
|
-
self._pv_components:
|
|
231
|
-
"left_hand",
|
|
232
|
-
"right_hand",
|
|
179
|
+
self._pv_components: list[str] = [
|
|
233
180
|
"head",
|
|
234
181
|
"torso",
|
|
235
182
|
]
|
|
183
|
+
self._log_subscriber = ServerLogSubscriber(self._zenoh_session)
|
|
184
|
+
self._hand_types: dict[str, HandType] = {}
|
|
236
185
|
|
|
237
186
|
# Register for automatic shutdown on signals if enabled
|
|
238
187
|
if auto_shutdown:
|
|
@@ -241,53 +190,11 @@ class Robot:
|
|
|
241
190
|
|
|
242
191
|
self._print_initialization_info(robot_model)
|
|
243
192
|
|
|
244
|
-
# Initialize robot
|
|
245
|
-
|
|
246
|
-
config_dict = omegaconf.OmegaConf.to_container(self._configs, resolve=True)
|
|
247
|
-
if not isinstance(config_dict, dict):
|
|
248
|
-
raise ValueError("Invalid configuration format")
|
|
249
|
-
self._init_components(cast(dict[str, Any], config_dict))
|
|
250
|
-
except Exception as e:
|
|
251
|
-
self.shutdown() # Clean up on initialization failure
|
|
252
|
-
raise RuntimeError(f"Failed to initialize robot components: {e}") from e
|
|
253
|
-
|
|
254
|
-
# Ensure all components are active
|
|
255
|
-
try:
|
|
256
|
-
self._wait_for_components()
|
|
257
|
-
except Exception as e:
|
|
258
|
-
self.shutdown() # Clean up on initialization failure
|
|
259
|
-
raise RuntimeError(f"Failed to activate components: {e}") from e
|
|
193
|
+
# Initialize robot components with safe error handling
|
|
194
|
+
self._safe_initialize_components()
|
|
260
195
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
self.sensors.wait_for_all_active()
|
|
264
|
-
except Exception as e:
|
|
265
|
-
self.shutdown() # Clean up on initialization failure
|
|
266
|
-
raise RuntimeError(f"Failed to initialize sensors: {e}") from e
|
|
267
|
-
|
|
268
|
-
# Check soc software version before proceeding
|
|
269
|
-
version_dict = self.get_software_version(show=False)
|
|
270
|
-
min_soc_version = dexcontrol.MIN_SOC_SOFTWARE_VERSION
|
|
271
|
-
soc_version = version_dict.get("soc", {}).get("software_version")
|
|
272
|
-
if soc_version is None:
|
|
273
|
-
logger.warning(
|
|
274
|
-
"Could not determine the software version of the remote driver ('soc'). "
|
|
275
|
-
"Please ensure the remote server is running and reachable."
|
|
276
|
-
)
|
|
277
|
-
elif soc_version < min_soc_version:
|
|
278
|
-
logger.warning(
|
|
279
|
-
f"The remote driver ('soc') software version is too old: {soc_version}. "
|
|
280
|
-
f"Minimum required version is {min_soc_version}.\n"
|
|
281
|
-
f"Please update the remote server or downgrade this project code to match the server version. "
|
|
282
|
-
f"Otherwise, some features may not work correctly or may encounter errors."
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
# Set default modes
|
|
286
|
-
try:
|
|
287
|
-
self._set_default_modes()
|
|
288
|
-
except Exception as e:
|
|
289
|
-
self.shutdown() # Clean up on initialization failure
|
|
290
|
-
raise RuntimeError(f"Failed to set default modes: {e}") from e
|
|
196
|
+
# Check version compatibility using new JSON interface
|
|
197
|
+
self._check_version_compatibility()
|
|
291
198
|
|
|
292
199
|
@property
|
|
293
200
|
def robot_model(self) -> str:
|
|
@@ -307,24 +214,12 @@ class Robot:
|
|
|
307
214
|
"""
|
|
308
215
|
return self._robot_name
|
|
309
216
|
|
|
310
|
-
def __enter__(self) -> Robot:
|
|
311
|
-
"""
|
|
312
|
-
|
|
313
|
-
Returns:
|
|
314
|
-
Self reference for context management.
|
|
315
|
-
"""
|
|
217
|
+
def __enter__(self) -> "Robot":
|
|
218
|
+
"""Enter context manager."""
|
|
316
219
|
return self
|
|
317
220
|
|
|
318
|
-
def __exit__(
|
|
319
|
-
|
|
320
|
-
) -> None:
|
|
321
|
-
"""Context manager exit with automatic shutdown.
|
|
322
|
-
|
|
323
|
-
Args:
|
|
324
|
-
exc_type: Type of the exception that occurred, if any.
|
|
325
|
-
exc_val: Exception instance that occurred, if any.
|
|
326
|
-
exc_tb: Traceback of the exception that occurred, if any.
|
|
327
|
-
"""
|
|
221
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
222
|
+
"""Exit context manager and clean up resources."""
|
|
328
223
|
self.shutdown()
|
|
329
224
|
|
|
330
225
|
def __del__(self) -> None:
|
|
@@ -340,41 +235,6 @@ class Robot:
|
|
|
340
235
|
# During interpreter shutdown, some modules might not be available
|
|
341
236
|
pass
|
|
342
237
|
|
|
343
|
-
@staticmethod
|
|
344
|
-
def cleanup_all() -> None:
|
|
345
|
-
"""Force cleanup of all Zenoh resources and threads.
|
|
346
|
-
|
|
347
|
-
This method can be called between Robot instances to ensure a clean state.
|
|
348
|
-
It's especially useful when creating multiple Robot instances in the same script.
|
|
349
|
-
|
|
350
|
-
Example:
|
|
351
|
-
robot1 = Robot()
|
|
352
|
-
robot1.shutdown()
|
|
353
|
-
Robot.cleanup_all() # Ensure clean state
|
|
354
|
-
|
|
355
|
-
robot2 = Robot() # Safe to create new instance
|
|
356
|
-
robot2.shutdown()
|
|
357
|
-
"""
|
|
358
|
-
import gc
|
|
359
|
-
|
|
360
|
-
# Force garbage collection to clean up any remaining resources
|
|
361
|
-
gc.collect()
|
|
362
|
-
|
|
363
|
-
# Give threads time to terminate
|
|
364
|
-
time.sleep(1.0)
|
|
365
|
-
|
|
366
|
-
# Log any remaining pyo3-closure threads
|
|
367
|
-
lingering = []
|
|
368
|
-
for thread in threading.enumerate():
|
|
369
|
-
if "pyo3-closure" in thread.name and thread.is_alive():
|
|
370
|
-
lingering.append(thread.name)
|
|
371
|
-
|
|
372
|
-
if lingering:
|
|
373
|
-
logger.debug(
|
|
374
|
-
f"Zenoh threads still active after cleanup: {lingering}. "
|
|
375
|
-
"This is normal and shouldn't affect new Robot instances."
|
|
376
|
-
)
|
|
377
|
-
|
|
378
238
|
def _print_initialization_info(self, robot_model: str | None) -> None:
|
|
379
239
|
"""Print initialization information.
|
|
380
240
|
|
|
@@ -393,40 +253,116 @@ class Robot:
|
|
|
393
253
|
|
|
394
254
|
console.print(table)
|
|
395
255
|
|
|
396
|
-
def
|
|
397
|
-
"""
|
|
256
|
+
def _safe_initialize_components(self) -> None:
|
|
257
|
+
"""Safely initialize all robot components with consolidated error handling.
|
|
398
258
|
|
|
399
|
-
|
|
400
|
-
|
|
259
|
+
This method consolidates the initialization of components, sensors, and
|
|
260
|
+
default modes into a single method with unified error handling.
|
|
401
261
|
|
|
402
262
|
Raises:
|
|
403
|
-
RuntimeError: If
|
|
263
|
+
RuntimeError: If any critical initialization step fails.
|
|
404
264
|
"""
|
|
265
|
+
initialization_steps = [
|
|
266
|
+
("robot components", self._initialize_robot_components),
|
|
267
|
+
("component activation", self._wait_for_components),
|
|
268
|
+
("sensors", self._initialize_sensors),
|
|
269
|
+
("default state", self._set_default_state),
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
for step_name, step_function in initialization_steps:
|
|
273
|
+
try:
|
|
274
|
+
step_function()
|
|
275
|
+
except Exception as e:
|
|
276
|
+
self.shutdown()
|
|
277
|
+
raise RuntimeError(
|
|
278
|
+
f"Robot initialization failed at {step_name}: {e}"
|
|
279
|
+
) from e
|
|
280
|
+
|
|
281
|
+
def _initialize_sensors(self) -> None:
|
|
282
|
+
"""Initialize sensors and wait for activation."""
|
|
283
|
+
self.sensors = Sensors(self._configs.sensors, self._zenoh_session)
|
|
284
|
+
self.sensors.wait_for_all_active()
|
|
285
|
+
|
|
286
|
+
def _initialize_robot_components(self) -> None:
|
|
287
|
+
"""Initialize robot components from configuration."""
|
|
288
|
+
config_dict = omegaconf.OmegaConf.to_container(self._configs, resolve=True)
|
|
289
|
+
config_dict = cast(dict[str, Any], config_dict)
|
|
290
|
+
|
|
291
|
+
initialized_components = []
|
|
292
|
+
failed_components = []
|
|
293
|
+
|
|
405
294
|
for component_name, component_config in config_dict.items():
|
|
406
295
|
if component_name == "sensors":
|
|
407
296
|
continue
|
|
408
297
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
298
|
+
try:
|
|
299
|
+
# Skip hand initialization if the hand is not present on hardware or unknown
|
|
300
|
+
if (
|
|
301
|
+
component_name in ["left_hand", "right_hand"]
|
|
302
|
+
and self._hand_types == {}
|
|
303
|
+
):
|
|
304
|
+
self._hand_types = self.query_hand_type()
|
|
305
|
+
if (
|
|
306
|
+
component_name in ["left_hand", "right_hand"]
|
|
307
|
+
and self._hand_types.get(component_name.split("_")[0])
|
|
308
|
+
== HandType.UNKNOWN
|
|
309
|
+
):
|
|
310
|
+
logger.info(
|
|
311
|
+
f"Skipping {component_name} initialization, no known hand detected."
|
|
312
|
+
)
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
component_config = getattr(self._configs, str(component_name))
|
|
316
|
+
if (
|
|
317
|
+
not hasattr(component_config, "_target_")
|
|
318
|
+
or not component_config._target_
|
|
319
|
+
):
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
# Create component configuration
|
|
323
|
+
temp_config = omegaconf.OmegaConf.create(
|
|
324
|
+
{
|
|
325
|
+
"_target_": component_config._target_,
|
|
326
|
+
"configs": {
|
|
327
|
+
k: v for k, v in component_config.items() if k != "_target_"
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
)
|
|
415
331
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
332
|
+
# Handle different hand types
|
|
333
|
+
if component_name in ["left_hand", "right_hand"]:
|
|
334
|
+
hand_type = self._hand_types.get(component_name.split("_")[0])
|
|
335
|
+
temp_config["hand_type"] = hand_type
|
|
336
|
+
|
|
337
|
+
# Instantiate component with error handling
|
|
338
|
+
component_instance = hydra.utils.instantiate(
|
|
339
|
+
temp_config, zenoh_session=self._zenoh_session
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Store component instance both as attribute and in tracking dictionaries
|
|
343
|
+
setattr(self, str(component_name), component_instance)
|
|
344
|
+
initialized_components.append(component_name)
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.error(f"Failed to initialize component {component_name}: {e}")
|
|
348
|
+
failed_components.append(component_name)
|
|
349
|
+
# Continue with other components rather than failing completely
|
|
350
|
+
|
|
351
|
+
# Report initialization summary
|
|
352
|
+
if failed_components:
|
|
353
|
+
logger.warning(
|
|
354
|
+
f"Failed to initialize components: {', '.join(failed_components)}"
|
|
426
355
|
)
|
|
427
|
-
setattr(self, str(component_name), component_instance)
|
|
428
356
|
|
|
429
|
-
|
|
357
|
+
# Raise error only if no critical components were initialized
|
|
358
|
+
critical_components = ["left_arm", "right_arm", "head", "torso", "chassis"]
|
|
359
|
+
initialized_critical = [
|
|
360
|
+
c for c in critical_components if c in initialized_components
|
|
361
|
+
]
|
|
362
|
+
if not initialized_critical:
|
|
363
|
+
raise RuntimeError("Failed to initialize any critical components")
|
|
364
|
+
|
|
365
|
+
def _set_default_state(self) -> None:
|
|
430
366
|
"""Set default control modes for robot components.
|
|
431
367
|
|
|
432
368
|
Raises:
|
|
@@ -434,7 +370,7 @@ class Robot:
|
|
|
434
370
|
"""
|
|
435
371
|
for arm in ["left_arm", "right_arm"]:
|
|
436
372
|
if component := getattr(self, arm, None):
|
|
437
|
-
component.
|
|
373
|
+
component.set_modes(["position"] * 7)
|
|
438
374
|
|
|
439
375
|
if head := getattr(self, "head", None):
|
|
440
376
|
head.set_mode("enable")
|
|
@@ -455,14 +391,19 @@ class Robot:
|
|
|
455
391
|
component_names: Final[list[str]] = [
|
|
456
392
|
"left_arm",
|
|
457
393
|
"right_arm",
|
|
458
|
-
"left_hand",
|
|
459
|
-
"right_hand",
|
|
460
394
|
"head",
|
|
461
395
|
"chassis",
|
|
462
396
|
"torso",
|
|
463
397
|
"battery",
|
|
464
398
|
"estop",
|
|
465
399
|
]
|
|
400
|
+
|
|
401
|
+
# Only add hands to component_names if they were actually initialized
|
|
402
|
+
if hasattr(self, "left_hand"):
|
|
403
|
+
component_names.append("left_hand")
|
|
404
|
+
if hasattr(self, "right_hand"):
|
|
405
|
+
component_names.append("right_hand")
|
|
406
|
+
|
|
466
407
|
if self._configs.heartbeat.enabled:
|
|
467
408
|
component_names.append("heartbeat")
|
|
468
409
|
|
|
@@ -520,43 +461,97 @@ class Robot:
|
|
|
520
461
|
f"Other components may work, but some features, e.g. collision avoidance, may not work correctly."
|
|
521
462
|
f"Please check the robot status immediately."
|
|
522
463
|
)
|
|
464
|
+
else:
|
|
465
|
+
logger.info("All motor components are active")
|
|
523
466
|
|
|
524
|
-
|
|
467
|
+
def get_component_map(self) -> dict[str, Any]:
|
|
468
|
+
"""Get the component mapping dictionary.
|
|
525
469
|
|
|
526
|
-
|
|
527
|
-
|
|
470
|
+
Returns:
|
|
471
|
+
Dictionary mapping component names to component instances.
|
|
472
|
+
"""
|
|
473
|
+
component_map = {
|
|
474
|
+
"left_arm": getattr(self, "left_arm", None),
|
|
475
|
+
"right_arm": getattr(self, "right_arm", None),
|
|
476
|
+
"torso": getattr(self, "torso", None),
|
|
477
|
+
"head": getattr(self, "head", None),
|
|
478
|
+
}
|
|
528
479
|
|
|
529
|
-
|
|
530
|
-
|
|
480
|
+
# Only add hands if they were initialized
|
|
481
|
+
if hasattr(self, "left_hand"):
|
|
482
|
+
component_map["left_hand"] = self.left_hand
|
|
483
|
+
if hasattr(self, "right_hand"):
|
|
484
|
+
component_map["right_hand"] = self.right_hand
|
|
531
485
|
|
|
532
|
-
|
|
533
|
-
|
|
486
|
+
# Remove None values
|
|
487
|
+
return {k: v for k, v in component_map.items() if v is not None}
|
|
488
|
+
|
|
489
|
+
def validate_component_names(self, joint_pos: dict[str, Any]) -> None:
|
|
490
|
+
"""Validate that all component names are valid and initialized.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
joint_pos: Joint position dictionary to validate.
|
|
534
494
|
|
|
535
495
|
Raises:
|
|
536
|
-
|
|
496
|
+
ValueError: If invalid component names are found with detailed guidance.
|
|
537
497
|
"""
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if config_path is None:
|
|
541
|
-
logger.warning("Using default zenoh config settings")
|
|
542
|
-
return zenoh.open(zenoh.Config())
|
|
543
|
-
return zenoh.open(zenoh.Config.from_file(config_path))
|
|
544
|
-
except Exception as e:
|
|
545
|
-
raise RuntimeError(f"Failed to initialize zenoh session: {e}") from e
|
|
498
|
+
if not joint_pos:
|
|
499
|
+
raise ValueError("Joint position dictionary cannot be empty")
|
|
546
500
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
501
|
+
component_map = self.get_component_map()
|
|
502
|
+
valid_components = set(component_map.keys())
|
|
503
|
+
provided_components = set(joint_pos.keys())
|
|
504
|
+
invalid_components = provided_components - valid_components
|
|
550
505
|
|
|
551
|
-
|
|
552
|
-
|
|
506
|
+
if invalid_components:
|
|
507
|
+
available_msg = (
|
|
508
|
+
f"Available components: {', '.join(sorted(valid_components))}"
|
|
509
|
+
)
|
|
510
|
+
invalid_msg = (
|
|
511
|
+
f"Invalid component names: {', '.join(sorted(invalid_components))}"
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Provide helpful suggestions for common mistakes
|
|
515
|
+
suggestions = []
|
|
516
|
+
for invalid in invalid_components:
|
|
517
|
+
if invalid in ["left_hand", "right_hand"]:
|
|
518
|
+
suggestions.append(f"'{invalid}' may not be connected or detected")
|
|
519
|
+
elif invalid.replace("_", "") in [
|
|
520
|
+
c.replace("_", "") for c in valid_components
|
|
521
|
+
]:
|
|
522
|
+
close_match = next(
|
|
523
|
+
(
|
|
524
|
+
c
|
|
525
|
+
for c in valid_components
|
|
526
|
+
if c.replace("_", "") == invalid.replace("_", "")
|
|
527
|
+
),
|
|
528
|
+
None,
|
|
529
|
+
)
|
|
530
|
+
if close_match:
|
|
531
|
+
suggestions.append(
|
|
532
|
+
f"Did you mean '{close_match}' instead of '{invalid}'?"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
error_msg = f"{invalid_msg}. {available_msg}."
|
|
536
|
+
if suggestions:
|
|
537
|
+
error_msg += f" Suggestions: {' '.join(suggestions)}"
|
|
538
|
+
|
|
539
|
+
raise ValueError(error_msg)
|
|
540
|
+
|
|
541
|
+
def _check_version_compatibility(self) -> None:
|
|
542
|
+
"""Check version compatibility between client and server.
|
|
543
|
+
|
|
544
|
+
This method uses the new JSON-based version interface to:
|
|
545
|
+
1. Compare client library version with server's minimum required version
|
|
546
|
+
2. Check server component versions for compatibility
|
|
547
|
+
3. Provide clear guidance for version mismatches
|
|
553
548
|
"""
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
549
|
+
try:
|
|
550
|
+
version_info = self.get_version_info(show=False)
|
|
551
|
+
check_version_compatibility(version_info)
|
|
552
|
+
except Exception as e:
|
|
553
|
+
# Log error but don't fail initialization for version check issues
|
|
554
|
+
logger.warning(f"Version compatibility check failed: {e}")
|
|
560
555
|
|
|
561
556
|
def shutdown(self) -> None:
|
|
562
557
|
"""Cleans up and closes all component connections.
|
|
@@ -569,7 +564,7 @@ class Robot:
|
|
|
569
564
|
logger.warning("Shutdown already called, skipping")
|
|
570
565
|
return
|
|
571
566
|
|
|
572
|
-
logger.info("Shutting down robot...")
|
|
567
|
+
logger.info("Shutting down robot components...")
|
|
573
568
|
self._shutdown_called = True
|
|
574
569
|
|
|
575
570
|
# Remove from active robots registry
|
|
@@ -591,9 +586,6 @@ class Robot:
|
|
|
591
586
|
f"Error stopping component {component.__class__.__name__}: {e}"
|
|
592
587
|
)
|
|
593
588
|
|
|
594
|
-
# Brief delay to ensure all stop operations complete
|
|
595
|
-
time.sleep(0.1)
|
|
596
|
-
|
|
597
589
|
# Shutdown sensors first (they may have background threads)
|
|
598
590
|
try:
|
|
599
591
|
if hasattr(self, "sensors") and self.sensors is not None:
|
|
@@ -616,80 +608,29 @@ class Robot:
|
|
|
616
608
|
# Brief delay to allow component shutdown to complete
|
|
617
609
|
time.sleep(0.1)
|
|
618
610
|
|
|
619
|
-
#
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
"""Close zenoh session in a separate thread."""
|
|
625
|
-
nonlocal zenoh_close_success, zenoh_close_exception
|
|
626
|
-
try:
|
|
627
|
-
# Brief delay for Zenoh cleanup
|
|
628
|
-
time.sleep(0.1)
|
|
629
|
-
self._zenoh_session.close()
|
|
630
|
-
zenoh_close_success = True
|
|
631
|
-
except Exception as e: # pylint: disable=broad-except
|
|
632
|
-
zenoh_close_exception = e
|
|
633
|
-
logger.warning(f"Zenoh session close error: {e}")
|
|
634
|
-
|
|
635
|
-
# Try to close zenoh session with timeout
|
|
636
|
-
close_thread = threading.Thread(target=_close_zenoh_session, daemon=True)
|
|
637
|
-
close_thread.start()
|
|
638
|
-
|
|
639
|
-
# Wait for close with reasonable timeout
|
|
640
|
-
close_timeout = 2.0
|
|
641
|
-
close_thread.join(timeout=close_timeout)
|
|
642
|
-
|
|
643
|
-
if close_thread.is_alive():
|
|
644
|
-
logger.warning(f"Zenoh session close timed out after {close_timeout}s")
|
|
645
|
-
# The thread is daemon, so it won't block the main thread
|
|
646
|
-
elif zenoh_close_success:
|
|
647
|
-
logger.debug("Zenoh session closed cleanly")
|
|
648
|
-
elif zenoh_close_exception:
|
|
649
|
-
logger.debug(
|
|
650
|
-
f"Zenoh session close completed with error: {zenoh_close_exception}"
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
# Give Zenoh threads more time to clean up properly
|
|
654
|
-
# This helps ensure threads terminate naturally
|
|
655
|
-
time.sleep(0.8)
|
|
656
|
-
|
|
657
|
-
# Check for lingering pyo3-closure threads
|
|
658
|
-
# These are internal Zenoh library threads
|
|
659
|
-
lingering_threads = []
|
|
660
|
-
for thread in threading.enumerate():
|
|
661
|
-
if (
|
|
662
|
-
"pyo3-closure" in thread.name
|
|
663
|
-
and thread.is_alive()
|
|
664
|
-
and not thread.daemon
|
|
665
|
-
):
|
|
666
|
-
lingering_threads.append(thread.name)
|
|
611
|
+
# Clean up log subscriber before closing zenoh session
|
|
612
|
+
try:
|
|
613
|
+
self._log_subscriber.shutdown()
|
|
614
|
+
except Exception as e: # pylint: disable=broad-except
|
|
615
|
+
logger.debug(f"Error shutting down log subscriber: {e}")
|
|
667
616
|
|
|
617
|
+
# Close Zenoh session
|
|
618
|
+
close_zenoh_session_with_timeout(self._zenoh_session)
|
|
619
|
+
lingering_threads = wait_for_zenoh_cleanup()
|
|
668
620
|
if lingering_threads:
|
|
669
|
-
|
|
670
|
-
f"Note: Zenoh library threads still active: {lingering_threads}. "
|
|
671
|
-
"These are internal library threads that should not prevent creating new Robot instances."
|
|
672
|
-
)
|
|
673
|
-
|
|
674
|
-
# If this is the last robot being shutdown and we're in the main module,
|
|
675
|
-
# help the script exit cleanly
|
|
676
|
-
import sys
|
|
677
|
-
|
|
678
|
-
if (
|
|
679
|
-
not _active_robots
|
|
680
|
-
and threading.current_thread() is threading.main_thread()
|
|
681
|
-
):
|
|
682
|
-
# Check if we're likely at the end of the main script
|
|
683
|
-
frame = sys._getframe()
|
|
684
|
-
while frame.f_back:
|
|
685
|
-
frame = frame.f_back
|
|
686
|
-
# If we're at the top level of a script (not in interactive mode)
|
|
687
|
-
if frame.f_code.co_name == "<module>" and not hasattr(sys, "ps1"):
|
|
688
|
-
logger.debug("Main script ending, forcing clean exit")
|
|
689
|
-
os._exit(0)
|
|
690
|
-
|
|
621
|
+
self._assist_clean_exit_if_needed()
|
|
691
622
|
logger.info("Robot shutdown complete")
|
|
692
623
|
|
|
624
|
+
def _assist_clean_exit_if_needed(self) -> None:
|
|
625
|
+
"""Assist with clean exit if this is the last robot and we're exiting."""
|
|
626
|
+
if (
|
|
627
|
+
not _active_robots # No more active robots
|
|
628
|
+
and threading.current_thread() is threading.main_thread() # In main thread
|
|
629
|
+
and not hasattr(sys, "ps1") # Not in interactive mode
|
|
630
|
+
):
|
|
631
|
+
logger.debug("Assisting clean exit due to lingering Zenoh threads")
|
|
632
|
+
sys.exit(0)
|
|
633
|
+
|
|
693
634
|
def is_shutdown(self) -> bool:
|
|
694
635
|
"""Check if the robot has been shutdown.
|
|
695
636
|
|
|
@@ -698,209 +639,6 @@ class Robot:
|
|
|
698
639
|
"""
|
|
699
640
|
return self._shutdown_called
|
|
700
641
|
|
|
701
|
-
def query_ntp(
|
|
702
|
-
self,
|
|
703
|
-
sample_count: int = 30,
|
|
704
|
-
show: bool = False,
|
|
705
|
-
timeout: float = 1.0,
|
|
706
|
-
device: Literal["soc", "jetson"] = "soc",
|
|
707
|
-
) -> dict[Literal["success", "offset", "rtt"], bool | float]:
|
|
708
|
-
"""Query the NTP server via zenoh for time synchronization and compute robust statistics.
|
|
709
|
-
|
|
710
|
-
Args:
|
|
711
|
-
sample_count: Number of NTP samples to request (default: 50).
|
|
712
|
-
show: Whether to print summary statistics using a rich table.
|
|
713
|
-
timeout: Timeout for the zenoh querier in seconds (default: 2.0).
|
|
714
|
-
device: Which device to query for NTP ("soc" or "jetson").
|
|
715
|
-
|
|
716
|
-
Returns:
|
|
717
|
-
Dictionary with keys:
|
|
718
|
-
- "success": True if any replies were received, False otherwise.
|
|
719
|
-
- "offset": Mean offset (in seconds) after removing RTT outliers.
|
|
720
|
-
- "rtt": Mean round-trip time (in seconds) after removing RTT outliers.
|
|
721
|
-
"""
|
|
722
|
-
if device == "soc":
|
|
723
|
-
ntp_key = resolve_key_name(self._configs.soc_ntp_query_name)
|
|
724
|
-
elif device == "jetson":
|
|
725
|
-
raise NotImplementedError("Jetson NTP query is not implemented yet")
|
|
726
|
-
time_offset = []
|
|
727
|
-
time_rtt = []
|
|
728
|
-
|
|
729
|
-
querier = self._zenoh_session.declare_querier(ntp_key, timeout=timeout)
|
|
730
|
-
time.sleep(0.1)
|
|
731
|
-
|
|
732
|
-
reply_count = 0
|
|
733
|
-
for i in range(sample_count):
|
|
734
|
-
request = dexcontrol_query_pb2.NTPRequest()
|
|
735
|
-
request.client_send_time_ns = time.time_ns()
|
|
736
|
-
request.sample_count = sample_count
|
|
737
|
-
request.sample_index = i
|
|
738
|
-
replies = querier.get(payload=request.SerializeToString())
|
|
739
|
-
|
|
740
|
-
for reply in replies:
|
|
741
|
-
reply_count += 1
|
|
742
|
-
if reply.ok and reply.ok.payload:
|
|
743
|
-
client_receive_time_ns = time.time_ns()
|
|
744
|
-
response = dexcontrol_query_pb2.NTPResponse()
|
|
745
|
-
response.ParseFromString(reply.ok.payload.to_bytes())
|
|
746
|
-
t0 = request.client_send_time_ns
|
|
747
|
-
t1 = response.server_receive_time_ns
|
|
748
|
-
t2 = response.server_send_time_ns
|
|
749
|
-
t3 = client_receive_time_ns
|
|
750
|
-
offset = ((t1 - t0) + (t2 - t3)) // 2 / 1e9
|
|
751
|
-
rtt = (t3 - t0) / 1e9
|
|
752
|
-
time_offset.append(offset)
|
|
753
|
-
time_rtt.append(rtt)
|
|
754
|
-
if i < sample_count - 1:
|
|
755
|
-
time.sleep(0.01)
|
|
756
|
-
|
|
757
|
-
querier.undeclare()
|
|
758
|
-
if reply_count == 0:
|
|
759
|
-
return {"success": False, "offset": 0, "rtt": 0}
|
|
760
|
-
|
|
761
|
-
stats = compute_ntp_stats(time_offset, time_rtt)
|
|
762
|
-
offset = stats["offset (mean)"]
|
|
763
|
-
rtt = stats["round_trip_time (mean)"]
|
|
764
|
-
if show:
|
|
765
|
-
show_ntp_stats(stats)
|
|
766
|
-
|
|
767
|
-
return {"success": True, "offset": offset, "rtt": rtt}
|
|
768
|
-
|
|
769
|
-
def get_software_version(
|
|
770
|
-
self, show: bool = True
|
|
771
|
-
) -> dict[str, TYPE_SOFTWARE_VERSION]:
|
|
772
|
-
"""Retrieve software version information for all components.
|
|
773
|
-
|
|
774
|
-
Args:
|
|
775
|
-
show: Whether to display the version information.
|
|
776
|
-
|
|
777
|
-
Returns:
|
|
778
|
-
Dictionary containing version information for all components.
|
|
779
|
-
|
|
780
|
-
Raises:
|
|
781
|
-
RuntimeError: If version information cannot be retrieved.
|
|
782
|
-
"""
|
|
783
|
-
try:
|
|
784
|
-
replies = self._zenoh_session.get(
|
|
785
|
-
resolve_key_name(self._configs.version_info_name)
|
|
786
|
-
)
|
|
787
|
-
version_dict = {}
|
|
788
|
-
for reply in replies:
|
|
789
|
-
if reply.ok and reply.ok.payload:
|
|
790
|
-
version_bytes = reply.ok.payload.to_bytes()
|
|
791
|
-
version_msg = cast(
|
|
792
|
-
dexcontrol_query_pb2.SoftwareVersion,
|
|
793
|
-
dexcontrol_query_pb2.SoftwareVersion.FromString(version_bytes),
|
|
794
|
-
)
|
|
795
|
-
version_dict = software_version_to_dict(version_msg)
|
|
796
|
-
break
|
|
797
|
-
|
|
798
|
-
if show:
|
|
799
|
-
show_software_version(version_dict)
|
|
800
|
-
return version_dict
|
|
801
|
-
except Exception as e:
|
|
802
|
-
raise RuntimeError(f"Failed to retrieve software versions: {e}") from e
|
|
803
|
-
|
|
804
|
-
def get_component_status(
|
|
805
|
-
self, show: bool = True
|
|
806
|
-
) -> dict[str, dict[str, bool | ComponentStatus]]:
|
|
807
|
-
"""Retrieve status information for all components.
|
|
808
|
-
|
|
809
|
-
Args:
|
|
810
|
-
show: Whether to display the status information.
|
|
811
|
-
|
|
812
|
-
Returns:
|
|
813
|
-
Dictionary containing status information for all components.
|
|
814
|
-
|
|
815
|
-
Raises:
|
|
816
|
-
RuntimeError: If status information cannot be retrieved.
|
|
817
|
-
"""
|
|
818
|
-
try:
|
|
819
|
-
replies = self._zenoh_session.get(
|
|
820
|
-
resolve_key_name(self._configs.status_info_name)
|
|
821
|
-
)
|
|
822
|
-
status_dict = {}
|
|
823
|
-
for reply in replies:
|
|
824
|
-
if reply.ok and reply.ok.payload:
|
|
825
|
-
status_bytes = reply.ok.payload.to_bytes()
|
|
826
|
-
status_msg = cast(
|
|
827
|
-
dexcontrol_query_pb2.ComponentStates,
|
|
828
|
-
dexcontrol_query_pb2.ComponentStates.FromString(status_bytes),
|
|
829
|
-
)
|
|
830
|
-
status_dict = status_to_dict(status_msg)
|
|
831
|
-
break
|
|
832
|
-
|
|
833
|
-
if show:
|
|
834
|
-
show_component_status(status_dict)
|
|
835
|
-
return status_dict
|
|
836
|
-
except Exception as e:
|
|
837
|
-
raise RuntimeError(f"Failed to retrieve component status: {e}") from e
|
|
838
|
-
|
|
839
|
-
def reboot_component(self, part: Literal["arm", "chassis", "torso"]) -> None:
|
|
840
|
-
"""Reboot a specific robot component.
|
|
841
|
-
|
|
842
|
-
Args:
|
|
843
|
-
part: Component to reboot ("arm", "chassis", or "torso").
|
|
844
|
-
|
|
845
|
-
Raises:
|
|
846
|
-
ValueError: If the specified component is invalid.
|
|
847
|
-
RuntimeError: If the reboot operation fails.
|
|
848
|
-
"""
|
|
849
|
-
component_map = {
|
|
850
|
-
"arm": dexcontrol_query_pb2.RebootComponent.Component.ARM,
|
|
851
|
-
"chassis": dexcontrol_query_pb2.RebootComponent.Component.CHASSIS,
|
|
852
|
-
"torso": dexcontrol_query_pb2.RebootComponent.Component.TORSO,
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if part not in component_map:
|
|
856
|
-
raise ValueError(f"Invalid component: {part}")
|
|
857
|
-
|
|
858
|
-
try:
|
|
859
|
-
query_msg = dexcontrol_query_pb2.RebootComponent(
|
|
860
|
-
component=component_map[part]
|
|
861
|
-
)
|
|
862
|
-
self._zenoh_session.get(
|
|
863
|
-
resolve_key_name(self._configs.reboot_query_name),
|
|
864
|
-
payload=query_msg.SerializeToString(),
|
|
865
|
-
)
|
|
866
|
-
logger.info(f"Rebooting component: {part}")
|
|
867
|
-
except Exception as e:
|
|
868
|
-
raise RuntimeError(f"Failed to reboot component {part}: {e}") from e
|
|
869
|
-
|
|
870
|
-
def clear_error(
|
|
871
|
-
self, part: Literal["left_arm", "right_arm", "chassis", "head"] | str
|
|
872
|
-
) -> None:
|
|
873
|
-
"""Clear error state for a specific component.
|
|
874
|
-
|
|
875
|
-
Args:
|
|
876
|
-
part: Component to clear error state for.
|
|
877
|
-
|
|
878
|
-
Raises:
|
|
879
|
-
ValueError: If the specified component is invalid.
|
|
880
|
-
RuntimeError: If the error clearing operation fails.
|
|
881
|
-
"""
|
|
882
|
-
component_map = {
|
|
883
|
-
"left_arm": dexcontrol_query_pb2.ClearError.Component.LEFT_ARM,
|
|
884
|
-
"right_arm": dexcontrol_query_pb2.ClearError.Component.RIGHT_ARM,
|
|
885
|
-
"chassis": dexcontrol_query_pb2.ClearError.Component.CHASSIS,
|
|
886
|
-
"head": dexcontrol_query_pb2.ClearError.Component.HEAD,
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
if part not in component_map:
|
|
890
|
-
raise ValueError(f"Invalid component: {part}")
|
|
891
|
-
|
|
892
|
-
try:
|
|
893
|
-
query_msg = dexcontrol_query_pb2.ClearError(component=component_map[part])
|
|
894
|
-
self._zenoh_session.get(
|
|
895
|
-
resolve_key_name(self._configs.clear_error_query_name),
|
|
896
|
-
handler=lambda reply: logger.info(f"Cleared error of {part}"),
|
|
897
|
-
payload=query_msg.SerializeToString(),
|
|
898
|
-
)
|
|
899
|
-
except Exception as e:
|
|
900
|
-
raise RuntimeError(
|
|
901
|
-
f"Failed to clear error for component {part}: {e}"
|
|
902
|
-
) from e
|
|
903
|
-
|
|
904
642
|
def get_joint_pos_dict(
|
|
905
643
|
self,
|
|
906
644
|
component: Literal[
|
|
@@ -924,14 +662,7 @@ class Robot:
|
|
|
924
662
|
KeyError: If an invalid component name is provided.
|
|
925
663
|
RuntimeError: If joint position retrieval fails.
|
|
926
664
|
"""
|
|
927
|
-
component_map =
|
|
928
|
-
"left_arm": self.left_arm,
|
|
929
|
-
"right_arm": self.right_arm,
|
|
930
|
-
"torso": self.torso,
|
|
931
|
-
"head": self.head,
|
|
932
|
-
"left_hand": self.left_hand,
|
|
933
|
-
"right_hand": self.right_hand,
|
|
934
|
-
}
|
|
665
|
+
component_map = self.get_component_map()
|
|
935
666
|
|
|
936
667
|
try:
|
|
937
668
|
if isinstance(component, str):
|
|
@@ -1021,10 +752,10 @@ class Robot:
|
|
|
1021
752
|
|
|
1022
753
|
try:
|
|
1023
754
|
start_time = time.time()
|
|
1024
|
-
component_map = self.
|
|
755
|
+
component_map = self.get_component_map()
|
|
1025
756
|
|
|
1026
757
|
# Validate component names
|
|
1027
|
-
self.
|
|
758
|
+
self.validate_component_names(joint_pos)
|
|
1028
759
|
|
|
1029
760
|
# Separate position-velocity controlled components from others
|
|
1030
761
|
pv_components = [c for c in joint_pos if c in self._pv_components]
|
|
@@ -1150,6 +881,10 @@ class Robot:
|
|
|
1150
881
|
|
|
1151
882
|
return adjusted_positions
|
|
1152
883
|
|
|
884
|
+
def have_hand(self, side: Literal["left", "right"]) -> bool:
|
|
885
|
+
"""Check if the robot has a hand."""
|
|
886
|
+
return self._hand_types.get(side) != HandType.UNKNOWN
|
|
887
|
+
|
|
1153
888
|
def _process_trajectory(
|
|
1154
889
|
self, trajectory: dict[str, np.ndarray | dict[str, np.ndarray]]
|
|
1155
890
|
) -> dict[str, dict[str, np.ndarray]]:
|
|
@@ -1215,14 +950,7 @@ class Robot:
|
|
|
1215
950
|
ValueError: If invalid component is specified.
|
|
1216
951
|
"""
|
|
1217
952
|
rate_limiter = RateLimiter(control_hz)
|
|
1218
|
-
component_map =
|
|
1219
|
-
"left_arm": self.left_arm,
|
|
1220
|
-
"right_arm": self.right_arm,
|
|
1221
|
-
"torso": self.torso,
|
|
1222
|
-
"head": self.head,
|
|
1223
|
-
"left_hand": self.left_hand,
|
|
1224
|
-
"right_hand": self.right_hand,
|
|
1225
|
-
}
|
|
953
|
+
component_map = self.get_component_map()
|
|
1226
954
|
|
|
1227
955
|
first_component = next(iter(processed_trajectory))
|
|
1228
956
|
trajectory_length = len(processed_trajectory[first_component]["position"])
|
|
@@ -1244,41 +972,6 @@ class Robot:
|
|
|
1244
972
|
)
|
|
1245
973
|
rate_limiter.sleep()
|
|
1246
974
|
|
|
1247
|
-
def _get_component_map(self) -> dict[str, Any]:
|
|
1248
|
-
"""Get the component mapping dictionary.
|
|
1249
|
-
|
|
1250
|
-
Returns:
|
|
1251
|
-
Dictionary mapping component names to component instances.
|
|
1252
|
-
"""
|
|
1253
|
-
return {
|
|
1254
|
-
"left_arm": self.left_arm,
|
|
1255
|
-
"right_arm": self.right_arm,
|
|
1256
|
-
"torso": self.torso,
|
|
1257
|
-
"head": self.head,
|
|
1258
|
-
"left_hand": self.left_hand,
|
|
1259
|
-
"right_hand": self.right_hand,
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
def _validate_component_names(
|
|
1263
|
-
self,
|
|
1264
|
-
joint_pos: dict[str, list[float] | np.ndarray],
|
|
1265
|
-
component_map: dict[str, Any],
|
|
1266
|
-
) -> None:
|
|
1267
|
-
"""Validate that all component names are valid.
|
|
1268
|
-
|
|
1269
|
-
Args:
|
|
1270
|
-
joint_pos: Joint position dictionary.
|
|
1271
|
-
component_map: Component mapping dictionary.
|
|
1272
|
-
|
|
1273
|
-
Raises:
|
|
1274
|
-
ValueError: If invalid component names are found.
|
|
1275
|
-
"""
|
|
1276
|
-
invalid_components = set(joint_pos.keys()) - set(component_map.keys())
|
|
1277
|
-
if invalid_components:
|
|
1278
|
-
raise ValueError(
|
|
1279
|
-
f"Invalid component names: {', '.join(invalid_components)}"
|
|
1280
|
-
)
|
|
1281
|
-
|
|
1282
975
|
def _set_pv_components(
|
|
1283
976
|
self,
|
|
1284
977
|
pv_components: list[str],
|