dexcontrol 0.2.4__py3-none-any.whl → 0.2.10__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.

@@ -25,7 +25,7 @@ _sym_db = _symbol_database.Default()
25
25
 
26
26
 
27
27
 
28
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x64\x65xcontrol_query.proto\x12\ndexcontrol\"Z\n\nSetArmMode\x12)\n\x04mode\x18\x01 \x01(\x0e\x32\x1b.dexcontrol.SetArmMode.Mode\"!\n\x04Mode\x12\x0c\n\x08POSITION\x10\x00\x12\x0b\n\x07\x44ISABLE\x10\x01\"Z\n\x0bSetHeadMode\x12*\n\x04mode\x18\x01 \x01(\x0e\x32\x1c.dexcontrol.SetHeadMode.Mode\"\x1f\n\x04Mode\x12\n\n\x06\x45NABLE\x10\x00\x12\x0b\n\x07\x44ISABLE\x10\x01\"\x1a\n\x08SetEstop\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"\x18\n\x06SetLed\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"\x82\x01\n\nClearError\x12\x33\n\tcomponent\x18\x01 \x01(\x0e\x32 .dexcontrol.ClearError.Component\"?\n\tComponent\x12\x0c\n\x08LEFT_ARM\x10\x00\x12\r\n\tRIGHT_ARM\x10\x01\x12\x08\n\x04HEAD\x10\x02\x12\x0b\n\x07\x43HASSIS\x10\x03\"y\n\x0fRebootComponent\x12\x38\n\tcomponent\x18\x01 \x01(\x0e\x32%.dexcontrol.RebootComponent.Component\",\n\tComponent\x12\x07\n\x03\x41RM\x10\x00\x12\t\n\x05TORSO\x10\x01\x12\x0b\n\x07\x43HASSIS\x10\x02\"X\n\x0f\x46irmwareVersion\x12\x18\n\x10hardware_version\x18\x01 \x01(\x05\x12\x18\n\x10software_version\x18\x02 \x01(\x05\x12\x11\n\tmain_hash\x18\x03 \x01(\t\"\xb2\x01\n\x0fSoftwareVersion\x12J\n\x10\x66irmware_version\x18\x01 \x03(\x0b\x32\x30.dexcontrol.SoftwareVersion.FirmwareVersionEntry\x1aS\n\x14\x46irmwareVersionEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.dexcontrol.FirmwareVersion:\x02\x38\x01\"\x9d\x01\n\x14SingleComponentState\x12\x11\n\tconnected\x18\x01 \x01(\x08\x12,\n\x07\x65nabled\x18\x02 \x01(\x0e\x32\x1b.dexcontrol.ComponentStatus\x12\x30\n\x0b\x65rror_state\x18\x03 \x01(\x0e\x32\x1b.dexcontrol.ComponentStatus\x12\x12\n\nerror_code\x18\x04 \x01(\t\"\x9b\x01\n\x0f\x43omponentStates\x12\x37\n\x06states\x18\x01 \x03(\x0b\x32\'.dexcontrol.ComponentStates.StatesEntry\x1aO\n\x0bStatesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12/\n\x05value\x18\x02 \x01(\x0b\x32 .dexcontrol.SingleComponentState:\x02\x38\x01*0\n\x0f\x43omponentStatus\x12\n\n\x06NORMAL\x10\x00\x12\x06\n\x02NA\x10\x01\x12\t\n\x05\x45RROR\x10\x02\x62\x06proto3')
28
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x64\x65xcontrol_query.proto\x12\ndexcontrol\"Z\n\nSetArmMode\x12)\n\x04mode\x18\x01 \x01(\x0e\x32\x1b.dexcontrol.SetArmMode.Mode\"!\n\x04Mode\x12\x0c\n\x08POSITION\x10\x00\x12\x0b\n\x07\x44ISABLE\x10\x01\"Z\n\x0bSetHeadMode\x12*\n\x04mode\x18\x01 \x01(\x0e\x32\x1c.dexcontrol.SetHeadMode.Mode\"\x1f\n\x04Mode\x12\n\n\x06\x45NABLE\x10\x00\x12\x0b\n\x07\x44ISABLE\x10\x01\"\x1a\n\x08SetEstop\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"\x18\n\x06SetLed\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"\x82\x01\n\nClearError\x12\x33\n\tcomponent\x18\x01 \x01(\x0e\x32 .dexcontrol.ClearError.Component\"?\n\tComponent\x12\x0c\n\x08LEFT_ARM\x10\x00\x12\r\n\tRIGHT_ARM\x10\x01\x12\x08\n\x04HEAD\x10\x02\x12\x0b\n\x07\x43HASSIS\x10\x03\"y\n\x0fRebootComponent\x12\x38\n\tcomponent\x18\x01 \x01(\x0e\x32%.dexcontrol.RebootComponent.Component\",\n\tComponent\x12\x07\n\x03\x41RM\x10\x00\x12\t\n\x05TORSO\x10\x01\x12\x0b\n\x07\x43HASSIS\x10\x02\"\x80\x01\n\x0f\x46irmwareVersion\x12\x18\n\x10hardware_version\x18\x01 \x01(\x05\x12\x18\n\x10software_version\x18\x02 \x01(\x05\x12\x14\n\x0c\x63ompile_time\x18\x03 \x01(\x05\x12\x11\n\tmain_hash\x18\x04 \x01(\t\x12\x10\n\x08sub_hash\x18\x05 \x01(\t\"\xb2\x01\n\x0fSoftwareVersion\x12J\n\x10\x66irmware_version\x18\x01 \x03(\x0b\x32\x30.dexcontrol.SoftwareVersion.FirmwareVersionEntry\x1aS\n\x14\x46irmwareVersionEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.dexcontrol.FirmwareVersion:\x02\x38\x01\"\x9d\x01\n\x14SingleComponentState\x12\x11\n\tconnected\x18\x01 \x01(\x08\x12,\n\x07\x65nabled\x18\x02 \x01(\x0e\x32\x1b.dexcontrol.ComponentStatus\x12\x30\n\x0b\x65rror_state\x18\x03 \x01(\x0e\x32\x1b.dexcontrol.ComponentStatus\x12\x12\n\nerror_code\x18\x04 \x01(\t\"\x9b\x01\n\x0f\x43omponentStates\x12\x37\n\x06states\x18\x01 \x03(\x0b\x32\'.dexcontrol.ComponentStates.StatesEntry\x1aO\n\x0bStatesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12/\n\x05value\x18\x02 \x01(\x0b\x32 .dexcontrol.SingleComponentState:\x02\x38\x01*0\n\x0f\x43omponentStatus\x12\n\n\x06NORMAL\x10\x00\x12\x06\n\x02NA\x10\x01\x12\t\n\x05\x45RROR\x10\x02\x62\x06proto3')
29
29
 
30
30
  _globals = globals()
31
31
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -36,8 +36,8 @@ if not _descriptor._USE_C_DESCRIPTORS:
36
36
  _globals['_SOFTWAREVERSION_FIRMWAREVERSIONENTRY']._serialized_options = b'8\001'
37
37
  _globals['_COMPONENTSTATES_STATESENTRY']._loaded_options = None
38
38
  _globals['_COMPONENTSTATES_STATESENTRY']._serialized_options = b'8\001'
39
- _globals['_COMPONENTSTATUS']._serialized_start=1121
40
- _globals['_COMPONENTSTATUS']._serialized_end=1169
39
+ _globals['_COMPONENTSTATUS']._serialized_start=1162
40
+ _globals['_COMPONENTSTATUS']._serialized_end=1210
41
41
  _globals['_SETARMMODE']._serialized_start=38
42
42
  _globals['_SETARMMODE']._serialized_end=128
43
43
  _globals['_SETARMMODE_MODE']._serialized_start=95
@@ -58,16 +58,16 @@ if not _descriptor._USE_C_DESCRIPTORS:
58
58
  _globals['_REBOOTCOMPONENT']._serialized_end=530
59
59
  _globals['_REBOOTCOMPONENT_COMPONENT']._serialized_start=486
60
60
  _globals['_REBOOTCOMPONENT_COMPONENT']._serialized_end=530
61
- _globals['_FIRMWAREVERSION']._serialized_start=532
62
- _globals['_FIRMWAREVERSION']._serialized_end=620
63
- _globals['_SOFTWAREVERSION']._serialized_start=623
64
- _globals['_SOFTWAREVERSION']._serialized_end=801
65
- _globals['_SOFTWAREVERSION_FIRMWAREVERSIONENTRY']._serialized_start=718
66
- _globals['_SOFTWAREVERSION_FIRMWAREVERSIONENTRY']._serialized_end=801
67
- _globals['_SINGLECOMPONENTSTATE']._serialized_start=804
68
- _globals['_SINGLECOMPONENTSTATE']._serialized_end=961
69
- _globals['_COMPONENTSTATES']._serialized_start=964
70
- _globals['_COMPONENTSTATES']._serialized_end=1119
71
- _globals['_COMPONENTSTATES_STATESENTRY']._serialized_start=1040
72
- _globals['_COMPONENTSTATES_STATESENTRY']._serialized_end=1119
61
+ _globals['_FIRMWAREVERSION']._serialized_start=533
62
+ _globals['_FIRMWAREVERSION']._serialized_end=661
63
+ _globals['_SOFTWAREVERSION']._serialized_start=664
64
+ _globals['_SOFTWAREVERSION']._serialized_end=842
65
+ _globals['_SOFTWAREVERSION_FIRMWAREVERSIONENTRY']._serialized_start=759
66
+ _globals['_SOFTWAREVERSION_FIRMWAREVERSIONENTRY']._serialized_end=842
67
+ _globals['_SINGLECOMPONENTSTATE']._serialized_start=845
68
+ _globals['_SINGLECOMPONENTSTATE']._serialized_end=1002
69
+ _globals['_COMPONENTSTATES']._serialized_start=1005
70
+ _globals['_COMPONENTSTATES']._serialized_end=1160
71
+ _globals['_COMPONENTSTATES_STATESENTRY']._serialized_start=1081
72
+ _globals['_COMPONENTSTATES_STATESENTRY']._serialized_end=1160
73
73
  # @@protoc_insertion_point(module_scope)
@@ -86,14 +86,18 @@ class RebootComponent(_message.Message):
86
86
  def __init__(self, component: _Optional[_Union[RebootComponent.Component, str]] = ...) -> None: ...
87
87
 
88
88
  class FirmwareVersion(_message.Message):
89
- __slots__ = ("hardware_version", "software_version", "main_hash")
89
+ __slots__ = ("hardware_version", "software_version", "compile_time", "main_hash", "sub_hash")
90
90
  HARDWARE_VERSION_FIELD_NUMBER: _ClassVar[int]
91
91
  SOFTWARE_VERSION_FIELD_NUMBER: _ClassVar[int]
92
+ COMPILE_TIME_FIELD_NUMBER: _ClassVar[int]
92
93
  MAIN_HASH_FIELD_NUMBER: _ClassVar[int]
94
+ SUB_HASH_FIELD_NUMBER: _ClassVar[int]
93
95
  hardware_version: int
94
96
  software_version: int
97
+ compile_time: int
95
98
  main_hash: str
96
- def __init__(self, hardware_version: _Optional[int] = ..., software_version: _Optional[int] = ..., main_hash: _Optional[str] = ...) -> None: ...
99
+ sub_hash: str
100
+ def __init__(self, hardware_version: _Optional[int] = ..., software_version: _Optional[int] = ..., compile_time: _Optional[int] = ..., main_hash: _Optional[str] = ..., sub_hash: _Optional[str] = ...) -> None: ...
97
101
 
98
102
  class SoftwareVersion(_message.Message):
99
103
  __slots__ = ("firmware_version",)
dexcontrol/robot.py CHANGED
@@ -51,6 +51,7 @@ from dexcontrol.sensors import Sensors
51
51
  from dexcontrol.utils.constants import ROBOT_NAME_ENV_VAR
52
52
  from dexcontrol.utils.os_utils import get_robot_model, resolve_key_name
53
53
  from dexcontrol.utils.pb_utils import (
54
+ TYPE_SOFTWARE_VERSION,
54
55
  ComponentStatus,
55
56
  software_version_to_dict,
56
57
  status_to_dict,
@@ -416,9 +417,12 @@ class Robot:
416
417
  timeout_sec: Final[int] = 8
417
418
  check_interval: Final[float] = 0.1 # Check every 100ms
418
419
 
419
- with console.status(
420
+ status = console.status(
420
421
  "[bold green]Waiting for components to become active..."
421
- ) as status:
422
+ )
423
+ status.start()
424
+
425
+ try:
422
426
  for name in component_names:
423
427
  # Check if shutdown was triggered
424
428
  if self._shutdown_called:
@@ -447,6 +451,8 @@ class Robot:
447
451
 
448
452
  # Wait a short interval before checking again
449
453
  time.sleep(check_interval)
454
+ finally:
455
+ status.stop()
450
456
 
451
457
  if not all(actives):
452
458
  self.shutdown()
@@ -527,34 +533,30 @@ class Robot:
527
533
  f"Error stopping component {component.__class__.__name__}: {e}"
528
534
  )
529
535
 
530
- # Longer delay to ensure all stop operations complete and background threads settle
531
- time.sleep(0.3)
536
+ # Brief delay to ensure all stop operations complete
537
+ time.sleep(0.1)
532
538
 
533
539
  # Shutdown sensors first (they may have background threads)
534
540
  try:
535
- self.sensors.shutdown()
536
- # Give extra time for all sensor subscribers to undeclare cleanly
537
- time.sleep(0.5)
541
+ if hasattr(self, "sensors") and self.sensors is not None:
542
+ self.sensors.shutdown()
543
+ # Give time for sensor subscribers to undeclare cleanly
544
+ time.sleep(0.2)
538
545
  except Exception as e: # pylint: disable=broad-except
539
546
  logger.error(f"Error shutting down sensors: {e}")
540
547
 
541
- # Additional delay for sensor shutdown to complete
542
- time.sleep(0.2)
543
-
544
548
  # Shutdown components in reverse order
545
549
  for component in reversed(self._components):
546
550
  if component is not None:
547
551
  try:
548
552
  component.shutdown()
549
- # Small delay between each component shutdown to prevent race conditions
550
- time.sleep(0.05)
551
553
  except Exception as e: # pylint: disable=broad-except
552
554
  logger.error(
553
555
  f"Error shutting down component {component.__class__.__name__}: {e}"
554
556
  )
555
557
 
556
- # Longer delay to allow all component shutdown to complete
557
- time.sleep(0.5)
558
+ # Brief delay to allow component shutdown to complete
559
+ time.sleep(0.1)
558
560
 
559
561
  # Enhanced Zenoh session close with better synchronization
560
562
  zenoh_close_success = False
@@ -563,8 +565,8 @@ class Robot:
563
565
  """Close zenoh session in a separate thread."""
564
566
  nonlocal zenoh_close_success
565
567
  try:
566
- # Give Zenoh even more time to clean up internal resources and settle threads
567
- time.sleep(0.5)
568
+ # Brief delay for Zenoh cleanup
569
+ time.sleep(0.1)
568
570
  self._zenoh_session.close()
569
571
  zenoh_close_success = True
570
572
  except Exception as e: # pylint: disable=broad-except
@@ -574,20 +576,19 @@ class Robot:
574
576
  close_thread = threading.Thread(target=_close_zenoh_session, daemon=False)
575
577
  close_thread.start()
576
578
 
577
- # Wait for close with extended timeout to avoid race conditions
579
+ # Wait for close with reasonable timeout
578
580
  close_thread.join(timeout=3.0)
579
581
 
580
582
  if close_thread.is_alive():
581
583
  logger.warning(
582
- "Zenoh session close timed out after 5s, continuing shutdown"
584
+ "Zenoh session close timed out after 1s, continuing shutdown"
583
585
  )
584
- # Force daemon mode so it doesn't block process exit
585
- close_thread.daemon = True
586
+ # Thread will be left to finish in background, but won't block shutdown
586
587
  elif zenoh_close_success:
587
588
  logger.debug("Zenoh session closed cleanly")
588
589
 
589
- # Extended final delay to allow any remaining Zenoh background threads to finish
590
- time.sleep(0.8)
590
+ # Brief final delay for cleanup
591
+ time.sleep(0.5)
591
592
 
592
593
  logger.info("Robot shutdown complete")
593
594
 
@@ -601,13 +602,7 @@ class Robot:
601
602
 
602
603
  def get_software_version(
603
604
  self, show: bool = True
604
- ) -> dict[
605
- str,
606
- dict[
607
- Literal["hardware_version", "software_version", "main_hash"],
608
- int | str,
609
- ],
610
- ]:
605
+ ) -> dict[str, TYPE_SOFTWARE_VERSION]:
611
606
  """Retrieve software version information for all components.
612
607
 
613
608
  Args:
@@ -744,14 +739,18 @@ class Robot:
744
739
 
745
740
  def get_joint_pos_dict(
746
741
  self,
747
- component: Literal["left_arm", "right_arm", "torso", "head"]
748
- | list[Literal["left_arm", "right_arm", "torso", "head"]],
742
+ component: Literal[
743
+ "left_arm", "right_arm", "torso", "head", "left_hand", "right_hand"
744
+ ]
745
+ | list[
746
+ Literal["left_arm", "right_arm", "torso", "head", "left_hand", "right_hand"]
747
+ ],
749
748
  ) -> dict[str, float]:
750
749
  """Get the joint positions of one or more robot components.
751
750
 
752
751
  Args:
753
752
  component: Component name or list of component names to get joint positions for.
754
- Valid components are "left_arm", "right_arm", "torso", and "head".
753
+ Valid components are "left_arm", "right_arm", "torso", "head", "left_hand", and "right_hand".
755
754
 
756
755
  Returns:
757
756
  Dictionary mapping joint names to joint positions.
@@ -766,6 +765,8 @@ class Robot:
766
765
  "right_arm": self.right_arm,
767
766
  "torso": self.torso,
768
767
  "head": self.head,
768
+ "left_hand": self.left_hand,
769
+ "right_hand": self.right_hand,
769
770
  }
770
771
 
771
772
  try:
@@ -831,6 +832,8 @@ class Robot:
831
832
  relative: bool = False,
832
833
  wait_time: float = 0.0,
833
834
  wait_kwargs: dict[str, Any] | None = None,
835
+ exit_on_reach: bool = False,
836
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
834
837
  ) -> None:
835
838
  """Set the joint positions of the robot.
836
839
 
@@ -841,7 +844,9 @@ class Robot:
841
844
  wait_time: Time to wait for movement completion in seconds.
842
845
  wait_kwargs: Additional parameters for trajectory generation.
843
846
  control_hz: Control frequency in Hz (default: 100).
844
- max_vel: Maximum velocity for trajectory (default: 0.5).
847
+ max_vel: Maximum velocity for trajectory (default: 3.).
848
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
849
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
845
850
 
846
851
  Raises:
847
852
  ValueError: If any component name is invalid.
@@ -851,6 +856,7 @@ class Robot:
851
856
  wait_kwargs = {}
852
857
 
853
858
  try:
859
+ start_time = time.time()
854
860
  component_map = self._get_component_map()
855
861
 
856
862
  # Validate component names
@@ -876,11 +882,69 @@ class Robot:
876
882
  relative,
877
883
  wait_time,
878
884
  wait_kwargs,
885
+ exit_on_reach=exit_on_reach,
886
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
879
887
  )
888
+ remaining_time = wait_time - (time.time() - start_time)
889
+ if remaining_time <= 0:
890
+ return
891
+
892
+ self._wait_for_multi_component_positions(
893
+ component_map,
894
+ pv_components,
895
+ joint_pos,
896
+ start_time,
897
+ wait_time,
898
+ exit_on_reach,
899
+ exit_on_reach_kwargs,
900
+ )
880
901
 
881
902
  except Exception as e:
882
903
  raise RuntimeError(f"Failed to set joint positions: {e}") from e
883
904
 
905
+ def _wait_for_multi_component_positions(
906
+ self,
907
+ component_map: dict[str, Any],
908
+ components: list[str],
909
+ joint_pos: dict[str, list[float] | np.ndarray],
910
+ start_time: float,
911
+ wait_time: float,
912
+ exit_on_reach: bool = False,
913
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
914
+ ) -> None:
915
+ """Wait for multiple components to reach target positions.
916
+
917
+ Args:
918
+ component_map: Mapping of component names to component instances.
919
+ components: List of component names to check.
920
+ joint_pos: Target joint positions for each component.
921
+ start_time: Time when the operation started.
922
+ wait_time: Maximum time to wait.
923
+ exit_on_reach: If True, exit early when all positions are reached.
924
+ exit_on_reach_kwargs: Optional parameters for position checking.
925
+ """
926
+ sleep_interval = 0.01 # Use consistent sleep interval
927
+
928
+ if exit_on_reach:
929
+ if components:
930
+ # Set default tolerance if not provided
931
+ exit_on_reach_kwargs = exit_on_reach_kwargs or {}
932
+
933
+ # Wait until all positions are reached or timeout
934
+ while time.time() - start_time < wait_time:
935
+ if all(
936
+ component_map[c].is_joint_pos_reached(
937
+ joint_pos[c], **exit_on_reach_kwargs
938
+ )
939
+ for c in components
940
+ ):
941
+ break
942
+ time.sleep(sleep_interval)
943
+ else:
944
+ # Simple wait without position checking
945
+ while time.time() - start_time < wait_time:
946
+ time.sleep(sleep_interval)
947
+
884
948
  def compensate_torso_pitch(self, joint_pos: np.ndarray, part: str) -> np.ndarray:
885
949
  """Compensate for torso pitch in joint positions.
886
950
 
@@ -1097,6 +1161,8 @@ class Robot:
1097
1161
  relative: bool,
1098
1162
  wait_time: float,
1099
1163
  wait_kwargs: dict[str, Any],
1164
+ exit_on_reach: bool = False,
1165
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
1100
1166
  ) -> None:
1101
1167
  """Set non-PV components with smooth trajectory over wait_time.
1102
1168
 
@@ -1107,9 +1173,11 @@ class Robot:
1107
1173
  relative: Whether positions are relative.
1108
1174
  wait_time: Time to wait for movement completion.
1109
1175
  wait_kwargs: Additional trajectory parameters.
1176
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
1177
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
1110
1178
  """
1111
- control_hz = wait_kwargs.get("control_hz", 100)
1112
- max_vel = wait_kwargs.get("max_vel", 0.5)
1179
+ control_hz = wait_kwargs.get("control_hz", self.left_arm._default_control_hz)
1180
+ max_vel = wait_kwargs.get("max_vel", self.left_arm._default_max_vel)
1113
1181
 
1114
1182
  # Generate trajectories for smooth motion during wait_time
1115
1183
  rate_limiter = RateLimiter(control_hz)
@@ -1141,5 +1209,12 @@ class Robot:
1141
1209
  break
1142
1210
 
1143
1211
  # Wait for any remaining time
1144
- while time.time() - start_time < wait_time:
1145
- rate_limiter.sleep()
1212
+ self._wait_for_multi_component_positions(
1213
+ component_map,
1214
+ non_pv_components,
1215
+ joint_pos,
1216
+ start_time,
1217
+ wait_time,
1218
+ exit_on_reach,
1219
+ exit_on_reach_kwargs,
1220
+ )
@@ -8,64 +8,94 @@
8
8
  # 2. Commercial License
9
9
  # For commercial licensing terms, contact: contact@dexmate.ai
10
10
 
11
- """RGB camera sensor implementation using RTC subscriber with Zenoh query."""
11
+ """RGB camera sensor implementation using RTC or Zenoh subscriber."""
12
12
 
13
13
  import logging
14
+ from typing import Optional, Union
14
15
 
15
16
  import numpy as np
16
17
  import zenoh
17
18
 
19
+ from dexcontrol.config.sensors.cameras import RGBCameraConfig
18
20
  from dexcontrol.utils.rtc_utils import create_rtc_subscriber_with_config
21
+ from dexcontrol.utils.subscribers.camera import RGBCameraSubscriber
22
+ from dexcontrol.utils.subscribers.rtc import RTCSubscriber
19
23
 
20
24
  logger = logging.getLogger(__name__)
21
25
 
22
26
 
23
27
  class RGBCameraSensor:
24
- """RGB camera sensor using RTC subscriber.
28
+ """RGB camera sensor that supports both RTC and standard Zenoh subscribers.
25
29
 
26
- This sensor provides RGB image data from a camera using RTC.
27
- It first queries Zenoh for RTC connection information, then creates
28
- a RTC subscriber for efficient data handling.
30
+ This sensor provides RGB image data from a camera. It can be configured to use
31
+ either a high-performance RTC subscriber for real-time video streams or a
32
+ standard Zenoh subscriber for raw image topics. The mode is controlled by the
33
+ `use_rtc` flag in the `RGBCameraConfig`.
29
34
  """
30
35
 
31
36
  def __init__(
32
37
  self,
33
- configs,
38
+ configs: RGBCameraConfig,
34
39
  zenoh_session: zenoh.Session,
35
- *args,
36
- **kwargs,
37
40
  ) -> None:
38
- """Initialize the RGB camera sensor.
41
+ """Initialize the RGB camera sensor based on the provided configuration.
39
42
 
40
43
  Args:
41
- configs: Configuration for the RGB camera sensor.
44
+ configs: Configuration object for the RGB camera sensor.
42
45
  zenoh_session: Active Zenoh session for communication.
43
46
  """
44
47
  self._name = configs.name
45
-
46
- # Create the RTC subscriber using the utility function
47
- self._subscriber = create_rtc_subscriber_with_config(
48
- zenoh_session=zenoh_session,
49
- config=configs.subscriber_config.rgb,
50
- name=f"{self._name}_subscriber",
51
- enable_fps_tracking=configs.enable_fps_tracking,
52
- fps_log_interval=configs.fps_log_interval,
53
- )
54
-
55
- if self._subscriber is None:
56
- logger.warning(f"Failed to create RTC subscriber for {self._name}")
57
- # Continue initialization without subscriber
48
+ self._subscriber: Optional[Union[RTCSubscriber, RGBCameraSubscriber]] = None
49
+
50
+ subscriber_config = configs.subscriber_config.get("rgb", {})
51
+ if not subscriber_config or not subscriber_config.get("enable", False):
52
+ logger.info(f"RGBCameraSensor '{self._name}' is disabled in config.")
53
+ return
54
+
55
+ try:
56
+ if configs.use_rtc:
57
+ logger.info(f"'{self._name}': Using RTC subscriber.")
58
+ self._subscriber = create_rtc_subscriber_with_config(
59
+ zenoh_session=zenoh_session,
60
+ config=subscriber_config,
61
+ name=f"{self._name}_subscriber",
62
+ enable_fps_tracking=configs.enable_fps_tracking,
63
+ fps_log_interval=configs.fps_log_interval,
64
+ )
65
+ else:
66
+ logger.info(f"'{self._name}': Using standard Zenoh subscriber.")
67
+ topic = subscriber_config.get("topic")
68
+ if topic:
69
+ self._subscriber = RGBCameraSubscriber(
70
+ topic=topic,
71
+ zenoh_session=zenoh_session,
72
+ name=f"{self._name}_subscriber",
73
+ enable_fps_tracking=configs.enable_fps_tracking,
74
+ fps_log_interval=configs.fps_log_interval,
75
+ )
76
+ else:
77
+ logger.warning(
78
+ f"No 'topic' specified for '{self._name}' in non-RTC mode."
79
+ )
80
+
81
+ if self._subscriber is None:
82
+ logger.warning(f"Failed to create subscriber for '{self._name}'.")
83
+
84
+ except Exception as e:
85
+ logger.error(f"Error creating subscriber for '{self._name}': {e}")
86
+ self._subscriber = None
58
87
 
59
88
  def shutdown(self) -> None:
60
- """Shutdown the camera sensor."""
89
+ """Shutdown the camera sensor and release all resources."""
61
90
  if self._subscriber:
62
91
  self._subscriber.shutdown()
92
+ logger.info(f"'{self._name}' sensor shut down.")
63
93
 
64
94
  def is_active(self) -> bool:
65
95
  """Check if the camera sensor is actively receiving data.
66
96
 
67
97
  Returns:
68
- True if receiving data, False otherwise.
98
+ True if the subscriber exists and is receiving data, False otherwise.
69
99
  """
70
100
  return self._subscriber.is_active() if self._subscriber else False
71
101
 
@@ -76,15 +106,18 @@ class RGBCameraSensor:
76
106
  timeout: Maximum time to wait in seconds.
77
107
 
78
108
  Returns:
79
- True if sensor becomes active, False if timeout is reached.
109
+ True if the sensor becomes active within the timeout, False otherwise.
80
110
  """
81
- return self._subscriber.wait_for_active(timeout) if self._subscriber else False
111
+ if not self._subscriber:
112
+ logger.warning(f"'{self._name}': Cannot wait, no subscriber initialized.")
113
+ return False
114
+ return self._subscriber.wait_for_active(timeout)
82
115
 
83
116
  def get_obs(self) -> np.ndarray | None:
84
- """Get the latest RGB image data.
117
+ """Get the latest observation (RGB image) from the sensor.
85
118
 
86
119
  Returns:
87
- Latest RGB image as numpy array (HxWxC) if available, None otherwise.
120
+ The latest RGB image as a numpy array (HxWxC) if available, otherwise None.
88
121
  """
89
122
  return self._subscriber.get_latest_data() if self._subscriber else None
90
123
 
@@ -105,3 +138,39 @@ class RGBCameraSensor:
105
138
  Sensor name string.
106
139
  """
107
140
  return self._name
141
+
142
+ @property
143
+ def height(self) -> int:
144
+ """Get the height of the camera image.
145
+
146
+ Returns:
147
+ Height of the camera image.
148
+ """
149
+ image = self.get_obs()
150
+ if image is None:
151
+ return 0
152
+ return image.shape[0]
153
+
154
+ @property
155
+ def width(self) -> int:
156
+ """Get the width of the camera image.
157
+
158
+ Returns:
159
+ Width of the camera image.
160
+ """
161
+ image = self.get_obs()
162
+ if image is None:
163
+ return 0
164
+ return image.shape[1]
165
+
166
+ @property
167
+ def resolution(self) -> tuple[int, int]:
168
+ """Get the resolution of the camera image.
169
+
170
+ Returns:
171
+ Resolution of the camera image.
172
+ """
173
+ image = self.get_obs()
174
+ if image is None:
175
+ return 0, 0
176
+ return image.shape[0], image.shape[1]