dexcontrol 0.2.1__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/__init__.py +45 -0
- dexcontrol/apps/dualsense_teleop_base.py +371 -0
- dexcontrol/config/__init__.py +14 -0
- dexcontrol/config/core/__init__.py +22 -0
- dexcontrol/config/core/arm.py +32 -0
- dexcontrol/config/core/chassis.py +22 -0
- dexcontrol/config/core/hand.py +42 -0
- dexcontrol/config/core/head.py +33 -0
- dexcontrol/config/core/misc.py +37 -0
- dexcontrol/config/core/torso.py +36 -0
- dexcontrol/config/sensors/__init__.py +4 -0
- dexcontrol/config/sensors/cameras/__init__.py +7 -0
- dexcontrol/config/sensors/cameras/gemini_camera.py +16 -0
- dexcontrol/config/sensors/cameras/rgb_camera.py +15 -0
- dexcontrol/config/sensors/imu/__init__.py +6 -0
- dexcontrol/config/sensors/imu/gemini_imu.py +15 -0
- dexcontrol/config/sensors/imu/nine_axis_imu.py +15 -0
- dexcontrol/config/sensors/lidar/__init__.py +6 -0
- dexcontrol/config/sensors/lidar/rplidar.py +15 -0
- dexcontrol/config/sensors/ultrasonic/__init__.py +6 -0
- dexcontrol/config/sensors/ultrasonic/ultrasonic.py +15 -0
- dexcontrol/config/sensors/vega_sensors.py +65 -0
- dexcontrol/config/vega.py +203 -0
- dexcontrol/core/__init__.py +0 -0
- dexcontrol/core/arm.py +324 -0
- dexcontrol/core/chassis.py +628 -0
- dexcontrol/core/component.py +834 -0
- dexcontrol/core/hand.py +170 -0
- dexcontrol/core/head.py +232 -0
- dexcontrol/core/misc.py +514 -0
- dexcontrol/core/torso.py +198 -0
- dexcontrol/proto/dexcontrol_msg_pb2.py +69 -0
- dexcontrol/proto/dexcontrol_msg_pb2.pyi +168 -0
- dexcontrol/proto/dexcontrol_query_pb2.py +73 -0
- dexcontrol/proto/dexcontrol_query_pb2.pyi +134 -0
- dexcontrol/robot.py +1091 -0
- dexcontrol/sensors/__init__.py +40 -0
- dexcontrol/sensors/camera/__init__.py +18 -0
- dexcontrol/sensors/camera/gemini_camera.py +139 -0
- dexcontrol/sensors/camera/rgb_camera.py +98 -0
- dexcontrol/sensors/imu/__init__.py +22 -0
- dexcontrol/sensors/imu/gemini_imu.py +139 -0
- dexcontrol/sensors/imu/nine_axis_imu.py +149 -0
- dexcontrol/sensors/lidar/__init__.py +3 -0
- dexcontrol/sensors/lidar/rplidar.py +164 -0
- dexcontrol/sensors/manager.py +185 -0
- dexcontrol/sensors/ultrasonic.py +110 -0
- dexcontrol/utils/__init__.py +15 -0
- dexcontrol/utils/constants.py +12 -0
- dexcontrol/utils/io_utils.py +26 -0
- dexcontrol/utils/motion_utils.py +194 -0
- dexcontrol/utils/os_utils.py +39 -0
- dexcontrol/utils/pb_utils.py +103 -0
- dexcontrol/utils/rate_limiter.py +167 -0
- dexcontrol/utils/reset_orbbec_camera_usb.py +98 -0
- dexcontrol/utils/subscribers/__init__.py +44 -0
- dexcontrol/utils/subscribers/base.py +260 -0
- dexcontrol/utils/subscribers/camera.py +328 -0
- dexcontrol/utils/subscribers/decoders.py +83 -0
- dexcontrol/utils/subscribers/generic.py +105 -0
- dexcontrol/utils/subscribers/imu.py +170 -0
- dexcontrol/utils/subscribers/lidar.py +195 -0
- dexcontrol/utils/subscribers/protobuf.py +106 -0
- dexcontrol/utils/timer.py +136 -0
- dexcontrol/utils/trajectory_utils.py +40 -0
- dexcontrol/utils/viz_utils.py +86 -0
- dexcontrol-0.2.1.dist-info/METADATA +369 -0
- dexcontrol-0.2.1.dist-info/RECORD +72 -0
- dexcontrol-0.2.1.dist-info/WHEEL +5 -0
- dexcontrol-0.2.1.dist-info/licenses/LICENSE +188 -0
- dexcontrol-0.2.1.dist-info/licenses/NOTICE +13 -0
- dexcontrol-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 with Commons Clause License
|
|
4
|
+
# Condition v1.0 [see LICENSE for details].
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import ruckig
|
|
9
|
+
from jaxtyping import Float
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ArmOfflineTrajectorySmoother:
|
|
13
|
+
"""Class to generate smooth trajectories that respect joint limits."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
dt: float = 0.01,
|
|
18
|
+
safety_factor: float = 2.0,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Initialize trajectory smoother with motion constraints.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
dt: Control cycle time in seconds. Must be positive.
|
|
24
|
+
safety_factor: Factor to scale down motion limits for additional safety.
|
|
25
|
+
Must be positive. Larger values mean more conservative motion.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: If dt or safety_factor is not positive.
|
|
29
|
+
"""
|
|
30
|
+
if dt <= 0:
|
|
31
|
+
raise ValueError("Control cycle must be positive")
|
|
32
|
+
if safety_factor <= 0:
|
|
33
|
+
raise ValueError("Safety factor must be positive")
|
|
34
|
+
|
|
35
|
+
self.arm_dof = 7 # 7 degrees of freedom for each arm
|
|
36
|
+
self.dt = dt
|
|
37
|
+
|
|
38
|
+
# Initialize Ruckig Online Trajectory Generation
|
|
39
|
+
self.otg = ruckig.Ruckig(self.arm_dof, dt)
|
|
40
|
+
self.inp = ruckig.InputParameter(self.arm_dof)
|
|
41
|
+
self.out = ruckig.OutputParameter(self.arm_dof)
|
|
42
|
+
|
|
43
|
+
# Set motion limits for each joint (in radians)
|
|
44
|
+
# Convert degrees to radians and apply safety factor
|
|
45
|
+
self.inp.max_velocity = (
|
|
46
|
+
np.deg2rad([180, 180, 220, 220, 220, 220, 220]) / safety_factor
|
|
47
|
+
)
|
|
48
|
+
self.inp.max_acceleration = (
|
|
49
|
+
np.deg2rad([600, 600, 600, 600, 600, 600, 600]) / safety_factor
|
|
50
|
+
)
|
|
51
|
+
self.inp.max_jerk = (
|
|
52
|
+
np.deg2rad([6000, 6000, 6000, 6000, 6000, 6000, 6000]) / safety_factor
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def smooth_trajectory(
|
|
56
|
+
self, waypoints: Float[np.ndarray, "N 7"], trajectory_dt: float
|
|
57
|
+
) -> tuple[Float[np.ndarray, "M 7"], Float[np.ndarray, "M 7"]]:
|
|
58
|
+
"""Generate time-optimal smooth trajectory through waypoints.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
waypoints: Array of joint positions with shape (N, 7) where N is the number
|
|
62
|
+
of waypoints and 7 is the number of joints.
|
|
63
|
+
trajectory_dt: Desired time duration between waypoints (unit: s).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A tuple containing:
|
|
67
|
+
- Position trajectory with shape (M, 7)
|
|
68
|
+
- Velocity trajectory with shape (M, 7)
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If fewer than 2 waypoints are provided or if they don't match
|
|
72
|
+
the expected 7-DOF format.
|
|
73
|
+
"""
|
|
74
|
+
if len(waypoints) < 2:
|
|
75
|
+
raise ValueError("At least two waypoints are required")
|
|
76
|
+
if waypoints.shape[1] != self.arm_dof:
|
|
77
|
+
raise ValueError(f"Waypoints must have shape (N, {self.arm_dof})")
|
|
78
|
+
|
|
79
|
+
pos_traj = []
|
|
80
|
+
vel_traj = []
|
|
81
|
+
n_intermediate_points = int(trajectory_dt / self.dt)
|
|
82
|
+
self.inp.current_position = waypoints[0]
|
|
83
|
+
|
|
84
|
+
# Generate smooth trajectory segments between waypoints
|
|
85
|
+
for i in range(1, len(waypoints)):
|
|
86
|
+
self.inp.target_position = waypoints[i]
|
|
87
|
+
self.inp.target_velocity = [0.0] * self.arm_dof
|
|
88
|
+
|
|
89
|
+
for _ in range(n_intermediate_points):
|
|
90
|
+
_ = self.otg.update(self.inp, self.out)
|
|
91
|
+
pos_traj.append(self.out.new_position)
|
|
92
|
+
vel_traj.append(self.out.new_velocity)
|
|
93
|
+
self.out.pass_to_input(self.inp)
|
|
94
|
+
|
|
95
|
+
return np.array(pos_traj), np.array(vel_traj)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ArmOnlineTrajectorySmoother:
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
init_qpos: np.ndarray,
|
|
102
|
+
control_cycle: float = 0.005,
|
|
103
|
+
safety_factor: float = 2.0,
|
|
104
|
+
) -> None:
|
|
105
|
+
self.dof = len(init_qpos)
|
|
106
|
+
|
|
107
|
+
# Initialize Ruckig
|
|
108
|
+
self.otg = ruckig.Ruckig(self.dof, control_cycle)
|
|
109
|
+
self.inp = ruckig.InputParameter(self.dof)
|
|
110
|
+
self.out = ruckig.OutputParameter(self.dof)
|
|
111
|
+
self.inp.current_position = init_qpos
|
|
112
|
+
|
|
113
|
+
# Set motion limits
|
|
114
|
+
self.inp.max_velocity = (
|
|
115
|
+
np.deg2rad([180, 180, 220, 220, 220, 220, 220]) / safety_factor
|
|
116
|
+
)
|
|
117
|
+
self.inp.max_acceleration = (
|
|
118
|
+
np.deg2rad([600, 600, 600, 600, 600, 600, 600]) / safety_factor
|
|
119
|
+
)
|
|
120
|
+
self.inp.max_jerk = (
|
|
121
|
+
np.deg2rad([6000, 6000, 6000, 6000, 6000, 6000, 6000]) / safety_factor
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def update(
|
|
125
|
+
self, target_position: np.ndarray | None = None
|
|
126
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
127
|
+
"""Update trajectory with new target position."""
|
|
128
|
+
if target_position is not None:
|
|
129
|
+
self.inp.target_position = target_position
|
|
130
|
+
self.inp.target_velocity = [0.0] * self.dof
|
|
131
|
+
|
|
132
|
+
_ = self.otg.update(self.inp, self.out)
|
|
133
|
+
self.out.pass_to_input(self.inp)
|
|
134
|
+
|
|
135
|
+
return np.array(self.out.new_position), np.array(self.out.new_velocity)
|
|
136
|
+
|
|
137
|
+
def reset(self, init_qpos: np.ndarray):
|
|
138
|
+
self.inp.current_position = init_qpos
|
|
139
|
+
self.inp.current_velocity = [0.0] * self.dof
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def linear_interpolation(
|
|
143
|
+
start: Float[np.ndarray, " DoF"],
|
|
144
|
+
end: Float[np.ndarray, " DoF"],
|
|
145
|
+
steps: int,
|
|
146
|
+
) -> Float[np.ndarray, "steps DoF"]:
|
|
147
|
+
"""Generate linear interpolation between start and end joint positions.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
start: Starting joint positions array of shape (DoF,)
|
|
151
|
+
end: Ending joint positions array of shape (DoF,)
|
|
152
|
+
steps: Number of interpolation steps (including start and end points)
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Array of interpolated joint positions with shape (steps, DoF)
|
|
156
|
+
where DoF is the degrees of freedom (matches input dimensions)
|
|
157
|
+
"""
|
|
158
|
+
t = np.linspace(0, 1, steps)[:, None]
|
|
159
|
+
return (1 - t) * start + t * end
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def plan_arm_move_to_target(
|
|
163
|
+
init_qpos: Float[np.ndarray, "7"],
|
|
164
|
+
target_qpos: Float[np.ndarray, "7"],
|
|
165
|
+
max_qv: float,
|
|
166
|
+
dt: float = 0.01,
|
|
167
|
+
) -> tuple[Float[np.ndarray, "steps 7"], Float[np.ndarray, "steps 7"]]:
|
|
168
|
+
"""Plan a smooth arm movement from initial to target joint positions.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
init_qpos: Initial joint positions array of shape (7,)
|
|
172
|
+
target_qpos: Target joint positions array of shape (7,)
|
|
173
|
+
max_qv: Maximum allowed joint velocity magnitude (radians/second)
|
|
174
|
+
dt: Control cycle time in seconds (default: 0.01)
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A tuple containing:
|
|
178
|
+
- Position trajectory with shape (steps, 7)
|
|
179
|
+
- Velocity trajectory with shape (steps, 7)
|
|
180
|
+
Both trajectories include the final target state.
|
|
181
|
+
"""
|
|
182
|
+
duration = np.linalg.norm(target_qpos - init_qpos) / max_qv
|
|
183
|
+
steps = int(duration / dt)
|
|
184
|
+
trajectory = linear_interpolation(init_qpos, target_qpos, steps)
|
|
185
|
+
|
|
186
|
+
# Smooth the trajectory while respecting motion constraints
|
|
187
|
+
smoother = ArmOfflineTrajectorySmoother(dt=dt)
|
|
188
|
+
pos_traj, vel_traj = smoother.smooth_trajectory(trajectory, dt)
|
|
189
|
+
|
|
190
|
+
# Append final target state with zero velocity
|
|
191
|
+
pos_traj = np.concatenate([pos_traj, target_qpos[None, :]])
|
|
192
|
+
vel_traj = np.concatenate([vel_traj, np.zeros((1, 7))])
|
|
193
|
+
|
|
194
|
+
return pos_traj, vel_traj
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Operating system utility functions."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from dexcontrol.utils.constants import ROBOT_NAME_ENV_VAR
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def resolve_key_name(key: str) -> str:
|
|
12
|
+
"""Resolves a key name for zenoh topic by prepending robot name.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
key: Original key name (e.g. 'lidar' or '/lidar')
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Resolved key with robot name prepended (e.g. 'robot/lidar')
|
|
19
|
+
"""
|
|
20
|
+
# Get robot name from env var or use default
|
|
21
|
+
robot_name: Final[str] = os.getenv(ROBOT_NAME_ENV_VAR, "robot")
|
|
22
|
+
|
|
23
|
+
# Remove leading slash if present
|
|
24
|
+
key = key.lstrip("/")
|
|
25
|
+
|
|
26
|
+
# Combine robot name and key with single slash
|
|
27
|
+
return f"{robot_name}/{key}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_robot_model() -> str:
|
|
31
|
+
"""Get the robot model from the environment variable."""
|
|
32
|
+
robot_model_abb_mapping = dict(vg="vega")
|
|
33
|
+
robot_name = os.getenv(ROBOT_NAME_ENV_VAR, "robot")
|
|
34
|
+
robot_model_abb = robot_name.split("/")[-1].split("-")[0][:2]
|
|
35
|
+
if robot_model_abb not in robot_model_abb_mapping:
|
|
36
|
+
raise ValueError(f"Unknown robot model: {robot_model_abb}")
|
|
37
|
+
model = robot_model_abb_mapping[robot_model_abb] + "-" + robot_name.split("-")[-1]
|
|
38
|
+
logger.info(f"The current robot model is: {model}")
|
|
39
|
+
return model
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 with Commons Clause License
|
|
4
|
+
# Condition v1.0 [see LICENSE for details].
|
|
5
|
+
|
|
6
|
+
"""Utility functions for Python data structures and protobuf messages."""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from dexcontrol.proto import dexcontrol_query_pb2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def software_version_to_dict(
|
|
15
|
+
version_msg: dexcontrol_query_pb2.SoftwareVersion,
|
|
16
|
+
) -> dict[str, dict[Literal["hardware_version", "software_version", "main_hash"], Any]]:
|
|
17
|
+
"""Convert a SoftwareVersion protobuf message to a dictionary.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
version_msg: SoftwareVersion protobuf message.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Dictionary containing version information with component names as keys.
|
|
24
|
+
"""
|
|
25
|
+
return {
|
|
26
|
+
key: {
|
|
27
|
+
"hardware_version": value.hardware_version,
|
|
28
|
+
"software_version": value.software_version,
|
|
29
|
+
"main_hash": value.main_hash,
|
|
30
|
+
}
|
|
31
|
+
for key, value in version_msg.firmware_version.items()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ComponentStatus(Enum):
|
|
36
|
+
"""Enum representing the status of a component."""
|
|
37
|
+
|
|
38
|
+
NORMAL = 0
|
|
39
|
+
NA = 1
|
|
40
|
+
ERROR = 2
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def status_to_enum(status: dexcontrol_query_pb2.ComponentStatus) -> ComponentStatus:
|
|
44
|
+
"""Convert a ComponentStatus protobuf message to a ComponentStatus enum.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
status: ComponentStatus protobuf message.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
ComponentStatus enum value.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If the status value is not recognized.
|
|
54
|
+
"""
|
|
55
|
+
status_map = {
|
|
56
|
+
dexcontrol_query_pb2.ComponentStatus.NORMAL: ComponentStatus.NORMAL,
|
|
57
|
+
dexcontrol_query_pb2.ComponentStatus.NA: ComponentStatus.NA,
|
|
58
|
+
dexcontrol_query_pb2.ComponentStatus.ERROR: ComponentStatus.ERROR,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if status not in status_map:
|
|
62
|
+
raise ValueError(f"Invalid status: {status}")
|
|
63
|
+
|
|
64
|
+
return status_map[status]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def status_to_dict(
|
|
68
|
+
status_msg: dexcontrol_query_pb2.ComponentStates,
|
|
69
|
+
) -> dict[str, dict[str, Any]]:
|
|
70
|
+
"""Convert a ComponentStates protobuf message to a dictionary.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
status_msg: ComponentStates protobuf message.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dictionary containing status information for each component.
|
|
77
|
+
"""
|
|
78
|
+
return {
|
|
79
|
+
name: {
|
|
80
|
+
"connected": state.connected,
|
|
81
|
+
"enabled": status_to_enum(state.enabled),
|
|
82
|
+
"error_state": status_to_enum(state.error_state),
|
|
83
|
+
"error_code": state.error_code,
|
|
84
|
+
}
|
|
85
|
+
for name, state in status_msg.states.items()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def convert_enum_to_str(d: dict) -> dict:
|
|
90
|
+
"""Convert enum values to their string representations in a dictionary.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
d: Dictionary containing enum values.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dictionary with enum values converted to their string representations.
|
|
97
|
+
"""
|
|
98
|
+
for key, value in d.items():
|
|
99
|
+
if isinstance(value, dict):
|
|
100
|
+
d[key] = convert_enum_to_str(value)
|
|
101
|
+
elif isinstance(value, Enum):
|
|
102
|
+
d[key] = value.name
|
|
103
|
+
return d
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 with Commons Clause License
|
|
4
|
+
# Condition v1.0 [see LICENSE for details].
|
|
5
|
+
|
|
6
|
+
"""Rate limiter utility for maintaining consistent execution rates."""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RateLimiter:
|
|
16
|
+
"""Class for limiting execution rate to a target frequency.
|
|
17
|
+
|
|
18
|
+
This class provides rate limiting functionality by sleeping between iterations to
|
|
19
|
+
maintain a desired execution frequency. It also tracks statistics about the achieved
|
|
20
|
+
rate and missed deadlines.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
period_sec: Time period between iterations in seconds.
|
|
24
|
+
target_rate_hz: Desired execution rate in Hz.
|
|
25
|
+
window_size: Size of moving average window for rate calculations.
|
|
26
|
+
last_time_sec: Timestamp of the last iteration.
|
|
27
|
+
next_time_sec: Scheduled timestamp for the next iteration.
|
|
28
|
+
start_time_sec: Timestamp when the rate limiter was initialized or reset.
|
|
29
|
+
duration_buffer: List of recent iteration durations for rate calculation.
|
|
30
|
+
missed_deadlines: Counter for iterations that missed their scheduled time.
|
|
31
|
+
iterations: Total number of iterations since initialization or reset.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, rate_hz: float, window_size: int = 50) -> None:
|
|
35
|
+
"""Initializes rate limiter.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
rate_hz: Desired rate in Hz.
|
|
39
|
+
window_size: Size of moving average window for rate calculations.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If rate_hz is not positive.
|
|
43
|
+
"""
|
|
44
|
+
if rate_hz <= 0:
|
|
45
|
+
raise ValueError("Rate must be positive")
|
|
46
|
+
|
|
47
|
+
self.period_sec: Final[float] = 1.0 / rate_hz
|
|
48
|
+
self.target_rate_hz: Final[float] = rate_hz
|
|
49
|
+
self.window_size: Final[int] = max(1, window_size)
|
|
50
|
+
|
|
51
|
+
# Initialize timing variables
|
|
52
|
+
now_sec = time.monotonic()
|
|
53
|
+
self.last_time_sec: float = now_sec
|
|
54
|
+
self.next_time_sec: float = now_sec + self.period_sec
|
|
55
|
+
self.start_time_sec: float = now_sec
|
|
56
|
+
|
|
57
|
+
# Initialize statistics
|
|
58
|
+
self.duration_buffer: list[float] = []
|
|
59
|
+
self.missed_deadlines: int = 0
|
|
60
|
+
self.iterations: int = 0
|
|
61
|
+
self._MAX_ITERATIONS: Final[int] = sys.maxsize - 1000 # Leave some buffer
|
|
62
|
+
|
|
63
|
+
def sleep(self) -> None:
|
|
64
|
+
"""Sleeps to maintain desired rate.
|
|
65
|
+
|
|
66
|
+
Sleeps for the appropriate duration to maintain the target rate. If the next
|
|
67
|
+
scheduled time has already passed, increments the missed deadlines counter.
|
|
68
|
+
Uses monotonic time for reliable timing regardless of system clock changes.
|
|
69
|
+
"""
|
|
70
|
+
current_time_sec = time.monotonic()
|
|
71
|
+
sleep_time_sec = self.next_time_sec - current_time_sec
|
|
72
|
+
|
|
73
|
+
if sleep_time_sec > 0:
|
|
74
|
+
time.sleep(sleep_time_sec)
|
|
75
|
+
else:
|
|
76
|
+
self.missed_deadlines += 1
|
|
77
|
+
|
|
78
|
+
# Update timing and statistics
|
|
79
|
+
now_sec = time.monotonic()
|
|
80
|
+
|
|
81
|
+
# Reset iterations if approaching max value
|
|
82
|
+
if self.iterations >= self._MAX_ITERATIONS:
|
|
83
|
+
logger.warning(
|
|
84
|
+
"Iteration counter approaching max value, resetting statistics"
|
|
85
|
+
)
|
|
86
|
+
self.reset()
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
self.iterations += 1
|
|
90
|
+
|
|
91
|
+
if self.iterations > 1: # Skip first iteration
|
|
92
|
+
duration_sec = now_sec - self.last_time_sec
|
|
93
|
+
if len(self.duration_buffer) >= self.window_size:
|
|
94
|
+
self.duration_buffer.pop(0)
|
|
95
|
+
self.duration_buffer.append(duration_sec)
|
|
96
|
+
|
|
97
|
+
self.last_time_sec = now_sec
|
|
98
|
+
|
|
99
|
+
# More efficient way to advance next_time_sec when multiple periods behind
|
|
100
|
+
periods_behind = max(
|
|
101
|
+
0, int((now_sec - self.next_time_sec) / self.period_sec) + 1
|
|
102
|
+
)
|
|
103
|
+
self.next_time_sec += periods_behind * self.period_sec
|
|
104
|
+
|
|
105
|
+
def get_actual_rate(self) -> float:
|
|
106
|
+
"""Calculates actual achieved rate in Hz using moving average.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Current execution rate based on recent iterations.
|
|
110
|
+
"""
|
|
111
|
+
if not self.duration_buffer:
|
|
112
|
+
return 0.0
|
|
113
|
+
avg_duration_sec = sum(self.duration_buffer) / len(self.duration_buffer)
|
|
114
|
+
return 0.0 if avg_duration_sec <= 0 else 1.0 / avg_duration_sec
|
|
115
|
+
|
|
116
|
+
def get_average_rate(self) -> float:
|
|
117
|
+
"""Calculates average rate over entire run.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Average execution rate since start or last reset.
|
|
121
|
+
"""
|
|
122
|
+
if self.iterations < 2:
|
|
123
|
+
return 0.0
|
|
124
|
+
total_time_sec = time.monotonic() - self.start_time_sec
|
|
125
|
+
return 0.0 if total_time_sec <= 0 else self.iterations / total_time_sec
|
|
126
|
+
|
|
127
|
+
def reset(self) -> None:
|
|
128
|
+
"""Resets the rate limiter state and statistics."""
|
|
129
|
+
now_sec = time.monotonic()
|
|
130
|
+
self.last_time_sec = now_sec
|
|
131
|
+
self.next_time_sec = now_sec + self.period_sec
|
|
132
|
+
self.start_time_sec = now_sec
|
|
133
|
+
self.duration_buffer.clear()
|
|
134
|
+
self.missed_deadlines = 0
|
|
135
|
+
self.iterations = 0
|
|
136
|
+
|
|
137
|
+
def get_stats(self) -> dict[str, float | int]:
|
|
138
|
+
"""Gets runtime statistics.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Dictionary containing execution statistics including actual rate,
|
|
142
|
+
average rate, target rate, missed deadlines and iteration count.
|
|
143
|
+
"""
|
|
144
|
+
return {
|
|
145
|
+
"actual_rate": self.get_actual_rate(),
|
|
146
|
+
"average_rate": self.get_average_rate(),
|
|
147
|
+
"target_rate": self.target_rate_hz,
|
|
148
|
+
"missed_deadlines": self.missed_deadlines,
|
|
149
|
+
"iterations": self.iterations,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
rate_limiter = RateLimiter(100.0) # 100Hz
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
while True:
|
|
158
|
+
rate_limiter.sleep()
|
|
159
|
+
actual_rate = rate_limiter.get_actual_rate()
|
|
160
|
+
logger.info(f"Rate: {actual_rate:.2f} Hz")
|
|
161
|
+
|
|
162
|
+
except KeyboardInterrupt:
|
|
163
|
+
stats = rate_limiter.get_stats()
|
|
164
|
+
logger.info("\nFinal stats:")
|
|
165
|
+
logger.info(f"Average rate: {stats['average_rate']:.2f} Hz")
|
|
166
|
+
logger.info(f"Final rate: {stats['actual_rate']:.2f} Hz")
|
|
167
|
+
logger.info(f"Missed deadlines: {stats['missed_deadlines']}")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 with Commons Clause License
|
|
4
|
+
# Condition v1.0 [see LICENSE for details].
|
|
5
|
+
|
|
6
|
+
"""Utility for resetting Orbbec camera USB connection."""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import pyudev
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
# Orbbec vendor ID constant
|
|
16
|
+
_ORBBEC_VENDOR_ID = "2bc5"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def check_root() -> None:
|
|
20
|
+
"""Verify the script is running with root privileges.
|
|
21
|
+
|
|
22
|
+
Exits the program if not running as root.
|
|
23
|
+
"""
|
|
24
|
+
if os.geteuid() != 0:
|
|
25
|
+
logger.error("This script must be run as root (sudo). Exiting...")
|
|
26
|
+
logger.info("Run with: sudo $(which python) reset_orbbec_camera_usb.py")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def reset_orbbec() -> bool:
|
|
31
|
+
"""Reset the USB connection for an Orbbec camera.
|
|
32
|
+
|
|
33
|
+
Simulates unplugging and replugging the device by toggling the USB
|
|
34
|
+
authorization state.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: True if camera was found and reset successfully, False otherwise.
|
|
38
|
+
"""
|
|
39
|
+
# Check for root privileges first
|
|
40
|
+
check_root()
|
|
41
|
+
|
|
42
|
+
# Initialize pyudev
|
|
43
|
+
context = pyudev.Context()
|
|
44
|
+
|
|
45
|
+
# Find Orbbec device
|
|
46
|
+
for device in context.list_devices(subsystem="usb"):
|
|
47
|
+
if device.properties.get("ID_VENDOR_ID") == _ORBBEC_VENDOR_ID:
|
|
48
|
+
return _reset_device(device)
|
|
49
|
+
|
|
50
|
+
logger.warning("Orbbec camera not found")
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _reset_device(device: pyudev.Device) -> bool:
|
|
55
|
+
"""Reset a specific USB device.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
device: pyudev Device object to reset
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
bool: True if reset was successful, False otherwise
|
|
62
|
+
"""
|
|
63
|
+
# Get the parent device (USB port)
|
|
64
|
+
port = device.find_parent("usb", "usb_device")
|
|
65
|
+
|
|
66
|
+
if port is None:
|
|
67
|
+
logger.warning("Could not find parent USB device")
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
# Construct path to authorized file
|
|
71
|
+
path = os.path.join(port.sys_path, "authorized")
|
|
72
|
+
|
|
73
|
+
if not os.path.exists(path):
|
|
74
|
+
logger.warning(f"Authorize file not found at: {path}")
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Simulate unplug
|
|
79
|
+
with open(path, "w") as f:
|
|
80
|
+
f.write("0")
|
|
81
|
+
logger.info("USB device deauthorized")
|
|
82
|
+
|
|
83
|
+
# Wait a moment for the system to process
|
|
84
|
+
time.sleep(1)
|
|
85
|
+
|
|
86
|
+
# Simulate plug
|
|
87
|
+
with open(path, "w") as f:
|
|
88
|
+
f.write("1")
|
|
89
|
+
|
|
90
|
+
logger.info(f"Orbbec camera reset successfully. Path: {path}")
|
|
91
|
+
return True
|
|
92
|
+
except IOError as e:
|
|
93
|
+
logger.error(f"Failed to reset device: {e}")
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
reset_orbbec()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 with Commons Clause License
|
|
4
|
+
# Condition v1.0 [see LICENSE for details].
|
|
5
|
+
|
|
6
|
+
"""Zenoh subscriber utilities for dexcontrol.
|
|
7
|
+
|
|
8
|
+
This module provides a collection of subscriber classes and utilities for handling
|
|
9
|
+
Zenoh communication in a flexible and reusable way.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .base import BaseZenohSubscriber, CustomDataHandler
|
|
13
|
+
from .camera import DepthCameraSubscriber, RGBCameraSubscriber, RGBDCameraSubscriber
|
|
14
|
+
from .decoders import (
|
|
15
|
+
DecoderFunction,
|
|
16
|
+
json_decoder,
|
|
17
|
+
protobuf_decoder,
|
|
18
|
+
raw_bytes_decoder,
|
|
19
|
+
string_decoder,
|
|
20
|
+
)
|
|
21
|
+
from .generic import GenericZenohSubscriber
|
|
22
|
+
from .imu import IMUSubscriber
|
|
23
|
+
from .lidar import LidarSubscriber
|
|
24
|
+
from .protobuf import ProtobufZenohSubscriber
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BaseZenohSubscriber",
|
|
28
|
+
"CustomDataHandler",
|
|
29
|
+
"GenericZenohSubscriber",
|
|
30
|
+
"ProtobufZenohSubscriber",
|
|
31
|
+
"DecoderFunction",
|
|
32
|
+
"protobuf_decoder",
|
|
33
|
+
"raw_bytes_decoder",
|
|
34
|
+
"json_decoder",
|
|
35
|
+
"string_decoder",
|
|
36
|
+
# Camera subscribers
|
|
37
|
+
"RGBCameraSubscriber",
|
|
38
|
+
"DepthCameraSubscriber",
|
|
39
|
+
"RGBDCameraSubscriber",
|
|
40
|
+
# Lidar subscriber
|
|
41
|
+
"LidarSubscriber",
|
|
42
|
+
# IMU subscriber
|
|
43
|
+
"IMUSubscriber",
|
|
44
|
+
]
|