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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dexcontrol might be problematic. Click here for more details.

dexcontrol/core/hand.py CHANGED
@@ -14,17 +14,24 @@ This module provides the Hand class for controlling a robotic hand through Zenoh
14
14
  communication. It handles joint position control and state monitoring.
15
15
  """
16
16
 
17
- from typing import Any
17
+ from enum import Enum
18
+ from typing import Any, cast
18
19
 
19
20
  import numpy as np
20
21
  import zenoh
21
22
  from jaxtyping import Float
22
23
 
23
24
  from dexcontrol.config.core.hand import HandConfig
24
- from dexcontrol.core.component import RobotJointComponent
25
+ from dexcontrol.core.component import RobotComponent, RobotJointComponent
25
26
  from dexcontrol.proto import dexcontrol_msg_pb2
26
27
 
27
28
 
29
+ class HandType(Enum):
30
+ UNKNOWN = "UNKNOWN"
31
+ HandF5D6_V1 = "HandF5D6_V1"
32
+ HandF5D6_V2 = "HandF5D6_V2"
33
+
34
+
28
35
  class Hand(RobotJointComponent):
29
36
  """Robot hand control class.
30
37
 
@@ -36,6 +43,7 @@ class Hand(RobotJointComponent):
36
43
  self,
37
44
  configs: HandConfig,
38
45
  zenoh_session: zenoh.Session,
46
+ hand_type: HandType = HandType.HandF5D6_V1,
39
47
  ) -> None:
40
48
  """Initialize the hand controller.
41
49
 
@@ -47,7 +55,7 @@ class Hand(RobotJointComponent):
47
55
  super().__init__(
48
56
  state_sub_topic=configs.state_sub_topic,
49
57
  control_pub_topic=configs.control_pub_topic,
50
- state_message_type=dexcontrol_msg_pb2.HandState,
58
+ state_message_type=dexcontrol_msg_pb2.MotorStateWithCurrent,
51
59
  zenoh_session=zenoh_session,
52
60
  joint_name=configs.joint_name,
53
61
  )
@@ -64,45 +72,11 @@ class Hand(RobotJointComponent):
64
72
  Args:
65
73
  joint_pos: Joint positions as list or numpy array.
66
74
  """
67
- control_msg = dexcontrol_msg_pb2.HandCommand()
75
+ control_msg = dexcontrol_msg_pb2.MotorPosCommand()
68
76
  joint_pos_array = self._convert_joint_cmd_to_array(joint_pos)
69
- control_msg.joint_pos.extend(joint_pos_array.tolist())
77
+ control_msg.pos.extend(joint_pos_array.tolist())
70
78
  self._publish_control(control_msg)
71
79
 
72
- def set_joint_pos(
73
- self,
74
- joint_pos: Float[np.ndarray, " N"] | list[float] | dict[str, float],
75
- relative: bool = False,
76
- wait_time: float = 0.0,
77
- wait_kwargs: dict[str, float] | None = None,
78
- exit_on_reach: bool = False,
79
- exit_on_reach_kwargs: dict[str, Any] | None = None,
80
- ) -> None:
81
- """Send joint position control commands to the hand.
82
-
83
- Args:
84
- joint_pos: Joint positions as either:
85
- - List of joint values [j1, j2, ..., jN]
86
- - Numpy array with shape (N,)
87
- - Dictionary mapping joint names to position values
88
- relative: If True, the joint positions are relative to the current position.
89
- wait_time: Time to wait after setting the joint positions.
90
- wait_kwargs: Optional parameters for trajectory generation (not used in Hand).
91
- exit_on_reach: If True, the function will exit when the joint positions are reached.
92
- exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
93
-
94
- Raises:
95
- ValueError: If joint_pos dictionary contains invalid joint names.
96
- """
97
- super().set_joint_pos(
98
- joint_pos,
99
- relative,
100
- wait_time,
101
- wait_kwargs,
102
- exit_on_reach,
103
- exit_on_reach_kwargs,
104
- )
105
-
106
80
  def open_hand(
107
81
  self,
108
82
  wait_time: float = 0.0,
@@ -152,6 +126,37 @@ class HandF5D6(Hand):
152
126
  the F5D6 hand model.
153
127
  """
154
128
 
129
+ def __init__(
130
+ self,
131
+ configs: HandConfig,
132
+ zenoh_session: zenoh.Session,
133
+ hand_type: HandType = HandType.HandF5D6_V1,
134
+ ) -> None:
135
+ super().__init__(configs, zenoh_session)
136
+
137
+ # Initialize touch sensor for F5D6_V2 hands
138
+ self._hand_type = hand_type
139
+ if self._hand_type == HandType.HandF5D6_V2:
140
+ self._touch_sensor = HandF5D6TouchSensor(
141
+ configs.touch_sensor_sub_topic, zenoh_session
142
+ )
143
+ elif self._hand_type == HandType.HandF5D6_V1:
144
+ self._touch_sensor = None
145
+ else:
146
+ raise ValueError(f"Invalid hand type: {self._hand_type}")
147
+
148
+ def get_finger_tip_force(self) -> Float[np.ndarray, "5"]:
149
+ """Get the force at the finger tips.
150
+
151
+ Returns:
152
+ Array of force values at the finger tips.
153
+ """
154
+ if self._touch_sensor is None:
155
+ raise ValueError(
156
+ f"Touch sensor not available for this hand type: {self._hand_type}"
157
+ )
158
+ return self._touch_sensor.get_fingertip_touch_net_force()
159
+
155
160
  def close_hand(
156
161
  self,
157
162
  wait_time: float = 0.0,
@@ -241,3 +246,33 @@ class HandF5D6(Hand):
241
246
  0
242
247
  ] * (1 - ratio)
243
248
  return intermediate_pos
249
+
250
+
251
+ class HandF5D6TouchSensor(RobotComponent):
252
+ """Wrench sensor reader for the robot arm.
253
+
254
+ This class provides methods to read wrench sensor data through Zenoh communication.
255
+ """
256
+
257
+ def __init__(self, state_sub_topic: str, zenoh_session: zenoh.Session) -> None:
258
+ """Initialize the wrench sensor reader.
259
+
260
+ Args:
261
+ state_sub_topic: Topic to subscribe to for wrench sensor data.
262
+ zenoh_session: Active Zenoh communication session for message passing.
263
+ """
264
+ super().__init__(
265
+ state_sub_topic=state_sub_topic,
266
+ zenoh_session=zenoh_session,
267
+ state_message_type=dexcontrol_msg_pb2.HandTouchSensorState,
268
+ )
269
+
270
+ def get_fingertip_touch_net_force(self) -> Float[np.ndarray, "5"]:
271
+ """Get the complete wrench sensor state.
272
+
273
+ Returns:
274
+ Dictionary containing wrench values and button states.
275
+ """
276
+ state = self._get_state()
277
+ hand_touch_state = cast(dexcontrol_msg_pb2.HandTouchSensorState, state)
278
+ return np.array(hand_touch_state.force)
dexcontrol/core/head.py CHANGED
@@ -55,7 +55,7 @@ class Head(RobotJointComponent):
55
55
  super().__init__(
56
56
  state_sub_topic=configs.state_sub_topic,
57
57
  control_pub_topic=configs.control_pub_topic,
58
- state_message_type=dexcontrol_msg_pb2.HeadState,
58
+ state_message_type=dexcontrol_msg_pb2.MotorStateWithTorque,
59
59
  zenoh_session=zenoh_session,
60
60
  joint_name=configs.joint_name,
61
61
  joint_limit=configs.joint_limit
@@ -164,9 +164,9 @@ class Head(RobotJointComponent):
164
164
  )
165
165
 
166
166
  # Create and send control message
167
- control_msg = dexcontrol_msg_pb2.HeadCommand()
168
- control_msg.joint_pos.extend(joint_pos.tolist())
169
- control_msg.joint_vel.extend(joint_vel.tolist())
167
+ control_msg = dexcontrol_msg_pb2.MotorPosVelCommand()
168
+ control_msg.pos.extend(joint_pos.tolist())
169
+ control_msg.vel.extend(joint_vel.tolist())
170
170
  self._publish_control(control_msg)
171
171
 
172
172
  # Wait if specified
@@ -202,7 +202,8 @@ class Head(RobotJointComponent):
202
202
 
203
203
  for reply in replies:
204
204
  if reply.ok is not None and reply.ok.payload is not None:
205
- logger.info(reply.ok.payload.to_string())
205
+ # TODO: handle the reply message of head mode
206
+ pass
206
207
  time.sleep(0.5)
207
208
 
208
209
  def get_joint_limit(self) -> Float[np.ndarray, "3 2"] | None:
dexcontrol/core/misc.py CHANGED
@@ -11,13 +11,14 @@
11
11
  """Miscellaneous robot components module.
12
12
 
13
13
  This module provides classes for various auxiliary robot components such as Battery,
14
- EStop (emergency stop), and UltraSonicSensor.
14
+ EStop (emergency stop), ServerLogSubscriber, and UltraSonicSensor.
15
15
  """
16
16
 
17
+ import json
17
18
  import os
18
19
  import threading
19
20
  import time
20
- from typing import TypeVar
21
+ from typing import Any, TypeVar, cast
21
22
 
22
23
  import zenoh
23
24
  from google.protobuf.message import Message
@@ -133,7 +134,7 @@ class Battery(RobotComponent):
133
134
  power = state.current * state.voltage
134
135
  power_style = self._get_power_style(power)
135
136
  table.add_row(
136
- "Power",
137
+ "Power Consumption",
137
138
  f"[{power_style}]{power:.2f}W[/] ([blue]{state.current:.2f}A[/] "
138
139
  f"× [blue]{state.voltage:.2f}V[/])",
139
140
  )
@@ -281,25 +282,40 @@ class EStop(RobotComponent):
281
282
  - software_estop_enabled: Software EStop enabled
282
283
  """
283
284
  state = self._get_state()
285
+ state = cast(dexcontrol_msg_pb2.EStopState, state)
284
286
  if state is None:
285
287
  return {
286
288
  "button_pressed": False,
287
289
  "software_estop_enabled": False,
288
290
  }
291
+ button_pressed = (
292
+ state.left_button_pressed
293
+ or state.right_button_pressed
294
+ or state.waist_button_pressed
295
+ or state.wireless_button_pressed
296
+ )
289
297
  return {
290
- "button_pressed": state.button_pressed,
298
+ "button_pressed": button_pressed,
291
299
  "software_estop_enabled": state.software_estop_enabled,
292
300
  }
293
301
 
294
302
  def is_button_pressed(self) -> bool:
295
303
  """Checks if the EStop button is pressed."""
296
304
  state = self._get_state()
297
- return state.button_pressed if state is not None else False
305
+ state = cast(dexcontrol_msg_pb2.EStopState, state)
306
+ button_pressed = (
307
+ state.left_button_pressed
308
+ or state.right_button_pressed
309
+ or state.waist_button_pressed
310
+ or state.wireless_button_pressed
311
+ )
312
+ return button_pressed
298
313
 
299
314
  def is_software_estop_enabled(self) -> bool:
300
315
  """Checks if the software EStop is enabled."""
301
316
  state = self._get_state()
302
- return state.software_estop_enabled if state is not None else False
317
+ state = cast(dexcontrol_msg_pb2.EStopState, state)
318
+ return state.software_estop_enabled
303
319
 
304
320
  def activate(self) -> None:
305
321
  """Activates the software emergency stop (E-Stop)."""
@@ -325,27 +341,19 @@ class EStop(RobotComponent):
325
341
 
326
342
  def show(self) -> None:
327
343
  """Displays the current EStop status as a formatted table with color indicators."""
328
- state = self._get_state()
329
-
330
344
  table = Table(title="E-Stop Status")
331
345
  table.add_column("Parameter", style="cyan")
332
346
  table.add_column("Value")
333
347
 
334
- if state is None:
335
- table.add_row("Status", "[red]No E-Stop data available[/]")
336
- console = Console()
337
- console.print(table)
338
- return
339
-
340
- button_style = "bold red" if state.button_pressed else "bold dark_green"
341
- table.add_row("Button Pressed", f"[{button_style}]{state.button_pressed}[/]")
348
+ button_pressed = self.is_button_pressed()
349
+ button_style = "bold red" if button_pressed else "bold dark_green"
350
+ table.add_row("Button Pressed", f"[{button_style}]{button_pressed}[/]")
342
351
 
343
- software_style = (
344
- "bold red" if state.software_estop_enabled else "bold dark_green"
345
- )
352
+ if_software_estop_enabled = self.is_software_estop_enabled()
353
+ software_style = "bold red" if if_software_estop_enabled else "bold dark_green"
346
354
  table.add_row(
347
355
  "Software E-Stop Enabled",
348
- f"[{software_style}]{state.software_estop_enabled}[/]",
356
+ f"[{software_style}]{if_software_estop_enabled}[/]",
349
357
  )
350
358
 
351
359
  console = Console()
@@ -651,9 +659,9 @@ class Heartbeat:
651
659
  last_value = status["last_value"]
652
660
  if last_value is not None:
653
661
  uptime_str = self._format_uptime(last_value)
654
- table.add_row("Robot server uptime", f"[blue]{uptime_str}[/]")
662
+ table.add_row("Robot Driver Uptime", f"[blue]{uptime_str}[/]")
655
663
  else:
656
- table.add_row("Robot server uptime", "[red]No data[/]")
664
+ table.add_row("Robot Driver Uptime", "[red]No data[/]")
657
665
 
658
666
  # Time since last heartbeat
659
667
  time_since = status["time_since_last"]
@@ -671,3 +679,145 @@ class Heartbeat:
671
679
 
672
680
  console = Console()
673
681
  console.print(table)
682
+
683
+
684
+ class ServerLogSubscriber:
685
+ """Server log subscriber that monitors and displays server log messages.
686
+
687
+ This class subscribes to the "logs" topic and handles incoming log messages
688
+ from the robot server. It provides formatted display of server logs with
689
+ proper error handling and validation.
690
+
691
+ The server sends log information via the "logs" topic as JSON with format:
692
+ {"timestamp": "ISO8601", "message": "text", "source": "robot_server"}
693
+
694
+ Attributes:
695
+ _zenoh_session: Zenoh session for communication.
696
+ _log_subscriber: Zenoh subscriber for log messages.
697
+ """
698
+
699
+ def __init__(self, zenoh_session: zenoh.Session) -> None:
700
+ """Initialize the ServerLogSubscriber.
701
+
702
+ Args:
703
+ zenoh_session: Active Zenoh session for communication.
704
+ """
705
+ self._zenoh_session = zenoh_session
706
+ self._log_subscriber = None
707
+ self._initialize()
708
+
709
+ def _initialize(self) -> None:
710
+ """Initialize the log subscriber with error handling."""
711
+
712
+ def log_handler(sample):
713
+ """Handle incoming log messages from the server."""
714
+ if not self._is_valid_log_sample(sample):
715
+ return
716
+
717
+ try:
718
+ log_data = self._parse_log_payload(sample.payload)
719
+ if log_data:
720
+ self._display_server_log(log_data)
721
+ except Exception as e:
722
+ logger.warning(f"Failed to process server log: {e}")
723
+
724
+ try:
725
+ # Subscribe to server logs topic
726
+ self._log_subscriber = self._zenoh_session.declare_subscriber(
727
+ "logs", log_handler
728
+ )
729
+ logger.debug("Server log subscriber initialized")
730
+ except Exception as e:
731
+ logger.warning(f"Failed to initialize server log subscriber: {e}")
732
+ self._log_subscriber = None
733
+
734
+ def _is_valid_log_sample(self, sample) -> bool:
735
+ """Check if log sample is valid.
736
+
737
+ Args:
738
+ sample: Zenoh sample to validate.
739
+
740
+ Returns:
741
+ True if sample is valid, False otherwise.
742
+ """
743
+ if sample is None or sample.payload is None:
744
+ logger.debug("Received empty log sample")
745
+ return False
746
+ return True
747
+
748
+ def _parse_log_payload(self, payload) -> dict[str, str] | None:
749
+ """Parse log payload and return structured data.
750
+
751
+ Args:
752
+ payload: Raw payload from Zenoh sample.
753
+
754
+ Returns:
755
+ Parsed log data as dictionary or None if parsing fails.
756
+ """
757
+ try:
758
+ payload_str = payload.to_bytes().decode("utf-8")
759
+ if not payload_str.strip():
760
+ logger.debug("Received empty log payload")
761
+ return None
762
+
763
+ log_data = json.loads(payload_str)
764
+
765
+ if not isinstance(log_data, dict):
766
+ logger.warning(
767
+ f"Invalid log data format: expected dict, got {type(log_data)}"
768
+ )
769
+ return None
770
+
771
+ return log_data
772
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
773
+ logger.warning(f"Failed to parse log payload: {e}")
774
+ return None
775
+
776
+ def _display_server_log(self, log_data: dict[str, str]) -> None:
777
+ """Display formatted server log message.
778
+
779
+ Args:
780
+ log_data: Parsed log data dictionary.
781
+ """
782
+ # Extract log information with safe defaults
783
+ timestamp = log_data.get("timestamp", "")
784
+ message = log_data.get("message", "")
785
+ source = log_data.get("source", "unknown")
786
+
787
+ # Validate critical fields
788
+ if not message:
789
+ logger.debug("Received log with empty message")
790
+ return
791
+
792
+ # Log the server message with clear identification
793
+ logger.info(f"[SERVER_LOG] [{timestamp}] [{source}] {message}")
794
+
795
+ def is_active(self) -> bool:
796
+ """Check if the log subscriber is active.
797
+
798
+ Returns:
799
+ True if subscriber is active, False otherwise.
800
+ """
801
+ return self._log_subscriber is not None
802
+
803
+ def shutdown(self) -> None:
804
+ """Clean up the log subscriber and release resources."""
805
+ if self._log_subscriber is not None:
806
+ try:
807
+ self._log_subscriber.undeclare()
808
+ self._log_subscriber = None
809
+ except Exception as e:
810
+ logger.error(f"Error cleaning up log subscriber: {e}")
811
+
812
+ def get_status(self) -> dict[str, Any]:
813
+ """Get the current status of the log subscriber.
814
+
815
+ Returns:
816
+ Dictionary containing status information:
817
+ - is_active: Whether the subscriber is active
818
+ - topic: The topic being subscribed to
819
+ """
820
+ return {
821
+ "is_active": self.is_active(),
822
+ "topic": "logs",
823
+ }