dexcontrol 0.2.7__py3-none-any.whl → 0.2.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. dexcontrol/__init__.py +1 -0
  2. dexcontrol/config/core/arm.py +6 -0
  3. dexcontrol/config/core/hand.py +5 -16
  4. dexcontrol/config/core/head.py +7 -8
  5. dexcontrol/config/core/misc.py +14 -1
  6. dexcontrol/config/core/torso.py +8 -4
  7. dexcontrol/config/sensors/cameras/__init__.py +2 -1
  8. dexcontrol/config/sensors/cameras/luxonis_camera.py +51 -0
  9. dexcontrol/config/sensors/cameras/rgb_camera.py +1 -1
  10. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  11. dexcontrol/config/sensors/vega_sensors.py +9 -1
  12. dexcontrol/config/vega.py +30 -2
  13. dexcontrol/core/arm.py +95 -43
  14. dexcontrol/core/component.py +92 -6
  15. dexcontrol/core/hand.py +81 -13
  16. dexcontrol/core/head.py +55 -26
  17. dexcontrol/core/misc.py +94 -13
  18. dexcontrol/core/torso.py +44 -11
  19. dexcontrol/proto/dexcontrol_msg_pb2.py +38 -36
  20. dexcontrol/proto/dexcontrol_msg_pb2.pyi +48 -20
  21. dexcontrol/proto/dexcontrol_query_pb2.py +7 -3
  22. dexcontrol/proto/dexcontrol_query_pb2.pyi +24 -0
  23. dexcontrol/robot.py +319 -75
  24. dexcontrol/sensors/__init__.py +2 -1
  25. dexcontrol/sensors/camera/__init__.py +2 -0
  26. dexcontrol/sensors/camera/luxonis_camera.py +169 -0
  27. dexcontrol/sensors/camera/rgb_camera.py +36 -0
  28. dexcontrol/sensors/camera/zed_camera.py +48 -8
  29. dexcontrol/sensors/imu/chassis_imu.py +5 -1
  30. dexcontrol/sensors/imu/zed_imu.py +3 -2
  31. dexcontrol/sensors/lidar/rplidar.py +1 -0
  32. dexcontrol/sensors/manager.py +3 -0
  33. dexcontrol/utils/constants.py +3 -0
  34. dexcontrol/utils/error_code.py +236 -0
  35. dexcontrol/utils/subscribers/lidar.py +1 -0
  36. dexcontrol/utils/trajectory_utils.py +17 -5
  37. dexcontrol/utils/viz_utils.py +86 -11
  38. dexcontrol/utils/zenoh_utils.py +39 -0
  39. {dexcontrol-0.2.7.dist-info → dexcontrol-0.2.12.dist-info}/METADATA +6 -4
  40. dexcontrol-0.2.12.dist-info/RECORD +75 -0
  41. dexcontrol-0.2.7.dist-info/RECORD +0 -72
  42. {dexcontrol-0.2.7.dist-info → dexcontrol-0.2.12.dist-info}/WHEEL +0 -0
  43. {dexcontrol-0.2.7.dist-info → dexcontrol-0.2.12.dist-info}/licenses/LICENSE +0 -0
dexcontrol/robot.py CHANGED
@@ -27,13 +27,7 @@ import signal
27
27
  import threading
28
28
  import time
29
29
  import weakref
30
- from typing import (
31
- TYPE_CHECKING,
32
- Any,
33
- Final,
34
- Literal,
35
- cast,
36
- )
30
+ from typing import TYPE_CHECKING, Any, Final, Literal, cast
37
31
 
38
32
  import hydra.utils
39
33
  import numpy as np
@@ -58,7 +52,12 @@ from dexcontrol.utils.pb_utils import (
58
52
  )
59
53
  from dexcontrol.utils.rate_limiter import RateLimiter
60
54
  from dexcontrol.utils.trajectory_utils import generate_linear_trajectory
61
- from dexcontrol.utils.viz_utils import show_component_status, show_software_version
55
+ from dexcontrol.utils.viz_utils import (
56
+ show_component_status,
57
+ show_ntp_stats,
58
+ show_software_version,
59
+ )
60
+ from dexcontrol.utils.zenoh_utils import compute_ntp_stats
62
61
 
63
62
  if TYPE_CHECKING:
64
63
  from dexcontrol.core.arm import Arm
@@ -84,14 +83,25 @@ def _signal_handler(signum: int, frame: Any) -> None:
84
83
  logger.info(f"Received signal {signum}, shutting down all active robots...")
85
84
  # Create a list copy to avoid modification during iteration
86
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
+
87
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
+
88
96
  logger.info(f"Shutting down robot: {robot}")
89
97
  try:
90
98
  robot.shutdown()
91
99
  except Exception as e: # pylint: disable=broad-except
92
100
  logger.error(f"Error during robot shutdown: {e}", exc_info=True)
101
+
93
102
  logger.info("All robots shutdown complete")
94
- os._exit(1)
103
+ # Force exit to ensure the process terminates
104
+ os._exit(0)
95
105
 
96
106
 
97
107
  def _register_signal_handlers() -> None:
@@ -255,6 +265,23 @@ class Robot:
255
265
  self.shutdown() # Clean up on initialization failure
256
266
  raise RuntimeError(f"Failed to initialize sensors: {e}") from e
257
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
+
258
285
  # Set default modes
259
286
  try:
260
287
  self._set_default_modes()
@@ -303,10 +330,50 @@ class Robot:
303
330
  def __del__(self) -> None:
304
331
  """Destructor to ensure cleanup."""
305
332
  if not self._shutdown_called:
306
- logger.warning(
307
- "Robot instance being destroyed without explicit shutdown call"
333
+ # Only log if the logger is still available (process might be terminating)
334
+ try:
335
+ logger.warning(
336
+ "Robot instance being destroyed without explicit shutdown call"
337
+ )
338
+ self.shutdown()
339
+ except Exception: # pylint: disable=broad-except
340
+ # During interpreter shutdown, some modules might not be available
341
+ pass
342
+
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."
308
376
  )
309
- self.shutdown()
310
377
 
311
378
  def _print_initialization_info(self, robot_model: str | None) -> None:
312
379
  """Print initialization information.
@@ -339,30 +406,25 @@ class Robot:
339
406
  if component_name == "sensors":
340
407
  continue
341
408
 
342
- try:
343
- component_config = getattr(self._configs, str(component_name))
344
- if (
345
- not hasattr(component_config, "_target_")
346
- or not component_config._target_
347
- ):
348
- continue
349
-
350
- temp_config = omegaconf.OmegaConf.create(
351
- {
352
- "_target_": component_config._target_,
353
- "configs": {
354
- k: v for k, v in component_config.items() if k != "_target_"
355
- },
356
- }
357
- )
358
- component_instance = hydra.utils.instantiate(
359
- temp_config, zenoh_session=self._zenoh_session
360
- )
361
- setattr(self, str(component_name), component_instance)
362
- except Exception as e:
363
- raise RuntimeError(
364
- f"Failed to initialize component {component_name}: {e}"
365
- ) from e
409
+ component_config = getattr(self._configs, str(component_name))
410
+ if (
411
+ not hasattr(component_config, "_target_")
412
+ or not component_config._target_
413
+ ):
414
+ continue
415
+
416
+ temp_config = omegaconf.OmegaConf.create(
417
+ {
418
+ "_target_": component_config._target_,
419
+ "configs": {
420
+ k: v for k, v in component_config.items() if k != "_target_"
421
+ },
422
+ }
423
+ )
424
+ component_instance = hydra.utils.instantiate(
425
+ temp_config, zenoh_session=self._zenoh_session
426
+ )
427
+ setattr(self, str(component_name), component_instance)
366
428
 
367
429
  def _set_default_modes(self) -> None:
368
430
  """Set default control modes for robot components.
@@ -372,21 +434,13 @@ class Robot:
372
434
  """
373
435
  for arm in ["left_arm", "right_arm"]:
374
436
  if component := getattr(self, arm, None):
375
- try:
376
- component.set_mode("position")
377
- except Exception as e:
378
- raise RuntimeError(
379
- f"Failed to set default mode for {arm}: {e}"
380
- ) from e
437
+ component.set_mode("position")
381
438
 
382
439
  if head := getattr(self, "head", None):
383
- try:
384
- head.set_mode("enable")
385
- home_pos = head.get_predefined_pose("home")
386
- home_pos = self.compensate_torso_pitch(home_pos, "head")
387
- head.set_joint_pos(home_pos)
388
- except Exception as e:
389
- raise RuntimeError(f"Failed to set default mode for head: {e}") from e
440
+ head.set_mode("enable")
441
+ home_pos = head.get_predefined_pose("home")
442
+ home_pos = self.compensate_torso_pitch(home_pos, "head")
443
+ head.set_joint_pos(home_pos)
390
444
 
391
445
  def _wait_for_components(self) -> None:
392
446
  """Waits for all critical components to become active.
@@ -414,7 +468,7 @@ class Robot:
414
468
 
415
469
  console = Console()
416
470
  actives: list[bool] = []
417
- timeout_sec: Final[int] = 8
471
+ timeout_sec: Final[float] = 5.0
418
472
  check_interval: Final[float] = 0.1 # Check every 100ms
419
473
 
420
474
  status = console.status(
@@ -445,7 +499,6 @@ class Robot:
445
499
 
446
500
  # Check if we've exceeded timeout
447
501
  if time.monotonic() - start_time >= timeout_sec:
448
- console.log(f"{name} failed to become active")
449
502
  actives.append(False)
450
503
  break
451
504
 
@@ -454,13 +507,18 @@ class Robot:
454
507
  finally:
455
508
  status.stop()
456
509
 
457
- if not all(actives):
510
+ if not any(actives):
458
511
  self.shutdown()
512
+ raise RuntimeError(f"No components activated within {timeout_sec}s")
513
+
514
+ if not all(actives):
459
515
  inactive = [
460
516
  name for name, active in zip(component_names, actives) if not active
461
517
  ]
462
- raise RuntimeError(
463
- f"Components failed to activate within {timeout_sec}s: {', '.join(inactive)}"
518
+ logger.error(
519
+ f"Components failed to activate within {timeout_sec}s: {', '.join(inactive)}.\n"
520
+ f"Other components may work, but some features, e.g. collision avoidance, may not work correctly."
521
+ f"Please check the robot status immediately."
464
522
  )
465
523
 
466
524
  logger.info("All components activated successfully")
@@ -560,35 +618,75 @@ class Robot:
560
618
 
561
619
  # Enhanced Zenoh session close with better synchronization
562
620
  zenoh_close_success = False
621
+ zenoh_close_exception = None
563
622
 
564
623
  def _close_zenoh_session():
565
624
  """Close zenoh session in a separate thread."""
566
- nonlocal zenoh_close_success
625
+ nonlocal zenoh_close_success, zenoh_close_exception
567
626
  try:
568
627
  # Brief delay for Zenoh cleanup
569
628
  time.sleep(0.1)
570
629
  self._zenoh_session.close()
571
630
  zenoh_close_success = True
572
631
  except Exception as e: # pylint: disable=broad-except
632
+ zenoh_close_exception = e
573
633
  logger.warning(f"Zenoh session close error: {e}")
574
634
 
575
- # Use non-daemon thread for proper cleanup
576
- close_thread = threading.Thread(target=_close_zenoh_session, daemon=False)
635
+ # Try to close zenoh session with timeout
636
+ close_thread = threading.Thread(target=_close_zenoh_session, daemon=True)
577
637
  close_thread.start()
578
638
 
579
639
  # Wait for close with reasonable timeout
580
- close_thread.join(timeout=3.0)
640
+ close_timeout = 2.0
641
+ close_thread.join(timeout=close_timeout)
581
642
 
582
643
  if close_thread.is_alive():
583
- logger.warning(
584
- "Zenoh session close timed out after 1s, continuing shutdown"
585
- )
586
- # Thread will be left to finish in background, but won't block shutdown
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
587
646
  elif zenoh_close_success:
588
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
+ )
589
652
 
590
- # Brief final delay for cleanup
591
- time.sleep(0.5)
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)
667
+
668
+ if lingering_threads:
669
+ logger.debug(
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)
592
690
 
593
691
  logger.info("Robot shutdown complete")
594
692
 
@@ -600,6 +698,74 @@ class Robot:
600
698
  """
601
699
  return self._shutdown_called
602
700
 
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
+
603
769
  def get_software_version(
604
770
  self, show: bool = True
605
771
  ) -> dict[str, TYPE_SOFTWARE_VERSION]:
@@ -727,9 +893,7 @@ class Robot:
727
893
  query_msg = dexcontrol_query_pb2.ClearError(component=component_map[part])
728
894
  self._zenoh_session.get(
729
895
  resolve_key_name(self._configs.clear_error_query_name),
730
- handler=lambda reply: logger.info(
731
- f"Cleared error of {part}: {reply.ok}"
732
- ),
896
+ handler=lambda reply: logger.info(f"Cleared error of {part}"),
733
897
  payload=query_msg.SerializeToString(),
734
898
  )
735
899
  except Exception as e:
@@ -739,14 +903,18 @@ class Robot:
739
903
 
740
904
  def get_joint_pos_dict(
741
905
  self,
742
- component: Literal["left_arm", "right_arm", "torso", "head"]
743
- | list[Literal["left_arm", "right_arm", "torso", "head"]],
906
+ component: Literal[
907
+ "left_arm", "right_arm", "torso", "head", "left_hand", "right_hand"
908
+ ]
909
+ | list[
910
+ Literal["left_arm", "right_arm", "torso", "head", "left_hand", "right_hand"]
911
+ ],
744
912
  ) -> dict[str, float]:
745
913
  """Get the joint positions of one or more robot components.
746
914
 
747
915
  Args:
748
916
  component: Component name or list of component names to get joint positions for.
749
- Valid components are "left_arm", "right_arm", "torso", and "head".
917
+ Valid components are "left_arm", "right_arm", "torso", "head", "left_hand", and "right_hand".
750
918
 
751
919
  Returns:
752
920
  Dictionary mapping joint names to joint positions.
@@ -761,6 +929,8 @@ class Robot:
761
929
  "right_arm": self.right_arm,
762
930
  "torso": self.torso,
763
931
  "head": self.head,
932
+ "left_hand": self.left_hand,
933
+ "right_hand": self.right_hand,
764
934
  }
765
935
 
766
936
  try:
@@ -826,6 +996,8 @@ class Robot:
826
996
  relative: bool = False,
827
997
  wait_time: float = 0.0,
828
998
  wait_kwargs: dict[str, Any] | None = None,
999
+ exit_on_reach: bool = False,
1000
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
829
1001
  ) -> None:
830
1002
  """Set the joint positions of the robot.
831
1003
 
@@ -836,7 +1008,9 @@ class Robot:
836
1008
  wait_time: Time to wait for movement completion in seconds.
837
1009
  wait_kwargs: Additional parameters for trajectory generation.
838
1010
  control_hz: Control frequency in Hz (default: 100).
839
- max_vel: Maximum velocity for trajectory (default: 0.5).
1011
+ max_vel: Maximum velocity for trajectory (default: 3.).
1012
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
1013
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
840
1014
 
841
1015
  Raises:
842
1016
  ValueError: If any component name is invalid.
@@ -846,6 +1020,7 @@ class Robot:
846
1020
  wait_kwargs = {}
847
1021
 
848
1022
  try:
1023
+ start_time = time.time()
849
1024
  component_map = self._get_component_map()
850
1025
 
851
1026
  # Validate component names
@@ -871,11 +1046,69 @@ class Robot:
871
1046
  relative,
872
1047
  wait_time,
873
1048
  wait_kwargs,
1049
+ exit_on_reach=exit_on_reach,
1050
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
874
1051
  )
1052
+ remaining_time = wait_time - (time.time() - start_time)
1053
+ if remaining_time <= 0:
1054
+ return
1055
+
1056
+ self._wait_for_multi_component_positions(
1057
+ component_map,
1058
+ pv_components,
1059
+ joint_pos,
1060
+ start_time,
1061
+ wait_time,
1062
+ exit_on_reach,
1063
+ exit_on_reach_kwargs,
1064
+ )
875
1065
 
876
1066
  except Exception as e:
877
1067
  raise RuntimeError(f"Failed to set joint positions: {e}") from e
878
1068
 
1069
+ def _wait_for_multi_component_positions(
1070
+ self,
1071
+ component_map: dict[str, Any],
1072
+ components: list[str],
1073
+ joint_pos: dict[str, list[float] | np.ndarray],
1074
+ start_time: float,
1075
+ wait_time: float,
1076
+ exit_on_reach: bool = False,
1077
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
1078
+ ) -> None:
1079
+ """Wait for multiple components to reach target positions.
1080
+
1081
+ Args:
1082
+ component_map: Mapping of component names to component instances.
1083
+ components: List of component names to check.
1084
+ joint_pos: Target joint positions for each component.
1085
+ start_time: Time when the operation started.
1086
+ wait_time: Maximum time to wait.
1087
+ exit_on_reach: If True, exit early when all positions are reached.
1088
+ exit_on_reach_kwargs: Optional parameters for position checking.
1089
+ """
1090
+ sleep_interval = 0.01 # Use consistent sleep interval
1091
+
1092
+ if exit_on_reach:
1093
+ if components:
1094
+ # Set default tolerance if not provided
1095
+ exit_on_reach_kwargs = exit_on_reach_kwargs or {}
1096
+
1097
+ # Wait until all positions are reached or timeout
1098
+ while time.time() - start_time < wait_time:
1099
+ if all(
1100
+ component_map[c].is_joint_pos_reached(
1101
+ joint_pos[c], **exit_on_reach_kwargs
1102
+ )
1103
+ for c in components
1104
+ ):
1105
+ break
1106
+ time.sleep(sleep_interval)
1107
+ else:
1108
+ # Simple wait without position checking
1109
+ while time.time() - start_time < wait_time:
1110
+ time.sleep(sleep_interval)
1111
+
879
1112
  def compensate_torso_pitch(self, joint_pos: np.ndarray, part: str) -> np.ndarray:
880
1113
  """Compensate for torso pitch in joint positions.
881
1114
 
@@ -1092,6 +1325,8 @@ class Robot:
1092
1325
  relative: bool,
1093
1326
  wait_time: float,
1094
1327
  wait_kwargs: dict[str, Any],
1328
+ exit_on_reach: bool = False,
1329
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
1095
1330
  ) -> None:
1096
1331
  """Set non-PV components with smooth trajectory over wait_time.
1097
1332
 
@@ -1102,9 +1337,11 @@ class Robot:
1102
1337
  relative: Whether positions are relative.
1103
1338
  wait_time: Time to wait for movement completion.
1104
1339
  wait_kwargs: Additional trajectory parameters.
1340
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
1341
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
1105
1342
  """
1106
- control_hz = wait_kwargs.get("control_hz", 100)
1107
- max_vel = wait_kwargs.get("max_vel", 0.5)
1343
+ control_hz = wait_kwargs.get("control_hz", self.left_arm._default_control_hz)
1344
+ max_vel = wait_kwargs.get("max_vel", self.left_arm._joint_vel_limit)
1108
1345
 
1109
1346
  # Generate trajectories for smooth motion during wait_time
1110
1347
  rate_limiter = RateLimiter(control_hz)
@@ -1136,5 +1373,12 @@ class Robot:
1136
1373
  break
1137
1374
 
1138
1375
  # Wait for any remaining time
1139
- while time.time() - start_time < wait_time:
1140
- rate_limiter.sleep()
1376
+ self._wait_for_multi_component_positions(
1377
+ component_map,
1378
+ non_pv_components,
1379
+ joint_pos,
1380
+ start_time,
1381
+ wait_time,
1382
+ exit_on_reach,
1383
+ exit_on_reach_kwargs,
1384
+ )
@@ -15,7 +15,7 @@ using Zenoh subscribers for data communication.
15
15
  """
16
16
 
17
17
  # Import camera sensors
18
- from .camera import RGBCameraSensor, ZedCameraSensor
18
+ from .camera import LuxonisCameraSensor, RGBCameraSensor, ZedCameraSensor
19
19
 
20
20
  # Import IMU sensors
21
21
  from .imu import ChassisIMUSensor, ZedIMUSensor
@@ -31,6 +31,7 @@ __all__ = [
31
31
  # Camera sensors
32
32
  "RGBCameraSensor",
33
33
  "ZedCameraSensor",
34
+ "LuxonisCameraSensor",
34
35
 
35
36
  # IMU sensors
36
37
  "ChassisIMUSensor",
@@ -14,10 +14,12 @@ This module provides camera sensor classes that use the specialized camera
14
14
  subscribers for RGB and RGBD camera data, matching the dexsensor structure.
15
15
  """
16
16
 
17
+ from .luxonis_camera import LuxonisCameraSensor
17
18
  from .rgb_camera import RGBCameraSensor
18
19
  from .zed_camera import ZedCameraSensor
19
20
 
20
21
  __all__ = [
21
22
  "RGBCameraSensor",
22
23
  "ZedCameraSensor",
24
+ "LuxonisCameraSensor",
23
25
  ]