Source code for go2.modules.lidar.lidar_module

import numpy as np
from typing import Callable
from typing_extensions import override

from ...core.module import DogModule
from ...hardware.hardware_type import HardwareType
from ...hardware.hardware_interface_lidar import HardwareInterfaceLIDAR
from ...hardware.native.native_hardware_lidar import NativeHardwareLIDAR
from ...hardware.virtual.virtual_hardware_lidar import VirtualHardwareLIDAR
from .callback_dispatcher import CallbackDispatcher
from .iox_receiver import IoxReceiver


[docs] class LIDARModule(DogModule): """ ``LIDARModule`` provides a simple API for: - Receiving decoded ``PointCloud2`` structures as xyz-(optionally intensity) numpy arrays Users should not access or construct this class directly. Rather, they should access it through the :class:`~core.controller.Go2Controller` instance. Parameters ---------- publish_hz : int The publishing frequency (in Hz) of decoded point cloud frames. This dictates how many times per second the registered callback is triggered. A lower publishing rate results in a longer point accumulation time per frame, yielding a denser point cloud with greater field-of-view (FOV) coverage, but introduces higher delay between publishings. (Although, the data transfer will always be an *O(1)* pointer-swap internally). Must be in the range [5, 100]. Default is 10. - When running in ``HardwareType.Native`` mode, ``publish_hz`` does **not** control the actual publish rate of the physical LIDAR sensor. You must configure that rate manually via the LIDAR's own driver/firmware settings. In this mode, ``publish_hz`` only governs how frequently this wrapper internally polls for new data over IPC from the ROS2 publisher node to this receiver. - When running in ``HardwareType.Virtual`` mode, ``publish_hz`` **does** matter, since we are mocking the real LIDAR and fully control when it publishes data. Important --------- When executing on ``HardwareType.Native`` this module launches ROS2 nodes. In such a case, it is **critical** that students **ALWAYS** call :meth:`Go2Controller.safe_shutdown` after normal (error free) script exit. """ def __init__(self, publish_hz: int = 10) -> None: super().__init__("LIDAR") self._publish_hz: int = publish_hz self._hardware: HardwareInterfaceLIDAR = None self._dispatcher: CallbackDispatcher = None self._iox_receiver: IoxReceiver = None @override def _initialize(self) -> None: if self._initialized: return if self._publish_hz < 5 or self._publish_hz > 100: raise ValueError(f"publish_hz must be in the range [5, 100]; got {self._publish_hz}") if self._hardware_type == HardwareType.NATIVE: self._hardware = NativeHardwareLIDAR() else: self._hardware = VirtualHardwareLIDAR() self._hardware._initialize() self._launch_bridge() self._initialized = True # Rationale: # I dont want these handling deps to leak into the hardware abstraction. # Since they are shared across both abstractions, we can just keep it here. def _launch_bridge(self) -> None: self._dispatcher = CallbackDispatcher() self._iox_receiver = IoxReceiver(self._dispatcher, self._publish_hz) self._iox_receiver.start()
[docs] def register_decoded_pointcloud_callback(self, callback: Callable[[int, np.ndarray], None]) -> None: """ Register a callback to receive decoded PointCloud2 data. The callback is triggered whenever a new raw point cloud sample is received via Iceoryx2. Parameters ---------- callback : Callable[[int, np.ndarray], None] A function to be called with: - **timestamp** (int): The source timestamp in nanoseconds. - **points** (np.ndarray): A **Fortran-contiguous** ``float32`` array of shape ``(3, N)`` for [x, y, z] or ``(4, N)`` if intensity is supported [x, y, z, intensity]. Important --------- - **Shared Views:** During each cycle, all subscribers receive **views** of the same underlying data. This is done for performance. Modifying data through the returned view is unsafe, as it affects the shared data. If you need to modify the data, create a **copy** first. - **Cache Locality & Iteration:** Because the array is Fortran-contiguous, the data is laid out column-by-column in memory (e.g., x0, y0, z0, x1, y1, z1...). This means all coordinates (plus intensity) for a single point are tightly packed together. Therefore, column-major iteration should be greatly prefered over row-major iteration to maximize cache efficiency. If you *need* a C-contiguous array (row-major) you can use use ``numpy.ascontiguousarray(array)``. """ self._dispatcher._register_decoded(callback)
@override def _shutdown(self) -> None: if self._hardware: self._hardware._shutdown() if self._iox_receiver: self._iox_receiver._shutdown() self._iox_receiver.join(timeout=2) self._initialized = False