dexcontrol 0.2.12__py3-none-any.whl → 0.3.0__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.
- dexcontrol/__init__.py +2 -1
- dexcontrol/config/core/chassis.py +9 -4
- dexcontrol/config/core/hand.py +1 -0
- dexcontrol/config/vega.py +4 -1
- dexcontrol/core/arm.py +32 -12
- dexcontrol/core/chassis.py +146 -115
- dexcontrol/core/component.py +42 -16
- dexcontrol/core/hand.py +74 -39
- dexcontrol/core/head.py +6 -5
- dexcontrol/core/misc.py +172 -22
- dexcontrol/core/robot_query_interface.py +440 -0
- dexcontrol/core/torso.py +4 -4
- dexcontrol/proto/dexcontrol_msg_pb2.py +27 -39
- dexcontrol/proto/dexcontrol_msg_pb2.pyi +75 -118
- dexcontrol/proto/dexcontrol_query_pb2.py +39 -39
- dexcontrol/proto/dexcontrol_query_pb2.pyi +17 -4
- dexcontrol/robot.py +259 -566
- dexcontrol/utils/os_utils.py +183 -1
- dexcontrol/utils/pb_utils.py +0 -22
- dexcontrol/utils/zenoh_utils.py +249 -2
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.0.dist-info}/METADATA +12 -1
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.0.dist-info}/RECORD +24 -23
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.0.dist-info}/WHEEL +0 -0
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.0.dist-info}/licenses/LICENSE +0 -0
dexcontrol/core/hand.py
CHANGED
|
@@ -14,17 +14,24 @@ This module provides the Hand class for controlling a robotic hand through Zenoh
|
|
|
14
14
|
communication. It handles joint position control and state monitoring.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
from
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any, cast
|
|
18
19
|
|
|
19
20
|
import numpy as np
|
|
20
21
|
import zenoh
|
|
21
22
|
from jaxtyping import Float
|
|
22
23
|
|
|
23
24
|
from dexcontrol.config.core.hand import HandConfig
|
|
24
|
-
from dexcontrol.core.component import RobotJointComponent
|
|
25
|
+
from dexcontrol.core.component import RobotComponent, RobotJointComponent
|
|
25
26
|
from dexcontrol.proto import dexcontrol_msg_pb2
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
class HandType(Enum):
|
|
30
|
+
UNKNOWN = "UNKNOWN"
|
|
31
|
+
HandF5D6_V1 = "HandF5D6_V1"
|
|
32
|
+
HandF5D6_V2 = "HandF5D6_V2"
|
|
33
|
+
|
|
34
|
+
|
|
28
35
|
class Hand(RobotJointComponent):
|
|
29
36
|
"""Robot hand control class.
|
|
30
37
|
|
|
@@ -36,6 +43,7 @@ class Hand(RobotJointComponent):
|
|
|
36
43
|
self,
|
|
37
44
|
configs: HandConfig,
|
|
38
45
|
zenoh_session: zenoh.Session,
|
|
46
|
+
hand_type: HandType = HandType.HandF5D6_V1,
|
|
39
47
|
) -> None:
|
|
40
48
|
"""Initialize the hand controller.
|
|
41
49
|
|
|
@@ -47,7 +55,7 @@ class Hand(RobotJointComponent):
|
|
|
47
55
|
super().__init__(
|
|
48
56
|
state_sub_topic=configs.state_sub_topic,
|
|
49
57
|
control_pub_topic=configs.control_pub_topic,
|
|
50
|
-
state_message_type=dexcontrol_msg_pb2.
|
|
58
|
+
state_message_type=dexcontrol_msg_pb2.MotorStateWithCurrent,
|
|
51
59
|
zenoh_session=zenoh_session,
|
|
52
60
|
joint_name=configs.joint_name,
|
|
53
61
|
)
|
|
@@ -64,45 +72,11 @@ class Hand(RobotJointComponent):
|
|
|
64
72
|
Args:
|
|
65
73
|
joint_pos: Joint positions as list or numpy array.
|
|
66
74
|
"""
|
|
67
|
-
control_msg = dexcontrol_msg_pb2.
|
|
75
|
+
control_msg = dexcontrol_msg_pb2.MotorPosCommand()
|
|
68
76
|
joint_pos_array = self._convert_joint_cmd_to_array(joint_pos)
|
|
69
|
-
control_msg.
|
|
77
|
+
control_msg.pos.extend(joint_pos_array.tolist())
|
|
70
78
|
self._publish_control(control_msg)
|
|
71
79
|
|
|
72
|
-
def set_joint_pos(
|
|
73
|
-
self,
|
|
74
|
-
joint_pos: Float[np.ndarray, " N"] | list[float] | dict[str, float],
|
|
75
|
-
relative: bool = False,
|
|
76
|
-
wait_time: float = 0.0,
|
|
77
|
-
wait_kwargs: dict[str, float] | None = None,
|
|
78
|
-
exit_on_reach: bool = False,
|
|
79
|
-
exit_on_reach_kwargs: dict[str, Any] | None = None,
|
|
80
|
-
) -> None:
|
|
81
|
-
"""Send joint position control commands to the hand.
|
|
82
|
-
|
|
83
|
-
Args:
|
|
84
|
-
joint_pos: Joint positions as either:
|
|
85
|
-
- List of joint values [j1, j2, ..., jN]
|
|
86
|
-
- Numpy array with shape (N,)
|
|
87
|
-
- Dictionary mapping joint names to position values
|
|
88
|
-
relative: If True, the joint positions are relative to the current position.
|
|
89
|
-
wait_time: Time to wait after setting the joint positions.
|
|
90
|
-
wait_kwargs: Optional parameters for trajectory generation (not used in Hand).
|
|
91
|
-
exit_on_reach: If True, the function will exit when the joint positions are reached.
|
|
92
|
-
exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
|
|
93
|
-
|
|
94
|
-
Raises:
|
|
95
|
-
ValueError: If joint_pos dictionary contains invalid joint names.
|
|
96
|
-
"""
|
|
97
|
-
super().set_joint_pos(
|
|
98
|
-
joint_pos,
|
|
99
|
-
relative,
|
|
100
|
-
wait_time,
|
|
101
|
-
wait_kwargs,
|
|
102
|
-
exit_on_reach,
|
|
103
|
-
exit_on_reach_kwargs,
|
|
104
|
-
)
|
|
105
|
-
|
|
106
80
|
def open_hand(
|
|
107
81
|
self,
|
|
108
82
|
wait_time: float = 0.0,
|
|
@@ -152,6 +126,37 @@ class HandF5D6(Hand):
|
|
|
152
126
|
the F5D6 hand model.
|
|
153
127
|
"""
|
|
154
128
|
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
configs: HandConfig,
|
|
132
|
+
zenoh_session: zenoh.Session,
|
|
133
|
+
hand_type: HandType = HandType.HandF5D6_V1,
|
|
134
|
+
) -> None:
|
|
135
|
+
super().__init__(configs, zenoh_session)
|
|
136
|
+
|
|
137
|
+
# Initialize touch sensor for F5D6_V2 hands
|
|
138
|
+
self._hand_type = hand_type
|
|
139
|
+
if self._hand_type == HandType.HandF5D6_V2:
|
|
140
|
+
self._touch_sensor = HandF5D6TouchSensor(
|
|
141
|
+
configs.touch_sensor_sub_topic, zenoh_session
|
|
142
|
+
)
|
|
143
|
+
elif self._hand_type == HandType.HandF5D6_V1:
|
|
144
|
+
self._touch_sensor = None
|
|
145
|
+
else:
|
|
146
|
+
raise ValueError(f"Invalid hand type: {self._hand_type}")
|
|
147
|
+
|
|
148
|
+
def get_finger_tip_force(self) -> Float[np.ndarray, "5"]:
|
|
149
|
+
"""Get the force at the finger tips.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Array of force values at the finger tips.
|
|
153
|
+
"""
|
|
154
|
+
if self._touch_sensor is None:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Touch sensor not available for this hand type: {self._hand_type}"
|
|
157
|
+
)
|
|
158
|
+
return self._touch_sensor.get_fingertip_touch_net_force()
|
|
159
|
+
|
|
155
160
|
def close_hand(
|
|
156
161
|
self,
|
|
157
162
|
wait_time: float = 0.0,
|
|
@@ -241,3 +246,33 @@ class HandF5D6(Hand):
|
|
|
241
246
|
0
|
|
242
247
|
] * (1 - ratio)
|
|
243
248
|
return intermediate_pos
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class HandF5D6TouchSensor(RobotComponent):
|
|
252
|
+
"""Wrench sensor reader for the robot arm.
|
|
253
|
+
|
|
254
|
+
This class provides methods to read wrench sensor data through Zenoh communication.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def __init__(self, state_sub_topic: str, zenoh_session: zenoh.Session) -> None:
|
|
258
|
+
"""Initialize the wrench sensor reader.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
state_sub_topic: Topic to subscribe to for wrench sensor data.
|
|
262
|
+
zenoh_session: Active Zenoh communication session for message passing.
|
|
263
|
+
"""
|
|
264
|
+
super().__init__(
|
|
265
|
+
state_sub_topic=state_sub_topic,
|
|
266
|
+
zenoh_session=zenoh_session,
|
|
267
|
+
state_message_type=dexcontrol_msg_pb2.HandTouchSensorState,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def get_fingertip_touch_net_force(self) -> Float[np.ndarray, "5"]:
|
|
271
|
+
"""Get the complete wrench sensor state.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Dictionary containing wrench values and button states.
|
|
275
|
+
"""
|
|
276
|
+
state = self._get_state()
|
|
277
|
+
hand_touch_state = cast(dexcontrol_msg_pb2.HandTouchSensorState, state)
|
|
278
|
+
return np.array(hand_touch_state.force)
|
dexcontrol/core/head.py
CHANGED
|
@@ -55,7 +55,7 @@ class Head(RobotJointComponent):
|
|
|
55
55
|
super().__init__(
|
|
56
56
|
state_sub_topic=configs.state_sub_topic,
|
|
57
57
|
control_pub_topic=configs.control_pub_topic,
|
|
58
|
-
state_message_type=dexcontrol_msg_pb2.
|
|
58
|
+
state_message_type=dexcontrol_msg_pb2.MotorStateWithTorque,
|
|
59
59
|
zenoh_session=zenoh_session,
|
|
60
60
|
joint_name=configs.joint_name,
|
|
61
61
|
joint_limit=configs.joint_limit
|
|
@@ -164,9 +164,9 @@ class Head(RobotJointComponent):
|
|
|
164
164
|
)
|
|
165
165
|
|
|
166
166
|
# Create and send control message
|
|
167
|
-
control_msg = dexcontrol_msg_pb2.
|
|
168
|
-
control_msg.
|
|
169
|
-
control_msg.
|
|
167
|
+
control_msg = dexcontrol_msg_pb2.MotorPosVelCommand()
|
|
168
|
+
control_msg.pos.extend(joint_pos.tolist())
|
|
169
|
+
control_msg.vel.extend(joint_vel.tolist())
|
|
170
170
|
self._publish_control(control_msg)
|
|
171
171
|
|
|
172
172
|
# Wait if specified
|
|
@@ -202,7 +202,8 @@ class Head(RobotJointComponent):
|
|
|
202
202
|
|
|
203
203
|
for reply in replies:
|
|
204
204
|
if reply.ok is not None and reply.ok.payload is not None:
|
|
205
|
-
|
|
205
|
+
# TODO: handle the reply message of head mode
|
|
206
|
+
pass
|
|
206
207
|
time.sleep(0.5)
|
|
207
208
|
|
|
208
209
|
def get_joint_limit(self) -> Float[np.ndarray, "3 2"] | None:
|
dexcontrol/core/misc.py
CHANGED
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
"""Miscellaneous robot components module.
|
|
12
12
|
|
|
13
13
|
This module provides classes for various auxiliary robot components such as Battery,
|
|
14
|
-
EStop (emergency stop), and UltraSonicSensor.
|
|
14
|
+
EStop (emergency stop), ServerLogSubscriber, and UltraSonicSensor.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
import json
|
|
17
18
|
import os
|
|
18
19
|
import threading
|
|
19
20
|
import time
|
|
20
|
-
from typing import TypeVar
|
|
21
|
+
from typing import Any, TypeVar, cast
|
|
21
22
|
|
|
22
23
|
import zenoh
|
|
23
24
|
from google.protobuf.message import Message
|
|
@@ -133,7 +134,7 @@ class Battery(RobotComponent):
|
|
|
133
134
|
power = state.current * state.voltage
|
|
134
135
|
power_style = self._get_power_style(power)
|
|
135
136
|
table.add_row(
|
|
136
|
-
"Power",
|
|
137
|
+
"Power Consumption",
|
|
137
138
|
f"[{power_style}]{power:.2f}W[/] ([blue]{state.current:.2f}A[/] "
|
|
138
139
|
f"× [blue]{state.voltage:.2f}V[/])",
|
|
139
140
|
)
|
|
@@ -281,25 +282,40 @@ class EStop(RobotComponent):
|
|
|
281
282
|
- software_estop_enabled: Software EStop enabled
|
|
282
283
|
"""
|
|
283
284
|
state = self._get_state()
|
|
285
|
+
state = cast(dexcontrol_msg_pb2.EStopState, state)
|
|
284
286
|
if state is None:
|
|
285
287
|
return {
|
|
286
288
|
"button_pressed": False,
|
|
287
289
|
"software_estop_enabled": False,
|
|
288
290
|
}
|
|
291
|
+
button_pressed = (
|
|
292
|
+
state.left_button_pressed
|
|
293
|
+
or state.right_button_pressed
|
|
294
|
+
or state.waist_button_pressed
|
|
295
|
+
or state.wireless_button_pressed
|
|
296
|
+
)
|
|
289
297
|
return {
|
|
290
|
-
"button_pressed":
|
|
298
|
+
"button_pressed": button_pressed,
|
|
291
299
|
"software_estop_enabled": state.software_estop_enabled,
|
|
292
300
|
}
|
|
293
301
|
|
|
294
302
|
def is_button_pressed(self) -> bool:
|
|
295
303
|
"""Checks if the EStop button is pressed."""
|
|
296
304
|
state = self._get_state()
|
|
297
|
-
|
|
305
|
+
state = cast(dexcontrol_msg_pb2.EStopState, state)
|
|
306
|
+
button_pressed = (
|
|
307
|
+
state.left_button_pressed
|
|
308
|
+
or state.right_button_pressed
|
|
309
|
+
or state.waist_button_pressed
|
|
310
|
+
or state.wireless_button_pressed
|
|
311
|
+
)
|
|
312
|
+
return button_pressed
|
|
298
313
|
|
|
299
314
|
def is_software_estop_enabled(self) -> bool:
|
|
300
315
|
"""Checks if the software EStop is enabled."""
|
|
301
316
|
state = self._get_state()
|
|
302
|
-
|
|
317
|
+
state = cast(dexcontrol_msg_pb2.EStopState, state)
|
|
318
|
+
return state.software_estop_enabled
|
|
303
319
|
|
|
304
320
|
def activate(self) -> None:
|
|
305
321
|
"""Activates the software emergency stop (E-Stop)."""
|
|
@@ -325,27 +341,19 @@ class EStop(RobotComponent):
|
|
|
325
341
|
|
|
326
342
|
def show(self) -> None:
|
|
327
343
|
"""Displays the current EStop status as a formatted table with color indicators."""
|
|
328
|
-
state = self._get_state()
|
|
329
|
-
|
|
330
344
|
table = Table(title="E-Stop Status")
|
|
331
345
|
table.add_column("Parameter", style="cyan")
|
|
332
346
|
table.add_column("Value")
|
|
333
347
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
console.print(table)
|
|
338
|
-
return
|
|
339
|
-
|
|
340
|
-
button_style = "bold red" if state.button_pressed else "bold dark_green"
|
|
341
|
-
table.add_row("Button Pressed", f"[{button_style}]{state.button_pressed}[/]")
|
|
348
|
+
button_pressed = self.is_button_pressed()
|
|
349
|
+
button_style = "bold red" if button_pressed else "bold dark_green"
|
|
350
|
+
table.add_row("Button Pressed", f"[{button_style}]{button_pressed}[/]")
|
|
342
351
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
)
|
|
352
|
+
if_software_estop_enabled = self.is_software_estop_enabled()
|
|
353
|
+
software_style = "bold red" if if_software_estop_enabled else "bold dark_green"
|
|
346
354
|
table.add_row(
|
|
347
355
|
"Software E-Stop Enabled",
|
|
348
|
-
f"[{software_style}]{
|
|
356
|
+
f"[{software_style}]{if_software_estop_enabled}[/]",
|
|
349
357
|
)
|
|
350
358
|
|
|
351
359
|
console = Console()
|
|
@@ -651,9 +659,9 @@ class Heartbeat:
|
|
|
651
659
|
last_value = status["last_value"]
|
|
652
660
|
if last_value is not None:
|
|
653
661
|
uptime_str = self._format_uptime(last_value)
|
|
654
|
-
table.add_row("Robot
|
|
662
|
+
table.add_row("Robot Driver Uptime", f"[blue]{uptime_str}[/]")
|
|
655
663
|
else:
|
|
656
|
-
table.add_row("Robot
|
|
664
|
+
table.add_row("Robot Driver Uptime", "[red]No data[/]")
|
|
657
665
|
|
|
658
666
|
# Time since last heartbeat
|
|
659
667
|
time_since = status["time_since_last"]
|
|
@@ -671,3 +679,145 @@ class Heartbeat:
|
|
|
671
679
|
|
|
672
680
|
console = Console()
|
|
673
681
|
console.print(table)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class ServerLogSubscriber:
|
|
685
|
+
"""Server log subscriber that monitors and displays server log messages.
|
|
686
|
+
|
|
687
|
+
This class subscribes to the "logs" topic and handles incoming log messages
|
|
688
|
+
from the robot server. It provides formatted display of server logs with
|
|
689
|
+
proper error handling and validation.
|
|
690
|
+
|
|
691
|
+
The server sends log information via the "logs" topic as JSON with format:
|
|
692
|
+
{"timestamp": "ISO8601", "message": "text", "source": "robot_server"}
|
|
693
|
+
|
|
694
|
+
Attributes:
|
|
695
|
+
_zenoh_session: Zenoh session for communication.
|
|
696
|
+
_log_subscriber: Zenoh subscriber for log messages.
|
|
697
|
+
"""
|
|
698
|
+
|
|
699
|
+
def __init__(self, zenoh_session: zenoh.Session) -> None:
|
|
700
|
+
"""Initialize the ServerLogSubscriber.
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
zenoh_session: Active Zenoh session for communication.
|
|
704
|
+
"""
|
|
705
|
+
self._zenoh_session = zenoh_session
|
|
706
|
+
self._log_subscriber = None
|
|
707
|
+
self._initialize()
|
|
708
|
+
|
|
709
|
+
def _initialize(self) -> None:
|
|
710
|
+
"""Initialize the log subscriber with error handling."""
|
|
711
|
+
|
|
712
|
+
def log_handler(sample):
|
|
713
|
+
"""Handle incoming log messages from the server."""
|
|
714
|
+
if not self._is_valid_log_sample(sample):
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
try:
|
|
718
|
+
log_data = self._parse_log_payload(sample.payload)
|
|
719
|
+
if log_data:
|
|
720
|
+
self._display_server_log(log_data)
|
|
721
|
+
except Exception as e:
|
|
722
|
+
logger.warning(f"Failed to process server log: {e}")
|
|
723
|
+
|
|
724
|
+
try:
|
|
725
|
+
# Subscribe to server logs topic
|
|
726
|
+
self._log_subscriber = self._zenoh_session.declare_subscriber(
|
|
727
|
+
"logs", log_handler
|
|
728
|
+
)
|
|
729
|
+
logger.debug("Server log subscriber initialized")
|
|
730
|
+
except Exception as e:
|
|
731
|
+
logger.warning(f"Failed to initialize server log subscriber: {e}")
|
|
732
|
+
self._log_subscriber = None
|
|
733
|
+
|
|
734
|
+
def _is_valid_log_sample(self, sample) -> bool:
|
|
735
|
+
"""Check if log sample is valid.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
sample: Zenoh sample to validate.
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
True if sample is valid, False otherwise.
|
|
742
|
+
"""
|
|
743
|
+
if sample is None or sample.payload is None:
|
|
744
|
+
logger.debug("Received empty log sample")
|
|
745
|
+
return False
|
|
746
|
+
return True
|
|
747
|
+
|
|
748
|
+
def _parse_log_payload(self, payload) -> dict[str, str] | None:
|
|
749
|
+
"""Parse log payload and return structured data.
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
payload: Raw payload from Zenoh sample.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
Parsed log data as dictionary or None if parsing fails.
|
|
756
|
+
"""
|
|
757
|
+
try:
|
|
758
|
+
payload_str = payload.to_bytes().decode("utf-8")
|
|
759
|
+
if not payload_str.strip():
|
|
760
|
+
logger.debug("Received empty log payload")
|
|
761
|
+
return None
|
|
762
|
+
|
|
763
|
+
log_data = json.loads(payload_str)
|
|
764
|
+
|
|
765
|
+
if not isinstance(log_data, dict):
|
|
766
|
+
logger.warning(
|
|
767
|
+
f"Invalid log data format: expected dict, got {type(log_data)}"
|
|
768
|
+
)
|
|
769
|
+
return None
|
|
770
|
+
|
|
771
|
+
return log_data
|
|
772
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
773
|
+
logger.warning(f"Failed to parse log payload: {e}")
|
|
774
|
+
return None
|
|
775
|
+
|
|
776
|
+
def _display_server_log(self, log_data: dict[str, str]) -> None:
|
|
777
|
+
"""Display formatted server log message.
|
|
778
|
+
|
|
779
|
+
Args:
|
|
780
|
+
log_data: Parsed log data dictionary.
|
|
781
|
+
"""
|
|
782
|
+
# Extract log information with safe defaults
|
|
783
|
+
timestamp = log_data.get("timestamp", "")
|
|
784
|
+
message = log_data.get("message", "")
|
|
785
|
+
source = log_data.get("source", "unknown")
|
|
786
|
+
|
|
787
|
+
# Validate critical fields
|
|
788
|
+
if not message:
|
|
789
|
+
logger.debug("Received log with empty message")
|
|
790
|
+
return
|
|
791
|
+
|
|
792
|
+
# Log the server message with clear identification
|
|
793
|
+
logger.info(f"[SERVER_LOG] [{timestamp}] [{source}] {message}")
|
|
794
|
+
|
|
795
|
+
def is_active(self) -> bool:
|
|
796
|
+
"""Check if the log subscriber is active.
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
True if subscriber is active, False otherwise.
|
|
800
|
+
"""
|
|
801
|
+
return self._log_subscriber is not None
|
|
802
|
+
|
|
803
|
+
def shutdown(self) -> None:
|
|
804
|
+
"""Clean up the log subscriber and release resources."""
|
|
805
|
+
if self._log_subscriber is not None:
|
|
806
|
+
try:
|
|
807
|
+
self._log_subscriber.undeclare()
|
|
808
|
+
self._log_subscriber = None
|
|
809
|
+
except Exception as e:
|
|
810
|
+
logger.error(f"Error cleaning up log subscriber: {e}")
|
|
811
|
+
|
|
812
|
+
def get_status(self) -> dict[str, Any]:
|
|
813
|
+
"""Get the current status of the log subscriber.
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
Dictionary containing status information:
|
|
817
|
+
- is_active: Whether the subscriber is active
|
|
818
|
+
- topic: The topic being subscribed to
|
|
819
|
+
"""
|
|
820
|
+
return {
|
|
821
|
+
"is_active": self.is_active(),
|
|
822
|
+
"topic": "logs",
|
|
823
|
+
}
|