dexcontrol 0.3.0__py3-none-any.whl → 0.3.2__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 (51) hide show
  1. dexcontrol/__init__.py +16 -7
  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/sensors/cameras/__init__.py +1 -2
  8. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  9. dexcontrol/config/sensors/vega_sensors.py +12 -18
  10. dexcontrol/core/arm.py +29 -25
  11. dexcontrol/core/chassis.py +3 -12
  12. dexcontrol/core/component.py +68 -43
  13. dexcontrol/core/hand.py +50 -52
  14. dexcontrol/core/head.py +14 -26
  15. dexcontrol/core/misc.py +188 -166
  16. dexcontrol/core/robot_query_interface.py +140 -117
  17. dexcontrol/core/torso.py +0 -4
  18. dexcontrol/robot.py +15 -37
  19. dexcontrol/sensors/__init__.py +1 -2
  20. dexcontrol/sensors/camera/__init__.py +0 -2
  21. dexcontrol/sensors/camera/base_camera.py +144 -0
  22. dexcontrol/sensors/camera/rgb_camera.py +67 -63
  23. dexcontrol/sensors/camera/zed_camera.py +89 -147
  24. dexcontrol/sensors/imu/chassis_imu.py +76 -56
  25. dexcontrol/sensors/imu/zed_imu.py +54 -43
  26. dexcontrol/sensors/lidar/rplidar.py +16 -20
  27. dexcontrol/sensors/manager.py +4 -11
  28. dexcontrol/sensors/ultrasonic.py +14 -27
  29. dexcontrol/utils/__init__.py +0 -11
  30. dexcontrol/utils/comm_helper.py +111 -0
  31. dexcontrol/utils/constants.py +1 -1
  32. dexcontrol/utils/os_utils.py +8 -22
  33. {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.2.dist-info}/METADATA +2 -1
  34. dexcontrol-0.3.2.dist-info/RECORD +68 -0
  35. dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
  36. dexcontrol/sensors/camera/luxonis_camera.py +0 -169
  37. dexcontrol/utils/rate_limiter.py +0 -172
  38. dexcontrol/utils/rtc_utils.py +0 -144
  39. dexcontrol/utils/subscribers/__init__.py +0 -52
  40. dexcontrol/utils/subscribers/base.py +0 -281
  41. dexcontrol/utils/subscribers/camera.py +0 -332
  42. dexcontrol/utils/subscribers/decoders.py +0 -88
  43. dexcontrol/utils/subscribers/generic.py +0 -110
  44. dexcontrol/utils/subscribers/imu.py +0 -175
  45. dexcontrol/utils/subscribers/lidar.py +0 -172
  46. dexcontrol/utils/subscribers/protobuf.py +0 -111
  47. dexcontrol/utils/subscribers/rtc.py +0 -316
  48. dexcontrol/utils/zenoh_utils.py +0 -369
  49. dexcontrol-0.3.0.dist-info/RECORD +0 -76
  50. {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.2.dist-info}/WHEEL +0 -0
  51. {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,68 @@
1
+ dexcontrol/__init__.py,sha256=IjeenMFvYyasRDb58qzVHdftwTH_nRgKDVLSmhH7g-k,2009
2
+ dexcontrol/robot.py,sha256=mXezG10UMVBYynE-WD5_XBOjvnjVBhYZjarP_W09TPs,40471
3
+ dexcontrol/apps/dualsense_teleop_base.py,sha256=HJ5aB9zaXdEUQtkd8Vrd0dQqYZrKH1nEjcSGSlDpYD4,12686
4
+ dexcontrol/comm/__init__.py,sha256=YV8Sfv15vuUUqR2lg_H97RCHWUaOblxbI5NgigP4kzc,1272
5
+ dexcontrol/comm/base.py,sha256=PjrEiK7fhGvJ6snaWziSeoNHiRe5I9JDiOs_ptLQC1s,12241
6
+ dexcontrol/comm/rtc.py,sha256=kGFppq3ewXIY4OQ92cExf7AohoSrLImYyb_zBFpUswI,13118
7
+ dexcontrol/comm/subscribers.py,sha256=eg_rdgEUDU8dMPpNsLYJhupxB52fKs7MJmfX9ung66Q,8946
8
+ dexcontrol/config/__init__.py,sha256=UVLNpzGD14e8g68rUZFXTh0B7FRx6uS0Eg_MecjinYM,520
9
+ dexcontrol/config/vega.py,sha256=Uh14vBOZoMAmFXQgjh-IK9x_W0j8YvqgUsoDNHx1ZsE,8443
10
+ dexcontrol/config/core/__init__.py,sha256=Ym2R1hr1iMKQuXcg16BpZfQtTb0hQ5Q7smUIMlwKfho,637
11
+ dexcontrol/config/core/arm.py,sha256=5hN1dQMe2H6oufaqgtZqx9vuB969DxM26leJqPsKEiA,1471
12
+ dexcontrol/config/core/chassis.py,sha256=2FjyFujg2q7aw8J9BklNM7eeLxscY9BSTlBvz-tLwOM,1092
13
+ dexcontrol/config/core/hand.py,sha256=r6XVyGCuwv7MFmaMLn7l3iPZUH376NZSmtsfLnznAgw,1033
14
+ dexcontrol/config/core/head.py,sha256=SLwZE-lYEOk8XAmW-Ex7VkLF2w5HLItwsA3Dc7n5FtE,1061
15
+ dexcontrol/config/core/misc.py,sha256=zHkJ144b6kbmMFE63wy_fgfo_6V-4XmM19hr6BUtQ0Y,1567
16
+ dexcontrol/config/core/torso.py,sha256=DCTFgN1_Gn4THkKy23sIHOedACQtQ7cET3g4AmkVPco,1460
17
+ dexcontrol/config/sensors/__init__.py,sha256=bYPMLxbbn5QeuPyA6OPGDS2JTYpnVvaZJT8PeILFjQY,252
18
+ dexcontrol/config/sensors/vega_sensors.py,sha256=m_Z5ywKxGBvP7pUr6dxLNnGEPI2jWt2hDtxln4uFnY8,2675
19
+ dexcontrol/config/sensors/cameras/__init__.py,sha256=GmwRW9ovZ_JcpD2QmzTO_in_LRBoRDorjMVGY6XgGI8,383
20
+ dexcontrol/config/sensors/cameras/rgb_camera.py,sha256=MN4SjyZlfbrQ3JKDDkT8HhC0Aiyc0bWfDLt4ik0Xcvs,1448
21
+ dexcontrol/config/sensors/cameras/zed_camera.py,sha256=UGAjXlOu5E0M3vjMSIOqdvNjK8mhx52OIDgs5RksqUc,1895
22
+ dexcontrol/config/sensors/imu/__init__.py,sha256=fW-DlevCvf_W8HV_fvLe9yIe-XL5op2mggoTKh-6fGQ,328
23
+ dexcontrol/config/sensors/imu/chassis_imu.py,sha256=3OlTTBH6k1QGM5c5bcg8NL3XUXzYA8gCLM8lpCq2KFM,559
24
+ dexcontrol/config/sensors/imu/zed_imu.py,sha256=y-dPI-XS6Kyq0WOf0wwuc2BgVnMN2hwCMxb0Vmwt4O4,550
25
+ dexcontrol/config/sensors/lidar/__init__.py,sha256=j8vFkF675Z7zKtCztJcyG7oSA_XqrD8OeQLEK0GACug,288
26
+ dexcontrol/config/sensors/lidar/rplidar.py,sha256=ybuT_f1ADWF3oGH1gi6D2F80TbJEm4vbm68Fe108OAA,541
27
+ dexcontrol/config/sensors/ultrasonic/__init__.py,sha256=-q83RhIMZJGVFVPYaA4hOugoG6wZw8EL6wJg7-HTSxU,294
28
+ dexcontrol/config/sensors/ultrasonic/ultrasonic.py,sha256=7b4dm1QOhy5_5RFVpY-frXZyDzqok0K1u7ed9gf3PL0,552
29
+ dexcontrol/core/__init__.py,sha256=bYPMLxbbn5QeuPyA6OPGDS2JTYpnVvaZJT8PeILFjQY,252
30
+ dexcontrol/core/arm.py,sha256=4T6s3WuzX7udq2RqesZkZ3KVq4ANmnJnvDFaLqR0X8k,16230
31
+ dexcontrol/core/chassis.py,sha256=cxiZv86v9vWSGoWJLZZFn-vEo4OCDlh5S-J9Z0iED0c,24189
32
+ dexcontrol/core/component.py,sha256=J7TscC7u4io38gS9SNaRvnBBH48LgKF6dRbSeHzl2Vk,36346
33
+ dexcontrol/core/hand.py,sha256=yRimScGR0wdL6nPaoEO2M1okc7tYBXMIwriLjpOs35g,9596
34
+ dexcontrol/core/head.py,sha256=Dd-Wk4-BiUM8q9LnTU017fijCwDT9QhFsU4v9Hqeo-E,9932
35
+ dexcontrol/core/misc.py,sha256=nCY-rOslBZyx1w7QUfu6M7VZvMeiTfWg2eFTjYAbI-M,30809
36
+ dexcontrol/core/robot_query_interface.py,sha256=JrZRH0YcOoFHHVjElxmesvOfjKv6YwzsbECLBudvOBk,17488
37
+ dexcontrol/core/torso.py,sha256=xOLfhivwNM-JR8nw6Lg1FdctXlmX-qvjD2nUgHm5iKw,8762
38
+ dexcontrol/proto/dexcontrol_msg_pb2.py,sha256=pIbSlweIVSU3hN13kZxPycySU_OYXPoHRuDh7NIBXho,5289
39
+ dexcontrol/proto/dexcontrol_msg_pb2.pyi,sha256=1KpTJnLq6KG2eijZ_vlFq1JsTPeCeLz1IdmoUhW6Tec,8722
40
+ dexcontrol/proto/dexcontrol_query_pb2.py,sha256=KXPCAaKb2n55MBxmhIaFvAoqbS9XsKD6RdztGNe0occ,6230
41
+ dexcontrol/proto/dexcontrol_query_pb2.pyi,sha256=CGwwceF5Ibt9U3R8x2e7GjVNv2GyuD5WM_nyN4YF2wQ,7822
42
+ dexcontrol/sensors/__init__.py,sha256=Dp06cuO_3xC6i4u5rHqfK5NqlIC5kaCue_bAtTC6JEE,960
43
+ dexcontrol/sensors/manager.py,sha256=bvoKH6NIHEjfWsnffoUaNU14CDPkgOV06NL7NwY6s2U,6762
44
+ dexcontrol/sensors/ultrasonic.py,sha256=1rXzA_5w5Lf6TgLvPLnNyiIAMS_cuTuJusLWEBkc_fc,2882
45
+ dexcontrol/sensors/camera/__init__.py,sha256=Vwe98I4Lvdv3F2UslOzKkeUkt5Rl2jSqbKlU6gIBeF0,609
46
+ dexcontrol/sensors/camera/base_camera.py,sha256=JqVb1b82CxMH1FleGobr5dxN18sbDaqotE8RuivGAJo,4741
47
+ dexcontrol/sensors/camera/rgb_camera.py,sha256=w_SLJuJvbQgsq_h3DHNrTk8ofn8XEoX18GPCc-19g1g,6344
48
+ dexcontrol/sensors/camera/zed_camera.py,sha256=zOLwszwk3lvlyajEvfPgEhrKrHhQeVBxyLH1ECOD9Gc,13590
49
+ dexcontrol/sensors/imu/__init__.py,sha256=bBC7_NSLJ5qLMvUYu2-9yXKO2bRpQLC0HyywBwnbM0A,768
50
+ dexcontrol/sensors/imu/chassis_imu.py,sha256=kRP7jAeDDim2C0gIV7xG31VSBO4dnXUZDMfh0JOzNDs,5611
51
+ dexcontrol/sensors/imu/zed_imu.py,sha256=rGT0S2TIj0JGdlJMQKNldtCclOYunCmS0xdzvjAhq9A,5628
52
+ dexcontrol/sensors/lidar/__init__.py,sha256=frF16HmeQnfbvH0dVJ4pPjD4TySF13wCk-O9L3Memeg,317
53
+ dexcontrol/sensors/lidar/rplidar.py,sha256=MrmxP99-YsfoRlYUZTCcmvNbF-YCOLynhoe2tCMpDnE,4191
54
+ dexcontrol/utils/__init__.py,sha256=bYPMLxbbn5QeuPyA6OPGDS2JTYpnVvaZJT8PeILFjQY,252
55
+ dexcontrol/utils/comm_helper.py,sha256=5KZPCc_HA3tnptlYlyYLjbi7H17BXqR_bwt9Fy_FjlQ,3572
56
+ dexcontrol/utils/constants.py,sha256=uzR6AW5LNU3Otmf0G8M4yg-0iaHLhKtJ5e-OPuqxr9g,783
57
+ dexcontrol/utils/error_code.py,sha256=iy840qnWn9wv_nVqyEDP8-l2CuXlPH2xXXW0k-5cHKk,7321
58
+ dexcontrol/utils/io_utils.py,sha256=4TYV33ufECo8fuQivrZR9vtSdwWYUiPvpAUSneEzOOs,850
59
+ dexcontrol/utils/motion_utils.py,sha256=p4kXQm_YorISDC2crrpY0gwCVw_yQCPv-acxPUSfh8w,7172
60
+ dexcontrol/utils/os_utils.py,sha256=F9cyAfI-h4JXT3HOlms2nxe6WI1CZuOA3QHUYQA7pk4,7560
61
+ dexcontrol/utils/pb_utils.py,sha256=zN4pMS9GV9OTj1TmhcWTaDmfmgttyIDFJEuOE5tbCS0,2508
62
+ dexcontrol/utils/timer.py,sha256=1sOYYEapbZ5aBqJwknClsxgjDx0FDRQuGEdcTGnYTCI,3948
63
+ dexcontrol/utils/trajectory_utils.py,sha256=TURFb0DeDey0416z4L7AXiWcKJYsgg_bB5AE_JPSpXY,1879
64
+ dexcontrol/utils/viz_utils.py,sha256=rKtZfu32-9D9CS4cSiil-oLub_MiKTJV6hURvJbKd0s,6295
65
+ dexcontrol-0.3.2.dist-info/METADATA,sha256=DgATx_wkGCMVaJ6uuJOJYgVEGjeslTfRuSOrPqQY3A8,37269
66
+ dexcontrol-0.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
67
+ dexcontrol-0.3.2.dist-info/licenses/LICENSE,sha256=0J2KCMNNnW5WZPK5x8xUiCxApBf7h83693ggSJYiue0,31745
68
+ dexcontrol-0.3.2.dist-info/RECORD,,
@@ -1,51 +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
- from dataclasses import dataclass, field
12
-
13
-
14
- @dataclass
15
- class LuxonisCameraConfig:
16
- """Configuration for LuxonisCameraSensor (wrist camera).
17
-
18
- Attributes:
19
- _target_: Target sensor class for Hydra instantiation.
20
- name: Logical name of the camera.
21
- enable: Whether this sensor is enabled.
22
- enable_fps_tracking: Enable FPS tracking logs.
23
- fps_log_interval: Frames between FPS logs.
24
- subscriber_config: Topics for RGB and Depth Zenoh subscribers.
25
- """
26
-
27
- _target_: str = "dexcontrol.sensors.camera.luxonis_camera.LuxonisCameraSensor"
28
- name: str = "wrist_camera"
29
- enable: bool = False
30
- enable_fps_tracking: bool = False
31
- fps_log_interval: int = 30
32
-
33
- # Note: Resolution is set by the publisher (dexsensor). The publisher now defaults
34
- # to 720p to match common Luxonis sensor capabilities (e.g., OV9782 supports 720p/800p).
35
- # These subscribers only define topics to consume.
36
- subscriber_config: dict = field(
37
- default_factory=lambda: {
38
- "left_rgb": {
39
- "enable": True,
40
- "topic": "camera/wrist/left_rgb",
41
- },
42
- "right_rgb": {
43
- "enable": True,
44
- "topic": "camera/wrist/right_rgb",
45
- },
46
- "depth": {
47
- "enable": False,
48
- "topic": "camera/wrist/depth",
49
- },
50
- }
51
- )
@@ -1,169 +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
- """Luxonis wrist camera sensor using RGB and depth Zenoh subscribers.
12
-
13
- This sensor mirrors the high-level API of other camera sensors. It subscribes to
14
- RGB (JPEG over Zenoh) and depth streams published by dexsensor's Luxonis camera
15
- pipeline and exposes a simple interface for getting images.
16
- """
17
-
18
- from __future__ import annotations
19
-
20
- import logging
21
- from typing import Any, Dict, Optional, Union
22
-
23
- import numpy as np
24
- import zenoh
25
-
26
- from dexcontrol.config.sensors.cameras.luxonis_camera import LuxonisCameraConfig
27
- from dexcontrol.utils.subscribers.camera import (
28
- DepthCameraSubscriber,
29
- RGBCameraSubscriber,
30
- )
31
-
32
- logger = logging.getLogger(__name__)
33
-
34
-
35
- class LuxonisCameraSensor:
36
- """RGBD camera sensor wrapper for Luxonis/OAK wrist camera.
37
-
38
- Provides access to RGB and depth frames via dedicated Zenoh subscribers.
39
- """
40
-
41
- def __init__(
42
- self,
43
- configs: LuxonisCameraConfig,
44
- zenoh_session: zenoh.Session,
45
- ) -> None:
46
- self._name = configs.name
47
- self._configs = configs
48
- self._zenoh_session = zenoh_session
49
-
50
- # Support left and right RGB streams + depth, mirroring ZED structure
51
- self._subscribers: Dict[str, Optional[Union[RGBCameraSubscriber, DepthCameraSubscriber]]] = {}
52
-
53
- subscriber_config = configs.subscriber_config
54
-
55
- try:
56
- # Create subscribers for left_rgb, right_rgb, depth (configurable enables)
57
- stream_defs: Dict[str, Dict[str, Any]] = {
58
- "left_rgb": subscriber_config.get("left_rgb", {}),
59
- "right_rgb": subscriber_config.get("right_rgb", {}),
60
- "depth": subscriber_config.get("depth", {}),
61
- }
62
-
63
- for stream_name, cfg in stream_defs.items():
64
- if not cfg or not cfg.get("enable", False):
65
- self._subscribers[stream_name] = None
66
- continue
67
- topic = cfg.get("topic")
68
- if not topic:
69
- logger.warning(f"'{self._name}': No topic configured for '{stream_name}'")
70
- self._subscribers[stream_name] = None
71
- continue
72
-
73
- if stream_name == "depth":
74
- self._subscribers[stream_name] = DepthCameraSubscriber(
75
- topic=topic,
76
- zenoh_session=self._zenoh_session,
77
- name=f"{self._name}_{stream_name}_subscriber",
78
- enable_fps_tracking=configs.enable_fps_tracking,
79
- fps_log_interval=configs.fps_log_interval,
80
- )
81
- else:
82
- self._subscribers[stream_name] = RGBCameraSubscriber(
83
- topic=topic,
84
- zenoh_session=self._zenoh_session,
85
- name=f"{self._name}_{stream_name}_subscriber",
86
- enable_fps_tracking=configs.enable_fps_tracking,
87
- fps_log_interval=configs.fps_log_interval,
88
- )
89
-
90
- except Exception as e:
91
- logger.error(f"Error creating Luxonis wrist camera subscribers: {e}")
92
-
93
- # Lifecycle
94
- def shutdown(self) -> None:
95
- for sub in self._subscribers.values():
96
- if sub:
97
- sub.shutdown()
98
- logger.info(f"'{self._name}' sensor shut down.")
99
-
100
- # Status
101
- def is_active(self) -> bool:
102
- return any(sub.is_active() for sub in self._subscribers.values() if sub is not None)
103
-
104
- def wait_for_active(self, timeout: float = 5.0, require_both: bool = False) -> bool:
105
- subs = [s for s in self._subscribers.values() if s is not None]
106
- if not subs:
107
- return False
108
- if require_both:
109
- return all(s.wait_for_active(timeout) for s in subs)
110
- return any(s.wait_for_active(timeout) for s in subs)
111
-
112
- # Data access
113
- def get_obs(
114
- self, obs_keys: Optional[list[str]] = None, include_timestamp: bool = False
115
- ) -> Dict[str, Optional[np.ndarray]]:
116
- """Get latest images.
117
-
118
- obs_keys can include any of: ["left_rgb", "right_rgb", "depth"]. If None, returns
119
- all available. If include_timestamp is True and the underlying subscriber returns
120
- (image, timestamp), that tuple is forwarded; otherwise only the image is returned.
121
- """
122
- keys_to_fetch = obs_keys or self.available_streams
123
-
124
- out: Dict[str, Optional[np.ndarray]] = {}
125
- for key in keys_to_fetch:
126
- sub = self._subscribers.get(key)
127
- data = sub.get_latest_data() if sub else None
128
- is_tuple_or_list = isinstance(data, (tuple, list))
129
- if include_timestamp:
130
- if not is_tuple_or_list and data is not None:
131
- logger.warning(f"Timestamp is not available yet for {key} stream.")
132
- out[key] = data
133
- else:
134
- out[key] = data[0] if is_tuple_or_list else data
135
- return out
136
-
137
- def get_rgb(self) -> Optional[np.ndarray]:
138
- # Backward-compat: return left_rgb if available else right_rgb
139
- for key in ("left_rgb", "right_rgb"):
140
- sub = self._subscribers.get(key)
141
- if sub:
142
- data = sub.get_latest_data()
143
- return data[0] if isinstance(data, (tuple, list)) else data
144
- return None
145
-
146
- def get_depth(self) -> Optional[np.ndarray]:
147
- sub = self._subscribers.get("depth")
148
- if not sub:
149
- return None
150
- data = sub.get_latest_data()
151
- return data[0] if isinstance(data, (tuple, list)) else data
152
-
153
- # Properties
154
- @property
155
- def fps(self) -> Dict[str, float]:
156
- return {name: sub.fps for name, sub in self._subscribers.items() if sub is not None}
157
-
158
- @property
159
- def name(self) -> str:
160
- return self._name
161
-
162
- @property
163
- def available_streams(self) -> list:
164
- return [name for name, sub in self._subscribers.items() if sub is not None]
165
-
166
- @property
167
- def active_streams(self) -> list:
168
- return [name for name, sub in self._subscribers.items() if sub and sub.is_active()]
169
-
@@ -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
- ]