dexcontrol 0.2.12__py3-none-any.whl → 0.3.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 (59) hide show
  1. dexcontrol/__init__.py +18 -8
  2. dexcontrol/apps/dualsense_teleop_base.py +1 -1
  3. dexcontrol/comm/__init__.py +51 -0
  4. dexcontrol/comm/base.py +421 -0
  5. dexcontrol/comm/rtc.py +400 -0
  6. dexcontrol/comm/subscribers.py +329 -0
  7. dexcontrol/config/core/chassis.py +9 -4
  8. dexcontrol/config/core/hand.py +1 -0
  9. dexcontrol/config/sensors/cameras/__init__.py +1 -2
  10. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  11. dexcontrol/config/sensors/vega_sensors.py +12 -18
  12. dexcontrol/config/vega.py +4 -1
  13. dexcontrol/core/arm.py +61 -37
  14. dexcontrol/core/chassis.py +141 -119
  15. dexcontrol/core/component.py +110 -59
  16. dexcontrol/core/hand.py +118 -85
  17. dexcontrol/core/head.py +18 -29
  18. dexcontrol/core/misc.py +327 -155
  19. dexcontrol/core/robot_query_interface.py +463 -0
  20. dexcontrol/core/torso.py +4 -8
  21. dexcontrol/proto/dexcontrol_msg_pb2.py +27 -39
  22. dexcontrol/proto/dexcontrol_msg_pb2.pyi +75 -118
  23. dexcontrol/proto/dexcontrol_query_pb2.py +39 -39
  24. dexcontrol/proto/dexcontrol_query_pb2.pyi +17 -4
  25. dexcontrol/robot.py +245 -574
  26. dexcontrol/sensors/__init__.py +1 -2
  27. dexcontrol/sensors/camera/__init__.py +0 -2
  28. dexcontrol/sensors/camera/base_camera.py +144 -0
  29. dexcontrol/sensors/camera/rgb_camera.py +67 -63
  30. dexcontrol/sensors/camera/zed_camera.py +89 -147
  31. dexcontrol/sensors/imu/chassis_imu.py +76 -56
  32. dexcontrol/sensors/imu/zed_imu.py +54 -43
  33. dexcontrol/sensors/lidar/rplidar.py +16 -20
  34. dexcontrol/sensors/manager.py +4 -11
  35. dexcontrol/sensors/ultrasonic.py +14 -27
  36. dexcontrol/utils/__init__.py +0 -11
  37. dexcontrol/utils/comm_helper.py +111 -0
  38. dexcontrol/utils/constants.py +1 -1
  39. dexcontrol/utils/os_utils.py +169 -1
  40. dexcontrol/utils/pb_utils.py +0 -22
  41. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.dist-info}/METADATA +13 -1
  42. dexcontrol-0.3.1.dist-info/RECORD +68 -0
  43. dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
  44. dexcontrol/sensors/camera/luxonis_camera.py +0 -169
  45. dexcontrol/utils/rate_limiter.py +0 -172
  46. dexcontrol/utils/rtc_utils.py +0 -144
  47. dexcontrol/utils/subscribers/__init__.py +0 -52
  48. dexcontrol/utils/subscribers/base.py +0 -281
  49. dexcontrol/utils/subscribers/camera.py +0 -332
  50. dexcontrol/utils/subscribers/decoders.py +0 -88
  51. dexcontrol/utils/subscribers/generic.py +0 -110
  52. dexcontrol/utils/subscribers/imu.py +0 -175
  53. dexcontrol/utils/subscribers/lidar.py +0 -172
  54. dexcontrol/utils/subscribers/protobuf.py +0 -111
  55. dexcontrol/utils/subscribers/rtc.py +0 -316
  56. dexcontrol/utils/zenoh_utils.py +0 -122
  57. dexcontrol-0.2.12.dist-info/RECORD +0 -75
  58. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.dist-info}/WHEEL +0 -0
  59. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -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
- """Rate limiter utility for maintaining consistent execution rates."""
12
-
13
- import sys
14
- import time
15
- from typing import Final
16
-
17
- from loguru import logger
18
-
19
-
20
- class RateLimiter:
21
- """Class for limiting execution rate to a target frequency.
22
-
23
- This class provides rate limiting functionality by sleeping between iterations to
24
- maintain a desired execution frequency. It also tracks statistics about the achieved
25
- rate and missed deadlines.
26
-
27
- Attributes:
28
- period_sec: Time period between iterations in seconds.
29
- target_rate_hz: Desired execution rate in Hz.
30
- window_size: Size of moving average window for rate calculations.
31
- last_time_sec: Timestamp of the last iteration.
32
- next_time_sec: Scheduled timestamp for the next iteration.
33
- start_time_sec: Timestamp when the rate limiter was initialized or reset.
34
- duration_buffer: List of recent iteration durations for rate calculation.
35
- missed_deadlines: Counter for iterations that missed their scheduled time.
36
- iterations: Total number of iterations since initialization or reset.
37
- """
38
-
39
- def __init__(self, rate_hz: float, window_size: int = 50) -> None:
40
- """Initializes rate limiter.
41
-
42
- Args:
43
- rate_hz: Desired rate in Hz.
44
- window_size: Size of moving average window for rate calculations.
45
-
46
- Raises:
47
- ValueError: If rate_hz is not positive.
48
- """
49
- if rate_hz <= 0:
50
- raise ValueError("Rate must be positive")
51
-
52
- self.period_sec: Final[float] = 1.0 / rate_hz
53
- self.target_rate_hz: Final[float] = rate_hz
54
- self.window_size: Final[int] = max(1, window_size)
55
-
56
- # Initialize timing variables
57
- now_sec = time.monotonic()
58
- self.last_time_sec: float = now_sec
59
- self.next_time_sec: float = now_sec + self.period_sec
60
- self.start_time_sec: float = now_sec
61
-
62
- # Initialize statistics
63
- self.duration_buffer: list[float] = []
64
- self.missed_deadlines: int = 0
65
- self.iterations: int = 0
66
- self._MAX_ITERATIONS: Final[int] = sys.maxsize - 1000 # Leave some buffer
67
-
68
- def sleep(self) -> None:
69
- """Sleeps to maintain desired rate.
70
-
71
- Sleeps for the appropriate duration to maintain the target rate. If the next
72
- scheduled time has already passed, increments the missed deadlines counter.
73
- Uses monotonic time for reliable timing regardless of system clock changes.
74
- """
75
- current_time_sec = time.monotonic()
76
- sleep_time_sec = self.next_time_sec - current_time_sec
77
-
78
- if sleep_time_sec > 0:
79
- time.sleep(sleep_time_sec)
80
- else:
81
- self.missed_deadlines += 1
82
-
83
- # Update timing and statistics
84
- now_sec = time.monotonic()
85
-
86
- # Reset iterations if approaching max value
87
- if self.iterations >= self._MAX_ITERATIONS:
88
- logger.warning(
89
- "Iteration counter approaching max value, resetting statistics"
90
- )
91
- self.reset()
92
- return
93
-
94
- self.iterations += 1
95
-
96
- if self.iterations > 1: # Skip first iteration
97
- duration_sec = now_sec - self.last_time_sec
98
- if len(self.duration_buffer) >= self.window_size:
99
- self.duration_buffer.pop(0)
100
- self.duration_buffer.append(duration_sec)
101
-
102
- self.last_time_sec = now_sec
103
-
104
- # More efficient way to advance next_time_sec when multiple periods behind
105
- periods_behind = max(
106
- 0, int((now_sec - self.next_time_sec) / self.period_sec) + 1
107
- )
108
- self.next_time_sec += periods_behind * self.period_sec
109
-
110
- def get_actual_rate(self) -> float:
111
- """Calculates actual achieved rate in Hz using moving average.
112
-
113
- Returns:
114
- Current execution rate based on recent iterations.
115
- """
116
- if not self.duration_buffer:
117
- return 0.0
118
- avg_duration_sec = sum(self.duration_buffer) / len(self.duration_buffer)
119
- return 0.0 if avg_duration_sec <= 0 else 1.0 / avg_duration_sec
120
-
121
- def get_average_rate(self) -> float:
122
- """Calculates average rate over entire run.
123
-
124
- Returns:
125
- Average execution rate since start or last reset.
126
- """
127
- if self.iterations < 2:
128
- return 0.0
129
- total_time_sec = time.monotonic() - self.start_time_sec
130
- return 0.0 if total_time_sec <= 0 else self.iterations / total_time_sec
131
-
132
- def reset(self) -> None:
133
- """Resets the rate limiter state and statistics."""
134
- now_sec = time.monotonic()
135
- self.last_time_sec = now_sec
136
- self.next_time_sec = now_sec + self.period_sec
137
- self.start_time_sec = now_sec
138
- self.duration_buffer.clear()
139
- self.missed_deadlines = 0
140
- self.iterations = 0
141
-
142
- def get_stats(self) -> dict[str, float | int]:
143
- """Gets runtime statistics.
144
-
145
- Returns:
146
- Dictionary containing execution statistics including actual rate,
147
- average rate, target rate, missed deadlines and iteration count.
148
- """
149
- return {
150
- "actual_rate": self.get_actual_rate(),
151
- "average_rate": self.get_average_rate(),
152
- "target_rate": self.target_rate_hz,
153
- "missed_deadlines": self.missed_deadlines,
154
- "iterations": self.iterations,
155
- }
156
-
157
-
158
- if __name__ == "__main__":
159
- rate_limiter = RateLimiter(100.0) # 100Hz
160
-
161
- try:
162
- while True:
163
- rate_limiter.sleep()
164
- actual_rate = rate_limiter.get_actual_rate()
165
- logger.info(f"Rate: {actual_rate:.2f} Hz")
166
-
167
- except KeyboardInterrupt:
168
- stats = rate_limiter.get_stats()
169
- logger.info("\nFinal stats:")
170
- logger.info(f"Average rate: {stats['average_rate']:.2f} Hz")
171
- logger.info(f"Final rate: {stats['actual_rate']:.2f} Hz")
172
- logger.info(f"Missed deadlines: {stats['missed_deadlines']}")
@@ -1,144 +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
- """RTC utilities for dexcontrol.
12
-
13
- This module provides utility functions for creating RTC subscribers
14
- that first query Zenoh for connection information.
15
- """
16
-
17
- import zenoh
18
- from loguru import logger
19
-
20
- from dexcontrol.utils.subscribers.rtc import RTCSubscriber
21
- from dexcontrol.utils.zenoh_utils import query_zenoh_json
22
-
23
-
24
- def query_rtc_info(
25
- zenoh_session: zenoh.Session,
26
- info_topic: str,
27
- timeout: float = 2.0,
28
- max_retries: int = 1,
29
- retry_delay: float = 0.5,
30
- ) -> dict | None:
31
- """Query Zenoh for RTC connection information.
32
-
33
- Args:
34
- zenoh_session: Active Zenoh session for communication.
35
- info_topic: Zenoh topic to query for RTC info.
36
- timeout: Maximum time to wait for a response in seconds.
37
- max_retries: Maximum number of retry attempts.
38
- retry_delay: Initial delay between retries (doubles each retry).
39
-
40
- Returns:
41
- Dictionary containing host and port information if successful, None otherwise.
42
- """
43
-
44
- # Use the general Zenoh query function
45
- info = query_zenoh_json(
46
- zenoh_session=zenoh_session,
47
- topic=info_topic,
48
- timeout=timeout,
49
- max_retries=max_retries,
50
- retry_delay=retry_delay,
51
- )
52
-
53
- return info
54
-
55
-
56
- def create_rtc_subscriber_from_zenoh(
57
- zenoh_session: zenoh.Session,
58
- info_topic: str,
59
- name: str = "rtc_subscriber",
60
- enable_fps_tracking: bool = True,
61
- fps_log_interval: int = 100,
62
- query_timeout: float = 2.0,
63
- max_retries: int = 1,
64
- ) -> RTCSubscriber | None:
65
- """Create a RTC subscriber by first querying Zenoh for connection info.
66
-
67
- Args:
68
- zenoh_session: Active Zenoh session for communication.
69
- info_topic: Zenoh topic to query for RTC connection info.
70
- name: Name for logging purposes.
71
- enable_fps_tracking: Whether to track and log FPS metrics.
72
- fps_log_interval: Number of frames between FPS calculations.
73
- query_timeout: Maximum time to wait for Zenoh query response.
74
- max_retries: Maximum number of retry attempts for Zenoh query.
75
-
76
- Returns:
77
- RTCSubscriber instance if successful, None otherwise.
78
- """
79
- # Query Zenoh for RTC connection information
80
- rtc_info = query_rtc_info(zenoh_session, info_topic, query_timeout, max_retries)
81
-
82
- if rtc_info is None:
83
- logger.error("Failed to get RTC connection info from Zenoh")
84
- return None
85
-
86
- url = rtc_info.get("signaling_url")
87
-
88
- if not url:
89
- logger.error(f"Invalid RTC info: url={url}")
90
- return None
91
-
92
- # Construct WebSocket URL
93
- ws_url = url
94
- logger.info(f"Creating RTC subscriber with URL: {ws_url}")
95
-
96
- try:
97
- # Create and return the RTC subscriber
98
- subscriber = RTCSubscriber(
99
- url=ws_url,
100
- name=name,
101
- enable_fps_tracking=enable_fps_tracking,
102
- fps_log_interval=fps_log_interval,
103
- )
104
- return subscriber
105
- except Exception as e:
106
- logger.error(f"Failed to create RTC subscriber: {e}")
107
- return None
108
-
109
-
110
- def create_rtc_subscriber_with_config(
111
- zenoh_session: zenoh.Session,
112
- config,
113
- name: str = "rtc_subscriber",
114
- enable_fps_tracking: bool = True,
115
- fps_log_interval: int = 100,
116
- ) -> RTCSubscriber | None:
117
- """Create a RTC subscriber using configuration object.
118
-
119
- Args:
120
- zenoh_session: Active Zenoh session for communication.
121
- config: Configuration object containing info_key.
122
- name: Name for logging purposes.
123
- enable_fps_tracking: Whether to track and log FPS metrics.
124
- fps_log_interval: Number of frames between FPS calculations.
125
- Returns:
126
- RTCSubscriber instance if successful, None otherwise.
127
- """
128
- if "info_key" not in config:
129
- logger.error("Config subscriber_config missing info_key")
130
- return None
131
-
132
- if not config["enable"]:
133
- logger.info(f"Skipping {name} because it is disabled")
134
- return None
135
-
136
- info_topic = config["info_key"]
137
-
138
- return create_rtc_subscriber_from_zenoh(
139
- zenoh_session=zenoh_session,
140
- info_topic=info_topic,
141
- name=name,
142
- enable_fps_tracking=enable_fps_tracking,
143
- fps_log_interval=fps_log_interval,
144
- )
@@ -1,52 +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
- """Zenoh subscriber utilities for dexcontrol.
12
-
13
- This module provides a collection of subscriber classes and utilities for handling
14
- Zenoh communication in a flexible and reusable way.
15
- """
16
-
17
- from .base import BaseZenohSubscriber, CustomDataHandler
18
- from .camera import DepthCameraSubscriber, RGBCameraSubscriber, RGBDCameraSubscriber
19
- from .decoders import (
20
- DecoderFunction,
21
- json_decoder,
22
- protobuf_decoder,
23
- raw_bytes_decoder,
24
- string_decoder,
25
- )
26
- from .generic import GenericZenohSubscriber
27
- from .imu import IMUSubscriber
28
- from .lidar import LidarSubscriber
29
- from .protobuf import ProtobufZenohSubscriber
30
- from .rtc import RTCSubscriber
31
-
32
- __all__ = [
33
- "BaseZenohSubscriber",
34
- "CustomDataHandler",
35
- "GenericZenohSubscriber",
36
- "ProtobufZenohSubscriber",
37
- "DecoderFunction",
38
- "protobuf_decoder",
39
- "raw_bytes_decoder",
40
- "json_decoder",
41
- "string_decoder",
42
- # Camera subscribers
43
- "RGBCameraSubscriber",
44
- "DepthCameraSubscriber",
45
- "RGBDCameraSubscriber",
46
- # Lidar subscriber
47
- "LidarSubscriber",
48
- # IMU subscriber
49
- "IMUSubscriber",
50
- # RTC subscriber
51
- "RTCSubscriber",
52
- ]
@@ -1,281 +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
- """Base Zenoh subscriber utilities.
12
-
13
- This module provides the abstract base class for all Zenoh subscribers
14
- and common utilities used across different subscriber implementations.
15
- """
16
-
17
- import threading
18
- import time
19
- from abc import ABC, abstractmethod
20
- from collections.abc import Callable
21
- from typing import Any, Final, TypeVar
22
-
23
- import zenoh
24
- from google.protobuf.message import Message
25
- from loguru import logger
26
-
27
- from dexcontrol.utils.os_utils import resolve_key_name
28
-
29
- # Type variable for Message subclasses
30
- M = TypeVar("M", bound=Message)
31
-
32
- # Type alias for custom data handler functions
33
- CustomDataHandler = Callable[[zenoh.Sample], None]
34
-
35
-
36
- class BaseZenohSubscriber(ABC):
37
- """Base class for Zenoh subscribers with configurable data handling.
38
-
39
- This class provides a common interface for subscribing to Zenoh topics
40
- and processing incoming data through configurable decoder functions.
41
- It handles the Zenoh communication setup and provides thread-safe access
42
- to the latest data.
43
-
44
- Attributes:
45
- _active: Whether subscriber is receiving data updates.
46
- _data_lock: Lock for thread-safe data access.
47
- _zenoh_session: Active Zenoh session for communication.
48
- _subscriber: Zenoh subscriber for data.
49
- _topic: The resolved Zenoh topic name.
50
- _enable_fps_tracking: Whether to track and log FPS metrics.
51
- _frame_count: Counter for frames processed since last FPS calculation.
52
- _fps: Most recently calculated frames per second value.
53
- _last_fps_time: Timestamp of last FPS calculation.
54
- _fps_log_interval: Number of frames between FPS calculations.
55
- _name: Name for logging purposes.
56
- _custom_data_handler: Optional custom data handler function.
57
- _last_data_time: Timestamp of last data received.
58
- """
59
-
60
- def __init__(
61
- self,
62
- topic: str,
63
- zenoh_session: zenoh.Session,
64
- name: str = "subscriber",
65
- enable_fps_tracking: bool = False,
66
- fps_log_interval: int = 100,
67
- custom_data_handler: CustomDataHandler | None = None,
68
- ) -> None:
69
- """Initialize the base Zenoh subscriber.
70
-
71
- Args:
72
- topic: Zenoh topic to subscribe to for data.
73
- zenoh_session: Active Zenoh session for communication.
74
- name: Name for logging purposes.
75
- enable_fps_tracking: Whether to track and log FPS metrics.
76
- fps_log_interval: Number of frames between FPS calculations.
77
- custom_data_handler: Optional custom function to handle incoming data.
78
- If provided, this will replace the default data
79
- handling logic entirely.
80
- """
81
- self._active: bool = False
82
- self._data_lock: Final[threading.RLock] = threading.RLock()
83
- self._zenoh_session: Final[zenoh.Session] = zenoh_session
84
- self._name = name
85
- self._custom_data_handler = custom_data_handler
86
-
87
- # Data freshness tracking
88
- self._last_data_time: float | None = None
89
-
90
- # FPS tracking
91
- self._enable_fps_tracking = enable_fps_tracking
92
- self._fps_log_interval = fps_log_interval
93
- self._frame_count = 0
94
- self._fps = 0.0
95
- self._last_fps_time = time.time()
96
-
97
- # Setup Zenoh subscriber
98
- self._topic: Final[str] = resolve_key_name(topic)
99
- self._subscriber: Final[zenoh.Subscriber] = zenoh_session.declare_subscriber(
100
- self._topic, self._data_handler_wrapper
101
- )
102
-
103
- logger.info(f"Initialized {self._name} subscriber for topic: {self._topic}")
104
-
105
- def _data_handler_wrapper(self, sample: zenoh.Sample) -> None:
106
- """Wrapper for data handling that calls either custom or default handler.
107
-
108
- Args:
109
- sample: Zenoh sample containing data.
110
- """
111
- # Update data freshness timestamp
112
- with self._data_lock:
113
- self._last_data_time = time.monotonic()
114
-
115
- # Call custom data handler if provided, otherwise call default handler
116
- if self._custom_data_handler is not None:
117
- try:
118
- self._custom_data_handler(sample)
119
- except Exception as e:
120
- logger.error(f"Custom data handler failed for {self._name}: {e}")
121
- else:
122
- # Call the default data handler
123
- self._data_handler(sample)
124
-
125
- @abstractmethod
126
- def _data_handler(self, sample: zenoh.Sample) -> None:
127
- """Handle incoming data.
128
-
129
- This method must be implemented by subclasses to process the specific
130
- type of data they handle.
131
-
132
- Args:
133
- sample: Zenoh sample containing data.
134
- """
135
- pass
136
-
137
- @abstractmethod
138
- def get_latest_data(self) -> Any | None:
139
- """Get the latest data.
140
-
141
- Returns:
142
- Latest data if available, None otherwise.
143
- """
144
- pass
145
-
146
- def _update_fps_metrics(self) -> None:
147
- """Update FPS tracking metrics.
148
-
149
- Increments frame counter and recalculates FPS at specified intervals.
150
- Only has an effect if fps_tracking was enabled during initialization.
151
- """
152
- if not self._enable_fps_tracking:
153
- return
154
-
155
- self._frame_count += 1
156
- if self._frame_count >= self._fps_log_interval:
157
- current_time = time.time()
158
- elapsed = current_time - self._last_fps_time
159
- self._fps = self._frame_count / elapsed
160
- logger.info(f"{self._name} {self._topic} frequency: {self._fps:.2f} Hz")
161
- self._frame_count = 0
162
- self._last_fps_time = current_time
163
-
164
- def wait_for_active(self, timeout: float = 5.0) -> bool:
165
- """Wait for the subscriber to start receiving data.
166
-
167
- Args:
168
- timeout: Maximum time to wait in seconds.
169
-
170
- Returns:
171
- True if subscriber becomes active, False if timeout is reached.
172
- """
173
- start_time = time.monotonic()
174
- check_interval = min(0.05, timeout / 10) # Check every 10ms or less
175
-
176
- while True:
177
- with self._data_lock:
178
- if self._active:
179
- return True
180
-
181
- elapsed = time.monotonic() - start_time
182
- if elapsed >= timeout:
183
- logger.error(f"No data received from {self._topic} after {timeout}s")
184
- return False
185
-
186
- # Sleep for the shorter of: remaining time or check interval
187
- sleep_time = min(check_interval, timeout - elapsed)
188
- time.sleep(sleep_time)
189
-
190
- def is_active(self) -> bool:
191
- """Check if the subscriber is actively receiving data.
192
-
193
- Returns:
194
- True if subscriber is active, False otherwise.
195
- """
196
- with self._data_lock:
197
- return self._active
198
-
199
- def shutdown(self) -> None:
200
- """Stop the subscriber and release resources."""
201
- # Mark as inactive first to prevent further data processing
202
- with self._data_lock:
203
- self._active = False
204
-
205
- # Small delay to allow any ongoing data processing to complete
206
- time.sleep(0.05)
207
-
208
- try:
209
- if hasattr(self, "_subscriber") and self._subscriber:
210
- self._subscriber.undeclare()
211
- except Exception as e:
212
- # Don't log "Undeclared subscriber" errors as warnings - they're expected during shutdown
213
- error_msg = str(e).lower()
214
- if not ("undeclared" in error_msg or "closed" in error_msg):
215
- logger.warning(f"Error undeclaring subscriber for {self._topic}: {e}")
216
-
217
- # Additional delay to allow Zenoh to process the undeclare operation
218
- time.sleep(0.02)
219
-
220
- @property
221
- def topic(self) -> str:
222
- """Get the Zenoh topic name.
223
-
224
- Returns:
225
- The resolved Zenoh topic name.
226
- """
227
- return self._topic
228
-
229
- @property
230
- def fps(self) -> float:
231
- """Get the current FPS measurement.
232
-
233
- Returns:
234
- Current frames per second measurement.
235
- """
236
- return self._fps
237
-
238
- @property
239
- def name(self) -> str:
240
- """Get the subscriber name.
241
-
242
- Returns:
243
- The subscriber name.
244
- """
245
- return self._name
246
-
247
- def is_data_fresh(self, max_age_seconds: float) -> bool:
248
- """Check if the most recent data is fresh (received within the specified time).
249
-
250
- This method checks if data has been received within the specified time window,
251
- regardless of whether the data content has changed. This is useful for
252
- detecting communication failures or stale data streams.
253
-
254
- Args:
255
- max_age_seconds: Maximum age in seconds for data to be considered fresh.
256
-
257
- Returns:
258
- True if fresh data was received within the time window, False otherwise.
259
- Returns False if no data has ever been received.
260
- """
261
- with self._data_lock:
262
- if self._last_data_time is None:
263
- return False
264
-
265
- current_time = time.monotonic()
266
- age = current_time - self._last_data_time
267
- return age <= max_age_seconds
268
-
269
- def get_time_since_last_data(self) -> float | None:
270
- """Get the time elapsed since the last data was received.
271
-
272
- Returns:
273
- Time in seconds since last data was received, or None if no data
274
- has ever been received.
275
- """
276
- with self._data_lock:
277
- if self._last_data_time is None:
278
- return None
279
-
280
- current_time = time.monotonic()
281
- return current_time - self._last_data_time