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
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
# Copyright (C) 2025 Dexmate Inc.
|
|
2
|
-
#
|
|
3
|
-
# This software is dual-licensed:
|
|
4
|
-
#
|
|
5
|
-
# 1. GNU Affero General Public License v3.0 (AGPL-3.0)
|
|
6
|
-
# See LICENSE-AGPL for details
|
|
7
|
-
#
|
|
8
|
-
# 2. Commercial License
|
|
9
|
-
# For commercial licensing terms, contact: contact@dexmate.ai
|
|
10
|
-
|
|
11
|
-
"""Luxonis wrist camera sensor using RGB and depth Zenoh subscribers.
|
|
12
|
-
|
|
13
|
-
This sensor mirrors the high-level API of other camera sensors. It subscribes to
|
|
14
|
-
RGB (JPEG over Zenoh) and depth streams published by dexsensor's Luxonis camera
|
|
15
|
-
pipeline and exposes a simple interface for getting images.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from __future__ import annotations
|
|
19
|
-
|
|
20
|
-
import logging
|
|
21
|
-
from typing import Any, Dict, Optional, Union
|
|
22
|
-
|
|
23
|
-
import numpy as np
|
|
24
|
-
import zenoh
|
|
25
|
-
|
|
26
|
-
from dexcontrol.config.sensors.cameras.luxonis_camera import LuxonisCameraConfig
|
|
27
|
-
from dexcontrol.utils.subscribers.camera import (
|
|
28
|
-
DepthCameraSubscriber,
|
|
29
|
-
RGBCameraSubscriber,
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
logger = logging.getLogger(__name__)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class LuxonisCameraSensor:
|
|
36
|
-
"""RGBD camera sensor wrapper for Luxonis/OAK wrist camera.
|
|
37
|
-
|
|
38
|
-
Provides access to RGB and depth frames via dedicated Zenoh subscribers.
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
def __init__(
|
|
42
|
-
self,
|
|
43
|
-
configs: LuxonisCameraConfig,
|
|
44
|
-
zenoh_session: zenoh.Session,
|
|
45
|
-
) -> None:
|
|
46
|
-
self._name = configs.name
|
|
47
|
-
self._configs = configs
|
|
48
|
-
self._zenoh_session = zenoh_session
|
|
49
|
-
|
|
50
|
-
# Support left and right RGB streams + depth, mirroring ZED structure
|
|
51
|
-
self._subscribers: Dict[str, Optional[Union[RGBCameraSubscriber, DepthCameraSubscriber]]] = {}
|
|
52
|
-
|
|
53
|
-
subscriber_config = configs.subscriber_config
|
|
54
|
-
|
|
55
|
-
try:
|
|
56
|
-
# Create subscribers for left_rgb, right_rgb, depth (configurable enables)
|
|
57
|
-
stream_defs: Dict[str, Dict[str, Any]] = {
|
|
58
|
-
"left_rgb": subscriber_config.get("left_rgb", {}),
|
|
59
|
-
"right_rgb": subscriber_config.get("right_rgb", {}),
|
|
60
|
-
"depth": subscriber_config.get("depth", {}),
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
for stream_name, cfg in stream_defs.items():
|
|
64
|
-
if not cfg or not cfg.get("enable", False):
|
|
65
|
-
self._subscribers[stream_name] = None
|
|
66
|
-
continue
|
|
67
|
-
topic = cfg.get("topic")
|
|
68
|
-
if not topic:
|
|
69
|
-
logger.warning(f"'{self._name}': No topic configured for '{stream_name}'")
|
|
70
|
-
self._subscribers[stream_name] = None
|
|
71
|
-
continue
|
|
72
|
-
|
|
73
|
-
if stream_name == "depth":
|
|
74
|
-
self._subscribers[stream_name] = DepthCameraSubscriber(
|
|
75
|
-
topic=topic,
|
|
76
|
-
zenoh_session=self._zenoh_session,
|
|
77
|
-
name=f"{self._name}_{stream_name}_subscriber",
|
|
78
|
-
enable_fps_tracking=configs.enable_fps_tracking,
|
|
79
|
-
fps_log_interval=configs.fps_log_interval,
|
|
80
|
-
)
|
|
81
|
-
else:
|
|
82
|
-
self._subscribers[stream_name] = RGBCameraSubscriber(
|
|
83
|
-
topic=topic,
|
|
84
|
-
zenoh_session=self._zenoh_session,
|
|
85
|
-
name=f"{self._name}_{stream_name}_subscriber",
|
|
86
|
-
enable_fps_tracking=configs.enable_fps_tracking,
|
|
87
|
-
fps_log_interval=configs.fps_log_interval,
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
except Exception as e:
|
|
91
|
-
logger.error(f"Error creating Luxonis wrist camera subscribers: {e}")
|
|
92
|
-
|
|
93
|
-
# Lifecycle
|
|
94
|
-
def shutdown(self) -> None:
|
|
95
|
-
for sub in self._subscribers.values():
|
|
96
|
-
if sub:
|
|
97
|
-
sub.shutdown()
|
|
98
|
-
logger.info(f"'{self._name}' sensor shut down.")
|
|
99
|
-
|
|
100
|
-
# Status
|
|
101
|
-
def is_active(self) -> bool:
|
|
102
|
-
return any(sub.is_active() for sub in self._subscribers.values() if sub is not None)
|
|
103
|
-
|
|
104
|
-
def wait_for_active(self, timeout: float = 5.0, require_both: bool = False) -> bool:
|
|
105
|
-
subs = [s for s in self._subscribers.values() if s is not None]
|
|
106
|
-
if not subs:
|
|
107
|
-
return False
|
|
108
|
-
if require_both:
|
|
109
|
-
return all(s.wait_for_active(timeout) for s in subs)
|
|
110
|
-
return any(s.wait_for_active(timeout) for s in subs)
|
|
111
|
-
|
|
112
|
-
# Data access
|
|
113
|
-
def get_obs(
|
|
114
|
-
self, obs_keys: Optional[list[str]] = None, include_timestamp: bool = False
|
|
115
|
-
) -> Dict[str, Optional[np.ndarray]]:
|
|
116
|
-
"""Get latest images.
|
|
117
|
-
|
|
118
|
-
obs_keys can include any of: ["left_rgb", "right_rgb", "depth"]. If None, returns
|
|
119
|
-
all available. If include_timestamp is True and the underlying subscriber returns
|
|
120
|
-
(image, timestamp), that tuple is forwarded; otherwise only the image is returned.
|
|
121
|
-
"""
|
|
122
|
-
keys_to_fetch = obs_keys or self.available_streams
|
|
123
|
-
|
|
124
|
-
out: Dict[str, Optional[np.ndarray]] = {}
|
|
125
|
-
for key in keys_to_fetch:
|
|
126
|
-
sub = self._subscribers.get(key)
|
|
127
|
-
data = sub.get_latest_data() if sub else None
|
|
128
|
-
is_tuple_or_list = isinstance(data, (tuple, list))
|
|
129
|
-
if include_timestamp:
|
|
130
|
-
if not is_tuple_or_list and data is not None:
|
|
131
|
-
logger.warning(f"Timestamp is not available yet for {key} stream.")
|
|
132
|
-
out[key] = data
|
|
133
|
-
else:
|
|
134
|
-
out[key] = data[0] if is_tuple_or_list else data
|
|
135
|
-
return out
|
|
136
|
-
|
|
137
|
-
def get_rgb(self) -> Optional[np.ndarray]:
|
|
138
|
-
# Backward-compat: return left_rgb if available else right_rgb
|
|
139
|
-
for key in ("left_rgb", "right_rgb"):
|
|
140
|
-
sub = self._subscribers.get(key)
|
|
141
|
-
if sub:
|
|
142
|
-
data = sub.get_latest_data()
|
|
143
|
-
return data[0] if isinstance(data, (tuple, list)) else data
|
|
144
|
-
return None
|
|
145
|
-
|
|
146
|
-
def get_depth(self) -> Optional[np.ndarray]:
|
|
147
|
-
sub = self._subscribers.get("depth")
|
|
148
|
-
if not sub:
|
|
149
|
-
return None
|
|
150
|
-
data = sub.get_latest_data()
|
|
151
|
-
return data[0] if isinstance(data, (tuple, list)) else data
|
|
152
|
-
|
|
153
|
-
# Properties
|
|
154
|
-
@property
|
|
155
|
-
def fps(self) -> Dict[str, float]:
|
|
156
|
-
return {name: sub.fps for name, sub in self._subscribers.items() if sub is not None}
|
|
157
|
-
|
|
158
|
-
@property
|
|
159
|
-
def name(self) -> str:
|
|
160
|
-
return self._name
|
|
161
|
-
|
|
162
|
-
@property
|
|
163
|
-
def available_streams(self) -> list:
|
|
164
|
-
return [name for name, sub in self._subscribers.items() if sub is not None]
|
|
165
|
-
|
|
166
|
-
@property
|
|
167
|
-
def active_streams(self) -> list:
|
|
168
|
-
return [name for name, sub in self._subscribers.items() if sub and sub.is_active()]
|
|
169
|
-
|
dexcontrol/utils/motion_utils.py
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
# Copyright (C) 2025 Dexmate Inc.
|
|
2
|
-
#
|
|
3
|
-
# This software is dual-licensed:
|
|
4
|
-
#
|
|
5
|
-
# 1. GNU Affero General Public License v3.0 (AGPL-3.0)
|
|
6
|
-
# See LICENSE-AGPL for details
|
|
7
|
-
#
|
|
8
|
-
# 2. Commercial License
|
|
9
|
-
# For commercial licensing terms, contact: contact@dexmate.ai
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import numpy as np
|
|
13
|
-
import ruckig
|
|
14
|
-
from jaxtyping import Float
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ArmOfflineTrajectorySmoother:
|
|
18
|
-
"""Class to generate smooth trajectories that respect joint limits."""
|
|
19
|
-
|
|
20
|
-
def __init__(
|
|
21
|
-
self,
|
|
22
|
-
dt: float = 0.01,
|
|
23
|
-
safety_factor: float = 2.0,
|
|
24
|
-
) -> None:
|
|
25
|
-
"""Initialize trajectory smoother with motion constraints.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
dt: Control cycle time in seconds. Must be positive.
|
|
29
|
-
safety_factor: Factor to scale down motion limits for additional safety.
|
|
30
|
-
Must be positive. Larger values mean more conservative motion.
|
|
31
|
-
|
|
32
|
-
Raises:
|
|
33
|
-
ValueError: If dt or safety_factor is not positive.
|
|
34
|
-
"""
|
|
35
|
-
if dt <= 0:
|
|
36
|
-
raise ValueError("Control cycle must be positive")
|
|
37
|
-
if safety_factor <= 0:
|
|
38
|
-
raise ValueError("Safety factor must be positive")
|
|
39
|
-
|
|
40
|
-
self.arm_dof = 7 # 7 degrees of freedom for each arm
|
|
41
|
-
self.dt = dt
|
|
42
|
-
|
|
43
|
-
# Initialize Ruckig Online Trajectory Generation
|
|
44
|
-
self.otg = ruckig.Ruckig(self.arm_dof, dt)
|
|
45
|
-
self.inp = ruckig.InputParameter(self.arm_dof)
|
|
46
|
-
self.out = ruckig.OutputParameter(self.arm_dof)
|
|
47
|
-
|
|
48
|
-
# Set motion limits for each joint (in radians)
|
|
49
|
-
# Convert degrees to radians and apply safety factor
|
|
50
|
-
self.inp.max_velocity = (
|
|
51
|
-
np.deg2rad([180, 180, 220, 220, 220, 220, 220]) / safety_factor
|
|
52
|
-
)
|
|
53
|
-
self.inp.max_acceleration = (
|
|
54
|
-
np.deg2rad([600, 600, 600, 600, 600, 600, 600]) / safety_factor
|
|
55
|
-
)
|
|
56
|
-
self.inp.max_jerk = (
|
|
57
|
-
np.deg2rad([6000, 6000, 6000, 6000, 6000, 6000, 6000]) / safety_factor
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
def smooth_trajectory(
|
|
61
|
-
self, waypoints: Float[np.ndarray, "N 7"], trajectory_dt: float
|
|
62
|
-
) -> tuple[Float[np.ndarray, "M 7"], Float[np.ndarray, "M 7"]]:
|
|
63
|
-
"""Generate time-optimal smooth trajectory through waypoints.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
waypoints: Array of joint positions with shape (N, 7) where N is the number
|
|
67
|
-
of waypoints and 7 is the number of joints.
|
|
68
|
-
trajectory_dt: Desired time duration between waypoints (unit: s).
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
A tuple containing:
|
|
72
|
-
- Position trajectory with shape (M, 7)
|
|
73
|
-
- Velocity trajectory with shape (M, 7)
|
|
74
|
-
|
|
75
|
-
Raises:
|
|
76
|
-
ValueError: If fewer than 2 waypoints are provided or if they don't match
|
|
77
|
-
the expected 7-DOF format.
|
|
78
|
-
"""
|
|
79
|
-
if len(waypoints) < 2:
|
|
80
|
-
raise ValueError("At least two waypoints are required")
|
|
81
|
-
if waypoints.shape[1] != self.arm_dof:
|
|
82
|
-
raise ValueError(f"Waypoints must have shape (N, {self.arm_dof})")
|
|
83
|
-
|
|
84
|
-
pos_traj = []
|
|
85
|
-
vel_traj = []
|
|
86
|
-
n_intermediate_points = int(trajectory_dt / self.dt)
|
|
87
|
-
self.inp.current_position = waypoints[0]
|
|
88
|
-
|
|
89
|
-
# Generate smooth trajectory segments between waypoints
|
|
90
|
-
for i in range(1, len(waypoints)):
|
|
91
|
-
self.inp.target_position = waypoints[i]
|
|
92
|
-
self.inp.target_velocity = [0.0] * self.arm_dof
|
|
93
|
-
|
|
94
|
-
for _ in range(n_intermediate_points):
|
|
95
|
-
_ = self.otg.update(self.inp, self.out)
|
|
96
|
-
pos_traj.append(self.out.new_position)
|
|
97
|
-
vel_traj.append(self.out.new_velocity)
|
|
98
|
-
self.out.pass_to_input(self.inp)
|
|
99
|
-
|
|
100
|
-
return np.array(pos_traj), np.array(vel_traj)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
class ArmOnlineTrajectorySmoother:
|
|
104
|
-
def __init__(
|
|
105
|
-
self,
|
|
106
|
-
init_qpos: np.ndarray,
|
|
107
|
-
control_cycle: float = 0.005,
|
|
108
|
-
safety_factor: float = 2.0,
|
|
109
|
-
) -> None:
|
|
110
|
-
self.dof = len(init_qpos)
|
|
111
|
-
|
|
112
|
-
# Initialize Ruckig
|
|
113
|
-
self.otg = ruckig.Ruckig(self.dof, control_cycle)
|
|
114
|
-
self.inp = ruckig.InputParameter(self.dof)
|
|
115
|
-
self.out = ruckig.OutputParameter(self.dof)
|
|
116
|
-
self.inp.current_position = init_qpos
|
|
117
|
-
|
|
118
|
-
# Set motion limits
|
|
119
|
-
self.inp.max_velocity = (
|
|
120
|
-
np.deg2rad([180, 180, 220, 220, 220, 220, 220]) / safety_factor
|
|
121
|
-
)
|
|
122
|
-
self.inp.max_acceleration = (
|
|
123
|
-
np.deg2rad([600, 600, 600, 600, 600, 600, 600]) / safety_factor
|
|
124
|
-
)
|
|
125
|
-
self.inp.max_jerk = (
|
|
126
|
-
np.deg2rad([6000, 6000, 6000, 6000, 6000, 6000, 6000]) / safety_factor
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
def update(
|
|
130
|
-
self, target_position: np.ndarray | None = None
|
|
131
|
-
) -> tuple[np.ndarray, np.ndarray]:
|
|
132
|
-
"""Update trajectory with new target position."""
|
|
133
|
-
if target_position is not None:
|
|
134
|
-
self.inp.target_position = target_position
|
|
135
|
-
self.inp.target_velocity = [0.0] * self.dof
|
|
136
|
-
|
|
137
|
-
_ = self.otg.update(self.inp, self.out)
|
|
138
|
-
self.out.pass_to_input(self.inp)
|
|
139
|
-
|
|
140
|
-
return np.array(self.out.new_position), np.array(self.out.new_velocity)
|
|
141
|
-
|
|
142
|
-
def reset(self, init_qpos: np.ndarray):
|
|
143
|
-
self.inp.current_position = init_qpos
|
|
144
|
-
self.inp.current_velocity = [0.0] * self.dof
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def linear_interpolation(
|
|
148
|
-
start: Float[np.ndarray, " DoF"],
|
|
149
|
-
end: Float[np.ndarray, " DoF"],
|
|
150
|
-
steps: int,
|
|
151
|
-
) -> Float[np.ndarray, "steps DoF"]:
|
|
152
|
-
"""Generate linear interpolation between start and end joint positions.
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
start: Starting joint positions array of shape (DoF,)
|
|
156
|
-
end: Ending joint positions array of shape (DoF,)
|
|
157
|
-
steps: Number of interpolation steps (including start and end points)
|
|
158
|
-
|
|
159
|
-
Returns:
|
|
160
|
-
Array of interpolated joint positions with shape (steps, DoF)
|
|
161
|
-
where DoF is the degrees of freedom (matches input dimensions)
|
|
162
|
-
"""
|
|
163
|
-
t = np.linspace(0, 1, steps)[:, None]
|
|
164
|
-
return (1 - t) * start + t * end
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def plan_arm_move_to_target(
|
|
168
|
-
init_qpos: Float[np.ndarray, "7"],
|
|
169
|
-
target_qpos: Float[np.ndarray, "7"],
|
|
170
|
-
max_qv: float,
|
|
171
|
-
dt: float = 0.01,
|
|
172
|
-
) -> tuple[Float[np.ndarray, "steps 7"], Float[np.ndarray, "steps 7"]]:
|
|
173
|
-
"""Plan a smooth arm movement from initial to target joint positions.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
init_qpos: Initial joint positions array of shape (7,)
|
|
177
|
-
target_qpos: Target joint positions array of shape (7,)
|
|
178
|
-
max_qv: Maximum allowed joint velocity magnitude (radians/second)
|
|
179
|
-
dt: Control cycle time in seconds (default: 0.01)
|
|
180
|
-
|
|
181
|
-
Returns:
|
|
182
|
-
A tuple containing:
|
|
183
|
-
- Position trajectory with shape (steps, 7)
|
|
184
|
-
- Velocity trajectory with shape (steps, 7)
|
|
185
|
-
Both trajectories include the final target state.
|
|
186
|
-
"""
|
|
187
|
-
duration = np.linalg.norm(target_qpos - init_qpos) / max_qv
|
|
188
|
-
steps = int(duration / dt)
|
|
189
|
-
trajectory = linear_interpolation(init_qpos, target_qpos, steps)
|
|
190
|
-
|
|
191
|
-
# Smooth the trajectory while respecting motion constraints
|
|
192
|
-
smoother = ArmOfflineTrajectorySmoother(dt=dt)
|
|
193
|
-
pos_traj, vel_traj = smoother.smooth_trajectory(trajectory, dt)
|
|
194
|
-
|
|
195
|
-
# Append final target state with zero velocity
|
|
196
|
-
pos_traj = np.concatenate([pos_traj, target_qpos[None, :]])
|
|
197
|
-
vel_traj = np.concatenate([vel_traj, np.zeros((1, 7))])
|
|
198
|
-
|
|
199
|
-
return pos_traj, vel_traj
|
dexcontrol/utils/rate_limiter.py
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
# Copyright (C) 2025 Dexmate Inc.
|
|
2
|
-
#
|
|
3
|
-
# This software is dual-licensed:
|
|
4
|
-
#
|
|
5
|
-
# 1. GNU Affero General Public License v3.0 (AGPL-3.0)
|
|
6
|
-
# See LICENSE-AGPL for details
|
|
7
|
-
#
|
|
8
|
-
# 2. Commercial License
|
|
9
|
-
# For commercial licensing terms, contact: contact@dexmate.ai
|
|
10
|
-
|
|
11
|
-
"""Rate limiter utility for maintaining consistent execution rates."""
|
|
12
|
-
|
|
13
|
-
import sys
|
|
14
|
-
import time
|
|
15
|
-
from typing import Final
|
|
16
|
-
|
|
17
|
-
from loguru import logger
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class RateLimiter:
|
|
21
|
-
"""Class for limiting execution rate to a target frequency.
|
|
22
|
-
|
|
23
|
-
This class provides rate limiting functionality by sleeping between iterations to
|
|
24
|
-
maintain a desired execution frequency. It also tracks statistics about the achieved
|
|
25
|
-
rate and missed deadlines.
|
|
26
|
-
|
|
27
|
-
Attributes:
|
|
28
|
-
period_sec: Time period between iterations in seconds.
|
|
29
|
-
target_rate_hz: Desired execution rate in Hz.
|
|
30
|
-
window_size: Size of moving average window for rate calculations.
|
|
31
|
-
last_time_sec: Timestamp of the last iteration.
|
|
32
|
-
next_time_sec: Scheduled timestamp for the next iteration.
|
|
33
|
-
start_time_sec: Timestamp when the rate limiter was initialized or reset.
|
|
34
|
-
duration_buffer: List of recent iteration durations for rate calculation.
|
|
35
|
-
missed_deadlines: Counter for iterations that missed their scheduled time.
|
|
36
|
-
iterations: Total number of iterations since initialization or reset.
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
def __init__(self, rate_hz: float, window_size: int = 50) -> None:
|
|
40
|
-
"""Initializes rate limiter.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
rate_hz: Desired rate in Hz.
|
|
44
|
-
window_size: Size of moving average window for rate calculations.
|
|
45
|
-
|
|
46
|
-
Raises:
|
|
47
|
-
ValueError: If rate_hz is not positive.
|
|
48
|
-
"""
|
|
49
|
-
if rate_hz <= 0:
|
|
50
|
-
raise ValueError("Rate must be positive")
|
|
51
|
-
|
|
52
|
-
self.period_sec: Final[float] = 1.0 / rate_hz
|
|
53
|
-
self.target_rate_hz: Final[float] = rate_hz
|
|
54
|
-
self.window_size: Final[int] = max(1, window_size)
|
|
55
|
-
|
|
56
|
-
# Initialize timing variables
|
|
57
|
-
now_sec = time.monotonic()
|
|
58
|
-
self.last_time_sec: float = now_sec
|
|
59
|
-
self.next_time_sec: float = now_sec + self.period_sec
|
|
60
|
-
self.start_time_sec: float = now_sec
|
|
61
|
-
|
|
62
|
-
# Initialize statistics
|
|
63
|
-
self.duration_buffer: list[float] = []
|
|
64
|
-
self.missed_deadlines: int = 0
|
|
65
|
-
self.iterations: int = 0
|
|
66
|
-
self._MAX_ITERATIONS: Final[int] = sys.maxsize - 1000 # Leave some buffer
|
|
67
|
-
|
|
68
|
-
def sleep(self) -> None:
|
|
69
|
-
"""Sleeps to maintain desired rate.
|
|
70
|
-
|
|
71
|
-
Sleeps for the appropriate duration to maintain the target rate. If the next
|
|
72
|
-
scheduled time has already passed, increments the missed deadlines counter.
|
|
73
|
-
Uses monotonic time for reliable timing regardless of system clock changes.
|
|
74
|
-
"""
|
|
75
|
-
current_time_sec = time.monotonic()
|
|
76
|
-
sleep_time_sec = self.next_time_sec - current_time_sec
|
|
77
|
-
|
|
78
|
-
if sleep_time_sec > 0:
|
|
79
|
-
time.sleep(sleep_time_sec)
|
|
80
|
-
else:
|
|
81
|
-
self.missed_deadlines += 1
|
|
82
|
-
|
|
83
|
-
# Update timing and statistics
|
|
84
|
-
now_sec = time.monotonic()
|
|
85
|
-
|
|
86
|
-
# Reset iterations if approaching max value
|
|
87
|
-
if self.iterations >= self._MAX_ITERATIONS:
|
|
88
|
-
logger.warning(
|
|
89
|
-
"Iteration counter approaching max value, resetting statistics"
|
|
90
|
-
)
|
|
91
|
-
self.reset()
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
self.iterations += 1
|
|
95
|
-
|
|
96
|
-
if self.iterations > 1: # Skip first iteration
|
|
97
|
-
duration_sec = now_sec - self.last_time_sec
|
|
98
|
-
if len(self.duration_buffer) >= self.window_size:
|
|
99
|
-
self.duration_buffer.pop(0)
|
|
100
|
-
self.duration_buffer.append(duration_sec)
|
|
101
|
-
|
|
102
|
-
self.last_time_sec = now_sec
|
|
103
|
-
|
|
104
|
-
# More efficient way to advance next_time_sec when multiple periods behind
|
|
105
|
-
periods_behind = max(
|
|
106
|
-
0, int((now_sec - self.next_time_sec) / self.period_sec) + 1
|
|
107
|
-
)
|
|
108
|
-
self.next_time_sec += periods_behind * self.period_sec
|
|
109
|
-
|
|
110
|
-
def get_actual_rate(self) -> float:
|
|
111
|
-
"""Calculates actual achieved rate in Hz using moving average.
|
|
112
|
-
|
|
113
|
-
Returns:
|
|
114
|
-
Current execution rate based on recent iterations.
|
|
115
|
-
"""
|
|
116
|
-
if not self.duration_buffer:
|
|
117
|
-
return 0.0
|
|
118
|
-
avg_duration_sec = sum(self.duration_buffer) / len(self.duration_buffer)
|
|
119
|
-
return 0.0 if avg_duration_sec <= 0 else 1.0 / avg_duration_sec
|
|
120
|
-
|
|
121
|
-
def get_average_rate(self) -> float:
|
|
122
|
-
"""Calculates average rate over entire run.
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
Average execution rate since start or last reset.
|
|
126
|
-
"""
|
|
127
|
-
if self.iterations < 2:
|
|
128
|
-
return 0.0
|
|
129
|
-
total_time_sec = time.monotonic() - self.start_time_sec
|
|
130
|
-
return 0.0 if total_time_sec <= 0 else self.iterations / total_time_sec
|
|
131
|
-
|
|
132
|
-
def reset(self) -> None:
|
|
133
|
-
"""Resets the rate limiter state and statistics."""
|
|
134
|
-
now_sec = time.monotonic()
|
|
135
|
-
self.last_time_sec = now_sec
|
|
136
|
-
self.next_time_sec = now_sec + self.period_sec
|
|
137
|
-
self.start_time_sec = now_sec
|
|
138
|
-
self.duration_buffer.clear()
|
|
139
|
-
self.missed_deadlines = 0
|
|
140
|
-
self.iterations = 0
|
|
141
|
-
|
|
142
|
-
def get_stats(self) -> dict[str, float | int]:
|
|
143
|
-
"""Gets runtime statistics.
|
|
144
|
-
|
|
145
|
-
Returns:
|
|
146
|
-
Dictionary containing execution statistics including actual rate,
|
|
147
|
-
average rate, target rate, missed deadlines and iteration count.
|
|
148
|
-
"""
|
|
149
|
-
return {
|
|
150
|
-
"actual_rate": self.get_actual_rate(),
|
|
151
|
-
"average_rate": self.get_average_rate(),
|
|
152
|
-
"target_rate": self.target_rate_hz,
|
|
153
|
-
"missed_deadlines": self.missed_deadlines,
|
|
154
|
-
"iterations": self.iterations,
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if __name__ == "__main__":
|
|
159
|
-
rate_limiter = RateLimiter(100.0) # 100Hz
|
|
160
|
-
|
|
161
|
-
try:
|
|
162
|
-
while True:
|
|
163
|
-
rate_limiter.sleep()
|
|
164
|
-
actual_rate = rate_limiter.get_actual_rate()
|
|
165
|
-
logger.info(f"Rate: {actual_rate:.2f} Hz")
|
|
166
|
-
|
|
167
|
-
except KeyboardInterrupt:
|
|
168
|
-
stats = rate_limiter.get_stats()
|
|
169
|
-
logger.info("\nFinal stats:")
|
|
170
|
-
logger.info(f"Average rate: {stats['average_rate']:.2f} Hz")
|
|
171
|
-
logger.info(f"Final rate: {stats['actual_rate']:.2f} Hz")
|
|
172
|
-
logger.info(f"Missed deadlines: {stats['missed_deadlines']}")
|
dexcontrol/utils/rtc_utils.py
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
# Copyright (C) 2025 Dexmate Inc.
|
|
2
|
-
#
|
|
3
|
-
# This software is dual-licensed:
|
|
4
|
-
#
|
|
5
|
-
# 1. GNU Affero General Public License v3.0 (AGPL-3.0)
|
|
6
|
-
# See LICENSE-AGPL for details
|
|
7
|
-
#
|
|
8
|
-
# 2. Commercial License
|
|
9
|
-
# For commercial licensing terms, contact: contact@dexmate.ai
|
|
10
|
-
|
|
11
|
-
"""RTC utilities for dexcontrol.
|
|
12
|
-
|
|
13
|
-
This module provides utility functions for creating RTC subscribers
|
|
14
|
-
that first query Zenoh for connection information.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
import zenoh
|
|
18
|
-
from loguru import logger
|
|
19
|
-
|
|
20
|
-
from dexcontrol.utils.subscribers.rtc import RTCSubscriber
|
|
21
|
-
from dexcontrol.utils.zenoh_utils import query_zenoh_json
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def query_rtc_info(
|
|
25
|
-
zenoh_session: zenoh.Session,
|
|
26
|
-
info_topic: str,
|
|
27
|
-
timeout: float = 2.0,
|
|
28
|
-
max_retries: int = 1,
|
|
29
|
-
retry_delay: float = 0.5,
|
|
30
|
-
) -> dict | None:
|
|
31
|
-
"""Query Zenoh for RTC connection information.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
zenoh_session: Active Zenoh session for communication.
|
|
35
|
-
info_topic: Zenoh topic to query for RTC info.
|
|
36
|
-
timeout: Maximum time to wait for a response in seconds.
|
|
37
|
-
max_retries: Maximum number of retry attempts.
|
|
38
|
-
retry_delay: Initial delay between retries (doubles each retry).
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
Dictionary containing host and port information if successful, None otherwise.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
# Use the general Zenoh query function
|
|
45
|
-
info = query_zenoh_json(
|
|
46
|
-
zenoh_session=zenoh_session,
|
|
47
|
-
topic=info_topic,
|
|
48
|
-
timeout=timeout,
|
|
49
|
-
max_retries=max_retries,
|
|
50
|
-
retry_delay=retry_delay,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
return info
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def create_rtc_subscriber_from_zenoh(
|
|
57
|
-
zenoh_session: zenoh.Session,
|
|
58
|
-
info_topic: str,
|
|
59
|
-
name: str = "rtc_subscriber",
|
|
60
|
-
enable_fps_tracking: bool = True,
|
|
61
|
-
fps_log_interval: int = 100,
|
|
62
|
-
query_timeout: float = 2.0,
|
|
63
|
-
max_retries: int = 1,
|
|
64
|
-
) -> RTCSubscriber | None:
|
|
65
|
-
"""Create a RTC subscriber by first querying Zenoh for connection info.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
zenoh_session: Active Zenoh session for communication.
|
|
69
|
-
info_topic: Zenoh topic to query for RTC connection info.
|
|
70
|
-
name: Name for logging purposes.
|
|
71
|
-
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
72
|
-
fps_log_interval: Number of frames between FPS calculations.
|
|
73
|
-
query_timeout: Maximum time to wait for Zenoh query response.
|
|
74
|
-
max_retries: Maximum number of retry attempts for Zenoh query.
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
RTCSubscriber instance if successful, None otherwise.
|
|
78
|
-
"""
|
|
79
|
-
# Query Zenoh for RTC connection information
|
|
80
|
-
rtc_info = query_rtc_info(zenoh_session, info_topic, query_timeout, max_retries)
|
|
81
|
-
|
|
82
|
-
if rtc_info is None:
|
|
83
|
-
logger.error("Failed to get RTC connection info from Zenoh")
|
|
84
|
-
return None
|
|
85
|
-
|
|
86
|
-
url = rtc_info.get("signaling_url")
|
|
87
|
-
|
|
88
|
-
if not url:
|
|
89
|
-
logger.error(f"Invalid RTC info: url={url}")
|
|
90
|
-
return None
|
|
91
|
-
|
|
92
|
-
# Construct WebSocket URL
|
|
93
|
-
ws_url = url
|
|
94
|
-
logger.info(f"Creating RTC subscriber with URL: {ws_url}")
|
|
95
|
-
|
|
96
|
-
try:
|
|
97
|
-
# Create and return the RTC subscriber
|
|
98
|
-
subscriber = RTCSubscriber(
|
|
99
|
-
url=ws_url,
|
|
100
|
-
name=name,
|
|
101
|
-
enable_fps_tracking=enable_fps_tracking,
|
|
102
|
-
fps_log_interval=fps_log_interval,
|
|
103
|
-
)
|
|
104
|
-
return subscriber
|
|
105
|
-
except Exception as e:
|
|
106
|
-
logger.error(f"Failed to create RTC subscriber: {e}")
|
|
107
|
-
return None
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def create_rtc_subscriber_with_config(
|
|
111
|
-
zenoh_session: zenoh.Session,
|
|
112
|
-
config,
|
|
113
|
-
name: str = "rtc_subscriber",
|
|
114
|
-
enable_fps_tracking: bool = True,
|
|
115
|
-
fps_log_interval: int = 100,
|
|
116
|
-
) -> RTCSubscriber | None:
|
|
117
|
-
"""Create a RTC subscriber using configuration object.
|
|
118
|
-
|
|
119
|
-
Args:
|
|
120
|
-
zenoh_session: Active Zenoh session for communication.
|
|
121
|
-
config: Configuration object containing info_key.
|
|
122
|
-
name: Name for logging purposes.
|
|
123
|
-
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
124
|
-
fps_log_interval: Number of frames between FPS calculations.
|
|
125
|
-
Returns:
|
|
126
|
-
RTCSubscriber instance if successful, None otherwise.
|
|
127
|
-
"""
|
|
128
|
-
if "info_key" not in config:
|
|
129
|
-
logger.error("Config subscriber_config missing info_key")
|
|
130
|
-
return None
|
|
131
|
-
|
|
132
|
-
if not config["enable"]:
|
|
133
|
-
logger.info(f"Skipping {name} because it is disabled")
|
|
134
|
-
return None
|
|
135
|
-
|
|
136
|
-
info_topic = config["info_key"]
|
|
137
|
-
|
|
138
|
-
return create_rtc_subscriber_from_zenoh(
|
|
139
|
-
zenoh_session=zenoh_session,
|
|
140
|
-
info_topic=info_topic,
|
|
141
|
-
name=name,
|
|
142
|
-
enable_fps_tracking=enable_fps_tracking,
|
|
143
|
-
fps_log_interval=fps_log_interval,
|
|
144
|
-
)
|