dexcontrol 0.2.4__py3-none-any.whl → 0.2.7__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.

@@ -11,13 +11,19 @@
11
11
  """ZED camera sensor implementation using RTC subscribers for RGB and Zenoh subscriber for depth."""
12
12
 
13
13
  import logging
14
+ import time
15
+ from typing import Any, Dict, Optional, Union
14
16
 
15
17
  import numpy as np
16
18
  import zenoh
17
19
 
20
+ from dexcontrol.config.sensors.cameras import ZedCameraConfig
18
21
  from dexcontrol.utils.os_utils import resolve_key_name
19
22
  from dexcontrol.utils.rtc_utils import create_rtc_subscriber_from_zenoh
20
- from dexcontrol.utils.subscribers.camera import DepthCameraSubscriber
23
+ from dexcontrol.utils.subscribers.camera import (
24
+ DepthCameraSubscriber,
25
+ RGBCameraSubscriber,
26
+ )
21
27
  from dexcontrol.utils.subscribers.rtc import RTCSubscriber
22
28
  from dexcontrol.utils.zenoh_utils import query_zenoh_json
23
29
 
@@ -34,289 +40,277 @@ except ImportError:
34
40
 
35
41
 
36
42
  class ZedCameraSensor:
37
- """ZED camera sensor using RTC subscribers for RGB and Zenoh subscriber for depth.
43
+ """ZED camera sensor for multi-stream (RGB, Depth) data acquisition.
38
44
 
39
- This sensor provides left RGB, right RGB, and depth image data from a ZED camera.
40
- RGB streams use RTC subscribers for efficient data handling, while depth uses
41
- regular Zenoh subscriber.
42
-
43
- Note: For depth data decoding, dexsensor package is required.
45
+ This sensor manages left RGB, right RGB, and depth data streams from a ZED
46
+ camera. It can be configured to use high-performance RTC subscribers for RGB
47
+ streams (`use_rtc=True`) or fall back to standard Zenoh subscribers
48
+ (`use_rtc=False`). The depth stream always uses a standard Zenoh subscriber.
44
49
  """
45
50
 
51
+ SubscriberType = Union[RTCSubscriber, DepthCameraSubscriber, RGBCameraSubscriber]
52
+
46
53
  def __init__(
47
54
  self,
48
- configs,
55
+ configs: ZedCameraConfig,
49
56
  zenoh_session: zenoh.Session,
50
- *args,
51
- **kwargs,
52
57
  ) -> None:
53
- """Initialize the ZED camera sensor.
58
+ """Initialize the ZED camera sensor and its subscribers.
54
59
 
55
60
  Args:
56
- configs: Configuration for the ZED camera sensor.
61
+ configs: Configuration object for the ZED camera.
57
62
  zenoh_session: Active Zenoh session for communication.
58
63
  """
59
64
  self._name = configs.name
60
65
  self._zenoh_session = zenoh_session
61
66
  self._configs = configs
67
+ self._subscribers: Dict[str, Optional[ZedCameraSensor.SubscriberType]] = {}
68
+ self._camera_info: Optional[Dict[str, Any]] = None
62
69
 
63
- # Initialize subscribers dictionary - RGB uses RTC, depth uses Zenoh
64
- self._subscribers: dict[str, RTCSubscriber | DepthCameraSubscriber | None] = {}
65
-
66
- # Create subscribers for each enabled stream
67
70
  self._create_subscribers()
71
+ self._query_camera_info()
72
+
73
+ def _create_subscriber(
74
+ self, stream_name: str, stream_config: Dict[str, Any]
75
+ ) -> Optional[SubscriberType]:
76
+ """Factory method to create a subscriber based on stream type and config."""
77
+ try:
78
+ if not stream_config.get("enable", False):
79
+ logger.info(f"'{self._name}': Stream '{stream_name}' is disabled.")
80
+ return None
81
+
82
+ # Create Depth subscriber
83
+ if stream_name == "depth":
84
+ topic = stream_config.get("topic")
85
+ if not topic:
86
+ logger.warning(f"'{self._name}': No 'topic' for depth stream.")
87
+ return None
88
+ logger.info(f"'{self._name}': Creating Zenoh depth subscriber.")
89
+ return DepthCameraSubscriber(
90
+ topic=topic,
91
+ zenoh_session=self._zenoh_session,
92
+ name=f"{self._name}_{stream_name}_subscriber",
93
+ enable_fps_tracking=self._configs.enable_fps_tracking,
94
+ fps_log_interval=self._configs.fps_log_interval,
95
+ )
96
+
97
+ # Create RGB subscriber (RTC or Zenoh)
98
+ if self._configs.use_rtc:
99
+ info_key = stream_config.get("info_key")
100
+ if not info_key:
101
+ logger.warning(f"'{self._name}': No 'info_key' for RTC stream '{stream_name}'.")
102
+ return None
103
+ logger.info(f"'{self._name}': Creating RTC subscriber for '{stream_name}'.")
104
+ return create_rtc_subscriber_from_zenoh(
105
+ zenoh_session=self._zenoh_session,
106
+ info_topic=info_key,
107
+ name=f"{self._name}_{stream_name}_subscriber",
108
+ enable_fps_tracking=self._configs.enable_fps_tracking,
109
+ fps_log_interval=self._configs.fps_log_interval,
110
+ )
111
+ else:
112
+ topic = stream_config.get("topic")
113
+ if not topic:
114
+ logger.warning(f"'{self._name}': No 'topic' for Zenoh stream '{stream_name}'.")
115
+ return None
116
+ logger.info(f"'{self._name}': Creating Zenoh RGB subscriber for '{stream_name}'.")
117
+ return RGBCameraSubscriber(
118
+ topic=topic,
119
+ zenoh_session=self._zenoh_session,
120
+ name=f"{self._name}_{stream_name}_subscriber",
121
+ enable_fps_tracking=self._configs.enable_fps_tracking,
122
+ fps_log_interval=self._configs.fps_log_interval,
123
+ )
124
+
125
+ except Exception as e:
126
+ logger.error(f"Error creating subscriber for '{self._name}/{stream_name}': {e}")
127
+ return None
68
128
 
69
129
  def _create_subscribers(self) -> None:
70
- """Create subscribers for each enabled stream - RTC for RGB, Zenoh for depth."""
130
+ """Create subscribers for all configured camera streams."""
71
131
  subscriber_config = self._configs.subscriber_config
72
-
73
- # Define stream types and their configurations
74
- streams = {
75
- 'left_rgb': subscriber_config.get('left_rgb', {}),
76
- 'right_rgb': subscriber_config.get('right_rgb', {}),
77
- 'depth': subscriber_config.get('depth', {})
132
+ stream_definitions = {
133
+ "left_rgb": subscriber_config.get("left_rgb", {}),
134
+ "right_rgb": subscriber_config.get("right_rgb", {}),
135
+ "depth": subscriber_config.get("depth", {}),
78
136
  }
79
137
 
80
- for stream_name, stream_config in streams.items():
81
- if stream_config.get('enable', False):
82
- try:
83
- if stream_name == 'depth':
84
- # Use regular Zenoh subscriber for depth
85
- topic = stream_config.get('topic')
86
- if topic:
87
- subscriber = DepthCameraSubscriber(
88
- topic=topic,
89
- zenoh_session=self._zenoh_session,
90
- name=f"{self._name}_{stream_name}_subscriber",
91
- enable_fps_tracking=self._configs.enable_fps_tracking,
92
- fps_log_interval=self._configs.fps_log_interval,
93
- )
94
- logger.info(f"Created Zenoh depth subscriber for {self._name} {stream_name}")
95
- self._subscribers[stream_name] = subscriber
96
- else:
97
- logger.warning(f"No topic found for {self._name} {stream_name}")
98
- self._subscribers[stream_name] = None
99
- else:
100
- # Use RTC subscriber for RGB streams
101
- info_key = stream_config.get('info_key')
102
- if info_key:
103
- subscriber = create_rtc_subscriber_from_zenoh(
104
- zenoh_session=self._zenoh_session,
105
- info_topic=info_key,
106
- name=f"{self._name}_{stream_name}_subscriber",
107
- enable_fps_tracking=self._configs.enable_fps_tracking,
108
- fps_log_interval=self._configs.fps_log_interval,
109
- )
110
-
111
- if subscriber is None:
112
- logger.warning(f"Failed to create RTC subscriber for {self._name} {stream_name}")
113
- else:
114
- logger.info(f"Created RTC subscriber for {self._name} {stream_name}")
115
-
116
- self._subscribers[stream_name] = subscriber
117
- else:
118
- logger.warning(f"No info_key found for {self._name} {stream_name}")
119
- self._subscribers[stream_name] = None
120
- except Exception as e:
121
- logger.error(f"Error creating subscriber for {self._name} {stream_name}: {e}")
122
- self._subscribers[stream_name] = None
123
- else:
124
- logger.info(f"Stream {stream_name} disabled for {self._name}")
125
- self._subscribers[stream_name] = None
126
-
127
- # Query for camera info - use info_key from one of the RGB streams
128
- enabled_rgb_configs = [config for config in [subscriber_config.get('left_rgb'), subscriber_config.get('right_rgb')] if config and config.get('enable')]
129
- if enabled_rgb_configs:
130
- info_key = resolve_key_name(enabled_rgb_configs[0].get('info_key')).rstrip('/')
131
- info_key_root = '/'.join(info_key.split('/')[:-2])
132
- info_key = f"{info_key_root}/info"
133
- info = query_zenoh_json(self._zenoh_session, info_key)
134
- self._camera_info = info
135
- if info is not None:
136
- self._depth_min = info.get('depth_min')
137
- self._depth_max = info.get('depth_max')
138
- else:
139
- logger.warning(f"No camera info found for {self._name}")
140
- self._depth_min = None
141
- self._depth_max = None
142
- else:
143
- logger.warning(f"No enabled RGB streams found for camera info query for {self._name}")
144
- self._camera_info = None
145
- self._depth_min = None
146
- self._depth_max = None
138
+ for name, config in stream_definitions.items():
139
+ self._subscribers[name] = self._create_subscriber(name, config)
147
140
 
148
- def _decode_depth_data(self, encoded_depth_data: bytes | None) -> np.ndarray | None:
149
- """Decode depth data from encoded bytes to actual depth values.
141
+ def _query_camera_info(self) -> None:
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
150
146
 
151
- Args:
152
- encoded_depth_data: Raw depth data as bytes.
147
+ enabled_rgb_streams = [
148
+ s
149
+ for s_name, s in self._subscribers.items()
150
+ if "rgb" in s_name and s is not None
151
+ ]
153
152
 
154
- Returns:
155
- Decoded depth data as numpy array (HxW).
153
+ if not enabled_rgb_streams:
154
+ logger.warning(f"'{self._name}': No enabled RGB streams to query for camera info.")
155
+ return
156
156
 
157
- Raises:
158
- RuntimeError: If dexsensor is not available for depth decoding.
159
- """
160
- if encoded_depth_data is None:
161
- return None
157
+ # Use the info_key from the first available RGB subscriber's config
158
+ first_stream_name = "left_rgb" if self._subscribers.get("left_rgb") else "right_rgb"
159
+ stream_config = self._configs.subscriber_config.get(first_stream_name, {})
160
+ info_key = stream_config.get("info_key")
162
161
 
163
- if not DEXSENSOR_AVAILABLE or decode_depth is None:
164
- raise RuntimeError(
165
- f"dexsensor is required for depth decoding in {self._name}. "
166
- "Please install dexsensor: pip install dexsensor"
167
- )
162
+ if not info_key:
163
+ logger.warning(f"'{self._name}': Could not find info_key for camera info query.")
164
+ return
168
165
 
169
166
  try:
170
- # Decode the depth data from bytes - this returns (depth, depth_min, depth_max)
171
- depth_decoded = decode_depth(encoded_depth_data)
172
- return depth_decoded
167
+ # Construct the root info key (e.g., 'camera/head/info')
168
+ resolved_key = resolve_key_name(info_key).rstrip("/")
169
+ info_key_root = "/".join(resolved_key.split("/")[:-2])
170
+ final_info_key = f"{info_key_root}/info"
171
+
172
+ logger.info(f"'{self._name}': Querying for camera info at '{final_info_key}'.")
173
+ self._camera_info = query_zenoh_json(self._zenoh_session, final_info_key)
174
+
175
+ if self._camera_info:
176
+ logger.info(f"'{self._name}': Successfully received camera info.")
177
+ else:
178
+ logger.warning(f"'{self._name}': No camera info found at '{final_info_key}'.")
173
179
  except Exception as e:
174
- raise RuntimeError(f"Failed to decode depth data for {self._name}: {e}")
180
+ logger.error(f"'{self._name}': Failed to query camera info: {e}")
175
181
 
176
182
  def shutdown(self) -> None:
177
- """Shutdown the camera sensor."""
183
+ """Shutdown all active subscribers for the camera sensor."""
184
+ logger.info(f"Shutting down all subscribers for '{self._name}'.")
178
185
  for stream_name, subscriber in self._subscribers.items():
179
186
  if subscriber:
180
187
  try:
181
188
  subscriber.shutdown()
182
- logger.info(f"Shut down {stream_name} subscriber for {self._name}")
189
+ logger.debug(f"'{self._name}': Subscriber '{stream_name}' shut down.")
183
190
  except Exception as e:
184
- logger.error(f"Error shutting down {stream_name} subscriber for {self._name}: {e}")
191
+ logger.error(
192
+ f"Error shutting down '{stream_name}' subscriber for '{self._name}': {e}"
193
+ )
194
+ logger.info(f"'{self._name}' sensor shut down.")
185
195
 
186
196
  def is_active(self) -> bool:
187
- """Check if any camera stream is actively receiving data.
197
+ """Check if any of the camera's subscribers are actively receiving data.
188
198
 
189
199
  Returns:
190
- True if at least one stream is receiving data, False otherwise.
200
+ True if at least one subscriber is active, False otherwise.
191
201
  """
192
- for subscriber in self._subscribers.values():
193
- if subscriber and subscriber.is_active():
194
- return True
195
- return False
202
+ return any(
203
+ sub.is_active() for sub in self._subscribers.values() if sub is not None
204
+ )
196
205
 
197
206
  def is_stream_active(self, stream_name: str) -> bool:
198
- """Check if a specific stream is actively receiving data.
207
+ """Check if a specific camera stream is actively receiving data.
199
208
 
200
209
  Args:
201
- stream_name: Name of the stream ('left_rgb', 'right_rgb', 'depth').
210
+ stream_name: The name of the stream (e.g., 'left_rgb', 'depth').
202
211
 
203
212
  Returns:
204
- True if the stream is receiving data, False otherwise.
213
+ True if the specified stream's subscriber is active, False otherwise.
205
214
  """
206
215
  subscriber = self._subscribers.get(stream_name)
207
216
  return subscriber.is_active() if subscriber else False
208
217
 
209
218
  def wait_for_active(self, timeout: float = 5.0, require_all: bool = False) -> bool:
210
- """Wait for camera streams to start receiving data.
219
+ """Wait for camera streams to become active.
211
220
 
212
221
  Args:
213
- timeout: Maximum time to wait in seconds.
214
- require_all: If True, wait for all enabled streams. If False, wait for any stream.
222
+ timeout: Maximum time to wait in seconds for each subscriber.
223
+ require_all: If True, waits for all enabled streams to become active.
224
+ If False, waits for at least one stream to become active.
215
225
 
216
226
  Returns:
217
- True if condition is met, False if timeout is reached.
227
+ True if the condition is met within the timeout, False otherwise.
218
228
  """
219
- active_subscribers = [sub for sub in self._subscribers.values() if sub is not None]
220
-
221
- if not active_subscribers:
222
- logger.warning(f"No active subscribers for {self._name}")
223
- return False
229
+ enabled_subscribers = [s for s in self._subscribers.values() if s is not None]
230
+ if not enabled_subscribers:
231
+ logger.warning(f"'{self._name}': No subscribers enabled, cannot wait.")
232
+ return True # No subscribers to wait for
224
233
 
225
234
  if require_all:
226
- # Wait for all subscribers to become active
227
- for subscriber in active_subscribers:
228
- if not subscriber.wait_for_active(timeout):
235
+ for sub in enabled_subscribers:
236
+ if not sub.wait_for_active(timeout):
237
+ logger.warning(f"'{self._name}': Timed out waiting for subscriber '{sub.name}'.")
229
238
  return False
239
+ logger.info(f"'{self._name}': All enabled streams are active.")
230
240
  return True
231
241
  else:
232
- # Wait for any subscriber to become active
233
- import time
234
242
  start_time = time.time()
235
243
  while time.time() - start_time < timeout:
236
244
  if self.is_active():
245
+ logger.info(f"'{self._name}': At least one stream is active.")
237
246
  return True
238
247
  time.sleep(0.1)
248
+ logger.warning(f"'{self._name}': Timed out waiting for any stream to become active.")
239
249
  return False
240
250
 
241
- def get_obs(self, obs_keys: list[str] | None = None) -> dict[str, np.ndarray]:
242
- """Get the latest image data from specified streams.
251
+ def get_obs(
252
+ self, obs_keys: Optional[list[str]] = None
253
+ ) -> Dict[str, Optional[np.ndarray]]:
254
+ """Get the latest observation data from specified camera streams.
243
255
 
244
256
  Args:
245
- obs_keys: List of stream names to get data from.
246
- If None, gets data from all enabled streams.
257
+ obs_keys: A list of stream names to retrieve data from (e.g.,
258
+ ['left_rgb', 'depth']). If None, retrieves data from all
259
+ enabled streams.
247
260
 
248
261
  Returns:
249
- Dictionary mapping stream names to image arrays (HxWxC for RGB, HxW for depth)
250
- if available, None otherwise.
251
-
252
- Raises:
253
- RuntimeError: If depth data is requested but dexsensor is not available.
262
+ A dictionary mapping stream names to their latest image data. The
263
+ image is a numpy array (HxWxC for RGB, HxW for depth) or None if
264
+ no data is available for that stream.
254
265
  """
255
- if obs_keys is None:
256
- obs_keys = list(self._subscribers.keys())
257
-
266
+ keys_to_fetch = obs_keys or self.available_streams
258
267
  obs_out = {}
259
- for key in obs_keys:
260
- if key in self._subscribers:
261
- subscriber = self._subscribers[key]
262
- if subscriber:
263
- raw_data = subscriber.get_latest_data()
264
- obs_out[key] = raw_data
265
- else:
266
- obs_out[key] = None
267
- else:
268
- logger.warning(f"Unknown stream key: {key} for {self._name}")
269
-
268
+ for key in keys_to_fetch:
269
+ subscriber = self._subscribers.get(key)
270
+ obs_out[key] = subscriber.get_latest_data() if subscriber else None
270
271
  return obs_out
271
272
 
272
- def get_left_rgb(self) -> np.ndarray | None:
273
- """Get the latest left RGB image.
273
+ def get_left_rgb(self) -> Optional[np.ndarray]:
274
+ """Get the latest image from the left RGB stream.
274
275
 
275
276
  Returns:
276
- Latest left RGB image as numpy array (HxWxC) if available, None otherwise.
277
+ The latest left RGB image as a numpy array, or None if not available.
277
278
  """
278
- subscriber = self._subscribers.get('left_rgb')
279
+ subscriber = self._subscribers.get("left_rgb")
279
280
  return subscriber.get_latest_data() if subscriber else None
280
281
 
281
- def get_right_rgb(self) -> np.ndarray | None:
282
- """Get the latest right RGB image.
282
+ def get_right_rgb(self) -> Optional[np.ndarray]:
283
+ """Get the latest image from the right RGB stream.
283
284
 
284
285
  Returns:
285
- Latest right RGB image as numpy array (HxWxC) if available, None otherwise.
286
+ The latest right RGB image as a numpy array, or None if not available.
286
287
  """
287
- subscriber = self._subscribers.get('right_rgb')
288
+ subscriber = self._subscribers.get("right_rgb")
288
289
  return subscriber.get_latest_data() if subscriber else None
289
290
 
290
- def get_depth(self) -> np.ndarray | None:
291
- """Get the latest depth image.
291
+ def get_depth(self) -> Optional[np.ndarray]:
292
+ """Get the latest image from the depth stream.
292
293
 
293
- Returns:
294
- Latest depth image as numpy array (HxW) if available, None otherwise.
294
+ The depth data is returned as a numpy array with values in meters.
295
295
 
296
- Raises:
297
- RuntimeError: If dexsensor is not available for depth decoding.
296
+ Returns:
297
+ The latest depth image as a numpy array, or None if not available.
298
298
  """
299
- subscriber = self._subscribers.get('depth')
300
- if not subscriber:
301
- return None
302
-
303
- # DepthCameraSubscriber already handles decoding
304
- return subscriber.get_latest_data()
299
+ subscriber = self._subscribers.get("depth")
300
+ return subscriber.get_latest_data() if subscriber else None
305
301
 
306
302
  @property
307
- def fps(self) -> dict[str, float]:
308
- """Get the current FPS measurement for each stream.
303
+ def fps(self) -> Dict[str, float]:
304
+ """Get the current FPS measurement for each active stream.
309
305
 
310
306
  Returns:
311
- Dictionary mapping stream names to their FPS measurements.
307
+ A dictionary mapping stream names to their FPS measurements.
312
308
  """
313
- fps_dict = {}
314
- for stream_name, subscriber in self._subscribers.items():
315
- if subscriber:
316
- fps_dict[stream_name] = subscriber.fps
317
- else:
318
- fps_dict[stream_name] = 0.0
319
- return fps_dict
309
+ return {
310
+ name: sub.fps
311
+ for name, sub in self._subscribers.items()
312
+ if sub is not None
313
+ }
320
314
 
321
315
  @property
322
316
  def name(self) -> str:
@@ -84,16 +84,9 @@ class RPLidarSensor:
84
84
  Returns:
85
85
  Latest scan data dictionary if available, None otherwise.
86
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
87
+ - ranges: Array of range measurements in meters
88
+ - angles: Array of corresponding angles in radians
89
+ - qualities: Array of quality values (0-255) if available, None otherwise
97
90
  """
98
91
  return self._subscriber.get_latest_data()
99
92
 
@@ -101,7 +94,7 @@ class RPLidarSensor:
101
94
  """Get the latest range measurements.
102
95
 
103
96
  Returns:
104
- Array of range measurements if available, None otherwise.
97
+ Array of range measurements in meters if available, None otherwise.
105
98
  """
106
99
  return self._subscriber.get_ranges()
107
100
 
@@ -109,27 +102,17 @@ class RPLidarSensor:
109
102
  """Get the latest angle measurements.
110
103
 
111
104
  Returns:
112
- Array of angle measurements if available, None otherwise.
105
+ Array of angle measurements in radians if available, None otherwise.
113
106
  """
114
107
  return self._subscriber.get_angles()
115
108
 
116
- def get_intensities(self) -> np.ndarray | None:
117
- """Get the latest intensity measurements.
109
+ def get_qualities(self) -> np.ndarray | None:
110
+ """Get the latest quality measurements.
118
111
 
119
112
  Returns:
120
- Array of intensity measurements if available, None otherwise.
113
+ Array of quality values (0-255) if available, None otherwise.
121
114
  """
122
- return self._subscriber.get_intensities()
123
-
124
- def get_scan_info(self) -> dict[str, float] | None:
125
- """Get scan metadata information.
126
-
127
- Returns:
128
- Dictionary with scan metadata if available, None otherwise.
129
- Contains: angle_min, angle_max, angle_increment, scan_time,
130
- time_increment, range_min, range_max
131
- """
132
- return self._subscriber.get_scan_info()
115
+ return self._subscriber.get_qualities()
133
116
 
134
117
  def get_point_count(self) -> int:
135
118
  """Get the number of points in the latest scan.
@@ -142,13 +125,13 @@ class RPLidarSensor:
142
125
  return len(ranges)
143
126
  return 0
144
127
 
145
- def has_intensities(self) -> bool:
146
- """Check if the latest scan data includes intensity information.
128
+ def has_qualities(self) -> bool:
129
+ """Check if the latest scan data includes quality information.
147
130
 
148
131
  Returns:
149
- True if intensity data is available, False otherwise.
132
+ True if quality data is available, False otherwise.
150
133
  """
151
- return self._subscriber.has_intensities()
134
+ return self._subscriber.has_qualities()
152
135
 
153
136
  @property
154
137
  def fps(self) -> float:
@@ -15,10 +15,14 @@ from typing import Any, Literal
15
15
 
16
16
  from dexcontrol.proto import dexcontrol_query_pb2
17
17
 
18
+ TYPE_SOFTWARE_VERSION = dict[
19
+ Literal["hardware_version", "software_version", "main_hash", "compile_time"], Any
20
+ ]
21
+
18
22
 
19
23
  def software_version_to_dict(
20
24
  version_msg: dexcontrol_query_pb2.SoftwareVersion,
21
- ) -> dict[str, dict[Literal["hardware_version", "software_version", "main_hash"], Any]]:
25
+ ) -> dict[str, TYPE_SOFTWARE_VERSION]:
22
26
  """Convert a SoftwareVersion protobuf message to a dictionary.
23
27
 
24
28
  Args:
@@ -32,6 +36,7 @@ def software_version_to_dict(
32
36
  "hardware_version": value.hardware_version,
33
37
  "software_version": value.software_version,
34
38
  "main_hash": value.main_hash,
39
+ "compile_time": value.compile_time,
35
40
  }
36
41
  for key, value in version_msg.firmware_version.items()
37
42
  }
@@ -99,7 +99,7 @@ class RGBCameraSubscriber(BaseZenohSubscriber):
99
99
  return None
100
100
 
101
101
  try:
102
- # Decode the RGB image
102
+ # Decode the image, which is typically in BGR format
103
103
  image = decode_image(self._latest_raw_data)
104
104
  return image
105
105
  except Exception as e: