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.

Files changed (72) hide show
  1. dexcontrol/__init__.py +45 -0
  2. dexcontrol/apps/dualsense_teleop_base.py +371 -0
  3. dexcontrol/config/__init__.py +14 -0
  4. dexcontrol/config/core/__init__.py +22 -0
  5. dexcontrol/config/core/arm.py +32 -0
  6. dexcontrol/config/core/chassis.py +22 -0
  7. dexcontrol/config/core/hand.py +42 -0
  8. dexcontrol/config/core/head.py +33 -0
  9. dexcontrol/config/core/misc.py +37 -0
  10. dexcontrol/config/core/torso.py +36 -0
  11. dexcontrol/config/sensors/__init__.py +4 -0
  12. dexcontrol/config/sensors/cameras/__init__.py +7 -0
  13. dexcontrol/config/sensors/cameras/gemini_camera.py +16 -0
  14. dexcontrol/config/sensors/cameras/rgb_camera.py +15 -0
  15. dexcontrol/config/sensors/imu/__init__.py +6 -0
  16. dexcontrol/config/sensors/imu/gemini_imu.py +15 -0
  17. dexcontrol/config/sensors/imu/nine_axis_imu.py +15 -0
  18. dexcontrol/config/sensors/lidar/__init__.py +6 -0
  19. dexcontrol/config/sensors/lidar/rplidar.py +15 -0
  20. dexcontrol/config/sensors/ultrasonic/__init__.py +6 -0
  21. dexcontrol/config/sensors/ultrasonic/ultrasonic.py +15 -0
  22. dexcontrol/config/sensors/vega_sensors.py +65 -0
  23. dexcontrol/config/vega.py +203 -0
  24. dexcontrol/core/__init__.py +0 -0
  25. dexcontrol/core/arm.py +324 -0
  26. dexcontrol/core/chassis.py +628 -0
  27. dexcontrol/core/component.py +834 -0
  28. dexcontrol/core/hand.py +170 -0
  29. dexcontrol/core/head.py +232 -0
  30. dexcontrol/core/misc.py +514 -0
  31. dexcontrol/core/torso.py +198 -0
  32. dexcontrol/proto/dexcontrol_msg_pb2.py +69 -0
  33. dexcontrol/proto/dexcontrol_msg_pb2.pyi +168 -0
  34. dexcontrol/proto/dexcontrol_query_pb2.py +73 -0
  35. dexcontrol/proto/dexcontrol_query_pb2.pyi +134 -0
  36. dexcontrol/robot.py +1091 -0
  37. dexcontrol/sensors/__init__.py +40 -0
  38. dexcontrol/sensors/camera/__init__.py +18 -0
  39. dexcontrol/sensors/camera/gemini_camera.py +139 -0
  40. dexcontrol/sensors/camera/rgb_camera.py +98 -0
  41. dexcontrol/sensors/imu/__init__.py +22 -0
  42. dexcontrol/sensors/imu/gemini_imu.py +139 -0
  43. dexcontrol/sensors/imu/nine_axis_imu.py +149 -0
  44. dexcontrol/sensors/lidar/__init__.py +3 -0
  45. dexcontrol/sensors/lidar/rplidar.py +164 -0
  46. dexcontrol/sensors/manager.py +185 -0
  47. dexcontrol/sensors/ultrasonic.py +110 -0
  48. dexcontrol/utils/__init__.py +15 -0
  49. dexcontrol/utils/constants.py +12 -0
  50. dexcontrol/utils/io_utils.py +26 -0
  51. dexcontrol/utils/motion_utils.py +194 -0
  52. dexcontrol/utils/os_utils.py +39 -0
  53. dexcontrol/utils/pb_utils.py +103 -0
  54. dexcontrol/utils/rate_limiter.py +167 -0
  55. dexcontrol/utils/reset_orbbec_camera_usb.py +98 -0
  56. dexcontrol/utils/subscribers/__init__.py +44 -0
  57. dexcontrol/utils/subscribers/base.py +260 -0
  58. dexcontrol/utils/subscribers/camera.py +328 -0
  59. dexcontrol/utils/subscribers/decoders.py +83 -0
  60. dexcontrol/utils/subscribers/generic.py +105 -0
  61. dexcontrol/utils/subscribers/imu.py +170 -0
  62. dexcontrol/utils/subscribers/lidar.py +195 -0
  63. dexcontrol/utils/subscribers/protobuf.py +106 -0
  64. dexcontrol/utils/timer.py +136 -0
  65. dexcontrol/utils/trajectory_utils.py +40 -0
  66. dexcontrol/utils/viz_utils.py +86 -0
  67. dexcontrol-0.2.1.dist-info/METADATA +369 -0
  68. dexcontrol-0.2.1.dist-info/RECORD +72 -0
  69. dexcontrol-0.2.1.dist-info/WHEEL +5 -0
  70. dexcontrol-0.2.1.dist-info/licenses/LICENSE +188 -0
  71. dexcontrol-0.2.1.dist-info/licenses/NOTICE +13 -0
  72. 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()