dexcontrol 0.2.10__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/__init__.py +2 -0
- dexcontrol/config/core/arm.py +5 -1
- dexcontrol/config/core/chassis.py +9 -4
- dexcontrol/config/core/hand.py +2 -1
- dexcontrol/config/core/head.py +7 -8
- dexcontrol/config/core/misc.py +14 -1
- dexcontrol/config/core/torso.py +8 -4
- dexcontrol/config/sensors/cameras/__init__.py +2 -1
- dexcontrol/config/sensors/cameras/luxonis_camera.py +51 -0
- dexcontrol/config/sensors/cameras/rgb_camera.py +1 -1
- dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
- dexcontrol/config/sensors/vega_sensors.py +9 -1
- dexcontrol/config/vega.py +34 -3
- dexcontrol/core/arm.py +103 -58
- dexcontrol/core/chassis.py +146 -115
- dexcontrol/core/component.py +83 -20
- dexcontrol/core/hand.py +74 -39
- dexcontrol/core/head.py +41 -28
- dexcontrol/core/misc.py +256 -25
- dexcontrol/core/robot_query_interface.py +440 -0
- dexcontrol/core/torso.py +28 -10
- dexcontrol/proto/dexcontrol_msg_pb2.py +27 -37
- dexcontrol/proto/dexcontrol_msg_pb2.pyi +111 -126
- dexcontrol/proto/dexcontrol_query_pb2.py +39 -35
- dexcontrol/proto/dexcontrol_query_pb2.pyi +41 -4
- dexcontrol/robot.py +266 -409
- dexcontrol/sensors/__init__.py +2 -1
- dexcontrol/sensors/camera/__init__.py +2 -0
- dexcontrol/sensors/camera/luxonis_camera.py +169 -0
- dexcontrol/sensors/camera/zed_camera.py +17 -8
- dexcontrol/sensors/imu/chassis_imu.py +5 -1
- dexcontrol/sensors/imu/zed_imu.py +3 -2
- dexcontrol/sensors/lidar/rplidar.py +1 -0
- dexcontrol/sensors/manager.py +3 -0
- dexcontrol/utils/constants.py +3 -0
- dexcontrol/utils/error_code.py +236 -0
- dexcontrol/utils/os_utils.py +183 -1
- dexcontrol/utils/pb_utils.py +0 -22
- dexcontrol/utils/subscribers/lidar.py +1 -0
- dexcontrol/utils/trajectory_utils.py +17 -5
- dexcontrol/utils/viz_utils.py +86 -11
- dexcontrol/utils/zenoh_utils.py +288 -2
- {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/METADATA +15 -2
- dexcontrol-0.3.0.dist-info/RECORD +76 -0
- dexcontrol-0.2.10.dist-info/RECORD +0 -72
- {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/WHEEL +0 -0
- {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/licenses/LICENSE +0 -0
dexcontrol/sensors/__init__.py
CHANGED
|
@@ -15,7 +15,7 @@ using Zenoh subscribers for data communication.
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
# Import camera sensors
|
|
18
|
-
from .camera import RGBCameraSensor, ZedCameraSensor
|
|
18
|
+
from .camera import LuxonisCameraSensor, RGBCameraSensor, ZedCameraSensor
|
|
19
19
|
|
|
20
20
|
# Import IMU sensors
|
|
21
21
|
from .imu import ChassisIMUSensor, ZedIMUSensor
|
|
@@ -31,6 +31,7 @@ __all__ = [
|
|
|
31
31
|
# Camera sensors
|
|
32
32
|
"RGBCameraSensor",
|
|
33
33
|
"ZedCameraSensor",
|
|
34
|
+
"LuxonisCameraSensor",
|
|
34
35
|
|
|
35
36
|
# IMU sensors
|
|
36
37
|
"ChassisIMUSensor",
|
|
@@ -14,10 +14,12 @@ This module provides camera sensor classes that use the specialized camera
|
|
|
14
14
|
subscribers for RGB and RGBD camera data, matching the dexsensor structure.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
from .luxonis_camera import LuxonisCameraSensor
|
|
17
18
|
from .rgb_camera import RGBCameraSensor
|
|
18
19
|
from .zed_camera import ZedCameraSensor
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
21
22
|
"RGBCameraSensor",
|
|
22
23
|
"ZedCameraSensor",
|
|
24
|
+
"LuxonisCameraSensor",
|
|
23
25
|
]
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
|
|
@@ -140,9 +140,6 @@ class ZedCameraSensor:
|
|
|
140
140
|
|
|
141
141
|
def _query_camera_info(self) -> None:
|
|
142
142
|
"""Query Zenoh for camera metadata if using RTC."""
|
|
143
|
-
if not self._configs.use_rtc:
|
|
144
|
-
logger.info(f"'{self._name}': Skipping camera info query in non-RTC mode.")
|
|
145
|
-
return
|
|
146
143
|
|
|
147
144
|
enabled_rgb_streams = [
|
|
148
145
|
s
|
|
@@ -171,7 +168,6 @@ class ZedCameraSensor:
|
|
|
171
168
|
|
|
172
169
|
logger.info(f"'{self._name}': Querying for camera info at '{final_info_key}'.")
|
|
173
170
|
self._camera_info = query_zenoh_json(self._zenoh_session, final_info_key)
|
|
174
|
-
|
|
175
171
|
if self._camera_info:
|
|
176
172
|
logger.info(f"'{self._name}': Successfully received camera info.")
|
|
177
173
|
else:
|
|
@@ -249,7 +245,8 @@ class ZedCameraSensor:
|
|
|
249
245
|
return False
|
|
250
246
|
|
|
251
247
|
def get_obs(
|
|
252
|
-
self, obs_keys: Optional[list[str]] = None
|
|
248
|
+
self, obs_keys: Optional[list[str]] = None,
|
|
249
|
+
include_timestamp: bool = False
|
|
253
250
|
) -> Dict[str, Optional[np.ndarray]]:
|
|
254
251
|
"""Get the latest observation data from specified camera streams.
|
|
255
252
|
|
|
@@ -257,17 +254,29 @@ class ZedCameraSensor:
|
|
|
257
254
|
obs_keys: A list of stream names to retrieve data from (e.g.,
|
|
258
255
|
['left_rgb', 'depth']). If None, retrieves data from all
|
|
259
256
|
enabled streams.
|
|
257
|
+
include_timestamp: If True, includes the timestamp in the observation data.
|
|
258
|
+
The timestamp data is not available for RTC streams.
|
|
260
259
|
|
|
261
260
|
Returns:
|
|
262
|
-
A dictionary mapping stream names to their latest image data. The
|
|
261
|
+
A dictionary mapping stream names to their latest image data with timestamp. The
|
|
263
262
|
image is a numpy array (HxWxC for RGB, HxW for depth) or None if
|
|
264
|
-
no data is available for that stream.
|
|
263
|
+
no data is available for that stream. If include_timestamp is True,
|
|
264
|
+
the value in the dictionary is a tuple with the image and timestamp.
|
|
265
265
|
"""
|
|
266
266
|
keys_to_fetch = obs_keys or self.available_streams
|
|
267
267
|
obs_out = {}
|
|
268
268
|
for key in keys_to_fetch:
|
|
269
269
|
subscriber = self._subscribers.get(key)
|
|
270
|
-
|
|
270
|
+
data = subscriber.get_latest_data() if subscriber else None
|
|
271
|
+
|
|
272
|
+
is_tuple_or_list = isinstance(data, (tuple, list))
|
|
273
|
+
|
|
274
|
+
if include_timestamp:
|
|
275
|
+
if not is_tuple_or_list:
|
|
276
|
+
logger.warning(f"Timestamp is not available yet for {key} stream.")
|
|
277
|
+
obs_out[key] = data
|
|
278
|
+
else:
|
|
279
|
+
obs_out[key] = data[0] if is_tuple_or_list else data
|
|
271
280
|
return obs_out
|
|
272
281
|
|
|
273
282
|
def get_left_rgb(self) -> Optional[np.ndarray]:
|
|
@@ -76,7 +76,7 @@ class ChassisIMUSensor:
|
|
|
76
76
|
"""
|
|
77
77
|
return self._subscriber.wait_for_active(timeout)
|
|
78
78
|
|
|
79
|
-
def get_obs(self, obs_keys: list[Literal['ang_vel', 'acc', 'quat']] | None = None) -> dict[
|
|
79
|
+
def get_obs(self, obs_keys: list[Literal['ang_vel', 'acc', 'quat']] | None = None) -> dict[Literal['ang_vel', 'acc', 'quat', 'timestamp_ns'], np.ndarray]:
|
|
80
80
|
"""Get observation data for the ZED IMU sensor.
|
|
81
81
|
|
|
82
82
|
Args:
|
|
@@ -89,6 +89,7 @@ class ChassisIMUSensor:
|
|
|
89
89
|
- 'ang_vel': Angular velocity from 'angular_velocity'
|
|
90
90
|
- 'acc': Linear acceleration from 'acceleration'
|
|
91
91
|
- 'quat': Orientation quaternion from 'orientation', in wxyz convention
|
|
92
|
+
- 'timestamp_ns': Timestamp in nanoseconds
|
|
92
93
|
"""
|
|
93
94
|
if obs_keys is None:
|
|
94
95
|
obs_keys = ['ang_vel', 'acc', 'quat']
|
|
@@ -110,6 +111,9 @@ class ChassisIMUSensor:
|
|
|
110
111
|
else:
|
|
111
112
|
raise ValueError(f"Invalid observation key: {key}")
|
|
112
113
|
|
|
114
|
+
if hasattr(data, 'timestamp_ns'):
|
|
115
|
+
obs_out['timestamp_ns'] = data.timestamp_ns
|
|
116
|
+
|
|
113
117
|
return obs_out
|
|
114
118
|
|
|
115
119
|
def get_acceleration(self) -> np.ndarray:
|
|
@@ -75,7 +75,7 @@ class ZedIMUSensor:
|
|
|
75
75
|
|
|
76
76
|
Args:
|
|
77
77
|
obs_keys: List of observation keys to retrieve. If None, returns all available data.
|
|
78
|
-
Valid keys: ['ang_vel', 'acc', 'quat']
|
|
78
|
+
Valid keys: ['ang_vel', 'acc', 'quat', 'timestamp']
|
|
79
79
|
|
|
80
80
|
Returns:
|
|
81
81
|
Dictionary with observation data including all IMU measurements.
|
|
@@ -83,6 +83,7 @@ class ZedIMUSensor:
|
|
|
83
83
|
- 'ang_vel': Angular velocity from 'angular_velocity'
|
|
84
84
|
- 'acc': Linear acceleration from 'acceleration'
|
|
85
85
|
- 'quat': Orientation quaternion from 'orientation'
|
|
86
|
+
- 'timestamp_ns': Timestamp from 'timestamp'
|
|
86
87
|
"""
|
|
87
88
|
if obs_keys is None:
|
|
88
89
|
obs_keys = ['ang_vel', 'acc', 'quat']
|
|
@@ -91,7 +92,7 @@ class ZedIMUSensor:
|
|
|
91
92
|
if data is None:
|
|
92
93
|
return None
|
|
93
94
|
|
|
94
|
-
obs_out = {}
|
|
95
|
+
obs_out = {'timestamp_ns': data['timestamp']}
|
|
95
96
|
|
|
96
97
|
for key in obs_keys:
|
|
97
98
|
if key == 'ang_vel':
|
|
@@ -87,6 +87,7 @@ class RPLidarSensor:
|
|
|
87
87
|
- ranges: Array of range measurements in meters
|
|
88
88
|
- angles: Array of corresponding angles in radians
|
|
89
89
|
- qualities: Array of quality values (0-255) if available, None otherwise
|
|
90
|
+
- timestamp: Timestamp in nanoseconds (int)
|
|
90
91
|
"""
|
|
91
92
|
return self._subscriber.get_latest_data()
|
|
92
93
|
|
dexcontrol/sensors/manager.py
CHANGED
|
@@ -26,6 +26,7 @@ from omegaconf import DictConfig, OmegaConf
|
|
|
26
26
|
from dexcontrol.config.sensors.vega_sensors import VegaSensorsConfig
|
|
27
27
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
29
|
+
from dexcontrol.sensors.camera.luxonis_camera import LuxonisCameraSensor
|
|
29
30
|
from dexcontrol.sensors.camera.rgb_camera import RGBCameraSensor
|
|
30
31
|
from dexcontrol.sensors.camera.zed_camera import ZedCameraSensor
|
|
31
32
|
from dexcontrol.sensors.imu.chassis_imu import ChassisIMUSensor
|
|
@@ -49,6 +50,8 @@ class Sensors:
|
|
|
49
50
|
if TYPE_CHECKING:
|
|
50
51
|
# Type annotations for dynamically created sensor attributes
|
|
51
52
|
head_camera: ZedCameraSensor
|
|
53
|
+
left_wrist_camera: LuxonisCameraSensor
|
|
54
|
+
right_wrist_camera: LuxonisCameraSensor
|
|
52
55
|
base_left_camera: RGBCameraSensor
|
|
53
56
|
base_right_camera: RGBCameraSensor
|
|
54
57
|
base_front_camera: RGBCameraSensor
|
dexcontrol/utils/constants.py
CHANGED
|
@@ -20,3 +20,6 @@ COMM_CFG_PATH_ENV_VAR: Final[str] = "DEXMATE_COMM_CFG_PATH"
|
|
|
20
20
|
|
|
21
21
|
# Environment variable to disable heartbeat monitoring
|
|
22
22
|
DISABLE_HEARTBEAT_ENV_VAR: Final[str] = "DEXCONTROL_DISABLE_HEARTBEAT"
|
|
23
|
+
|
|
24
|
+
# Environment variable to disable estop checking
|
|
25
|
+
DISABLE_ESTOP_CHECKING_ENV_VAR: Final[str] = "DEXCONTROL_DISABLE_ESTOP_CHECKING"
|
|
@@ -0,0 +1,236 @@
|
|
|
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
|
+
"""Error code interpretation utilities for robot components."""
|
|
12
|
+
|
|
13
|
+
from typing import Dict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ErrorCodeInterpreter:
|
|
17
|
+
"""Interprets error codes for different robot components."""
|
|
18
|
+
|
|
19
|
+
# Arm error codes (left_arm and right_arm)
|
|
20
|
+
ARM_ERROR_CODES = {
|
|
21
|
+
0x00000020: "Overtemperature",
|
|
22
|
+
0x00000080: "Encoder error",
|
|
23
|
+
0x00000200: "Software error",
|
|
24
|
+
0x00000400: "Temperature sensor error",
|
|
25
|
+
0x00000800: "Position limit exceeded",
|
|
26
|
+
0x00002000: "Position tracking error exceeded",
|
|
27
|
+
0x00004000: "Current detection error",
|
|
28
|
+
0x00008000: "Brake failure",
|
|
29
|
+
0x00010000: "Position command limit exceeded",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Head error codes
|
|
33
|
+
HEAD_ERROR_CODES = {
|
|
34
|
+
0x00: "Disabled",
|
|
35
|
+
0x04: "Enabled",
|
|
36
|
+
0x20: "Overvoltage",
|
|
37
|
+
0x24: "Undervoltage",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Chassis wheel error codes (common)
|
|
41
|
+
CHASSIS_COMMON_CODES = {
|
|
42
|
+
0x00000000: "No error",
|
|
43
|
+
0x00000004: "Overvoltage",
|
|
44
|
+
0x00000008: "Undervoltage",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Left drive wheel motor specific
|
|
48
|
+
CHASSIS_LEFT_DRIVE_CODES = {
|
|
49
|
+
0x00000010: "Overcurrent",
|
|
50
|
+
0x00000020: "Overload",
|
|
51
|
+
0x00000080: "Encoder deviation",
|
|
52
|
+
0x00000200: "Reference voltage error",
|
|
53
|
+
0x00000800: "Hall sensor error",
|
|
54
|
+
0x00001000: "Motor overtemperature",
|
|
55
|
+
0x00002000: "Encoder error",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Right drive wheel motor specific
|
|
59
|
+
CHASSIS_RIGHT_DRIVE_CODES = {
|
|
60
|
+
0x00100000: "Overcurrent",
|
|
61
|
+
0x00200000: "Overload",
|
|
62
|
+
0x00800000: "Encoder deviation",
|
|
63
|
+
0x02000000: "Reference voltage error",
|
|
64
|
+
0x08000000: "Hall sensor error",
|
|
65
|
+
0x10000000: "Motor overtemperature",
|
|
66
|
+
0x20000000: "Encoder error",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Steering wheel error codes (left and right)
|
|
70
|
+
CHASSIS_STEERING_CODES = {
|
|
71
|
+
0xFC00000C: "Undervoltage",
|
|
72
|
+
0xFC000010: "Overvoltage",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Torso motor error codes
|
|
76
|
+
TORSO_ERROR_CODES = {
|
|
77
|
+
0x00000008: "Overvoltage",
|
|
78
|
+
0x00000010: "Undervoltage",
|
|
79
|
+
0x00000040: "Startup error",
|
|
80
|
+
0x00000080: "Speed feedback error",
|
|
81
|
+
0x00000100: "Overcurrent",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# BMS (Battery Management System) alarm codes
|
|
85
|
+
BMS_ERROR_CODES = {
|
|
86
|
+
0x0004: "Single cell overvoltage",
|
|
87
|
+
0x0008: "Single cell undervoltage",
|
|
88
|
+
0x0010: "Total overvoltage",
|
|
89
|
+
0x0020: "Total undervoltage",
|
|
90
|
+
0x0040: "High temperature",
|
|
91
|
+
0x0080: "Low temperature",
|
|
92
|
+
0x0100: "Discharge overcurrent",
|
|
93
|
+
0x0200: "Charge overcurrent",
|
|
94
|
+
# Bits 8-15 are reserved
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def interpret_error(cls, component: str, error_code: int) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Interpret error code for a specific component.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
component: Component name (e.g., 'left_arm', 'right_arm', 'head', etc.)
|
|
104
|
+
error_code: Error code to interpret
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Human-readable error description or hex code if unknown
|
|
108
|
+
"""
|
|
109
|
+
component_lower = component.lower()
|
|
110
|
+
|
|
111
|
+
# Arms (left and right)
|
|
112
|
+
if "left_arm" in component_lower or "right_arm" in component_lower:
|
|
113
|
+
return cls._interpret_bitmask_errors(error_code, cls.ARM_ERROR_CODES)
|
|
114
|
+
|
|
115
|
+
# Hands (left and right) - same as arms
|
|
116
|
+
elif "left_hand" in component_lower or "right_hand" in component_lower:
|
|
117
|
+
return cls._interpret_bitmask_errors(error_code, cls.ARM_ERROR_CODES)
|
|
118
|
+
|
|
119
|
+
# Head
|
|
120
|
+
elif "head" in component_lower:
|
|
121
|
+
return cls.HEAD_ERROR_CODES.get(
|
|
122
|
+
error_code, f"Unknown error: 0x{error_code:02X}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Chassis (includes all wheel errors)
|
|
126
|
+
elif "chassis" in component_lower:
|
|
127
|
+
return cls._interpret_chassis_errors(error_code)
|
|
128
|
+
|
|
129
|
+
# Torso
|
|
130
|
+
elif "torso" in component_lower:
|
|
131
|
+
return cls._interpret_bitmask_errors(error_code, cls.TORSO_ERROR_CODES)
|
|
132
|
+
|
|
133
|
+
# BMS (Battery Management System)
|
|
134
|
+
elif "bms" in component_lower:
|
|
135
|
+
return cls._interpret_bitmask_errors(error_code, cls.BMS_ERROR_CODES)
|
|
136
|
+
|
|
137
|
+
else:
|
|
138
|
+
return f"Unknown component error: 0x{error_code:08X}"
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def _interpret_bitmask_errors(
|
|
142
|
+
cls, error_code: int, error_dict: Dict[int, str]
|
|
143
|
+
) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Interpret bitmask-style error codes.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
error_code: Error code with multiple possible bits set
|
|
149
|
+
error_dict: Dictionary mapping bit masks to error descriptions
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Comma-separated list of active errors or hex code if none found
|
|
153
|
+
"""
|
|
154
|
+
if error_code == 0:
|
|
155
|
+
return "No error"
|
|
156
|
+
|
|
157
|
+
errors = []
|
|
158
|
+
for mask, description in error_dict.items():
|
|
159
|
+
if error_code & mask:
|
|
160
|
+
errors.append(description)
|
|
161
|
+
|
|
162
|
+
if errors:
|
|
163
|
+
return ", ".join(errors)
|
|
164
|
+
else:
|
|
165
|
+
return f"Unknown error: 0x{error_code:08X}"
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def _interpret_chassis_errors(cls, error_code: int) -> str:
|
|
169
|
+
"""
|
|
170
|
+
Interpret chassis-specific error codes (includes all wheel errors).
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
error_code: Error code to interpret
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Human-readable error description
|
|
177
|
+
"""
|
|
178
|
+
# Check steering wheel codes first (they have specific values)
|
|
179
|
+
if error_code in cls.CHASSIS_STEERING_CODES:
|
|
180
|
+
return cls.CHASSIS_STEERING_CODES[error_code]
|
|
181
|
+
|
|
182
|
+
# Check common codes
|
|
183
|
+
if error_code == 0:
|
|
184
|
+
return "No error"
|
|
185
|
+
|
|
186
|
+
errors = []
|
|
187
|
+
|
|
188
|
+
# Check common chassis errors
|
|
189
|
+
for mask, description in cls.CHASSIS_COMMON_CODES.items():
|
|
190
|
+
if mask != 0 and (error_code & mask):
|
|
191
|
+
errors.append(description)
|
|
192
|
+
|
|
193
|
+
# Check left drive wheel specific
|
|
194
|
+
for mask, description in cls.CHASSIS_LEFT_DRIVE_CODES.items():
|
|
195
|
+
if error_code & mask:
|
|
196
|
+
errors.append(f"Left drive: {description}")
|
|
197
|
+
|
|
198
|
+
# Check right drive wheel specific
|
|
199
|
+
for mask, description in cls.CHASSIS_RIGHT_DRIVE_CODES.items():
|
|
200
|
+
if error_code & mask:
|
|
201
|
+
errors.append(f"Right drive: {description}")
|
|
202
|
+
|
|
203
|
+
if errors:
|
|
204
|
+
return ", ".join(errors)
|
|
205
|
+
else:
|
|
206
|
+
return f"Unknown chassis error: 0x{error_code:08X}"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_error_description(component: str, error_code: int) -> str:
|
|
210
|
+
"""
|
|
211
|
+
Convenience function to get error description.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
component: Component name
|
|
215
|
+
error_code: Error code to interpret
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Human-readable error description
|
|
219
|
+
"""
|
|
220
|
+
return ErrorCodeInterpreter.interpret_error(component, error_code)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_multiple_errors(components_errors: Dict[str, int]) -> Dict[str, str]:
|
|
224
|
+
"""
|
|
225
|
+
Get error descriptions for multiple components.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
components_errors: Dictionary mapping component names to error codes
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Dictionary mapping component names to error descriptions
|
|
232
|
+
"""
|
|
233
|
+
return {
|
|
234
|
+
component: get_error_description(component, error_code)
|
|
235
|
+
for component, error_code in components_errors.items()
|
|
236
|
+
}
|