dexcontrol 0.2.12__py3-none-any.whl → 0.3.4__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.
- dexcontrol/__init__.py +17 -8
- dexcontrol/apps/dualsense_teleop_base.py +1 -1
- dexcontrol/comm/__init__.py +51 -0
- dexcontrol/comm/rtc.py +401 -0
- dexcontrol/comm/subscribers.py +329 -0
- dexcontrol/config/core/chassis.py +9 -4
- dexcontrol/config/core/hand.py +1 -0
- dexcontrol/config/sensors/cameras/__init__.py +1 -2
- dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
- dexcontrol/config/sensors/vega_sensors.py +12 -18
- dexcontrol/config/vega.py +4 -1
- dexcontrol/core/arm.py +66 -42
- dexcontrol/core/chassis.py +142 -120
- dexcontrol/core/component.py +107 -58
- dexcontrol/core/hand.py +119 -86
- dexcontrol/core/head.py +22 -33
- dexcontrol/core/misc.py +331 -158
- dexcontrol/core/robot_query_interface.py +467 -0
- dexcontrol/core/torso.py +5 -9
- dexcontrol/robot.py +245 -574
- dexcontrol/sensors/__init__.py +1 -2
- dexcontrol/sensors/camera/__init__.py +0 -2
- dexcontrol/sensors/camera/base_camera.py +150 -0
- dexcontrol/sensors/camera/rgb_camera.py +68 -64
- dexcontrol/sensors/camera/zed_camera.py +140 -164
- dexcontrol/sensors/imu/chassis_imu.py +81 -62
- dexcontrol/sensors/imu/zed_imu.py +54 -43
- dexcontrol/sensors/lidar/rplidar.py +16 -20
- dexcontrol/sensors/manager.py +4 -14
- dexcontrol/sensors/ultrasonic.py +15 -28
- dexcontrol/utils/__init__.py +0 -11
- dexcontrol/utils/comm_helper.py +110 -0
- dexcontrol/utils/constants.py +1 -1
- dexcontrol/utils/error_code.py +2 -4
- dexcontrol/utils/os_utils.py +172 -4
- dexcontrol/utils/pb_utils.py +6 -28
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/METADATA +16 -3
- dexcontrol-0.3.4.dist-info/RECORD +62 -0
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/WHEEL +1 -1
- dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
- dexcontrol/proto/dexcontrol_msg_pb2.py +0 -73
- dexcontrol/proto/dexcontrol_msg_pb2.pyi +0 -220
- dexcontrol/proto/dexcontrol_query_pb2.py +0 -77
- dexcontrol/proto/dexcontrol_query_pb2.pyi +0 -162
- dexcontrol/sensors/camera/luxonis_camera.py +0 -169
- dexcontrol/utils/motion_utils.py +0 -199
- dexcontrol/utils/rate_limiter.py +0 -172
- dexcontrol/utils/rtc_utils.py +0 -144
- dexcontrol/utils/subscribers/__init__.py +0 -52
- dexcontrol/utils/subscribers/base.py +0 -281
- dexcontrol/utils/subscribers/camera.py +0 -332
- dexcontrol/utils/subscribers/decoders.py +0 -88
- dexcontrol/utils/subscribers/generic.py +0 -110
- dexcontrol/utils/subscribers/imu.py +0 -175
- dexcontrol/utils/subscribers/lidar.py +0 -172
- dexcontrol/utils/subscribers/protobuf.py +0 -111
- dexcontrol/utils/subscribers/rtc.py +0 -316
- dexcontrol/utils/zenoh_utils.py +0 -122
- dexcontrol-0.2.12.dist-info/RECORD +0 -75
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/licenses/LICENSE +0 -0
dexcontrol/core/chassis.py
CHANGED
|
@@ -17,15 +17,90 @@ Zenoh communication. It handles steering and wheel velocity control.
|
|
|
17
17
|
import time
|
|
18
18
|
|
|
19
19
|
import numpy as np
|
|
20
|
-
import
|
|
20
|
+
from dexcomm.serialization.protobuf import control_msg_pb2
|
|
21
|
+
from dexcomm.utils import RateLimiter
|
|
22
|
+
from jaxtyping import Float
|
|
21
23
|
|
|
22
24
|
from dexcontrol.config.core import ChassisConfig
|
|
23
25
|
from dexcontrol.core.component import RobotJointComponent
|
|
24
|
-
from dexcontrol.proto import dexcontrol_msg_pb2
|
|
25
|
-
from dexcontrol.utils.rate_limiter import RateLimiter
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
class
|
|
28
|
+
class ChassisSteer(RobotJointComponent):
|
|
29
|
+
"""Robot hand control class.
|
|
30
|
+
|
|
31
|
+
This class provides methods to control a robotic hand by publishing commands and
|
|
32
|
+
receiving state information through Zenoh communication.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
configs: ChassisConfig,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize the hand controller.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
configs: Hand configuration parameters containing communication topics
|
|
43
|
+
and predefined hand positions.
|
|
44
|
+
"""
|
|
45
|
+
super().__init__(
|
|
46
|
+
state_sub_topic=configs.steer_state_sub_topic,
|
|
47
|
+
control_pub_topic=configs.steer_control_pub_topic,
|
|
48
|
+
state_message_type=control_msg_pb2.MotorStateWithCurrent,
|
|
49
|
+
joint_name=configs.steer_joint_name,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _send_position_command(
|
|
53
|
+
self, joint_pos: Float[np.ndarray, " 2"] | list[float]
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Send joint position control commands to the hand.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
joint_pos: Joint positions as list or numpy array.
|
|
59
|
+
"""
|
|
60
|
+
control_msg = control_msg_pb2.MotorPosVelCommand()
|
|
61
|
+
joint_pos_array = self._convert_joint_cmd_to_array(joint_pos)
|
|
62
|
+
control_msg.pos.extend(joint_pos_array.tolist())
|
|
63
|
+
self._publish_control(control_msg)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ChassisDrive(RobotJointComponent):
|
|
67
|
+
"""Robot base control class.
|
|
68
|
+
|
|
69
|
+
This class provides methods to control a robot's wheeled base by publishing commands
|
|
70
|
+
and receiving state information through Zenoh communication.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
configs: ChassisConfig,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Initialize the base controller.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
configs: Base configuration parameters containing communication topics.
|
|
81
|
+
"""
|
|
82
|
+
super().__init__(
|
|
83
|
+
state_sub_topic=configs.drive_state_sub_topic,
|
|
84
|
+
control_pub_topic=configs.drive_control_pub_topic,
|
|
85
|
+
state_message_type=control_msg_pb2.MotorStateWithCurrent,
|
|
86
|
+
joint_name=configs.drive_joint_name,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _send_position_command(
|
|
90
|
+
self, joint_pos: Float[np.ndarray, " 2"] | list[float]
|
|
91
|
+
) -> None:
|
|
92
|
+
raise NotImplementedError("ChassisDrive does not support position control.")
|
|
93
|
+
|
|
94
|
+
def _send_velocity_command(
|
|
95
|
+
self, joint_vel: Float[np.ndarray, " 2"] | list[float]
|
|
96
|
+
) -> None:
|
|
97
|
+
control_msg = control_msg_pb2.MotorVelCommand()
|
|
98
|
+
joint_vel_array = self._convert_joint_cmd_to_array(joint_vel)
|
|
99
|
+
control_msg.vel.extend(joint_vel_array.tolist())
|
|
100
|
+
self._publish_control(control_msg)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Chassis:
|
|
29
104
|
"""Robot base control class.
|
|
30
105
|
|
|
31
106
|
This class provides methods to control a robot's wheeled base by publishing commands
|
|
@@ -44,21 +119,15 @@ class Chassis(RobotJointComponent):
|
|
|
44
119
|
def __init__(
|
|
45
120
|
self,
|
|
46
121
|
configs: ChassisConfig,
|
|
47
|
-
zenoh_session: zenoh.Session,
|
|
48
122
|
) -> None:
|
|
49
123
|
"""Initialize the base controller.
|
|
50
124
|
|
|
51
125
|
Args:
|
|
52
126
|
configs: Base configuration parameters containing communication topics.
|
|
53
|
-
zenoh_session: Active Zenoh communication session for message passing.
|
|
54
127
|
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
state_message_type=dexcontrol_msg_pb2.ChassisState,
|
|
59
|
-
zenoh_session=zenoh_session,
|
|
60
|
-
joint_name=configs.joint_name,
|
|
61
|
-
)
|
|
128
|
+
self.chassis_steer = ChassisSteer(configs)
|
|
129
|
+
self.chassis_drive = ChassisDrive(configs)
|
|
130
|
+
|
|
62
131
|
self.max_vel = configs.max_vel
|
|
63
132
|
self._center_to_wheel_axis_dist = configs.center_to_wheel_axis_dist
|
|
64
133
|
self._wheels_dist = configs.wheels_dist
|
|
@@ -96,7 +165,8 @@ class Chassis(RobotJointComponent):
|
|
|
96
165
|
def shutdown(self) -> None:
|
|
97
166
|
"""Shutdown the base by stopping all motion."""
|
|
98
167
|
self.stop()
|
|
99
|
-
|
|
168
|
+
self.chassis_steer.shutdown()
|
|
169
|
+
self.chassis_drive.shutdown()
|
|
100
170
|
|
|
101
171
|
@property
|
|
102
172
|
def steering_angle(self) -> np.ndarray:
|
|
@@ -106,8 +176,7 @@ class Chassis(RobotJointComponent):
|
|
|
106
176
|
Numpy array of shape (2,) containing current steering angles in radians.
|
|
107
177
|
Index 0 is left wheel, index 1 is right wheel.
|
|
108
178
|
"""
|
|
109
|
-
|
|
110
|
-
return np.array([state.left.steering_pos, state.right.steering_pos])
|
|
179
|
+
return self.chassis_steer.get_joint_pos()
|
|
111
180
|
|
|
112
181
|
@property
|
|
113
182
|
def wheel_velocity(self) -> np.ndarray:
|
|
@@ -117,8 +186,17 @@ class Chassis(RobotJointComponent):
|
|
|
117
186
|
Numpy array of shape (2,) containing current wheel velocities in m/s.
|
|
118
187
|
Index 0 is left wheel, index 1 is right wheel.
|
|
119
188
|
"""
|
|
120
|
-
|
|
121
|
-
|
|
189
|
+
return self.chassis_drive.get_joint_vel()
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def wheel_encoder_pos(self) -> np.ndarray:
|
|
193
|
+
"""Get current wheel encoder positions.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Numpy array of shape (2,) containing current wheel encoder positions in radians.
|
|
197
|
+
Index 0 is left wheel, index 1 is right wheel.
|
|
198
|
+
"""
|
|
199
|
+
return self.chassis_drive.get_joint_pos()
|
|
122
200
|
|
|
123
201
|
def set_velocity(
|
|
124
202
|
self,
|
|
@@ -356,24 +434,23 @@ class Chassis(RobotJointComponent):
|
|
|
356
434
|
Args:
|
|
357
435
|
joint_id: Optional ID(s) of specific joints to query.
|
|
358
436
|
0: left steering position
|
|
359
|
-
1:
|
|
360
|
-
2:
|
|
437
|
+
1: right steering position
|
|
438
|
+
2: left wheel position
|
|
361
439
|
3: right wheel position
|
|
362
440
|
|
|
363
441
|
Returns:
|
|
364
442
|
Array of joint positions in the order:
|
|
365
|
-
[left_steering_pos,
|
|
443
|
+
[left_steering_pos, right_steering_pos, left_wheel_pos, right_wheel_pos]
|
|
366
444
|
"""
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return self._extract_joint_info(joint_pos, joint_id=joint_id)
|
|
445
|
+
steer_pos = self.chassis_steer.get_joint_pos()
|
|
446
|
+
drive_pos = self.chassis_drive.get_joint_pos()
|
|
447
|
+
joint_pos = np.concatenate([steer_pos, drive_pos])
|
|
448
|
+
return RobotJointComponent._extract_joint_info(joint_pos, joint_id=joint_id)
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
def joint_name(self) -> list[str]:
|
|
452
|
+
"""Get the joint names of the chassis."""
|
|
453
|
+
return self.chassis_steer.joint_name + self.chassis_drive.joint_name
|
|
377
454
|
|
|
378
455
|
def get_joint_pos_dict(
|
|
379
456
|
self, joint_id: list[int] | int | None = None
|
|
@@ -383,58 +460,22 @@ class Chassis(RobotJointComponent):
|
|
|
383
460
|
Args:
|
|
384
461
|
joint_id: Optional ID(s) of specific joints to query.
|
|
385
462
|
0: left steering position
|
|
386
|
-
1:
|
|
387
|
-
2:
|
|
463
|
+
1: right steering position
|
|
464
|
+
2: left wheel position
|
|
388
465
|
3: right wheel position
|
|
389
466
|
|
|
390
467
|
Returns:
|
|
391
468
|
Dictionary mapping joint names to position values.
|
|
392
469
|
"""
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
1: right wheel
|
|
403
|
-
|
|
404
|
-
Returns:
|
|
405
|
-
Numpy array containing:
|
|
406
|
-
[:, 0]: Current steering positions in radians
|
|
407
|
-
[:, 1]: Current wheel positions in radians
|
|
408
|
-
[:, 2]: Current wheel velocities in m/s
|
|
409
|
-
"""
|
|
410
|
-
state = self._get_state()
|
|
411
|
-
wheel_states = np.array(
|
|
412
|
-
[
|
|
413
|
-
[state.left.steering_pos, state.left.wheel_pos, state.left.wheel_vel],
|
|
414
|
-
[
|
|
415
|
-
state.right.steering_pos,
|
|
416
|
-
state.right.wheel_pos,
|
|
417
|
-
state.right.wheel_vel,
|
|
418
|
-
],
|
|
419
|
-
]
|
|
420
|
-
)
|
|
421
|
-
return self._extract_joint_info(wheel_states, joint_id=joint_id)
|
|
422
|
-
|
|
423
|
-
def get_joint_state_dict(
|
|
424
|
-
self, joint_id: list[int] | int | None = None
|
|
425
|
-
) -> dict[str, np.ndarray]:
|
|
426
|
-
"""Get current wheel states as a dictionary.
|
|
427
|
-
|
|
428
|
-
Args:
|
|
429
|
-
joint_id: Optional ID(s) of specific joints to query.
|
|
430
|
-
0: left wheel
|
|
431
|
-
1: right wheel
|
|
432
|
-
|
|
433
|
-
Returns:
|
|
434
|
-
Dictionary mapping joint names to arrays of [steering_pos, wheel_pos, wheel_vel]
|
|
435
|
-
"""
|
|
436
|
-
values = self.get_joint_state(joint_id)
|
|
437
|
-
return self._convert_to_dict(values, joint_id)
|
|
470
|
+
steer_pos = self.chassis_steer.get_joint_pos_dict()
|
|
471
|
+
drive_pos = self.chassis_drive.get_joint_pos_dict()
|
|
472
|
+
if joint_id is None:
|
|
473
|
+
return {**steer_pos, **drive_pos}
|
|
474
|
+
else:
|
|
475
|
+
joint_names = self.joint_name
|
|
476
|
+
if isinstance(joint_id, int):
|
|
477
|
+
joint_id = [joint_id]
|
|
478
|
+
return {joint_names[i]: steer_pos[joint_names[i]] for i in joint_id}
|
|
438
479
|
|
|
439
480
|
def set_motion_state(
|
|
440
481
|
self,
|
|
@@ -527,10 +568,19 @@ class Chassis(RobotJointComponent):
|
|
|
527
568
|
# Set default control frequency if not provided
|
|
528
569
|
control_hz = wait_kwargs.get("control_hz", 50)
|
|
529
570
|
rate_limiter = RateLimiter(control_hz)
|
|
530
|
-
start_time = time.time()
|
|
531
571
|
|
|
532
|
-
|
|
572
|
+
now_sec = time.monotonic()
|
|
573
|
+
duration_sec = max(wait_time - 1.0, 0.0)
|
|
574
|
+
end_time_sec = now_sec + duration_sec
|
|
575
|
+
|
|
576
|
+
while True:
|
|
577
|
+
# Ensure command is sent at least once
|
|
533
578
|
self._send_single_command(steering_angle, wheel_velocity)
|
|
579
|
+
|
|
580
|
+
# Exit if we've reached the allotted duration
|
|
581
|
+
if time.monotonic() >= end_time_sec:
|
|
582
|
+
break
|
|
583
|
+
|
|
534
584
|
rate_limiter.sleep()
|
|
535
585
|
|
|
536
586
|
def _send_single_command(
|
|
@@ -544,24 +594,16 @@ class Chassis(RobotJointComponent):
|
|
|
544
594
|
steering_angle: Target steering angles.
|
|
545
595
|
wheel_velocity: Target wheel velocities.
|
|
546
596
|
"""
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
steering_angle, 0, "L_wheel_j1", state.left.steering_pos
|
|
553
|
-
)
|
|
554
|
-
control_msg.right.steering_pos = _get_value(
|
|
555
|
-
steering_angle, 1, "R_wheel_j1", state.right.steering_pos
|
|
556
|
-
)
|
|
557
|
-
control_msg.left.wheel_vel = _get_value(
|
|
558
|
-
wheel_velocity, 0, "L_wheel_j2", state.left.wheel_vel
|
|
559
|
-
)
|
|
560
|
-
control_msg.right.wheel_vel = _get_value(
|
|
561
|
-
wheel_velocity, 1, "R_wheel_j2", state.right.wheel_vel
|
|
562
|
-
)
|
|
597
|
+
if isinstance(steering_angle, float):
|
|
598
|
+
steering_angle = np.array([steering_angle, steering_angle])
|
|
599
|
+
self.chassis_steer._send_position_command(steering_angle)
|
|
600
|
+
elif isinstance(steering_angle, np.ndarray):
|
|
601
|
+
self.chassis_steer._send_position_command(steering_angle)
|
|
563
602
|
|
|
564
|
-
|
|
603
|
+
if isinstance(wheel_velocity, float):
|
|
604
|
+
wheel_velocity = np.array([wheel_velocity, wheel_velocity])
|
|
605
|
+
elif isinstance(wheel_velocity, np.ndarray):
|
|
606
|
+
self.chassis_drive._send_velocity_command(wheel_velocity)
|
|
565
607
|
|
|
566
608
|
def _compute_wheel_control(
|
|
567
609
|
self, wheel_velocity: np.ndarray, current_angle: float
|
|
@@ -608,26 +650,6 @@ class Chassis(RobotJointComponent):
|
|
|
608
650
|
|
|
609
651
|
return steering_angle, wheel_speed
|
|
610
652
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
idx: int,
|
|
615
|
-
key: str,
|
|
616
|
-
fallback: float,
|
|
617
|
-
) -> float:
|
|
618
|
-
"""Helper function to extract values with fallback to current state.
|
|
619
|
-
|
|
620
|
-
Args:
|
|
621
|
-
data: Input scalar, array or dictionary containing values.
|
|
622
|
-
idx: Index to access if data is array.
|
|
623
|
-
key: Key to access if data is dictionary.
|
|
624
|
-
fallback: Default value if key/index not found.
|
|
625
|
-
|
|
626
|
-
Returns:
|
|
627
|
-
Extracted float value.
|
|
628
|
-
"""
|
|
629
|
-
if isinstance(data, dict):
|
|
630
|
-
return float(data.get(key, fallback))
|
|
631
|
-
if isinstance(data, (int, float)):
|
|
632
|
-
return float(data)
|
|
633
|
-
return float(data[idx])
|
|
653
|
+
def is_active(self) -> bool:
|
|
654
|
+
"""Check if the chassis is active."""
|
|
655
|
+
return self.chassis_steer.is_active() and self.chassis_drive.is_active()
|
dexcontrol/core/component.py
CHANGED
|
@@ -8,24 +8,25 @@
|
|
|
8
8
|
# 2. Commercial License
|
|
9
9
|
# For commercial licensing terms, contact: contact@dexmate.ai
|
|
10
10
|
|
|
11
|
-
"""Base module for robot components
|
|
11
|
+
"""Base module for robot components using DexComm communication.
|
|
12
12
|
|
|
13
|
-
This module provides base classes for robot components that
|
|
14
|
-
It includes RobotComponent for state-only components
|
|
15
|
-
components that also support control commands.
|
|
13
|
+
This module provides base classes for robot components that use DexComm's
|
|
14
|
+
Raw API for communication. It includes RobotComponent for state-only components
|
|
15
|
+
and RobotJointComponent for components that also support control commands.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
import time
|
|
19
19
|
from typing import Any, Final, Mapping, TypeVar
|
|
20
20
|
|
|
21
21
|
import numpy as np
|
|
22
|
-
import
|
|
22
|
+
from dexcomm import Publisher, Subscriber
|
|
23
|
+
from dexcomm.serialization import deserialize_protobuf, serialize_protobuf
|
|
23
24
|
from google.protobuf.message import Message
|
|
24
25
|
from jaxtyping import Float
|
|
25
26
|
from loguru import logger
|
|
26
27
|
|
|
28
|
+
from dexcontrol.utils.comm_helper import get_zenoh_config_path
|
|
27
29
|
from dexcontrol.utils.os_utils import resolve_key_name
|
|
28
|
-
from dexcontrol.utils.subscribers import ProtobufZenohSubscriber
|
|
29
30
|
|
|
30
31
|
# Type variable for Message subclasses
|
|
31
32
|
M = TypeVar("M", bound=Message)
|
|
@@ -47,41 +48,55 @@ class RobotComponent:
|
|
|
47
48
|
def __init__(
|
|
48
49
|
self,
|
|
49
50
|
state_sub_topic: str,
|
|
50
|
-
zenoh_session: zenoh.Session,
|
|
51
51
|
state_message_type: type[M],
|
|
52
52
|
) -> None:
|
|
53
53
|
"""Initializes RobotComponent.
|
|
54
54
|
|
|
55
55
|
Args:
|
|
56
56
|
state_sub_topic: Topic to subscribe to for state updates.
|
|
57
|
-
zenoh_session: Active Zenoh session for communication.
|
|
58
57
|
state_message_type: Protobuf message class for component state.
|
|
59
58
|
"""
|
|
60
59
|
self._state_message_type = state_message_type
|
|
61
|
-
self.
|
|
62
|
-
self.
|
|
60
|
+
self._latest_state: M | None = None
|
|
61
|
+
self._is_active = False
|
|
62
|
+
self._init_subscriber(state_sub_topic, state_message_type)
|
|
63
63
|
|
|
64
64
|
def _init_subscriber(
|
|
65
65
|
self,
|
|
66
66
|
state_sub_topic: str,
|
|
67
67
|
state_message_type: type[M],
|
|
68
|
-
zenoh_session: zenoh.Session,
|
|
69
68
|
) -> None:
|
|
70
|
-
"""Initialize the
|
|
69
|
+
"""Initialize the subscriber for state updates using DexComm.
|
|
71
70
|
|
|
72
71
|
Args:
|
|
73
72
|
state_sub_topic: Topic to subscribe to for state updates.
|
|
74
73
|
state_message_type: Protobuf message class for component state.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
"""
|
|
75
|
+
# Resolve topic with robot namespace
|
|
76
|
+
full_topic = resolve_key_name(state_sub_topic)
|
|
77
|
+
|
|
78
|
+
# Create DexComm subscriber with protobuf deserialization
|
|
79
|
+
self._subscriber = Subscriber(
|
|
80
|
+
topic=full_topic,
|
|
81
|
+
callback=self._on_state_update,
|
|
82
|
+
deserializer=lambda data: deserialize_protobuf(data, state_message_type),
|
|
83
|
+
config=get_zenoh_config_path(),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
logger.debug(
|
|
87
|
+
f"Created subscriber for {self.__class__.__name__} on {full_topic}"
|
|
83
88
|
)
|
|
84
89
|
|
|
90
|
+
def _on_state_update(self, state: M) -> None:
|
|
91
|
+
"""Handle incoming state updates.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
state: Deserialized protobuf state message.
|
|
95
|
+
"""
|
|
96
|
+
self._latest_state = state
|
|
97
|
+
if state:
|
|
98
|
+
self._is_active = True
|
|
99
|
+
|
|
85
100
|
def _get_state(self) -> M:
|
|
86
101
|
"""Gets the current state of the component.
|
|
87
102
|
|
|
@@ -91,10 +106,9 @@ class RobotComponent:
|
|
|
91
106
|
Raises:
|
|
92
107
|
None: If no state data is available.
|
|
93
108
|
"""
|
|
94
|
-
|
|
95
|
-
if state is None:
|
|
109
|
+
if self._latest_state is None:
|
|
96
110
|
logger.error(f"No state data available for {self.__class__.__name__}")
|
|
97
|
-
return
|
|
111
|
+
return self._latest_state
|
|
98
112
|
|
|
99
113
|
def wait_for_active(self, timeout: float = 5.0) -> bool:
|
|
100
114
|
"""Waits for the component to start receiving state updates.
|
|
@@ -105,7 +119,12 @@ class RobotComponent:
|
|
|
105
119
|
Returns:
|
|
106
120
|
True if component becomes active, False if timeout is reached.
|
|
107
121
|
"""
|
|
108
|
-
|
|
122
|
+
start_time = time.time()
|
|
123
|
+
while time.time() - start_time < timeout:
|
|
124
|
+
if self._is_active:
|
|
125
|
+
return True
|
|
126
|
+
time.sleep(0.1)
|
|
127
|
+
return False
|
|
109
128
|
|
|
110
129
|
def is_active(self) -> bool:
|
|
111
130
|
"""Check if component is receiving state updates.
|
|
@@ -113,7 +132,7 @@ class RobotComponent:
|
|
|
113
132
|
Returns:
|
|
114
133
|
True if component is active, False otherwise.
|
|
115
134
|
"""
|
|
116
|
-
return self.
|
|
135
|
+
return self._is_active
|
|
117
136
|
|
|
118
137
|
def shutdown(self) -> None:
|
|
119
138
|
"""Cleans up Zenoh resources."""
|
|
@@ -149,7 +168,7 @@ class RobotJointComponent(RobotComponent):
|
|
|
149
168
|
Extends RobotComponent to add APIs for interacting with joints.
|
|
150
169
|
|
|
151
170
|
Attributes:
|
|
152
|
-
_publisher:
|
|
171
|
+
_publisher: Publisher for control commands (Zenoh or dexcomm).
|
|
153
172
|
_joint_name: List of joint names for this component.
|
|
154
173
|
_pose_pool: Dictionary of predefined poses for this component.
|
|
155
174
|
"""
|
|
@@ -178,7 +197,6 @@ class RobotJointComponent(RobotComponent):
|
|
|
178
197
|
state_sub_topic: str,
|
|
179
198
|
control_pub_topic: str,
|
|
180
199
|
state_message_type: type[M],
|
|
181
|
-
zenoh_session: zenoh.Session,
|
|
182
200
|
joint_name: list[str] | None = None,
|
|
183
201
|
joint_limit: list[list[float]] | None = None,
|
|
184
202
|
joint_vel_limit: list[float] | None = None,
|
|
@@ -190,16 +208,24 @@ class RobotJointComponent(RobotComponent):
|
|
|
190
208
|
state_sub_topic: Topic to subscribe to for state updates.
|
|
191
209
|
control_pub_topic: Topic to publish control commands.
|
|
192
210
|
state_message_type: Protobuf message class for component state.
|
|
193
|
-
zenoh_session: Active Zenoh session for communication.
|
|
194
211
|
joint_name: List of joint names for this component.
|
|
212
|
+
joint_limit: Joint position limits.
|
|
213
|
+
joint_vel_limit: Joint velocity limits.
|
|
195
214
|
pose_pool: Dictionary of predefined poses for this component.
|
|
196
215
|
"""
|
|
197
|
-
super().__init__(state_sub_topic,
|
|
216
|
+
super().__init__(state_sub_topic, state_message_type)
|
|
198
217
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
218
|
+
# Resolve topic with robot namespace
|
|
219
|
+
full_topic = resolve_key_name(control_pub_topic)
|
|
220
|
+
|
|
221
|
+
# Create DexComm publisher with protobuf serialization
|
|
222
|
+
self._publisher: Final[Publisher] = Publisher(
|
|
223
|
+
topic=full_topic,
|
|
224
|
+
serializer=serialize_protobuf,
|
|
225
|
+
config=get_zenoh_config_path(),
|
|
202
226
|
)
|
|
227
|
+
|
|
228
|
+
logger.debug(f"Created publisher for {self.__class__.__name__} on {full_topic}")
|
|
203
229
|
self._joint_name: list[str] | None = joint_name
|
|
204
230
|
self._joint_limit: np.ndarray | None = (
|
|
205
231
|
np.array(joint_limit) if joint_limit else None
|
|
@@ -218,22 +244,19 @@ class RobotJointComponent(RobotComponent):
|
|
|
218
244
|
Args:
|
|
219
245
|
control_msg: Protobuf control message to publish.
|
|
220
246
|
"""
|
|
221
|
-
|
|
222
|
-
self._publisher.
|
|
247
|
+
# DexComm publisher with protobuf serializer handles this
|
|
248
|
+
self._publisher.publish(control_msg)
|
|
223
249
|
|
|
224
250
|
def shutdown(self) -> None:
|
|
225
|
-
"""Cleans up all
|
|
251
|
+
"""Cleans up all communication resources."""
|
|
226
252
|
super().shutdown()
|
|
227
253
|
try:
|
|
228
254
|
if hasattr(self, "_publisher") and self._publisher:
|
|
229
|
-
self._publisher.
|
|
255
|
+
self._publisher.shutdown()
|
|
230
256
|
except Exception as e:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
logger.warning(
|
|
235
|
-
f"Error undeclaring publisher for {self.__class__.__name__}: {e}"
|
|
236
|
-
)
|
|
257
|
+
logger.warning(
|
|
258
|
+
f"Error shutting down publisher for {self.__class__.__name__}: {e}"
|
|
259
|
+
)
|
|
237
260
|
|
|
238
261
|
@property
|
|
239
262
|
def joint_name(self) -> list[str]:
|
|
@@ -311,9 +334,9 @@ class RobotJointComponent(RobotComponent):
|
|
|
311
334
|
ValueError: If joint positions are not available for this component.
|
|
312
335
|
"""
|
|
313
336
|
state = self._get_state()
|
|
314
|
-
if not hasattr(state, "
|
|
337
|
+
if not hasattr(state, "pos"):
|
|
315
338
|
raise ValueError("Joint positions are not available for this component.")
|
|
316
|
-
joint_pos = np.array(state.
|
|
339
|
+
joint_pos = np.array(state.pos, dtype=np.float32)
|
|
317
340
|
return self._extract_joint_info(joint_pos, joint_id=joint_id)
|
|
318
341
|
|
|
319
342
|
def get_joint_pos_dict(
|
|
@@ -349,9 +372,9 @@ class RobotJointComponent(RobotComponent):
|
|
|
349
372
|
ValueError: If joint velocities are not available for this component.
|
|
350
373
|
"""
|
|
351
374
|
state = self._get_state()
|
|
352
|
-
if not hasattr(state, "
|
|
375
|
+
if not hasattr(state, "vel"):
|
|
353
376
|
raise ValueError("Joint velocities are not available for this component.")
|
|
354
|
-
joint_vel = np.array(state.
|
|
377
|
+
joint_vel = np.array(state.vel, dtype=np.float32)
|
|
355
378
|
return self._extract_joint_info(joint_vel, joint_id=joint_id)
|
|
356
379
|
|
|
357
380
|
def get_joint_vel_dict(
|
|
@@ -386,11 +409,31 @@ class RobotJointComponent(RobotComponent):
|
|
|
386
409
|
ValueError: If joint currents are not available for this component.
|
|
387
410
|
"""
|
|
388
411
|
state = self._get_state()
|
|
389
|
-
if not hasattr(state, "
|
|
412
|
+
if not hasattr(state, "cur"):
|
|
390
413
|
raise ValueError("Joint currents are not available for this component.")
|
|
391
|
-
joint_cur = np.array(state.
|
|
414
|
+
joint_cur = np.array(state.cur, dtype=np.float32)
|
|
392
415
|
return self._extract_joint_info(joint_cur, joint_id=joint_id)
|
|
393
416
|
|
|
417
|
+
def get_joint_torque(
|
|
418
|
+
self, joint_id: list[int] | int | None = None
|
|
419
|
+
) -> Float[np.ndarray, " N"]:
|
|
420
|
+
"""Gets the torque of all joints in the component.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
joint_id: Optional ID(s) of specific joints to query.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Array of joint torques in component-specific units (Nm).
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
ValueError: If joint torques are not available for this component.
|
|
430
|
+
"""
|
|
431
|
+
state = self._get_state()
|
|
432
|
+
if not hasattr(state, "torque"):
|
|
433
|
+
raise ValueError("Joint torques are not available for this component.")
|
|
434
|
+
joint_torque = np.array(state.torque, dtype=np.float32)
|
|
435
|
+
return self._extract_joint_info(joint_torque, joint_id=joint_id)
|
|
436
|
+
|
|
394
437
|
def get_joint_current_dict(
|
|
395
438
|
self, joint_id: list[int] | int | None = None
|
|
396
439
|
) -> dict[str, float]:
|
|
@@ -421,9 +464,9 @@ class RobotJointComponent(RobotComponent):
|
|
|
421
464
|
ValueError: If joint error codes are not available for this component.
|
|
422
465
|
"""
|
|
423
466
|
state = self._get_state()
|
|
424
|
-
if not hasattr(state, "
|
|
467
|
+
if not hasattr(state, "error"):
|
|
425
468
|
raise ValueError("Joint error codes are not available for this component.")
|
|
426
|
-
joint_err = np.array(state.
|
|
469
|
+
joint_err = np.array(state.error, dtype=np.uint32)
|
|
427
470
|
return self._extract_joint_info(joint_err, joint_id=joint_id)
|
|
428
471
|
|
|
429
472
|
def get_joint_err_dict(
|
|
@@ -457,22 +500,27 @@ class RobotJointComponent(RobotComponent):
|
|
|
457
500
|
ValueError: If joint positions or velocities are not available.
|
|
458
501
|
"""
|
|
459
502
|
state = self._get_state()
|
|
460
|
-
if not hasattr(state, "
|
|
503
|
+
if not hasattr(state, "pos") or not hasattr(state, "vel"):
|
|
461
504
|
raise ValueError(
|
|
462
505
|
"Joint positions or velocities are not available for this component."
|
|
463
506
|
)
|
|
464
507
|
|
|
465
508
|
# Create initial state array with positions and velocities
|
|
466
|
-
joint_pos = np.array(state.
|
|
467
|
-
joint_vel = np.array(state.
|
|
509
|
+
joint_pos = np.array(state.pos, dtype=np.float32)
|
|
510
|
+
joint_vel = np.array(state.vel, dtype=np.float32)
|
|
468
511
|
|
|
469
|
-
if hasattr(state, "
|
|
512
|
+
if hasattr(state, "cur"):
|
|
470
513
|
# If currents are available, include them
|
|
471
|
-
joint_cur = np.array(state.
|
|
514
|
+
joint_cur = np.array(state.cur, dtype=np.float32)
|
|
472
515
|
joint_state = np.stack([joint_pos, joint_vel, joint_cur], axis=1)
|
|
516
|
+
elif hasattr(state, "torque"):
|
|
517
|
+
# If torques are available, include them
|
|
518
|
+
joint_torque = np.array(state.torque, dtype=np.float32)
|
|
519
|
+
joint_state = np.stack([joint_pos, joint_vel, joint_torque], axis=1)
|
|
473
520
|
else:
|
|
474
|
-
|
|
475
|
-
|
|
521
|
+
raise ValueError(
|
|
522
|
+
f"Either current or torque should be available for this {self.__class__.__name__}."
|
|
523
|
+
)
|
|
476
524
|
|
|
477
525
|
return self._extract_joint_info(joint_state, joint_id=joint_id)
|
|
478
526
|
|
|
@@ -543,8 +591,9 @@ class RobotJointComponent(RobotComponent):
|
|
|
543
591
|
joint_cmd = self._convert_joint_cmd_to_array(joint_cmd)
|
|
544
592
|
return self.get_joint_pos() + joint_cmd
|
|
545
593
|
|
|
594
|
+
@staticmethod
|
|
546
595
|
def _extract_joint_info(
|
|
547
|
-
|
|
596
|
+
joint_info: np.ndarray, joint_id: list[int] | int | None = None
|
|
548
597
|
) -> np.ndarray:
|
|
549
598
|
"""Extract the joint information of the component as a numpy array.
|
|
550
599
|
|