dexcontrol 0.2.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.
- dexcontrol/__init__.py +45 -0
- dexcontrol/apps/dualsense_teleop_base.py +371 -0
- dexcontrol/config/__init__.py +14 -0
- dexcontrol/config/core/__init__.py +22 -0
- dexcontrol/config/core/arm.py +32 -0
- dexcontrol/config/core/chassis.py +22 -0
- dexcontrol/config/core/hand.py +42 -0
- dexcontrol/config/core/head.py +33 -0
- dexcontrol/config/core/misc.py +37 -0
- dexcontrol/config/core/torso.py +36 -0
- dexcontrol/config/sensors/__init__.py +4 -0
- dexcontrol/config/sensors/cameras/__init__.py +7 -0
- dexcontrol/config/sensors/cameras/gemini_camera.py +16 -0
- dexcontrol/config/sensors/cameras/rgb_camera.py +15 -0
- dexcontrol/config/sensors/imu/__init__.py +6 -0
- dexcontrol/config/sensors/imu/gemini_imu.py +15 -0
- dexcontrol/config/sensors/imu/nine_axis_imu.py +15 -0
- dexcontrol/config/sensors/lidar/__init__.py +6 -0
- dexcontrol/config/sensors/lidar/rplidar.py +15 -0
- dexcontrol/config/sensors/ultrasonic/__init__.py +6 -0
- dexcontrol/config/sensors/ultrasonic/ultrasonic.py +15 -0
- dexcontrol/config/sensors/vega_sensors.py +65 -0
- dexcontrol/config/vega.py +203 -0
- dexcontrol/core/__init__.py +0 -0
- dexcontrol/core/arm.py +324 -0
- dexcontrol/core/chassis.py +628 -0
- dexcontrol/core/component.py +834 -0
- dexcontrol/core/hand.py +170 -0
- dexcontrol/core/head.py +232 -0
- dexcontrol/core/misc.py +514 -0
- dexcontrol/core/torso.py +198 -0
- dexcontrol/proto/dexcontrol_msg_pb2.py +69 -0
- dexcontrol/proto/dexcontrol_msg_pb2.pyi +168 -0
- dexcontrol/proto/dexcontrol_query_pb2.py +73 -0
- dexcontrol/proto/dexcontrol_query_pb2.pyi +134 -0
- dexcontrol/robot.py +1091 -0
- dexcontrol/sensors/__init__.py +40 -0
- dexcontrol/sensors/camera/__init__.py +18 -0
- dexcontrol/sensors/camera/gemini_camera.py +139 -0
- dexcontrol/sensors/camera/rgb_camera.py +98 -0
- dexcontrol/sensors/imu/__init__.py +22 -0
- dexcontrol/sensors/imu/gemini_imu.py +139 -0
- dexcontrol/sensors/imu/nine_axis_imu.py +149 -0
- dexcontrol/sensors/lidar/__init__.py +3 -0
- dexcontrol/sensors/lidar/rplidar.py +164 -0
- dexcontrol/sensors/manager.py +185 -0
- dexcontrol/sensors/ultrasonic.py +110 -0
- dexcontrol/utils/__init__.py +15 -0
- dexcontrol/utils/constants.py +12 -0
- dexcontrol/utils/io_utils.py +26 -0
- dexcontrol/utils/motion_utils.py +194 -0
- dexcontrol/utils/os_utils.py +39 -0
- dexcontrol/utils/pb_utils.py +103 -0
- dexcontrol/utils/rate_limiter.py +167 -0
- dexcontrol/utils/reset_orbbec_camera_usb.py +98 -0
- dexcontrol/utils/subscribers/__init__.py +44 -0
- dexcontrol/utils/subscribers/base.py +260 -0
- dexcontrol/utils/subscribers/camera.py +328 -0
- dexcontrol/utils/subscribers/decoders.py +83 -0
- dexcontrol/utils/subscribers/generic.py +105 -0
- dexcontrol/utils/subscribers/imu.py +170 -0
- dexcontrol/utils/subscribers/lidar.py +195 -0
- dexcontrol/utils/subscribers/protobuf.py +106 -0
- dexcontrol/utils/timer.py +136 -0
- dexcontrol/utils/trajectory_utils.py +40 -0
- dexcontrol/utils/viz_utils.py +86 -0
- dexcontrol-0.2.1.dist-info/METADATA +369 -0
- dexcontrol-0.2.1.dist-info/RECORD +72 -0
- dexcontrol-0.2.1.dist-info/WHEEL +5 -0
- dexcontrol-0.2.1.dist-info/licenses/LICENSE +188 -0
- dexcontrol-0.2.1.dist-info/licenses/NOTICE +13 -0
- dexcontrol-0.2.1.dist-info/top_level.txt +1 -0
dexcontrol/robot.py
ADDED
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
# Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 with Commons Clause License
|
|
4
|
+
# Condition v1.0 [see LICENSE for details].
|
|
5
|
+
|
|
6
|
+
"""Main robot interface module.
|
|
7
|
+
|
|
8
|
+
This module provides the main Robot class that serves as the primary interface for
|
|
9
|
+
controlling and monitoring a robot system. It handles component initialization,
|
|
10
|
+
status monitoring, and system-wide operations.
|
|
11
|
+
|
|
12
|
+
The Robot class manages initialization and coordination of various robot components
|
|
13
|
+
including arms, hands, head, chassis, torso, and sensors. It provides methods for
|
|
14
|
+
system-wide operations like status monitoring, trajectory execution, and component
|
|
15
|
+
control.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import signal
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
import weakref
|
|
25
|
+
from typing import (
|
|
26
|
+
TYPE_CHECKING,
|
|
27
|
+
Any,
|
|
28
|
+
Final,
|
|
29
|
+
Literal,
|
|
30
|
+
cast,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
import hydra.utils
|
|
34
|
+
import numpy as np
|
|
35
|
+
import omegaconf
|
|
36
|
+
import zenoh
|
|
37
|
+
from loguru import logger
|
|
38
|
+
from rich.console import Console
|
|
39
|
+
|
|
40
|
+
import dexcontrol
|
|
41
|
+
from dexcontrol.config.vega import VegaConfig, get_vega_config
|
|
42
|
+
from dexcontrol.core.component import RobotComponent
|
|
43
|
+
from dexcontrol.proto import dexcontrol_query_pb2
|
|
44
|
+
from dexcontrol.sensors import Sensors
|
|
45
|
+
from dexcontrol.utils.constants import ROBOT_NAME_ENV_VAR
|
|
46
|
+
from dexcontrol.utils.os_utils import get_robot_model, resolve_key_name
|
|
47
|
+
from dexcontrol.utils.pb_utils import (
|
|
48
|
+
ComponentStatus,
|
|
49
|
+
software_version_to_dict,
|
|
50
|
+
status_to_dict,
|
|
51
|
+
)
|
|
52
|
+
from dexcontrol.utils.rate_limiter import RateLimiter
|
|
53
|
+
from dexcontrol.utils.trajectory_utils import generate_linear_trajectory
|
|
54
|
+
from dexcontrol.utils.viz_utils import show_component_status, show_software_version
|
|
55
|
+
|
|
56
|
+
if TYPE_CHECKING:
|
|
57
|
+
from dexcontrol.core.arm import Arm
|
|
58
|
+
from dexcontrol.core.chassis import Chassis
|
|
59
|
+
from dexcontrol.core.hand import Hand
|
|
60
|
+
from dexcontrol.core.head import Head
|
|
61
|
+
from dexcontrol.core.misc import Battery, EStop, Heartbeat
|
|
62
|
+
from dexcontrol.core.torso import Torso
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Global registry to track active Robot instances for signal handling
|
|
66
|
+
_active_robots: weakref.WeakSet[Robot] = weakref.WeakSet()
|
|
67
|
+
_signal_handlers_registered: bool = False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _signal_handler(signum: int, frame: Any) -> None:
|
|
71
|
+
"""Signal handler to shutdown all active Robot instances.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
signum: Signal number received (e.g., SIGINT, SIGTERM).
|
|
75
|
+
frame: Current stack frame (unused).
|
|
76
|
+
"""
|
|
77
|
+
logger.info(f"Received signal {signum}, shutting down all active robots...")
|
|
78
|
+
# Create a list copy to avoid modification during iteration
|
|
79
|
+
robots_to_shutdown = list(_active_robots)
|
|
80
|
+
for robot in robots_to_shutdown:
|
|
81
|
+
logger.info(f"Shutting down robot: {robot}")
|
|
82
|
+
try:
|
|
83
|
+
robot.shutdown()
|
|
84
|
+
except Exception as e: # pylint: disable=broad-except
|
|
85
|
+
logger.error(f"Error during robot shutdown: {e}", exc_info=True)
|
|
86
|
+
logger.info("All robots shutdown complete")
|
|
87
|
+
os._exit(1)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _register_signal_handlers() -> None:
|
|
91
|
+
"""Register signal handlers for graceful shutdown.
|
|
92
|
+
|
|
93
|
+
This function ensures signal handlers are registered only once and sets up
|
|
94
|
+
handlers for SIGINT (Ctrl+C), SIGTERM, and SIGHUP (on Unix systems).
|
|
95
|
+
"""
|
|
96
|
+
global _signal_handlers_registered
|
|
97
|
+
if _signal_handlers_registered:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Register handlers for common termination signals
|
|
101
|
+
signal.signal(signal.SIGINT, _signal_handler) # Ctrl+C
|
|
102
|
+
signal.signal(signal.SIGTERM, _signal_handler) # Termination signal
|
|
103
|
+
|
|
104
|
+
# On Unix systems, also handle SIGHUP
|
|
105
|
+
if hasattr(signal, "SIGHUP"):
|
|
106
|
+
signal.signal(signal.SIGHUP, _signal_handler)
|
|
107
|
+
|
|
108
|
+
_signal_handlers_registered = True
|
|
109
|
+
logger.debug("Signal handlers registered for graceful robot shutdown")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ComponentConfig(omegaconf.DictConfig):
|
|
113
|
+
"""Type hints for component configuration."""
|
|
114
|
+
|
|
115
|
+
_target_: str
|
|
116
|
+
configs: dict[str, Any]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Robot:
|
|
120
|
+
"""Main interface class for robot control and monitoring.
|
|
121
|
+
|
|
122
|
+
This class serves as the primary interface for interacting with a robot system.
|
|
123
|
+
It manages initialization and coordination of various robot components including
|
|
124
|
+
arms, hands, head, chassis, torso, and sensors. It provides methods for
|
|
125
|
+
system-wide operations like status monitoring, trajectory execution, and component
|
|
126
|
+
control.
|
|
127
|
+
|
|
128
|
+
The Robot class supports context manager usage and automatic cleanup on program
|
|
129
|
+
interruption through signal handlers.
|
|
130
|
+
|
|
131
|
+
Example usage:
|
|
132
|
+
# Using context manager (recommended)
|
|
133
|
+
with Robot() as robot:
|
|
134
|
+
robot.set_joint_pos({"left_arm": [0, 0, 0, 0, 0, 0, 0]})
|
|
135
|
+
|
|
136
|
+
# Manual usage with explicit shutdown
|
|
137
|
+
robot = Robot()
|
|
138
|
+
try:
|
|
139
|
+
robot.set_joint_pos({"left_arm": [0, 0, 0, 0, 0, 0, 0]})
|
|
140
|
+
finally:
|
|
141
|
+
robot.shutdown()
|
|
142
|
+
|
|
143
|
+
Attributes:
|
|
144
|
+
left_arm: Left arm component interface.
|
|
145
|
+
right_arm: Right arm component interface.
|
|
146
|
+
left_hand: Left hand component interface.
|
|
147
|
+
right_hand: Right hand component interface.
|
|
148
|
+
head: Head component interface.
|
|
149
|
+
chassis: Chassis component interface.
|
|
150
|
+
torso: Torso component interface.
|
|
151
|
+
battery: Battery monitoring interface.
|
|
152
|
+
estop: Emergency stop interface.
|
|
153
|
+
heartbeat: Heartbeat monitoring interface.
|
|
154
|
+
sensors: Sensor systems interface.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
# Type annotations for dynamically created attributes
|
|
158
|
+
left_arm: Arm
|
|
159
|
+
right_arm: Arm
|
|
160
|
+
left_hand: Hand
|
|
161
|
+
right_hand: Hand
|
|
162
|
+
head: Head
|
|
163
|
+
chassis: Chassis
|
|
164
|
+
torso: Torso
|
|
165
|
+
battery: Battery
|
|
166
|
+
estop: EStop
|
|
167
|
+
heartbeat: Heartbeat
|
|
168
|
+
sensors: Sensors
|
|
169
|
+
_shutdown_called: bool = False
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
robot_model: str | None = None,
|
|
174
|
+
configs: VegaConfig | None = None,
|
|
175
|
+
zenoh_config_file: str | None = None,
|
|
176
|
+
auto_shutdown: bool = True,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Initializes the Robot with the given configuration.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
robot_model: Optional robot variant name (e.g., "vega_rc2", "vega_1").
|
|
182
|
+
If configs is None, this will be used to get the appropriate config.
|
|
183
|
+
Ignored if configs is provided.
|
|
184
|
+
configs: Configuration parameters for all robot components.
|
|
185
|
+
If None, will use the configuration specified by robot_model.
|
|
186
|
+
zenoh_config_file: Optional path to the zenoh config file.
|
|
187
|
+
Defaults to None to use system defaults.
|
|
188
|
+
auto_shutdown: Whether to automatically register signal handlers for
|
|
189
|
+
graceful shutdown on program interruption. Default is True.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
RuntimeError: If any critical component fails to become active within timeout.
|
|
193
|
+
ValueError: If robot_model is invalid or configs cannot be loaded.
|
|
194
|
+
"""
|
|
195
|
+
if robot_model is None:
|
|
196
|
+
robot_model = get_robot_model()
|
|
197
|
+
self._robot_model: Final[str] = robot_model
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
self._configs: Final[VegaConfig] = configs or get_vega_config(robot_model)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
raise ValueError(f"Failed to load robot configuration: {e}") from e
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
self._zenoh_session: Final[zenoh.Session] = self._init_zenoh_session(
|
|
206
|
+
zenoh_config_file
|
|
207
|
+
)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
raise RuntimeError(f"Failed to initialize zenoh session: {e}") from e
|
|
210
|
+
|
|
211
|
+
self._components: list[RobotComponent] = []
|
|
212
|
+
self._robot_name: Final[str] = os.getenv(ROBOT_NAME_ENV_VAR, "robot")
|
|
213
|
+
self._pv_components: Final[list[str]] = [
|
|
214
|
+
"left_hand",
|
|
215
|
+
"right_hand",
|
|
216
|
+
"head",
|
|
217
|
+
"torso",
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
# Register for automatic shutdown on signals if enabled
|
|
221
|
+
if auto_shutdown:
|
|
222
|
+
_register_signal_handlers()
|
|
223
|
+
_active_robots.add(self)
|
|
224
|
+
|
|
225
|
+
self._print_initialization_info(robot_model)
|
|
226
|
+
|
|
227
|
+
# Initialize robot body components dynamically
|
|
228
|
+
try:
|
|
229
|
+
config_dict = omegaconf.OmegaConf.to_container(self._configs, resolve=True)
|
|
230
|
+
if not isinstance(config_dict, dict):
|
|
231
|
+
raise ValueError("Invalid configuration format")
|
|
232
|
+
self._init_components(cast(dict[str, Any], config_dict))
|
|
233
|
+
except Exception as e:
|
|
234
|
+
self.shutdown() # Clean up on initialization failure
|
|
235
|
+
raise RuntimeError(f"Failed to initialize robot components: {e}") from e
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
self.sensors = Sensors(self._configs.sensors, self._zenoh_session)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
self.shutdown() # Clean up on initialization failure
|
|
241
|
+
raise RuntimeError(f"Failed to initialize sensors: {e}") from e
|
|
242
|
+
|
|
243
|
+
# Ensure all components are active
|
|
244
|
+
try:
|
|
245
|
+
self._wait_for_components()
|
|
246
|
+
except Exception as e:
|
|
247
|
+
self.shutdown() # Clean up on initialization failure
|
|
248
|
+
raise RuntimeError(f"Failed to activate components: {e}") from e
|
|
249
|
+
|
|
250
|
+
# Set default modes
|
|
251
|
+
try:
|
|
252
|
+
self._set_default_modes()
|
|
253
|
+
except Exception as e:
|
|
254
|
+
self.shutdown() # Clean up on initialization failure
|
|
255
|
+
raise RuntimeError(f"Failed to set default modes: {e}") from e
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def robot_model(self) -> str:
|
|
259
|
+
"""Get the robot model.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
The robot model.
|
|
263
|
+
"""
|
|
264
|
+
return self._robot_model
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def robot_name(self) -> str:
|
|
268
|
+
"""Get the robot name.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
The robot name.
|
|
272
|
+
"""
|
|
273
|
+
return self._robot_name
|
|
274
|
+
|
|
275
|
+
def __enter__(self) -> Robot:
|
|
276
|
+
"""Context manager entry.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Self reference for context management.
|
|
280
|
+
"""
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
def __exit__(
|
|
284
|
+
self, exc_type: type | None, exc_val: Exception | None, exc_tb: Any
|
|
285
|
+
) -> None:
|
|
286
|
+
"""Context manager exit with automatic shutdown.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
exc_type: Type of the exception that occurred, if any.
|
|
290
|
+
exc_val: Exception instance that occurred, if any.
|
|
291
|
+
exc_tb: Traceback of the exception that occurred, if any.
|
|
292
|
+
"""
|
|
293
|
+
self.shutdown()
|
|
294
|
+
|
|
295
|
+
def __del__(self) -> None:
|
|
296
|
+
"""Destructor to ensure cleanup."""
|
|
297
|
+
if not self._shutdown_called:
|
|
298
|
+
logger.warning(
|
|
299
|
+
"Robot instance being destroyed without explicit shutdown call"
|
|
300
|
+
)
|
|
301
|
+
self.shutdown()
|
|
302
|
+
|
|
303
|
+
def _print_initialization_info(self, robot_model: str | None) -> None:
|
|
304
|
+
"""Print initialization information.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
robot_model: The robot model being initialized.
|
|
308
|
+
"""
|
|
309
|
+
print("=" * 50)
|
|
310
|
+
print(f"Robot name: {self._robot_name}")
|
|
311
|
+
if robot_model:
|
|
312
|
+
print(f"Robot model: {robot_model}")
|
|
313
|
+
print(f"Communication config file: {dexcontrol.COMM_CFG_PATH}")
|
|
314
|
+
print("=" * 50)
|
|
315
|
+
|
|
316
|
+
def _init_components(self, config_dict: dict[str, Any]) -> None:
|
|
317
|
+
"""Initialize robot components from configuration.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
config_dict: Configuration dictionary for components.
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
RuntimeError: If component initialization fails.
|
|
324
|
+
"""
|
|
325
|
+
for component_name, component_config in config_dict.items():
|
|
326
|
+
if component_name == "sensors":
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
component_config = getattr(self._configs, str(component_name))
|
|
331
|
+
if (
|
|
332
|
+
not hasattr(component_config, "_target_")
|
|
333
|
+
or not component_config._target_
|
|
334
|
+
):
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
temp_config = omegaconf.OmegaConf.create(
|
|
338
|
+
{
|
|
339
|
+
"_target_": component_config._target_,
|
|
340
|
+
"configs": {
|
|
341
|
+
k: v for k, v in component_config.items() if k != "_target_"
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
component_instance = hydra.utils.instantiate(
|
|
346
|
+
temp_config, zenoh_session=self._zenoh_session
|
|
347
|
+
)
|
|
348
|
+
setattr(self, str(component_name), component_instance)
|
|
349
|
+
except Exception as e:
|
|
350
|
+
raise RuntimeError(
|
|
351
|
+
f"Failed to initialize component {component_name}: {e}"
|
|
352
|
+
) from e
|
|
353
|
+
|
|
354
|
+
def _set_default_modes(self) -> None:
|
|
355
|
+
"""Set default control modes for robot components.
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
RuntimeError: If setting default mode fails for any component.
|
|
359
|
+
"""
|
|
360
|
+
for arm in ["left_arm", "right_arm"]:
|
|
361
|
+
if component := getattr(self, arm, None):
|
|
362
|
+
try:
|
|
363
|
+
component.set_mode("position")
|
|
364
|
+
except Exception as e:
|
|
365
|
+
raise RuntimeError(
|
|
366
|
+
f"Failed to set default mode for {arm}: {e}"
|
|
367
|
+
) from e
|
|
368
|
+
|
|
369
|
+
if head := getattr(self, "head", None):
|
|
370
|
+
try:
|
|
371
|
+
head.set_mode("enable")
|
|
372
|
+
home_pos = head.get_predefined_pose("home")
|
|
373
|
+
home_pos = self.compensate_torso_pitch(home_pos, "head")
|
|
374
|
+
head.set_joint_pos(home_pos)
|
|
375
|
+
except Exception as e:
|
|
376
|
+
raise RuntimeError(f"Failed to set default mode for head: {e}") from e
|
|
377
|
+
|
|
378
|
+
def _wait_for_components(self) -> None:
|
|
379
|
+
"""Waits for all critical components to become active.
|
|
380
|
+
|
|
381
|
+
This method monitors the activation status of essential robot components
|
|
382
|
+
and ensures they are properly initialized before proceeding.
|
|
383
|
+
|
|
384
|
+
Raises:
|
|
385
|
+
RuntimeError: If any component fails to activate within the timeout period
|
|
386
|
+
or if shutdown is triggered during activation.
|
|
387
|
+
"""
|
|
388
|
+
component_names: Final[list[str]] = [
|
|
389
|
+
"left_arm",
|
|
390
|
+
"right_arm",
|
|
391
|
+
"left_hand",
|
|
392
|
+
"right_hand",
|
|
393
|
+
"head",
|
|
394
|
+
"chassis",
|
|
395
|
+
"torso",
|
|
396
|
+
"battery",
|
|
397
|
+
"estop",
|
|
398
|
+
]
|
|
399
|
+
if self._configs.heartbeat.enabled:
|
|
400
|
+
component_names.append("heartbeat")
|
|
401
|
+
|
|
402
|
+
console = Console()
|
|
403
|
+
actives: list[bool] = []
|
|
404
|
+
timeout_sec: Final[int] = 8
|
|
405
|
+
check_interval: Final[float] = 0.1 # Check every 100ms
|
|
406
|
+
|
|
407
|
+
with console.status(
|
|
408
|
+
"[bold green]Waiting for components to become active..."
|
|
409
|
+
) as status:
|
|
410
|
+
for name in component_names:
|
|
411
|
+
# Check if shutdown was triggered
|
|
412
|
+
if self._shutdown_called:
|
|
413
|
+
raise RuntimeError("Shutdown triggered during component activation")
|
|
414
|
+
|
|
415
|
+
status.update(f"Waiting for {name} to become active...")
|
|
416
|
+
if component := getattr(self, name, None):
|
|
417
|
+
start_time = time.monotonic()
|
|
418
|
+
while True:
|
|
419
|
+
if self._shutdown_called:
|
|
420
|
+
raise RuntimeError(
|
|
421
|
+
"Shutdown triggered during component activation"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Try a quick active check first
|
|
425
|
+
if component.is_active():
|
|
426
|
+
actives.append(True)
|
|
427
|
+
self._components.append(component)
|
|
428
|
+
break
|
|
429
|
+
|
|
430
|
+
# Check if we've exceeded timeout
|
|
431
|
+
if time.monotonic() - start_time >= timeout_sec:
|
|
432
|
+
console.log(f"{name} failed to become active")
|
|
433
|
+
actives.append(False)
|
|
434
|
+
break
|
|
435
|
+
|
|
436
|
+
# Wait a short interval before checking again
|
|
437
|
+
time.sleep(check_interval)
|
|
438
|
+
|
|
439
|
+
if not all(actives):
|
|
440
|
+
self.shutdown()
|
|
441
|
+
inactive = [
|
|
442
|
+
name for name, active in zip(component_names, actives) if not active
|
|
443
|
+
]
|
|
444
|
+
raise RuntimeError(
|
|
445
|
+
f"Components failed to activate within {timeout_sec}s: {', '.join(inactive)}"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
logger.info("All components activated successfully")
|
|
449
|
+
|
|
450
|
+
def _init_zenoh_session(self, zenoh_config_file: str | None) -> zenoh.Session:
|
|
451
|
+
"""Initializes Zenoh communication session.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
zenoh_config_file: Path to zenoh configuration file.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Initialized zenoh session.
|
|
458
|
+
|
|
459
|
+
Raises:
|
|
460
|
+
RuntimeError: If zenoh session initialization fails.
|
|
461
|
+
"""
|
|
462
|
+
try:
|
|
463
|
+
config_path = zenoh_config_file or self._get_default_zenoh_config()
|
|
464
|
+
if config_path is None:
|
|
465
|
+
logger.warning("Using default zenoh config settings")
|
|
466
|
+
return zenoh.open(zenoh.Config())
|
|
467
|
+
return zenoh.open(zenoh.Config.from_file(config_path))
|
|
468
|
+
except Exception as e:
|
|
469
|
+
raise RuntimeError(f"Failed to initialize zenoh session: {e}") from e
|
|
470
|
+
|
|
471
|
+
@staticmethod
|
|
472
|
+
def _get_default_zenoh_config() -> str | None:
|
|
473
|
+
"""Gets the default zenoh configuration file path.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Path to default config file if it exists, None otherwise.
|
|
477
|
+
"""
|
|
478
|
+
default_path = dexcontrol.COMM_CFG_PATH
|
|
479
|
+
if not default_path.exists():
|
|
480
|
+
logger.warning(f"Zenoh config file not found at {default_path}")
|
|
481
|
+
logger.warning("Please use dextop to set up the zenoh config file")
|
|
482
|
+
return None
|
|
483
|
+
return str(default_path)
|
|
484
|
+
|
|
485
|
+
def shutdown(self) -> None:
|
|
486
|
+
"""Cleans up and closes all component connections.
|
|
487
|
+
|
|
488
|
+
This method ensures proper cleanup of all components and communication
|
|
489
|
+
channels. It is automatically called when using the context manager
|
|
490
|
+
or when the object is garbage collected.
|
|
491
|
+
"""
|
|
492
|
+
if self._shutdown_called:
|
|
493
|
+
logger.warning("Shutdown already called, skipping")
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
logger.info("Shutting down robot...")
|
|
497
|
+
self._shutdown_called = True
|
|
498
|
+
|
|
499
|
+
# Remove from active robots registry
|
|
500
|
+
try:
|
|
501
|
+
_active_robots.discard(self)
|
|
502
|
+
except Exception: # pylint: disable=broad-except
|
|
503
|
+
pass # WeakSet may already have removed it
|
|
504
|
+
|
|
505
|
+
# Shutdown components in reverse order
|
|
506
|
+
for component in reversed(self._components):
|
|
507
|
+
if component is not None:
|
|
508
|
+
try:
|
|
509
|
+
component.shutdown()
|
|
510
|
+
except Exception as e: # pylint: disable=broad-except
|
|
511
|
+
logger.error(
|
|
512
|
+
f"Error shutting down component {component.__class__.__name__}: {e}"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
self.sensors.shutdown()
|
|
517
|
+
except Exception as e: # pylint: disable=broad-except
|
|
518
|
+
logger.error(f"Error shutting down sensors: {e}")
|
|
519
|
+
|
|
520
|
+
# Use timeout-based zenoh session close to avoid long waits
|
|
521
|
+
def _close_zenoh_session():
|
|
522
|
+
"""Close zenoh session in a separate thread."""
|
|
523
|
+
try:
|
|
524
|
+
self._zenoh_session.close()
|
|
525
|
+
except Exception as e: # pylint: disable=broad-except
|
|
526
|
+
logger.debug(f"Zenoh session close error: {e}")
|
|
527
|
+
|
|
528
|
+
close_thread = threading.Thread(target=_close_zenoh_session, daemon=True)
|
|
529
|
+
close_thread.start()
|
|
530
|
+
|
|
531
|
+
# Wait for close with timeout
|
|
532
|
+
close_thread.join(timeout=0.5) # 500ms timeout
|
|
533
|
+
if close_thread.is_alive():
|
|
534
|
+
logger.warning("Zenoh session close timed out, continuing shutdown")
|
|
535
|
+
else:
|
|
536
|
+
logger.debug("Zenoh session closed successfully")
|
|
537
|
+
|
|
538
|
+
logger.info("Robot shutdown complete")
|
|
539
|
+
|
|
540
|
+
def is_shutdown(self) -> bool:
|
|
541
|
+
"""Check if the robot has been shutdown.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
True if the robot has been shutdown, False otherwise.
|
|
545
|
+
"""
|
|
546
|
+
return self._shutdown_called
|
|
547
|
+
|
|
548
|
+
def get_software_version(
|
|
549
|
+
self, show: bool = True
|
|
550
|
+
) -> dict[
|
|
551
|
+
str,
|
|
552
|
+
dict[
|
|
553
|
+
Literal["hardware_version", "software_version", "main_hash"],
|
|
554
|
+
int | str,
|
|
555
|
+
],
|
|
556
|
+
]:
|
|
557
|
+
"""Retrieve software version information for all components.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
show: Whether to display the version information.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Dictionary containing version information for all components.
|
|
564
|
+
|
|
565
|
+
Raises:
|
|
566
|
+
RuntimeError: If version information cannot be retrieved.
|
|
567
|
+
"""
|
|
568
|
+
try:
|
|
569
|
+
replies = self._zenoh_session.get(
|
|
570
|
+
resolve_key_name(self._configs.version_info_name)
|
|
571
|
+
)
|
|
572
|
+
version_dict = {}
|
|
573
|
+
for reply in replies:
|
|
574
|
+
if reply.ok and reply.ok.payload:
|
|
575
|
+
version_bytes = reply.ok.payload.to_bytes()
|
|
576
|
+
version_msg = cast(
|
|
577
|
+
dexcontrol_query_pb2.SoftwareVersion,
|
|
578
|
+
dexcontrol_query_pb2.SoftwareVersion.FromString(version_bytes),
|
|
579
|
+
)
|
|
580
|
+
version_dict = software_version_to_dict(version_msg)
|
|
581
|
+
break
|
|
582
|
+
|
|
583
|
+
if show:
|
|
584
|
+
show_software_version(version_dict)
|
|
585
|
+
return version_dict
|
|
586
|
+
except Exception as e:
|
|
587
|
+
raise RuntimeError(f"Failed to retrieve software versions: {e}") from e
|
|
588
|
+
|
|
589
|
+
def get_component_status(
|
|
590
|
+
self, show: bool = True
|
|
591
|
+
) -> dict[str, dict[str, bool | ComponentStatus]]:
|
|
592
|
+
"""Retrieve status information for all components.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
show: Whether to display the status information.
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
Dictionary containing status information for all components.
|
|
599
|
+
|
|
600
|
+
Raises:
|
|
601
|
+
RuntimeError: If status information cannot be retrieved.
|
|
602
|
+
"""
|
|
603
|
+
try:
|
|
604
|
+
replies = self._zenoh_session.get(
|
|
605
|
+
resolve_key_name(self._configs.status_info_name)
|
|
606
|
+
)
|
|
607
|
+
status_dict = {}
|
|
608
|
+
for reply in replies:
|
|
609
|
+
if reply.ok and reply.ok.payload:
|
|
610
|
+
status_bytes = reply.ok.payload.to_bytes()
|
|
611
|
+
status_msg = cast(
|
|
612
|
+
dexcontrol_query_pb2.ComponentStates,
|
|
613
|
+
dexcontrol_query_pb2.ComponentStates.FromString(status_bytes),
|
|
614
|
+
)
|
|
615
|
+
status_dict = status_to_dict(status_msg)
|
|
616
|
+
break
|
|
617
|
+
|
|
618
|
+
if show:
|
|
619
|
+
show_component_status(status_dict)
|
|
620
|
+
return status_dict
|
|
621
|
+
except Exception as e:
|
|
622
|
+
raise RuntimeError(f"Failed to retrieve component status: {e}") from e
|
|
623
|
+
|
|
624
|
+
def reboot_component(self, part: Literal["arm", "chassis", "torso"]) -> None:
|
|
625
|
+
"""Reboot a specific robot component.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
part: Component to reboot ("arm", "chassis", or "torso").
|
|
629
|
+
|
|
630
|
+
Raises:
|
|
631
|
+
ValueError: If the specified component is invalid.
|
|
632
|
+
RuntimeError: If the reboot operation fails.
|
|
633
|
+
"""
|
|
634
|
+
component_map = {
|
|
635
|
+
"arm": dexcontrol_query_pb2.RebootComponent.Component.ARM,
|
|
636
|
+
"chassis": dexcontrol_query_pb2.RebootComponent.Component.CHASSIS,
|
|
637
|
+
"torso": dexcontrol_query_pb2.RebootComponent.Component.TORSO,
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if part not in component_map:
|
|
641
|
+
raise ValueError(f"Invalid component: {part}")
|
|
642
|
+
|
|
643
|
+
try:
|
|
644
|
+
query_msg = dexcontrol_query_pb2.RebootComponent(
|
|
645
|
+
component=component_map[part]
|
|
646
|
+
)
|
|
647
|
+
self._zenoh_session.get(
|
|
648
|
+
resolve_key_name(self._configs.reboot_query_name),
|
|
649
|
+
payload=query_msg.SerializeToString(),
|
|
650
|
+
)
|
|
651
|
+
logger.info(f"Rebooting component: {part}")
|
|
652
|
+
except Exception as e:
|
|
653
|
+
raise RuntimeError(f"Failed to reboot component {part}: {e}") from e
|
|
654
|
+
|
|
655
|
+
def clear_error(
|
|
656
|
+
self, part: Literal["left_arm", "right_arm", "chassis", "head"] | str
|
|
657
|
+
) -> None:
|
|
658
|
+
"""Clear error state for a specific component.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
part: Component to clear error state for.
|
|
662
|
+
|
|
663
|
+
Raises:
|
|
664
|
+
ValueError: If the specified component is invalid.
|
|
665
|
+
RuntimeError: If the error clearing operation fails.
|
|
666
|
+
"""
|
|
667
|
+
component_map = {
|
|
668
|
+
"left_arm": dexcontrol_query_pb2.ClearError.Component.LEFT_ARM,
|
|
669
|
+
"right_arm": dexcontrol_query_pb2.ClearError.Component.RIGHT_ARM,
|
|
670
|
+
"chassis": dexcontrol_query_pb2.ClearError.Component.CHASSIS,
|
|
671
|
+
"head": dexcontrol_query_pb2.ClearError.Component.HEAD,
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if part not in component_map:
|
|
675
|
+
raise ValueError(f"Invalid component: {part}")
|
|
676
|
+
|
|
677
|
+
try:
|
|
678
|
+
query_msg = dexcontrol_query_pb2.ClearError(component=component_map[part])
|
|
679
|
+
self._zenoh_session.get(
|
|
680
|
+
resolve_key_name(self._configs.clear_error_query_name),
|
|
681
|
+
handler=lambda reply: logger.info(
|
|
682
|
+
f"Cleared error of {part}: {reply.ok}"
|
|
683
|
+
),
|
|
684
|
+
payload=query_msg.SerializeToString(),
|
|
685
|
+
)
|
|
686
|
+
except Exception as e:
|
|
687
|
+
raise RuntimeError(
|
|
688
|
+
f"Failed to clear error for component {part}: {e}"
|
|
689
|
+
) from e
|
|
690
|
+
|
|
691
|
+
def get_joint_pos_dict(
|
|
692
|
+
self,
|
|
693
|
+
component: Literal["left_arm", "right_arm", "torso", "head"]
|
|
694
|
+
| list[Literal["left_arm", "right_arm", "torso", "head"]],
|
|
695
|
+
) -> dict[str, float]:
|
|
696
|
+
"""Get the joint positions of one or more robot components.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
component: Component name or list of component names to get joint positions for.
|
|
700
|
+
Valid components are "left_arm", "right_arm", "torso", and "head".
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
Dictionary mapping joint names to joint positions.
|
|
704
|
+
|
|
705
|
+
Raises:
|
|
706
|
+
ValueError: If component is not a string or list.
|
|
707
|
+
KeyError: If an invalid component name is provided.
|
|
708
|
+
RuntimeError: If joint position retrieval fails.
|
|
709
|
+
"""
|
|
710
|
+
component_map = {
|
|
711
|
+
"left_arm": self.left_arm,
|
|
712
|
+
"right_arm": self.right_arm,
|
|
713
|
+
"torso": self.torso,
|
|
714
|
+
"head": self.head,
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
try:
|
|
718
|
+
if isinstance(component, str):
|
|
719
|
+
if component not in component_map:
|
|
720
|
+
raise KeyError(f"Invalid component name: {component}")
|
|
721
|
+
return component_map[component].get_joint_pos_dict()
|
|
722
|
+
elif isinstance(component, list):
|
|
723
|
+
joint_pos_dict = {}
|
|
724
|
+
for c in component:
|
|
725
|
+
if c not in component_map:
|
|
726
|
+
raise KeyError(f"Invalid component name: {c}")
|
|
727
|
+
joint_pos_dict.update(component_map[c].get_joint_pos_dict())
|
|
728
|
+
return joint_pos_dict
|
|
729
|
+
else:
|
|
730
|
+
raise ValueError("Component must be a string or list of strings")
|
|
731
|
+
except (KeyError, ValueError) as e:
|
|
732
|
+
raise e
|
|
733
|
+
except Exception as e:
|
|
734
|
+
raise RuntimeError(f"Failed to get joint positions: {e}") from e
|
|
735
|
+
|
|
736
|
+
def execute_trajectory(
|
|
737
|
+
self,
|
|
738
|
+
trajectory: dict[str, np.ndarray | dict[str, np.ndarray]],
|
|
739
|
+
control_hz: float = 100,
|
|
740
|
+
relative: bool = False,
|
|
741
|
+
) -> None:
|
|
742
|
+
"""Execute a trajectory on the robot.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
trajectory: Dictionary mapping component names to either:
|
|
746
|
+
- numpy arrays of joint positions
|
|
747
|
+
- dictionaries with 'position' and optional 'velocity' keys
|
|
748
|
+
control_hz: Control frequency in Hz.
|
|
749
|
+
relative: Whether positions are relative to current position.
|
|
750
|
+
|
|
751
|
+
Raises:
|
|
752
|
+
ValueError: If trajectory is empty or components have different trajectory lengths.
|
|
753
|
+
ValueError: If trajectory format is invalid.
|
|
754
|
+
RuntimeError: If trajectory execution fails.
|
|
755
|
+
"""
|
|
756
|
+
if not trajectory:
|
|
757
|
+
raise ValueError("Trajectory must be a non-empty dictionary")
|
|
758
|
+
|
|
759
|
+
try:
|
|
760
|
+
# Process trajectory to standardize format
|
|
761
|
+
processed_trajectory = self._process_trajectory(trajectory)
|
|
762
|
+
|
|
763
|
+
# Validate trajectory lengths
|
|
764
|
+
self._validate_trajectory_lengths(processed_trajectory)
|
|
765
|
+
|
|
766
|
+
# Execute trajectory
|
|
767
|
+
self._execute_processed_trajectory(
|
|
768
|
+
processed_trajectory, control_hz, relative
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
except Exception as e:
|
|
772
|
+
raise RuntimeError(f"Failed to execute trajectory: {e}") from e
|
|
773
|
+
|
|
774
|
+
def set_joint_pos(
|
|
775
|
+
self,
|
|
776
|
+
joint_pos: dict[str, list[float] | np.ndarray],
|
|
777
|
+
relative: bool = False,
|
|
778
|
+
wait_time: float = 0.0,
|
|
779
|
+
wait_kwargs: dict[str, Any] | None = None,
|
|
780
|
+
) -> None:
|
|
781
|
+
"""Set the joint positions of the robot.
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
joint_pos: Dictionary mapping component names to joint positions.
|
|
785
|
+
Values can be either lists of floats or numpy arrays.
|
|
786
|
+
relative: Whether to set positions relative to current position.
|
|
787
|
+
wait_time: Time to wait for movement completion in seconds.
|
|
788
|
+
wait_kwargs: Additional parameters for trajectory generation.
|
|
789
|
+
control_hz: Control frequency in Hz (default: 100).
|
|
790
|
+
max_vel: Maximum velocity for trajectory (default: 0.5).
|
|
791
|
+
|
|
792
|
+
Raises:
|
|
793
|
+
ValueError: If any component name is invalid.
|
|
794
|
+
RuntimeError: If joint position setting fails.
|
|
795
|
+
"""
|
|
796
|
+
if wait_kwargs is None:
|
|
797
|
+
wait_kwargs = {}
|
|
798
|
+
|
|
799
|
+
try:
|
|
800
|
+
component_map = self._get_component_map()
|
|
801
|
+
|
|
802
|
+
# Validate component names
|
|
803
|
+
self._validate_component_names(joint_pos, component_map)
|
|
804
|
+
|
|
805
|
+
# Separate position-velocity controlled components from others
|
|
806
|
+
pv_components = [c for c in joint_pos if c in self._pv_components]
|
|
807
|
+
non_pv_components = [c for c in joint_pos if c not in self._pv_components]
|
|
808
|
+
|
|
809
|
+
# Set PV components immediately
|
|
810
|
+
self._set_pv_components(pv_components, joint_pos, component_map, relative)
|
|
811
|
+
|
|
812
|
+
# Handle non-PV components based on wait_time
|
|
813
|
+
if wait_time <= 0:
|
|
814
|
+
self._set_non_pv_components_immediate(
|
|
815
|
+
non_pv_components, joint_pos, component_map, relative
|
|
816
|
+
)
|
|
817
|
+
else:
|
|
818
|
+
self._set_non_pv_components_with_trajectory(
|
|
819
|
+
non_pv_components,
|
|
820
|
+
joint_pos,
|
|
821
|
+
component_map,
|
|
822
|
+
relative,
|
|
823
|
+
wait_time,
|
|
824
|
+
wait_kwargs,
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
except Exception as e:
|
|
828
|
+
raise RuntimeError(f"Failed to set joint positions: {e}") from e
|
|
829
|
+
|
|
830
|
+
def compensate_torso_pitch(self, joint_pos: np.ndarray, part: str) -> np.ndarray:
|
|
831
|
+
"""Compensate for torso pitch in joint positions.
|
|
832
|
+
|
|
833
|
+
Args:
|
|
834
|
+
joint_pos: Joint positions to compensate.
|
|
835
|
+
robot: Robot instance.
|
|
836
|
+
part: Component name for which joint positions are being compensated.
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
Compensated joint positions.
|
|
840
|
+
"""
|
|
841
|
+
# Supported robot models
|
|
842
|
+
SUPPORTED_MODELS = {"vega-1", "vega-rc2", "vega-rc1"}
|
|
843
|
+
|
|
844
|
+
if self.robot_model not in SUPPORTED_MODELS:
|
|
845
|
+
raise ValueError(
|
|
846
|
+
f"Unsupported robot model: {self.robot_model}. "
|
|
847
|
+
f"Supported models: {SUPPORTED_MODELS}"
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
torso_pitch = self.torso.pitch_angle
|
|
851
|
+
|
|
852
|
+
# Calculate pitch adjustment based on body part
|
|
853
|
+
if part == "right_arm":
|
|
854
|
+
pitch_adjustment = -torso_pitch
|
|
855
|
+
elif part == "left_arm":
|
|
856
|
+
pitch_adjustment = torso_pitch
|
|
857
|
+
elif part == "head":
|
|
858
|
+
pitch_adjustment = torso_pitch - np.pi / 2
|
|
859
|
+
else:
|
|
860
|
+
raise ValueError(
|
|
861
|
+
f"Unsupported body part: {part}. "
|
|
862
|
+
f"Supported parts: left_arm, right_arm, head"
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
# Create a copy to avoid modifying the original array
|
|
866
|
+
adjusted_positions = joint_pos.copy()
|
|
867
|
+
adjusted_positions[0] += pitch_adjustment
|
|
868
|
+
|
|
869
|
+
return adjusted_positions
|
|
870
|
+
|
|
871
|
+
def _process_trajectory(
|
|
872
|
+
self, trajectory: dict[str, np.ndarray | dict[str, np.ndarray]]
|
|
873
|
+
) -> dict[str, dict[str, np.ndarray]]:
|
|
874
|
+
"""Process trajectory to standardize format.
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
trajectory: Raw trajectory data.
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
Processed trajectory with standardized format.
|
|
881
|
+
|
|
882
|
+
Raises:
|
|
883
|
+
ValueError: If trajectory format is invalid.
|
|
884
|
+
"""
|
|
885
|
+
processed_trajectory: dict[str, dict[str, np.ndarray]] = {}
|
|
886
|
+
for component, data in trajectory.items():
|
|
887
|
+
if isinstance(data, np.ndarray):
|
|
888
|
+
processed_trajectory[component] = {"position": data}
|
|
889
|
+
elif isinstance(data, dict) and "position" in data:
|
|
890
|
+
processed_trajectory[component] = data
|
|
891
|
+
else:
|
|
892
|
+
raise ValueError(f"Invalid trajectory format for component {component}")
|
|
893
|
+
return processed_trajectory
|
|
894
|
+
|
|
895
|
+
def _validate_trajectory_lengths(
|
|
896
|
+
self, processed_trajectory: dict[str, dict[str, np.ndarray]]
|
|
897
|
+
) -> None:
|
|
898
|
+
"""Validate that all trajectory components have consistent lengths.
|
|
899
|
+
|
|
900
|
+
Args:
|
|
901
|
+
processed_trajectory: Processed trajectory data.
|
|
902
|
+
|
|
903
|
+
Raises:
|
|
904
|
+
ValueError: If trajectory lengths are inconsistent.
|
|
905
|
+
"""
|
|
906
|
+
first_component = next(iter(processed_trajectory))
|
|
907
|
+
first_length = len(processed_trajectory[first_component]["position"])
|
|
908
|
+
|
|
909
|
+
for component, data in processed_trajectory.items():
|
|
910
|
+
if len(data["position"]) != first_length:
|
|
911
|
+
raise ValueError(
|
|
912
|
+
f"Component {component} has different trajectory length"
|
|
913
|
+
)
|
|
914
|
+
if "velocity" in data and len(data["velocity"]) != first_length:
|
|
915
|
+
raise ValueError(
|
|
916
|
+
f"Velocity length for {component} doesn't match position length"
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
def _execute_processed_trajectory(
|
|
920
|
+
self,
|
|
921
|
+
processed_trajectory: dict[str, dict[str, np.ndarray]],
|
|
922
|
+
control_hz: float,
|
|
923
|
+
relative: bool,
|
|
924
|
+
) -> None:
|
|
925
|
+
"""Execute the processed trajectory.
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
processed_trajectory: Processed trajectory data.
|
|
929
|
+
control_hz: Control frequency in Hz.
|
|
930
|
+
relative: Whether positions are relative to current position.
|
|
931
|
+
|
|
932
|
+
Raises:
|
|
933
|
+
ValueError: If invalid component is specified.
|
|
934
|
+
"""
|
|
935
|
+
rate_limiter = RateLimiter(control_hz)
|
|
936
|
+
component_map = {
|
|
937
|
+
"left_arm": self.left_arm,
|
|
938
|
+
"right_arm": self.right_arm,
|
|
939
|
+
"torso": self.torso,
|
|
940
|
+
"head": self.head,
|
|
941
|
+
"left_hand": self.left_hand,
|
|
942
|
+
"right_hand": self.right_hand,
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
first_component = next(iter(processed_trajectory))
|
|
946
|
+
trajectory_length = len(processed_trajectory[first_component]["position"])
|
|
947
|
+
|
|
948
|
+
for i in range(trajectory_length):
|
|
949
|
+
for c, data in processed_trajectory.items():
|
|
950
|
+
if c not in component_map:
|
|
951
|
+
raise ValueError(f"Invalid component: {c}")
|
|
952
|
+
|
|
953
|
+
position = data["position"][i]
|
|
954
|
+
if "velocity" in data:
|
|
955
|
+
velocity = data["velocity"][i]
|
|
956
|
+
component_map[c].set_joint_pos_vel(
|
|
957
|
+
position, velocity, relative=relative, wait_time=0.0
|
|
958
|
+
)
|
|
959
|
+
else:
|
|
960
|
+
component_map[c].set_joint_pos(
|
|
961
|
+
position, relative=relative, wait_time=0.0
|
|
962
|
+
)
|
|
963
|
+
rate_limiter.sleep()
|
|
964
|
+
|
|
965
|
+
def _get_component_map(self) -> dict[str, Any]:
|
|
966
|
+
"""Get the component mapping dictionary.
|
|
967
|
+
|
|
968
|
+
Returns:
|
|
969
|
+
Dictionary mapping component names to component instances.
|
|
970
|
+
"""
|
|
971
|
+
return {
|
|
972
|
+
"left_arm": self.left_arm,
|
|
973
|
+
"right_arm": self.right_arm,
|
|
974
|
+
"torso": self.torso,
|
|
975
|
+
"head": self.head,
|
|
976
|
+
"left_hand": self.left_hand,
|
|
977
|
+
"right_hand": self.right_hand,
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
def _validate_component_names(
|
|
981
|
+
self,
|
|
982
|
+
joint_pos: dict[str, list[float] | np.ndarray],
|
|
983
|
+
component_map: dict[str, Any],
|
|
984
|
+
) -> None:
|
|
985
|
+
"""Validate that all component names are valid.
|
|
986
|
+
|
|
987
|
+
Args:
|
|
988
|
+
joint_pos: Joint position dictionary.
|
|
989
|
+
component_map: Component mapping dictionary.
|
|
990
|
+
|
|
991
|
+
Raises:
|
|
992
|
+
ValueError: If invalid component names are found.
|
|
993
|
+
"""
|
|
994
|
+
invalid_components = set(joint_pos.keys()) - set(component_map.keys())
|
|
995
|
+
if invalid_components:
|
|
996
|
+
raise ValueError(
|
|
997
|
+
f"Invalid component names: {', '.join(invalid_components)}"
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
def _set_pv_components(
|
|
1001
|
+
self,
|
|
1002
|
+
pv_components: list[str],
|
|
1003
|
+
joint_pos: dict[str, list[float] | np.ndarray],
|
|
1004
|
+
component_map: dict[str, Any],
|
|
1005
|
+
relative: bool,
|
|
1006
|
+
) -> None:
|
|
1007
|
+
"""Set position-velocity controlled components immediately.
|
|
1008
|
+
|
|
1009
|
+
Args:
|
|
1010
|
+
pv_components: List of PV component names.
|
|
1011
|
+
joint_pos: Joint position dictionary.
|
|
1012
|
+
component_map: Component mapping dictionary.
|
|
1013
|
+
relative: Whether positions are relative.
|
|
1014
|
+
"""
|
|
1015
|
+
for c in pv_components:
|
|
1016
|
+
component_map[c].set_joint_pos(
|
|
1017
|
+
joint_pos[c], relative=relative, wait_time=0.0
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
def _set_non_pv_components_immediate(
|
|
1021
|
+
self,
|
|
1022
|
+
non_pv_components: list[str],
|
|
1023
|
+
joint_pos: dict[str, list[float] | np.ndarray],
|
|
1024
|
+
component_map: dict[str, Any],
|
|
1025
|
+
relative: bool,
|
|
1026
|
+
) -> None:
|
|
1027
|
+
"""Set non-PV components immediately without trajectory.
|
|
1028
|
+
|
|
1029
|
+
Args:
|
|
1030
|
+
non_pv_components: List of non-PV component names.
|
|
1031
|
+
joint_pos: Joint position dictionary.
|
|
1032
|
+
component_map: Component mapping dictionary.
|
|
1033
|
+
relative: Whether positions are relative.
|
|
1034
|
+
"""
|
|
1035
|
+
for c in non_pv_components:
|
|
1036
|
+
component_map[c].set_joint_pos(joint_pos[c], relative=relative)
|
|
1037
|
+
|
|
1038
|
+
def _set_non_pv_components_with_trajectory(
|
|
1039
|
+
self,
|
|
1040
|
+
non_pv_components: list[str],
|
|
1041
|
+
joint_pos: dict[str, list[float] | np.ndarray],
|
|
1042
|
+
component_map: dict[str, Any],
|
|
1043
|
+
relative: bool,
|
|
1044
|
+
wait_time: float,
|
|
1045
|
+
wait_kwargs: dict[str, Any],
|
|
1046
|
+
) -> None:
|
|
1047
|
+
"""Set non-PV components with smooth trajectory over wait_time.
|
|
1048
|
+
|
|
1049
|
+
Args:
|
|
1050
|
+
non_pv_components: List of non-PV component names.
|
|
1051
|
+
joint_pos: Joint position dictionary.
|
|
1052
|
+
component_map: Component mapping dictionary.
|
|
1053
|
+
relative: Whether positions are relative.
|
|
1054
|
+
wait_time: Time to wait for movement completion.
|
|
1055
|
+
wait_kwargs: Additional trajectory parameters.
|
|
1056
|
+
"""
|
|
1057
|
+
control_hz = wait_kwargs.get("control_hz", 100)
|
|
1058
|
+
max_vel = wait_kwargs.get("max_vel", 0.5)
|
|
1059
|
+
|
|
1060
|
+
# Generate trajectories for smooth motion during wait_time
|
|
1061
|
+
rate_limiter = RateLimiter(control_hz)
|
|
1062
|
+
non_pv_component_traj = {}
|
|
1063
|
+
max_traj_steps = 0
|
|
1064
|
+
|
|
1065
|
+
# Calculate trajectories for each component
|
|
1066
|
+
for c in non_pv_components:
|
|
1067
|
+
current_joint_pos = component_map[c].get_joint_pos().copy()
|
|
1068
|
+
target_pos = joint_pos[c]
|
|
1069
|
+
# Convert to numpy array if it's a list
|
|
1070
|
+
if isinstance(target_pos, list):
|
|
1071
|
+
target_pos = np.array(target_pos)
|
|
1072
|
+
non_pv_component_traj[c], steps = generate_linear_trajectory(
|
|
1073
|
+
current_joint_pos, target_pos, max_vel, control_hz
|
|
1074
|
+
)
|
|
1075
|
+
max_traj_steps = max(max_traj_steps, steps)
|
|
1076
|
+
|
|
1077
|
+
# Execute trajectories with timing
|
|
1078
|
+
start_time = time.time()
|
|
1079
|
+
for step in range(max_traj_steps):
|
|
1080
|
+
for c in non_pv_components:
|
|
1081
|
+
if step < len(non_pv_component_traj[c]):
|
|
1082
|
+
component_map[c].set_joint_pos(
|
|
1083
|
+
non_pv_component_traj[c][step], relative=relative, wait_time=0.0
|
|
1084
|
+
)
|
|
1085
|
+
rate_limiter.sleep()
|
|
1086
|
+
if time.time() - start_time > wait_time:
|
|
1087
|
+
break
|
|
1088
|
+
|
|
1089
|
+
# Wait for any remaining time
|
|
1090
|
+
while time.time() - start_time < wait_time:
|
|
1091
|
+
rate_limiter.sleep()
|