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