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,105 @@
|
|
|
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
|
+
"""Generic Zenoh subscriber with configurable decoder functions.
|
|
7
|
+
|
|
8
|
+
This module provides a flexible subscriber that can handle any type of data
|
|
9
|
+
by accepting decoder functions that transform raw Zenoh bytes into the desired format.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import zenoh
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
from .base import BaseZenohSubscriber, CustomDataHandler
|
|
18
|
+
from .decoders import DecoderFunction
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GenericZenohSubscriber(BaseZenohSubscriber):
|
|
22
|
+
"""Generic Zenoh subscriber with configurable decoder function.
|
|
23
|
+
|
|
24
|
+
This subscriber can handle any type of data by accepting a decoder function
|
|
25
|
+
that transforms the raw Zenoh bytes into the desired data format.
|
|
26
|
+
Uses lazy decoding - data is only decoded when requested.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
topic: str,
|
|
32
|
+
zenoh_session: zenoh.Session,
|
|
33
|
+
decoder: DecoderFunction | None = None,
|
|
34
|
+
name: str = "generic_subscriber",
|
|
35
|
+
enable_fps_tracking: bool = False,
|
|
36
|
+
fps_log_interval: int = 100,
|
|
37
|
+
custom_data_handler: CustomDataHandler | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize the generic Zenoh subscriber.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
topic: Zenoh topic to subscribe to for data.
|
|
43
|
+
zenoh_session: Active Zenoh session for communication.
|
|
44
|
+
decoder: Optional function to decode raw bytes into desired format.
|
|
45
|
+
If None, raw bytes are returned.
|
|
46
|
+
name: Name for logging purposes.
|
|
47
|
+
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
48
|
+
fps_log_interval: Number of frames between FPS calculations.
|
|
49
|
+
custom_data_handler: Optional custom function to handle incoming data.
|
|
50
|
+
If provided, this will replace the default data
|
|
51
|
+
handling logic entirely.
|
|
52
|
+
"""
|
|
53
|
+
super().__init__(
|
|
54
|
+
topic,
|
|
55
|
+
zenoh_session,
|
|
56
|
+
name,
|
|
57
|
+
enable_fps_tracking,
|
|
58
|
+
fps_log_interval,
|
|
59
|
+
custom_data_handler,
|
|
60
|
+
)
|
|
61
|
+
self._decoder = decoder
|
|
62
|
+
self._latest_raw_data: zenoh.ZBytes = zenoh.ZBytes("")
|
|
63
|
+
|
|
64
|
+
def _data_handler(self, sample: zenoh.Sample) -> None:
|
|
65
|
+
"""Handle incoming data.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
sample: Zenoh sample containing data.
|
|
69
|
+
"""
|
|
70
|
+
with self._data_lock:
|
|
71
|
+
self._latest_raw_data = sample.payload
|
|
72
|
+
self._active = True
|
|
73
|
+
|
|
74
|
+
self._update_fps_metrics()
|
|
75
|
+
|
|
76
|
+
def get_latest_data(self) -> Any | None:
|
|
77
|
+
"""Get the latest data.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Latest decoded data if decoder is provided and decoding succeeded,
|
|
81
|
+
otherwise raw bytes. None if no data received.
|
|
82
|
+
"""
|
|
83
|
+
with self._data_lock:
|
|
84
|
+
if not self._active:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
if self._decoder is not None:
|
|
88
|
+
try:
|
|
89
|
+
return self._decoder(self._latest_raw_data)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(f"Failed to decode data for {self._name}: {e}")
|
|
92
|
+
return None
|
|
93
|
+
else:
|
|
94
|
+
return self._latest_raw_data.to_bytes()
|
|
95
|
+
|
|
96
|
+
def get_latest_raw_data(self) -> bytes | None:
|
|
97
|
+
"""Get the latest raw data bytes.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Latest raw data bytes if available, None otherwise.
|
|
101
|
+
"""
|
|
102
|
+
with self._data_lock:
|
|
103
|
+
if not self._active:
|
|
104
|
+
return None
|
|
105
|
+
return self._latest_raw_data.to_bytes()
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
"""IMU Zenoh subscriber for inertial measurement data.
|
|
7
|
+
|
|
8
|
+
This module provides a specialized subscriber for IMU data,
|
|
9
|
+
using the serialization format from dexsensor.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import zenoh
|
|
16
|
+
from loguru import logger
|
|
17
|
+
|
|
18
|
+
from .base import BaseZenohSubscriber, CustomDataHandler
|
|
19
|
+
|
|
20
|
+
# Import IMU serialization functions from dexsensor
|
|
21
|
+
try:
|
|
22
|
+
from dexsensor.serialization.imu import decode_imu_data
|
|
23
|
+
except ImportError:
|
|
24
|
+
logger.error(
|
|
25
|
+
"Failed to import dexsensor IMU serialization functions. Please install dexsensor."
|
|
26
|
+
)
|
|
27
|
+
decode_imu_data = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IMUSubscriber(BaseZenohSubscriber):
|
|
31
|
+
"""Zenoh subscriber for IMU data.
|
|
32
|
+
|
|
33
|
+
This subscriber handles IMU data encoded using the dexsensor
|
|
34
|
+
IMU serialization format with compression.
|
|
35
|
+
Uses lazy decoding - data is only decoded when requested.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
topic: str,
|
|
41
|
+
zenoh_session: zenoh.Session,
|
|
42
|
+
name: str = "imu_subscriber",
|
|
43
|
+
enable_fps_tracking: bool = True,
|
|
44
|
+
fps_log_interval: int = 50,
|
|
45
|
+
custom_data_handler: CustomDataHandler | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initialize the IMU subscriber.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
topic: Zenoh topic to subscribe to for IMU data.
|
|
51
|
+
zenoh_session: Active Zenoh session for communication.
|
|
52
|
+
name: Name for logging purposes.
|
|
53
|
+
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
54
|
+
fps_log_interval: Number of frames between FPS calculations.
|
|
55
|
+
custom_data_handler: Optional custom function to handle incoming data.
|
|
56
|
+
If provided, this will replace the default data
|
|
57
|
+
handling logic entirely.
|
|
58
|
+
"""
|
|
59
|
+
super().__init__(
|
|
60
|
+
topic,
|
|
61
|
+
zenoh_session,
|
|
62
|
+
name,
|
|
63
|
+
enable_fps_tracking,
|
|
64
|
+
fps_log_interval,
|
|
65
|
+
custom_data_handler,
|
|
66
|
+
)
|
|
67
|
+
self._latest_raw_data: bytes | None = None
|
|
68
|
+
|
|
69
|
+
def _data_handler(self, sample: zenoh.Sample) -> None:
|
|
70
|
+
"""Handle incoming IMU data.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
sample: Zenoh sample containing encoded IMU data.
|
|
74
|
+
"""
|
|
75
|
+
with self._data_lock:
|
|
76
|
+
self._latest_raw_data = sample.payload.to_bytes()
|
|
77
|
+
self._active = True
|
|
78
|
+
|
|
79
|
+
self._update_fps_metrics()
|
|
80
|
+
|
|
81
|
+
def get_latest_data(self) -> dict[str, Any] | None:
|
|
82
|
+
"""Get the latest IMU data.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Latest IMU data dictionary if available, None otherwise.
|
|
86
|
+
Dictionary contains:
|
|
87
|
+
- acceleration: Linear acceleration [x, y, z] in m/s²
|
|
88
|
+
- angular_velocity: Angular velocity [x, y, z] in rad/s
|
|
89
|
+
- orientation: Orientation quaternion [x, y, z, w]
|
|
90
|
+
- magnetometer: Magnetic field [x, y, z] in µT (if available)
|
|
91
|
+
- timestamp: Timestamp of the measurement
|
|
92
|
+
"""
|
|
93
|
+
with self._data_lock:
|
|
94
|
+
if self._latest_raw_data is None:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
if decode_imu_data is None:
|
|
98
|
+
logger.error(
|
|
99
|
+
f"Cannot decode IMU data for {self._name}: dexsensor not available"
|
|
100
|
+
)
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# Decode the IMU data
|
|
105
|
+
imu_data = decode_imu_data(self._latest_raw_data)
|
|
106
|
+
# Return a copy to avoid external modifications
|
|
107
|
+
return {
|
|
108
|
+
key: value.copy() if isinstance(value, np.ndarray) else value
|
|
109
|
+
for key, value in imu_data.items()
|
|
110
|
+
}
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(f"Failed to decode IMU data for {self._name}: {e}")
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def get_acceleration(self) -> np.ndarray | None:
|
|
116
|
+
"""Get the latest linear acceleration.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Linear acceleration [x, y, z] in m/s² if available, None otherwise.
|
|
120
|
+
"""
|
|
121
|
+
imu_data = self.get_latest_data()
|
|
122
|
+
if imu_data is not None:
|
|
123
|
+
return imu_data["acceleration"]
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def get_angular_velocity(self) -> np.ndarray | None:
|
|
127
|
+
"""Get the latest angular velocity.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Angular velocity [x, y, z] in rad/s if available, None otherwise.
|
|
131
|
+
"""
|
|
132
|
+
imu_data = self.get_latest_data()
|
|
133
|
+
if imu_data is not None:
|
|
134
|
+
return imu_data["angular_velocity"]
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def get_orientation(self) -> np.ndarray | None:
|
|
138
|
+
"""Get the latest orientation quaternion.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Orientation quaternion [x, y, z, w] if available, None otherwise.
|
|
142
|
+
"""
|
|
143
|
+
imu_data = self.get_latest_data()
|
|
144
|
+
if imu_data is not None:
|
|
145
|
+
return imu_data["orientation"]
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def get_magnetometer(self) -> np.ndarray | None:
|
|
149
|
+
"""Get the latest magnetometer reading.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Magnetic field [x, y, z] in µT if available, None otherwise.
|
|
153
|
+
"""
|
|
154
|
+
imu_data = self.get_latest_data()
|
|
155
|
+
if imu_data is not None and "magnetometer" in imu_data:
|
|
156
|
+
magnetometer = imu_data["magnetometer"]
|
|
157
|
+
return magnetometer if magnetometer is not None else None
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def has_magnetometer(self) -> bool:
|
|
161
|
+
"""Check if the latest IMU data includes magnetometer information.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if magnetometer data is available, False otherwise.
|
|
165
|
+
"""
|
|
166
|
+
imu_data = self.get_latest_data()
|
|
167
|
+
if imu_data is not None:
|
|
168
|
+
magnetometer = imu_data.get("magnetometer")
|
|
169
|
+
return magnetometer is not None and len(magnetometer) > 0
|
|
170
|
+
return False
|
|
@@ -0,0 +1,195 @@
|
|
|
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
|
+
"""LIDAR Zenoh subscriber for scan data.
|
|
7
|
+
|
|
8
|
+
This module provides a specialized subscriber for LIDAR scan data,
|
|
9
|
+
using the serialization format from dexsensor.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import zenoh
|
|
16
|
+
from loguru import logger
|
|
17
|
+
|
|
18
|
+
from .base import BaseZenohSubscriber, CustomDataHandler
|
|
19
|
+
|
|
20
|
+
# Import lidar serialization functions from dexsensor
|
|
21
|
+
try:
|
|
22
|
+
from dexsensor.serialization.lidar import decode_scan_data
|
|
23
|
+
except ImportError:
|
|
24
|
+
logger.error(
|
|
25
|
+
"Failed to import dexsensor lidar serialization functions. Please install dexsensor."
|
|
26
|
+
)
|
|
27
|
+
decode_scan_data = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LidarSubscriber(BaseZenohSubscriber):
|
|
31
|
+
"""Zenoh subscriber for LIDAR scan data.
|
|
32
|
+
|
|
33
|
+
This subscriber handles LIDAR scan data encoded using the dexsensor
|
|
34
|
+
lidar serialization format with compression.
|
|
35
|
+
Uses lazy decoding - data is only decoded when requested.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
topic: str,
|
|
41
|
+
zenoh_session: zenoh.Session,
|
|
42
|
+
name: str = "lidar_subscriber",
|
|
43
|
+
enable_fps_tracking: bool = True,
|
|
44
|
+
fps_log_interval: int = 50,
|
|
45
|
+
custom_data_handler: CustomDataHandler | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initialize the LIDAR subscriber.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
topic: Zenoh topic to subscribe to for LIDAR data.
|
|
51
|
+
zenoh_session: Active Zenoh session for communication.
|
|
52
|
+
name: Name for logging purposes.
|
|
53
|
+
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
54
|
+
fps_log_interval: Number of frames between FPS calculations.
|
|
55
|
+
custom_data_handler: Optional custom function to handle incoming data.
|
|
56
|
+
If provided, this will replace the default data
|
|
57
|
+
handling logic entirely.
|
|
58
|
+
"""
|
|
59
|
+
super().__init__(
|
|
60
|
+
topic,
|
|
61
|
+
zenoh_session,
|
|
62
|
+
name,
|
|
63
|
+
enable_fps_tracking,
|
|
64
|
+
fps_log_interval,
|
|
65
|
+
custom_data_handler,
|
|
66
|
+
)
|
|
67
|
+
self._latest_raw_data: bytes | None = None
|
|
68
|
+
|
|
69
|
+
def _data_handler(self, sample: zenoh.Sample) -> None:
|
|
70
|
+
"""Handle incoming LIDAR scan data.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
sample: Zenoh sample containing encoded LIDAR scan data.
|
|
74
|
+
"""
|
|
75
|
+
with self._data_lock:
|
|
76
|
+
self._latest_raw_data = sample.payload.to_bytes()
|
|
77
|
+
self._active = True
|
|
78
|
+
|
|
79
|
+
self._update_fps_metrics()
|
|
80
|
+
|
|
81
|
+
def get_latest_data(self) -> dict[str, Any] | None:
|
|
82
|
+
"""Get the latest LIDAR scan data.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Latest scan data dictionary if available, None otherwise.
|
|
86
|
+
Dictionary contains:
|
|
87
|
+
- ranges: Array of range measurements
|
|
88
|
+
- angles: Array of corresponding angles
|
|
89
|
+
- intensities: Array of intensity values (if available)
|
|
90
|
+
- angle_min: Minimum angle of the scan
|
|
91
|
+
- angle_max: Maximum angle of the scan
|
|
92
|
+
- angle_increment: Angular distance between measurements
|
|
93
|
+
- scan_time: Time for a complete scan
|
|
94
|
+
- time_increment: Time between measurements
|
|
95
|
+
- range_min: Minimum range value
|
|
96
|
+
- range_max: Maximum range value
|
|
97
|
+
"""
|
|
98
|
+
with self._data_lock:
|
|
99
|
+
if self._latest_raw_data is None:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
if decode_scan_data is None:
|
|
103
|
+
logger.error(
|
|
104
|
+
f"Cannot decode LIDAR scan for {self._name}: dexsensor not available"
|
|
105
|
+
)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Decode the LIDAR scan data
|
|
110
|
+
scan_data = decode_scan_data(self._latest_raw_data)
|
|
111
|
+
# Return a copy to avoid external modifications
|
|
112
|
+
return {
|
|
113
|
+
key: value.copy() if isinstance(value, np.ndarray) else value
|
|
114
|
+
for key, value in scan_data.items()
|
|
115
|
+
}
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"Failed to decode LIDAR scan for {self._name}: {e}")
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def get_latest_scan(self) -> dict[str, Any] | None:
|
|
121
|
+
"""Get the latest LIDAR scan data.
|
|
122
|
+
|
|
123
|
+
Alias for get_latest_data() for clarity.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Latest scan data dictionary if available, None otherwise.
|
|
127
|
+
"""
|
|
128
|
+
return self.get_latest_data()
|
|
129
|
+
|
|
130
|
+
def get_ranges(self) -> np.ndarray | None:
|
|
131
|
+
"""Get the latest range measurements.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Array of range measurements if available, None otherwise.
|
|
135
|
+
"""
|
|
136
|
+
scan_data = self.get_latest_data()
|
|
137
|
+
if scan_data is not None:
|
|
138
|
+
return scan_data["ranges"]
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
def get_angles(self) -> np.ndarray | None:
|
|
142
|
+
"""Get the latest angle measurements.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Array of angle measurements if available, None otherwise.
|
|
146
|
+
"""
|
|
147
|
+
scan_data = self.get_latest_data()
|
|
148
|
+
if scan_data is not None:
|
|
149
|
+
return scan_data["angles"]
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
def get_intensities(self) -> np.ndarray | None:
|
|
153
|
+
"""Get the latest intensity measurements.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Array of intensity measurements if available, None otherwise.
|
|
157
|
+
"""
|
|
158
|
+
scan_data = self.get_latest_data()
|
|
159
|
+
if scan_data is not None and "intensities" in scan_data:
|
|
160
|
+
intensities = scan_data["intensities"]
|
|
161
|
+
return intensities if intensities is not None else None
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def get_scan_info(self) -> dict[str, float] | None:
|
|
165
|
+
"""Get scan metadata information.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Dictionary with scan metadata if available, None otherwise.
|
|
169
|
+
Contains: angle_min, angle_max, angle_increment, scan_time,
|
|
170
|
+
time_increment, range_min, range_max
|
|
171
|
+
"""
|
|
172
|
+
scan_data = self.get_latest_data()
|
|
173
|
+
if scan_data is not None:
|
|
174
|
+
return {
|
|
175
|
+
"angle_min": scan_data["angle_min"],
|
|
176
|
+
"angle_max": scan_data["angle_max"],
|
|
177
|
+
"angle_increment": scan_data["angle_increment"],
|
|
178
|
+
"scan_time": scan_data["scan_time"],
|
|
179
|
+
"time_increment": scan_data["time_increment"],
|
|
180
|
+
"range_min": scan_data["range_min"],
|
|
181
|
+
"range_max": scan_data["range_max"],
|
|
182
|
+
}
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
def has_intensities(self) -> bool:
|
|
186
|
+
"""Check if the latest scan data includes intensity information.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if intensity data is available, False otherwise.
|
|
190
|
+
"""
|
|
191
|
+
scan_data = self.get_latest_data()
|
|
192
|
+
if scan_data is not None:
|
|
193
|
+
intensities = scan_data.get("intensities")
|
|
194
|
+
return intensities is not None and len(intensities) > 0
|
|
195
|
+
return False
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
"""Protobuf-specific Zenoh subscriber.
|
|
7
|
+
|
|
8
|
+
This module provides a subscriber specifically designed for handling protobuf messages
|
|
9
|
+
with automatic parsing and type safety. Uses lazy decoding for efficiency.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import TypeVar, cast
|
|
13
|
+
|
|
14
|
+
import zenoh
|
|
15
|
+
from google.protobuf.message import Message
|
|
16
|
+
from loguru import logger
|
|
17
|
+
|
|
18
|
+
from .base import BaseZenohSubscriber, CustomDataHandler
|
|
19
|
+
|
|
20
|
+
M = TypeVar("M", bound=Message)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ProtobufZenohSubscriber(BaseZenohSubscriber):
|
|
24
|
+
"""Zenoh subscriber specifically for protobuf messages.
|
|
25
|
+
|
|
26
|
+
This subscriber automatically handles protobuf message parsing and provides
|
|
27
|
+
type-safe access to the latest message data. Uses lazy decoding - protobuf
|
|
28
|
+
messages are only parsed when requested.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
topic: str,
|
|
34
|
+
zenoh_session: zenoh.Session,
|
|
35
|
+
message_type: type[M],
|
|
36
|
+
name: str = "protobuf_subscriber",
|
|
37
|
+
enable_fps_tracking: bool = False,
|
|
38
|
+
fps_log_interval: int = 100,
|
|
39
|
+
custom_data_handler: CustomDataHandler | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Initialize the protobuf Zenoh subscriber.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
topic: Zenoh topic to subscribe to for protobuf messages.
|
|
45
|
+
zenoh_session: Active Zenoh session for communication.
|
|
46
|
+
message_type: Protobuf message class to parse incoming data.
|
|
47
|
+
name: Name for logging purposes.
|
|
48
|
+
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
49
|
+
fps_log_interval: Number of frames between FPS calculations.
|
|
50
|
+
custom_data_handler: Optional custom function to handle incoming data.
|
|
51
|
+
If provided, this will replace the default data
|
|
52
|
+
handling logic entirely.
|
|
53
|
+
"""
|
|
54
|
+
super().__init__(
|
|
55
|
+
topic,
|
|
56
|
+
zenoh_session,
|
|
57
|
+
name,
|
|
58
|
+
enable_fps_tracking,
|
|
59
|
+
fps_log_interval,
|
|
60
|
+
custom_data_handler,
|
|
61
|
+
)
|
|
62
|
+
self._message_type = message_type
|
|
63
|
+
self._latest_raw_data: zenoh.ZBytes = zenoh.ZBytes("")
|
|
64
|
+
|
|
65
|
+
def _data_handler(self, sample: zenoh.Sample) -> None:
|
|
66
|
+
"""Handle incoming protobuf data.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
sample: Zenoh sample containing protobuf data.
|
|
70
|
+
"""
|
|
71
|
+
with self._data_lock:
|
|
72
|
+
self._latest_raw_data = sample.payload
|
|
73
|
+
self._active = True
|
|
74
|
+
|
|
75
|
+
self._update_fps_metrics()
|
|
76
|
+
|
|
77
|
+
def get_latest_data(self) -> M | None:
|
|
78
|
+
"""Get the latest protobuf message.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Latest parsed protobuf message if available and parsing succeeded,
|
|
82
|
+
None otherwise.
|
|
83
|
+
"""
|
|
84
|
+
with self._data_lock:
|
|
85
|
+
if not self._active:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
# Parse protobuf message on demand
|
|
89
|
+
try:
|
|
90
|
+
message = self._message_type()
|
|
91
|
+
message.ParseFromString(self._latest_raw_data.to_bytes())
|
|
92
|
+
return cast(M, message)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Failed to parse protobuf message for {self._name}: {e}")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def get_latest_raw_data(self) -> bytes | None:
|
|
98
|
+
"""Get the latest raw protobuf data bytes.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Latest raw protobuf data bytes if available, None otherwise.
|
|
102
|
+
"""
|
|
103
|
+
with self._data_lock:
|
|
104
|
+
if not self._active:
|
|
105
|
+
return None
|
|
106
|
+
return self._latest_raw_data.to_bytes()
|