dexcontrol 0.3.0__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 (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 +137 -114
  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.1.dist-info}/METADATA +2 -1
  34. dexcontrol-0.3.1.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.1.dist-info}/WHEEL +0 -0
  51. {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.1.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 zenoh
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.constants import DISABLE_HEARTBEAT_ENV_VAR
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, zenoh_session: zenoh.Session) -> None:
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
- self._zenoh_session.get(
262
+ call_service(
271
263
  resolve_key_name(self._estop_query_name),
272
- handler=lambda reply: logger.info(f"Set E-Stop to {enable}"),
273
- payload=query_msg.SerializeToString(),
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
- # Create a generic subscriber for the heartbeat topic
404
- self._subscriber = GenericZenohSubscriber(
405
- topic=configs.heartbeat_topic,
406
- zenoh_session=zenoh_session,
407
- decoder=self._decode_heartbeat,
408
- name="heartbeat_monitor",
409
- enable_fps_tracking=False,
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
- logger.info(f"Heartbeat monitor started with {self._timeout_seconds}s timeout")
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: zenoh.ZBytes) -> float:
433
+ def _decode_heartbeat(self, data) -> float:
421
434
  """Decode heartbeat data from raw bytes.
422
435
 
423
436
  Args:
424
- data: Raw Zenoh bytes containing heartbeat value.
437
+ data: Raw bytes containing heartbeat value.
425
438
 
426
439
  Returns:
427
440
  Decoded heartbeat timestamp value in seconds.
428
441
  """
429
- # Decode UTF-8 string and convert to float
430
- # Publisher sends: str(self.timecount_now).encode() in milliseconds
431
- timestamp_str = data.to_bytes().decode("utf-8")
432
- timestamp_ms = float(timestamp_str)
433
- # Convert from milliseconds to seconds
434
- return timestamp_ms / 1000.0
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 not self._enabled or self._subscriber is None:
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 monitoring if paused
483
+ # Skip if paused
444
484
  with self._paused_lock:
445
- is_paused = self._paused
446
- if is_paused:
447
- self._shutdown_event.wait(0.1)
448
- continue
449
-
450
- # Check if fresh data is being received within the timeout period
451
- if self._subscriber.is_active():
452
- if not self._subscriber.is_data_fresh(self._timeout_seconds):
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
- def resume(self) -> None:
497
- """Resume heartbeat monitoring after being paused.
498
-
499
- This re-enables the heartbeat timeout checking that was temporarily
500
- disabled by pause(). The monitor will immediately start checking
501
- for fresh heartbeat data again.
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
- # sleep for some time to make sure the heartbeat subscriber is resumed
513
- time.sleep(self._timeout_seconds)
514
- logger.info("Heartbeat monitoring RESUMED - safety mechanism re-enabled")
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 not self._enabled or self._subscriber is None:
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": False,
583
+ "enabled": self._enabled,
544
584
  "paused": False,
545
585
  }
546
586
 
547
- last_value = self._subscriber.get_latest_data()
548
- time_since_last = self._subscriber.get_time_since_last_data()
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._subscriber.is_active(),
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": True,
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 not self._enabled or self._subscriber is None:
608
+ if self._subscriber is None:
569
609
  return False
570
- return self._subscriber.is_active()
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 status
639
- enabled = status.get("enabled", True)
640
- if not enabled:
641
- table.add_row("Status", "[yellow]DISABLED[/]")
642
- table.add_row(
643
- "Reason", f"[yellow]Disabled via {DISABLE_HEARTBEAT_ENV_VAR}[/]"
644
- )
645
- console = Console()
646
- console.print(table)
647
- return
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
- # Paused status
650
- paused = status.get("paused", False)
651
- if paused:
652
- table.add_row("Status", "[yellow]PAUSED[/]")
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
- time_since = status["time_since_last"]
668
- timeout_seconds = status["timeout_seconds"]
669
- if time_since is not None and isinstance(timeout_seconds, (int, float)):
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
- "bold red" if time_since > timeout_seconds * 0.8 else "bold dark_green"
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("Time Since Last", f"[{time_style}]{time_since:.2f}s[/]")
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
- console = Console()
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, 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
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(sample):
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(sample.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 = self._zenoh_session.declare_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
- payload_str = payload.to_bytes().decode("utf-8")
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.undeclare()
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}")