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.

Files changed (72) hide show
  1. dexcontrol/__init__.py +45 -0
  2. dexcontrol/apps/dualsense_teleop_base.py +371 -0
  3. dexcontrol/config/__init__.py +14 -0
  4. dexcontrol/config/core/__init__.py +22 -0
  5. dexcontrol/config/core/arm.py +32 -0
  6. dexcontrol/config/core/chassis.py +22 -0
  7. dexcontrol/config/core/hand.py +42 -0
  8. dexcontrol/config/core/head.py +33 -0
  9. dexcontrol/config/core/misc.py +37 -0
  10. dexcontrol/config/core/torso.py +36 -0
  11. dexcontrol/config/sensors/__init__.py +4 -0
  12. dexcontrol/config/sensors/cameras/__init__.py +7 -0
  13. dexcontrol/config/sensors/cameras/gemini_camera.py +16 -0
  14. dexcontrol/config/sensors/cameras/rgb_camera.py +15 -0
  15. dexcontrol/config/sensors/imu/__init__.py +6 -0
  16. dexcontrol/config/sensors/imu/gemini_imu.py +15 -0
  17. dexcontrol/config/sensors/imu/nine_axis_imu.py +15 -0
  18. dexcontrol/config/sensors/lidar/__init__.py +6 -0
  19. dexcontrol/config/sensors/lidar/rplidar.py +15 -0
  20. dexcontrol/config/sensors/ultrasonic/__init__.py +6 -0
  21. dexcontrol/config/sensors/ultrasonic/ultrasonic.py +15 -0
  22. dexcontrol/config/sensors/vega_sensors.py +65 -0
  23. dexcontrol/config/vega.py +203 -0
  24. dexcontrol/core/__init__.py +0 -0
  25. dexcontrol/core/arm.py +324 -0
  26. dexcontrol/core/chassis.py +628 -0
  27. dexcontrol/core/component.py +834 -0
  28. dexcontrol/core/hand.py +170 -0
  29. dexcontrol/core/head.py +232 -0
  30. dexcontrol/core/misc.py +514 -0
  31. dexcontrol/core/torso.py +198 -0
  32. dexcontrol/proto/dexcontrol_msg_pb2.py +69 -0
  33. dexcontrol/proto/dexcontrol_msg_pb2.pyi +168 -0
  34. dexcontrol/proto/dexcontrol_query_pb2.py +73 -0
  35. dexcontrol/proto/dexcontrol_query_pb2.pyi +134 -0
  36. dexcontrol/robot.py +1091 -0
  37. dexcontrol/sensors/__init__.py +40 -0
  38. dexcontrol/sensors/camera/__init__.py +18 -0
  39. dexcontrol/sensors/camera/gemini_camera.py +139 -0
  40. dexcontrol/sensors/camera/rgb_camera.py +98 -0
  41. dexcontrol/sensors/imu/__init__.py +22 -0
  42. dexcontrol/sensors/imu/gemini_imu.py +139 -0
  43. dexcontrol/sensors/imu/nine_axis_imu.py +149 -0
  44. dexcontrol/sensors/lidar/__init__.py +3 -0
  45. dexcontrol/sensors/lidar/rplidar.py +164 -0
  46. dexcontrol/sensors/manager.py +185 -0
  47. dexcontrol/sensors/ultrasonic.py +110 -0
  48. dexcontrol/utils/__init__.py +15 -0
  49. dexcontrol/utils/constants.py +12 -0
  50. dexcontrol/utils/io_utils.py +26 -0
  51. dexcontrol/utils/motion_utils.py +194 -0
  52. dexcontrol/utils/os_utils.py +39 -0
  53. dexcontrol/utils/pb_utils.py +103 -0
  54. dexcontrol/utils/rate_limiter.py +167 -0
  55. dexcontrol/utils/reset_orbbec_camera_usb.py +98 -0
  56. dexcontrol/utils/subscribers/__init__.py +44 -0
  57. dexcontrol/utils/subscribers/base.py +260 -0
  58. dexcontrol/utils/subscribers/camera.py +328 -0
  59. dexcontrol/utils/subscribers/decoders.py +83 -0
  60. dexcontrol/utils/subscribers/generic.py +105 -0
  61. dexcontrol/utils/subscribers/imu.py +170 -0
  62. dexcontrol/utils/subscribers/lidar.py +195 -0
  63. dexcontrol/utils/subscribers/protobuf.py +106 -0
  64. dexcontrol/utils/timer.py +136 -0
  65. dexcontrol/utils/trajectory_utils.py +40 -0
  66. dexcontrol/utils/viz_utils.py +86 -0
  67. dexcontrol-0.2.1.dist-info/METADATA +369 -0
  68. dexcontrol-0.2.1.dist-info/RECORD +72 -0
  69. dexcontrol-0.2.1.dist-info/WHEEL +5 -0
  70. dexcontrol-0.2.1.dist-info/licenses/LICENSE +188 -0
  71. dexcontrol-0.2.1.dist-info/licenses/NOTICE +13 -0
  72. 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()