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.
- dexcontrol/__init__.py +16 -7
- dexcontrol/apps/dualsense_teleop_base.py +1 -1
- dexcontrol/comm/__init__.py +51 -0
- dexcontrol/comm/base.py +421 -0
- dexcontrol/comm/rtc.py +400 -0
- dexcontrol/comm/subscribers.py +329 -0
- dexcontrol/config/sensors/cameras/__init__.py +1 -2
- dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
- dexcontrol/config/sensors/vega_sensors.py +12 -18
- dexcontrol/core/arm.py +29 -25
- dexcontrol/core/chassis.py +3 -12
- dexcontrol/core/component.py +68 -43
- dexcontrol/core/hand.py +50 -52
- dexcontrol/core/head.py +14 -26
- dexcontrol/core/misc.py +188 -166
- dexcontrol/core/robot_query_interface.py +140 -117
- dexcontrol/core/torso.py +0 -4
- dexcontrol/robot.py +15 -37
- dexcontrol/sensors/__init__.py +1 -2
- dexcontrol/sensors/camera/__init__.py +0 -2
- dexcontrol/sensors/camera/base_camera.py +144 -0
- dexcontrol/sensors/camera/rgb_camera.py +67 -63
- dexcontrol/sensors/camera/zed_camera.py +89 -147
- dexcontrol/sensors/imu/chassis_imu.py +76 -56
- dexcontrol/sensors/imu/zed_imu.py +54 -43
- dexcontrol/sensors/lidar/rplidar.py +16 -20
- dexcontrol/sensors/manager.py +4 -11
- dexcontrol/sensors/ultrasonic.py +14 -27
- dexcontrol/utils/__init__.py +0 -11
- dexcontrol/utils/comm_helper.py +111 -0
- dexcontrol/utils/constants.py +1 -1
- dexcontrol/utils/os_utils.py +8 -22
- {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.2.dist-info}/METADATA +2 -1
- dexcontrol-0.3.2.dist-info/RECORD +68 -0
- dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
- dexcontrol/sensors/camera/luxonis_camera.py +0 -169
- dexcontrol/utils/rate_limiter.py +0 -172
- dexcontrol/utils/rtc_utils.py +0 -144
- dexcontrol/utils/subscribers/__init__.py +0 -52
- dexcontrol/utils/subscribers/base.py +0 -281
- dexcontrol/utils/subscribers/camera.py +0 -332
- dexcontrol/utils/subscribers/decoders.py +0 -88
- dexcontrol/utils/subscribers/generic.py +0 -110
- dexcontrol/utils/subscribers/imu.py +0 -175
- dexcontrol/utils/subscribers/lidar.py +0 -172
- dexcontrol/utils/subscribers/protobuf.py +0 -111
- dexcontrol/utils/subscribers/rtc.py +0 -316
- dexcontrol/utils/zenoh_utils.py +0 -369
- dexcontrol-0.3.0.dist-info/RECORD +0 -76
- {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.2.dist-info}/WHEEL +0 -0
- {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.2.dist-info}/licenses/LICENSE +0 -0
dexcontrol/core/misc.py
CHANGED
|
@@ -20,7 +20,7 @@ import threading
|
|
|
20
20
|
import time
|
|
21
21
|
from typing import Any, TypeVar, cast
|
|
22
22
|
|
|
23
|
-
import
|
|
23
|
+
from dexcomm import Subscriber, call_service
|
|
24
24
|
from google.protobuf.message import Message
|
|
25
25
|
from loguru import logger
|
|
26
26
|
from rich.console import Console
|
|
@@ -29,9 +29,8 @@ from rich.table import Table
|
|
|
29
29
|
from dexcontrol.config.core import BatteryConfig, EStopConfig, HeartbeatConfig
|
|
30
30
|
from dexcontrol.core.component import RobotComponent
|
|
31
31
|
from dexcontrol.proto import dexcontrol_msg_pb2, dexcontrol_query_pb2
|
|
32
|
-
from dexcontrol.utils.
|
|
32
|
+
from dexcontrol.utils.comm_helper import get_zenoh_config_path
|
|
33
33
|
from dexcontrol.utils.os_utils import resolve_key_name
|
|
34
|
-
from dexcontrol.utils.subscribers.generic import GenericZenohSubscriber
|
|
35
34
|
|
|
36
35
|
# Type variable for Message subclasses
|
|
37
36
|
M = TypeVar("M", bound=Message)
|
|
@@ -50,16 +49,13 @@ class Battery(RobotComponent):
|
|
|
50
49
|
_shutdown_event: Event to signal thread shutdown.
|
|
51
50
|
"""
|
|
52
51
|
|
|
53
|
-
def __init__(self, configs: BatteryConfig
|
|
52
|
+
def __init__(self, configs: BatteryConfig) -> None:
|
|
54
53
|
"""Initialize the Battery component.
|
|
55
54
|
|
|
56
55
|
Args:
|
|
57
56
|
configs: Battery configuration containing subscription topics.
|
|
58
|
-
zenoh_session: Active Zenoh session for communication.
|
|
59
57
|
"""
|
|
60
|
-
super().__init__(
|
|
61
|
-
configs.state_sub_topic, zenoh_session, dexcontrol_msg_pb2.BMSState
|
|
62
|
-
)
|
|
58
|
+
super().__init__(configs.state_sub_topic, dexcontrol_msg_pb2.BMSState)
|
|
63
59
|
self._console = Console()
|
|
64
60
|
self._shutdown_event = threading.Event()
|
|
65
61
|
self._monitor_thread = threading.Thread(
|
|
@@ -219,18 +215,14 @@ class EStop(RobotComponent):
|
|
|
219
215
|
def __init__(
|
|
220
216
|
self,
|
|
221
217
|
configs: EStopConfig,
|
|
222
|
-
zenoh_session: zenoh.Session,
|
|
223
218
|
) -> None:
|
|
224
219
|
"""Initialize the EStop component.
|
|
225
220
|
|
|
226
221
|
Args:
|
|
227
222
|
configs: EStop configuration containing subscription topics.
|
|
228
|
-
zenoh_session: Active Zenoh session for communication.
|
|
229
223
|
"""
|
|
230
224
|
self._enabled = configs.enabled
|
|
231
|
-
super().__init__(
|
|
232
|
-
configs.state_sub_topic, zenoh_session, dexcontrol_msg_pb2.EStopState
|
|
233
|
-
)
|
|
225
|
+
super().__init__(configs.state_sub_topic, dexcontrol_msg_pb2.EStopState)
|
|
234
226
|
self._estop_query_name = configs.estop_query_name
|
|
235
227
|
if not self._enabled:
|
|
236
228
|
logger.warning("EStop monitoring is DISABLED via configuration")
|
|
@@ -267,11 +259,15 @@ class EStop(RobotComponent):
|
|
|
267
259
|
enable: If True, activates the software E-Stop. If False, deactivates it.
|
|
268
260
|
"""
|
|
269
261
|
query_msg = dexcontrol_query_pb2.SetEstop(enable=enable)
|
|
270
|
-
|
|
262
|
+
call_service(
|
|
271
263
|
resolve_key_name(self._estop_query_name),
|
|
272
|
-
|
|
273
|
-
|
|
264
|
+
request=query_msg,
|
|
265
|
+
timeout=0.05,
|
|
266
|
+
config=get_zenoh_config_path(),
|
|
267
|
+
request_serializer=lambda x: x.SerializeToString(),
|
|
268
|
+
response_deserializer=None,
|
|
274
269
|
)
|
|
270
|
+
logger.info(f"Set E-Stop to {enable}")
|
|
275
271
|
|
|
276
272
|
def get_status(self) -> dict[str, bool]:
|
|
277
273
|
"""Gets the current EStop state information.
|
|
@@ -380,138 +376,182 @@ class Heartbeat:
|
|
|
380
376
|
def __init__(
|
|
381
377
|
self,
|
|
382
378
|
configs: HeartbeatConfig,
|
|
383
|
-
zenoh_session: zenoh.Session,
|
|
384
379
|
) -> None:
|
|
385
380
|
"""Initialize the Heartbeat monitor.
|
|
386
381
|
|
|
387
382
|
Args:
|
|
388
383
|
configs: Heartbeat configuration containing topic and timeout settings.
|
|
389
|
-
zenoh_session: Active Zenoh session for communication.
|
|
390
384
|
"""
|
|
391
385
|
self._timeout_seconds = configs.timeout_seconds
|
|
392
386
|
self._enabled = configs.enabled
|
|
393
387
|
self._paused = False
|
|
394
388
|
self._paused_lock = threading.Lock()
|
|
395
389
|
self._shutdown_event = threading.Event()
|
|
396
|
-
if not self._enabled:
|
|
397
|
-
logger.info("Heartbeat monitoring is DISABLED via configuration")
|
|
398
|
-
# Create a dummy subscriber that's never active
|
|
399
|
-
self._subscriber = None
|
|
400
|
-
self._monitor_thread = None
|
|
401
|
-
return
|
|
402
390
|
|
|
403
|
-
#
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
391
|
+
# Always create the subscriber to monitor heartbeat, regardless of enabled state
|
|
392
|
+
def heartbeat_callback(data):
|
|
393
|
+
"""Process heartbeat data and update internal state."""
|
|
394
|
+
try:
|
|
395
|
+
decoded_data = self._decode_heartbeat(data)
|
|
396
|
+
# Store the decoded heartbeat data for monitoring
|
|
397
|
+
self._latest_heartbeat_data = decoded_data
|
|
398
|
+
self._last_heartbeat_time = time.time()
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.debug(f"Failed to process heartbeat data: {e}")
|
|
401
|
+
|
|
402
|
+
# Define a simple deserializer that just returns the raw data
|
|
403
|
+
def heartbeat_deserializer(data: bytes) -> bytes:
|
|
404
|
+
"""Pass through deserializer for raw heartbeat data."""
|
|
405
|
+
return data
|
|
406
|
+
|
|
407
|
+
self._subscriber = Subscriber(
|
|
408
|
+
topic=resolve_key_name(configs.heartbeat_topic),
|
|
409
|
+
callback=heartbeat_callback,
|
|
410
|
+
deserializer=heartbeat_deserializer,
|
|
411
|
+
config=get_zenoh_config_path(),
|
|
410
412
|
)
|
|
411
413
|
|
|
414
|
+
# Initialize tracking variables
|
|
415
|
+
self._latest_heartbeat_data = None
|
|
416
|
+
self._last_heartbeat_time = None
|
|
417
|
+
|
|
412
418
|
# Start monitoring thread
|
|
413
419
|
self._monitor_thread = threading.Thread(
|
|
414
420
|
target=self._heartbeat_monitor, daemon=True
|
|
415
421
|
)
|
|
416
422
|
self._monitor_thread.start()
|
|
417
423
|
|
|
418
|
-
|
|
424
|
+
if not self._enabled:
|
|
425
|
+
logger.info(
|
|
426
|
+
"Heartbeat monitoring is DISABLED - will monitor but not exit on timeout"
|
|
427
|
+
)
|
|
428
|
+
else:
|
|
429
|
+
logger.info(
|
|
430
|
+
f"Heartbeat monitor started with {self._timeout_seconds}s timeout"
|
|
431
|
+
)
|
|
419
432
|
|
|
420
|
-
def _decode_heartbeat(self, data
|
|
433
|
+
def _decode_heartbeat(self, data) -> float:
|
|
421
434
|
"""Decode heartbeat data from raw bytes.
|
|
422
435
|
|
|
423
436
|
Args:
|
|
424
|
-
data: Raw
|
|
437
|
+
data: Raw bytes containing heartbeat value.
|
|
425
438
|
|
|
426
439
|
Returns:
|
|
427
440
|
Decoded heartbeat timestamp value in seconds.
|
|
428
441
|
"""
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
442
|
+
try:
|
|
443
|
+
# Handle different data formats
|
|
444
|
+
if isinstance(data, bytes):
|
|
445
|
+
timestamp_str = data.decode("utf-8")
|
|
446
|
+
elif isinstance(data, str):
|
|
447
|
+
timestamp_str = data
|
|
448
|
+
else:
|
|
449
|
+
# If it's something else, try to convert to string
|
|
450
|
+
timestamp_str = str(data)
|
|
451
|
+
|
|
452
|
+
# Parse the timestamp (expected to be in milliseconds)
|
|
453
|
+
timestamp_ms = float(timestamp_str)
|
|
454
|
+
# Convert from milliseconds to seconds
|
|
455
|
+
return timestamp_ms / 1000.0
|
|
456
|
+
except (ValueError, AttributeError, UnicodeDecodeError) as e:
|
|
457
|
+
logger.debug(
|
|
458
|
+
f"Failed to decode heartbeat data: {e}, data type: {type(data)}"
|
|
459
|
+
)
|
|
460
|
+
raise
|
|
461
|
+
|
|
462
|
+
def _is_subscriber_active(self) -> bool:
|
|
463
|
+
"""Check if the subscriber is active (has received data)."""
|
|
464
|
+
return self._last_heartbeat_time is not None
|
|
465
|
+
|
|
466
|
+
def _get_time_since_last_data(self) -> float | None:
|
|
467
|
+
"""Get time since last heartbeat data was received."""
|
|
468
|
+
if self._last_heartbeat_time is None:
|
|
469
|
+
return None
|
|
470
|
+
return time.time() - self._last_heartbeat_time
|
|
471
|
+
|
|
472
|
+
def _get_latest_heartbeat_data(self) -> float | None:
|
|
473
|
+
"""Get the latest heartbeat data."""
|
|
474
|
+
return self._latest_heartbeat_data
|
|
435
475
|
|
|
436
476
|
def _heartbeat_monitor(self) -> None:
|
|
437
477
|
"""Background thread that continuously monitors heartbeat signal."""
|
|
438
|
-
if
|
|
478
|
+
if self._subscriber is None:
|
|
439
479
|
return
|
|
440
480
|
|
|
441
481
|
while not self._shutdown_event.is_set():
|
|
442
482
|
try:
|
|
443
|
-
# Skip
|
|
483
|
+
# Skip if paused
|
|
444
484
|
with self._paused_lock:
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if self.
|
|
452
|
-
|
|
453
|
-
time_since_fresh = self._subscriber.get_time_since_last_data()
|
|
454
|
-
if time_since_fresh is not None:
|
|
455
|
-
logger.critical(
|
|
456
|
-
f"HEARTBEAT TIMEOUT! No fresh heartbeat data received for {time_since_fresh:.2f}s "
|
|
457
|
-
f"(timeout: {self._timeout_seconds}s). Low-level controller may have failed. "
|
|
458
|
-
"Exiting program immediately for safety."
|
|
459
|
-
)
|
|
460
|
-
else:
|
|
461
|
-
logger.critical(
|
|
462
|
-
f"HEARTBEAT TIMEOUT! No heartbeat data ever received "
|
|
463
|
-
f"(timeout: {self._timeout_seconds}s). Low-level controller may have failed. "
|
|
464
|
-
"Exiting program immediately for safety."
|
|
465
|
-
)
|
|
466
|
-
# Exit immediately for safety
|
|
467
|
-
os._exit(1)
|
|
485
|
+
if self._paused:
|
|
486
|
+
self._shutdown_event.wait(0.1)
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
# Check timeout
|
|
490
|
+
time_since_last = self._get_time_since_last_data()
|
|
491
|
+
if time_since_last and time_since_last > self._timeout_seconds:
|
|
492
|
+
self._handle_timeout(time_since_last)
|
|
468
493
|
|
|
469
494
|
# Check every 50ms for responsive monitoring
|
|
470
495
|
self._shutdown_event.wait(0.05)
|
|
471
496
|
|
|
472
497
|
except Exception as e:
|
|
473
498
|
logger.error(f"Heartbeat monitor error: {e}")
|
|
474
|
-
# Continue monitoring even if there's an error
|
|
475
499
|
self._shutdown_event.wait(0.1)
|
|
476
500
|
|
|
501
|
+
def _handle_timeout(self, time_since_last: float) -> None:
|
|
502
|
+
"""Handle heartbeat timeout based on enabled state."""
|
|
503
|
+
if self._enabled:
|
|
504
|
+
logger.critical(
|
|
505
|
+
f"HEARTBEAT TIMEOUT! No fresh heartbeat data received for {time_since_last:.2f}s "
|
|
506
|
+
f"(timeout: {self._timeout_seconds}s). Low-level controller may have failed. "
|
|
507
|
+
"Exiting program immediately for safety."
|
|
508
|
+
)
|
|
509
|
+
os._exit(1)
|
|
510
|
+
else:
|
|
511
|
+
# Log warning only once per timeout period to avoid spam
|
|
512
|
+
if (
|
|
513
|
+
not hasattr(self, "_last_warning_time")
|
|
514
|
+
or time.time() - self._last_warning_time > self._timeout_seconds
|
|
515
|
+
):
|
|
516
|
+
logger.warning(
|
|
517
|
+
f"Heartbeat timeout detected ({time_since_last:.2f}s > {self._timeout_seconds}s) "
|
|
518
|
+
"but exit is disabled"
|
|
519
|
+
)
|
|
520
|
+
self._last_warning_time = time.time()
|
|
521
|
+
|
|
477
522
|
def pause(self) -> None:
|
|
478
523
|
"""Pause heartbeat monitoring temporarily.
|
|
479
524
|
|
|
480
525
|
When paused, the heartbeat monitor will not check for timeouts or exit
|
|
481
526
|
the program. This is useful for scenarios where you need to temporarily
|
|
482
527
|
disable safety monitoring (e.g., during system maintenance or testing).
|
|
483
|
-
|
|
484
|
-
Warning: Use with caution as this disables a critical safety mechanism.
|
|
485
528
|
"""
|
|
486
|
-
if not self._enabled:
|
|
487
|
-
logger.warning("Cannot pause heartbeat monitoring - it's already disabled")
|
|
488
|
-
return
|
|
489
|
-
|
|
490
529
|
with self._paused_lock:
|
|
530
|
+
if self._paused:
|
|
531
|
+
return
|
|
491
532
|
self._paused = True
|
|
492
|
-
logger.warning(
|
|
493
|
-
"Heartbeat monitoring PAUSED - safety mechanism temporarily disabled"
|
|
494
|
-
)
|
|
495
533
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
"""
|
|
503
|
-
if not self._enabled:
|
|
504
|
-
logger.warning("Cannot resume heartbeat monitoring - it's disabled")
|
|
505
|
-
return
|
|
534
|
+
if self._enabled:
|
|
535
|
+
logger.warning(
|
|
536
|
+
"Heartbeat monitoring PAUSED - safety mechanism temporarily disabled"
|
|
537
|
+
)
|
|
538
|
+
else:
|
|
539
|
+
logger.info("Heartbeat monitoring paused (exit already disabled)")
|
|
506
540
|
|
|
541
|
+
def resume(self) -> None:
|
|
542
|
+
"""Resume heartbeat monitoring after being paused."""
|
|
507
543
|
with self._paused_lock:
|
|
508
544
|
if not self._paused:
|
|
509
|
-
logger.info("Heartbeat monitoring is already active")
|
|
510
545
|
return
|
|
511
546
|
self._paused = False
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
547
|
+
|
|
548
|
+
# Wait briefly to allow fresh heartbeat data
|
|
549
|
+
time.sleep(0.1)
|
|
550
|
+
|
|
551
|
+
if self._enabled:
|
|
552
|
+
logger.info("Heartbeat monitoring RESUMED - safety mechanism re-enabled")
|
|
553
|
+
else:
|
|
554
|
+
logger.info("Heartbeat monitoring resumed (exit still disabled)")
|
|
515
555
|
|
|
516
556
|
def is_paused(self) -> bool:
|
|
517
557
|
"""Check if heartbeat monitoring is currently paused.
|
|
@@ -534,28 +574,28 @@ class Heartbeat:
|
|
|
534
574
|
- enabled: Whether heartbeat monitoring is enabled (bool)
|
|
535
575
|
- paused: Whether heartbeat monitoring is paused (bool)
|
|
536
576
|
"""
|
|
537
|
-
if
|
|
577
|
+
if self._subscriber is None:
|
|
538
578
|
return {
|
|
539
579
|
"is_active": False,
|
|
540
580
|
"last_value": None,
|
|
541
581
|
"time_since_last": None,
|
|
542
582
|
"timeout_seconds": self._timeout_seconds,
|
|
543
|
-
"enabled":
|
|
583
|
+
"enabled": self._enabled,
|
|
544
584
|
"paused": False,
|
|
545
585
|
}
|
|
546
586
|
|
|
547
|
-
last_value = self.
|
|
548
|
-
time_since_last = self.
|
|
587
|
+
last_value = self._get_latest_heartbeat_data()
|
|
588
|
+
time_since_last = self._get_time_since_last_data()
|
|
549
589
|
|
|
550
590
|
with self._paused_lock:
|
|
551
591
|
paused = self._paused
|
|
552
592
|
|
|
553
593
|
return {
|
|
554
|
-
"is_active": self.
|
|
594
|
+
"is_active": self._is_subscriber_active(),
|
|
555
595
|
"last_value": last_value,
|
|
556
596
|
"time_since_last": time_since_last,
|
|
557
597
|
"timeout_seconds": self._timeout_seconds,
|
|
558
|
-
"enabled":
|
|
598
|
+
"enabled": self._enabled,
|
|
559
599
|
"paused": paused,
|
|
560
600
|
}
|
|
561
601
|
|
|
@@ -565,9 +605,9 @@ class Heartbeat:
|
|
|
565
605
|
Returns:
|
|
566
606
|
True if heartbeat is active, False otherwise.
|
|
567
607
|
"""
|
|
568
|
-
if
|
|
608
|
+
if self._subscriber is None:
|
|
569
609
|
return False
|
|
570
|
-
return self.
|
|
610
|
+
return self._is_subscriber_active()
|
|
571
611
|
|
|
572
612
|
@staticmethod
|
|
573
613
|
def _format_uptime(seconds: float) -> str:
|
|
@@ -617,8 +657,6 @@ class Heartbeat:
|
|
|
617
657
|
|
|
618
658
|
def shutdown(self) -> None:
|
|
619
659
|
"""Shuts down the heartbeat monitor and stops monitoring thread."""
|
|
620
|
-
if not self._enabled:
|
|
621
|
-
return
|
|
622
660
|
self._shutdown_event.set()
|
|
623
661
|
if self._monitor_thread and self._monitor_thread.is_alive():
|
|
624
662
|
self._monitor_thread.join(timeout=2.0) # Extended timeout
|
|
@@ -635,50 +673,45 @@ class Heartbeat:
|
|
|
635
673
|
table.add_column("Parameter", style="cyan")
|
|
636
674
|
table.add_column("Value")
|
|
637
675
|
|
|
638
|
-
# Enabled
|
|
639
|
-
|
|
640
|
-
if not enabled:
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
676
|
+
# Mode: Enabled/Disabled and Paused state
|
|
677
|
+
mode_parts = []
|
|
678
|
+
if not status["enabled"]:
|
|
679
|
+
mode_parts.append("[yellow]Exit Disabled[/]")
|
|
680
|
+
if status["paused"]:
|
|
681
|
+
mode_parts.append("[yellow]Paused[/]")
|
|
682
|
+
if not mode_parts:
|
|
683
|
+
mode_parts.append("[green]Active[/]")
|
|
684
|
+
table.add_row("Mode", " | ".join(mode_parts))
|
|
685
|
+
|
|
686
|
+
# Signal status
|
|
687
|
+
active_style = "green" if status["is_active"] else "red"
|
|
688
|
+
table.add_row(
|
|
689
|
+
"Signal",
|
|
690
|
+
f"[{active_style}]{'Receiving' if status['is_active'] else 'No Signal'}[/]",
|
|
691
|
+
)
|
|
648
692
|
|
|
649
|
-
#
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
table.add_row("
|
|
653
|
-
else:
|
|
654
|
-
# Active status
|
|
655
|
-
active_style = "bold dark_green" if status["is_active"] else "bold red"
|
|
656
|
-
table.add_row("Signal Active", f"[{active_style}]{status['is_active']}[/]")
|
|
657
|
-
|
|
658
|
-
# Last heartbeat value (robot uptime)
|
|
659
|
-
last_value = status["last_value"]
|
|
660
|
-
if last_value is not None:
|
|
661
|
-
uptime_str = self._format_uptime(last_value)
|
|
662
|
-
table.add_row("Robot Driver Uptime", f"[blue]{uptime_str}[/]")
|
|
663
|
-
else:
|
|
664
|
-
table.add_row("Robot Driver Uptime", "[red]No data[/]")
|
|
693
|
+
# Robot uptime
|
|
694
|
+
if status["last_value"] is not None:
|
|
695
|
+
uptime_str = self._format_uptime(status["last_value"])
|
|
696
|
+
table.add_row("Robot Uptime", f"[blue]{uptime_str}[/]")
|
|
665
697
|
|
|
666
698
|
# Time since last heartbeat
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
699
|
+
if status["time_since_last"] is not None:
|
|
700
|
+
time_since = status["time_since_last"]
|
|
701
|
+
timeout = status["timeout_seconds"]
|
|
670
702
|
time_style = (
|
|
671
|
-
"
|
|
703
|
+
"red"
|
|
704
|
+
if time_since > timeout
|
|
705
|
+
else "yellow"
|
|
706
|
+
if time_since > timeout * 0.5
|
|
707
|
+
else "green"
|
|
672
708
|
)
|
|
673
|
-
table.add_row("
|
|
674
|
-
else:
|
|
675
|
-
table.add_row("Time Since Last", "[yellow]N/A[/]")
|
|
709
|
+
table.add_row("Last Heartbeat", f"[{time_style}]{time_since:.1f}s ago[/]")
|
|
676
710
|
|
|
677
711
|
# Timeout setting
|
|
678
|
-
table.add_row("Timeout", f"[blue]{timeout_seconds}s[/]")
|
|
712
|
+
table.add_row("Timeout", f"[blue]{status['timeout_seconds']}s[/]")
|
|
679
713
|
|
|
680
|
-
|
|
681
|
-
console.print(table)
|
|
714
|
+
Console().print(table)
|
|
682
715
|
|
|
683
716
|
|
|
684
717
|
class ServerLogSubscriber:
|
|
@@ -696,55 +729,34 @@ class ServerLogSubscriber:
|
|
|
696
729
|
_log_subscriber: Zenoh subscriber for log messages.
|
|
697
730
|
"""
|
|
698
731
|
|
|
699
|
-
def __init__(self
|
|
700
|
-
"""Initialize the ServerLogSubscriber.
|
|
701
|
-
|
|
702
|
-
Args:
|
|
703
|
-
zenoh_session: Active Zenoh session for communication.
|
|
704
|
-
"""
|
|
705
|
-
self._zenoh_session = zenoh_session
|
|
732
|
+
def __init__(self) -> None:
|
|
733
|
+
"""Initialize the ServerLogSubscriber."""
|
|
734
|
+
# DexComm will handle the communication
|
|
706
735
|
self._log_subscriber = None
|
|
707
736
|
self._initialize()
|
|
708
737
|
|
|
709
738
|
def _initialize(self) -> None:
|
|
710
739
|
"""Initialize the log subscriber with error handling."""
|
|
711
740
|
|
|
712
|
-
def log_handler(
|
|
741
|
+
def log_handler(payload):
|
|
713
742
|
"""Handle incoming log messages from the server."""
|
|
714
|
-
if not self._is_valid_log_sample(sample):
|
|
715
|
-
return
|
|
716
|
-
|
|
717
743
|
try:
|
|
718
|
-
log_data = self._parse_log_payload(
|
|
744
|
+
log_data = self._parse_log_payload(payload)
|
|
719
745
|
if log_data:
|
|
720
746
|
self._display_server_log(log_data)
|
|
721
747
|
except Exception as e:
|
|
722
748
|
logger.warning(f"Failed to process server log: {e}")
|
|
723
749
|
|
|
724
750
|
try:
|
|
725
|
-
# Subscribe to server logs topic
|
|
726
|
-
self._log_subscriber =
|
|
727
|
-
"logs", log_handler
|
|
751
|
+
# Subscribe to server logs topic using DexComm
|
|
752
|
+
self._log_subscriber = Subscriber(
|
|
753
|
+
topic="logs", callback=log_handler, config=get_zenoh_config_path()
|
|
728
754
|
)
|
|
729
755
|
logger.debug("Server log subscriber initialized")
|
|
730
756
|
except Exception as e:
|
|
731
757
|
logger.warning(f"Failed to initialize server log subscriber: {e}")
|
|
732
758
|
self._log_subscriber = None
|
|
733
759
|
|
|
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
760
|
def _parse_log_payload(self, payload) -> dict[str, str] | None:
|
|
749
761
|
"""Parse log payload and return structured data.
|
|
750
762
|
|
|
@@ -755,7 +767,17 @@ class ServerLogSubscriber:
|
|
|
755
767
|
Parsed log data as dictionary or None if parsing fails.
|
|
756
768
|
"""
|
|
757
769
|
try:
|
|
758
|
-
|
|
770
|
+
if hasattr(payload, "to_bytes"):
|
|
771
|
+
# Handle zenoh-style payload
|
|
772
|
+
payload_str = payload.to_bytes().decode("utf-8")
|
|
773
|
+
else:
|
|
774
|
+
# Handle raw bytes from DexComm
|
|
775
|
+
payload_str = (
|
|
776
|
+
payload.decode("utf-8")
|
|
777
|
+
if isinstance(payload, bytes)
|
|
778
|
+
else str(payload)
|
|
779
|
+
)
|
|
780
|
+
|
|
759
781
|
if not payload_str.strip():
|
|
760
782
|
logger.debug("Received empty log payload")
|
|
761
783
|
return None
|
|
@@ -804,7 +826,7 @@ class ServerLogSubscriber:
|
|
|
804
826
|
"""Clean up the log subscriber and release resources."""
|
|
805
827
|
if self._log_subscriber is not None:
|
|
806
828
|
try:
|
|
807
|
-
self._log_subscriber.
|
|
829
|
+
self._log_subscriber.shutdown()
|
|
808
830
|
self._log_subscriber = None
|
|
809
831
|
except Exception as e:
|
|
810
832
|
logger.error(f"Error cleaning up log subscriber: {e}")
|