Files
DriverLIdar/docs/diagnostics.md
loctv 5b2c74bd36 refactor: restructure lidarlib into xlidar-driver plugin SDK
- LidarManager facade (liblidar_manager.so): dlopen plugin discovery,
  available_drivers map<driver_id, PluginRegistry>, create_lidar_device,
  config.json load/save with legacy lidarlib migration
- Common LidarDriverInterface + DriverInfo/DeviceConfig plugin ABI
  (extern C get_driver_info / create_driver_instance)
- Plugins: driver_rplidar (ported from xlocd, Slamtec SDK), driver_olei,
  driver_sick_code (TiM CoLa-A), driver_sick_safety (nanoScan3), driver_espe
- Diagnostics extended with rplidar health + firmware; FOV filter window,
  range override and legacy remap window unified in DeviceConfig
- Rewritten README, diagnostics doc and examples (list_drivers, example,
  lidar_app)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:30:56 +07:00

12 KiB
Raw Blame History

Nghiên cứu: Dữ liệu chẩn đoán (diagnosis) của các driver xlidar

Tài liệu này tổng hợp những gì các gói dữ liệu OLEI mang theo về tình trạng thiết bị (self-diagnostics), ngoài dữ liệu điểm quét. Kết quả nghiên cứu này là cơ sở cho API xlidar::Diagnostics / LidarDriverInterface::get_diagnostics() (header include/lidar_diagnostics.hpp).

Điểm quan trọng: lidar OLEI không có kênh/query chẩn đoán riêng — driver chỉ nhận UDP thụ động, thiết bị không nhận lệnh hỏi trạng thái. Toàn bộ thông tin chẩn đoán được nhúng trong header của chính gói dữ liệu quét, nên "lấy data diagnosis" = decode header của stream đang chạy, không tốn thêm băng thông hay round-trip nào.

1. Family A (Frame ID 0xFAF0 — VB, VF, LR-1F)

Header 20 byte, thông tin chẩn đoán nằm ở các offset sau:

Offset Kích thước Trường Ý nghĩa
[4] u8 distance_scale mm/count — gián tiếp cho biết chế độ đo
[5] u8 error_status Byte lỗi thiết bị, xem bảng bit bên dưới
[10-11] u16 LE rotation_raw Tốc độ quay motor (raw, đơn vị chưa xác minh — nghi là RPM hoặc Hz×100)
[12-15] u32 LE timestamp Đồng hồ thiết bị (ms) — dùng phát hiện thiết bị treo/reset

Bit map của error_status (byte [5])

Bit Mask Tên Ý nghĩa khi = 1
0 0x01 Monitor Khối giám sát/motor bất thường (motor không đạt tốc độ, mất đồng bộ encoder)
1 0x02 Voltage Điện áp nguồn ngoài dải cho phép
2 0x04 Temperature Nhiệt độ bên trong bất thường (quá nóng/quá lạnh)
3-7 0xF8 Reserved Chưa định nghĩa trong tài liệu OLEI; driver vẫn coi ≠0 là fault và log dạng reserved(0xXX)

Ghi chú thực nghiệm:

  • Trên thiết bị chạy bình thường byte này luôn 0x00; chưa tái tạo được fault thật trên phần cứng (chưa thử hạ áp/che gương), nên ý nghĩa bit lấy theo tài liệu giao thức OLEI, chưa verify từng bit bằng lỗi thật.
  • error_status lặp lại trong mỗi packet (~22°/packet), nhưng driver chỉ chốt giá trị theo vòng quét (lần flush_scan() gần nhất) — đủ nhanh (10-20 Hz) và nhất quán với ScanResult.

2. Family B (Frame ID 0xFEF0 — LR-1BS5, LR-1FMI)

Header 40 byte mang tên model ASCII tại [7-16]distance_scale tại [6], nhưng không có byte lỗi, không có timestamp. Đã dò toàn bộ 40 byte header trên stream thật của LR-1FMI: các byte còn lại là hằng số/counter, không thấy trường nào đổi giá trị khi thiết bị hoạt động — kết luận Family B không phát dữ liệu chẩn đoán trên wire.

Chẩn đoán khả dụng duy nhất với Family B là gián tiếp:

  • Mất gói / ErrorCode::Timeout từ recv_scan() → thiết bị ngắt kết nối hoặc treo.
  • Số điểm mỗi vòng tụt bất thường (LR-1FMI chuẩn ~2400 điểm/vòng) → nghi bẩn kính / lỗi quang học.

get_diagnostics() với Family B trả về valid = true sau scan đầu tiên nhưng error_status = 0 và mọi trường optional là nullopt — nghĩa là "không có thông tin", không đồng nghĩa "thiết bị khỏe".

3. Family C / Protocol V3 (Magic 0xFEAC — GS1-5)

Header 48 byte, giàu thông tin trạng thái nhất (GS1-5 là dòng có field an toàn kiểu safety-scanner). Các trường chẩn đoán (port từ driver C# OleiGS15Driver.cs, chưa verify trên phần cứng thật):

Offset Kích thước Trường Ý nghĩa (theo driver C#)
[24-25] u16 LE scan_frequency_raw Tần số quét raw (nghi Hz×100)
[28-29] u16 LE input_status Trạng thái các chân input số
[30-31] u16 LE output_status Trạng thái các chân output số (OSSD?)
[32-35] u32 LE field_status Trạng thái các vùng field an toàn (bit nào = vùng nào chưa rõ)
[44-47] u32 LE status_flags Cờ trạng thái tổng — bit map chưa có tài liệu

Vì bit map chưa xác minh, driver truyền nguyên giá trị raw qua Diagnostics (các trường std::optional) thay vì decode sai. Khi có tài liệu V3 chính thức hoặc thiết bị GS1-5 để thử, bổ sung decode tại decode_diagnostics() trong include/lidar_interface.hpp (và bảng field trong plugins/driver_olei/olei_driver.cpp).

4. SICK TiM (TCP/CoLa-A — telegram LMDscandata)

Khác OLEI, TiM có hai đường lấy chẩn đoán:

  1. Nhúng trong stream (driver dùng đường này): telegram sSN LMDscandata mang cặp Device Status ngay sau SerialNumber. Theo SICK Telegram Listing (8014631):

    Cặp giá trị Ý nghĩa
    0 0 OK
    0 1 Error — thiết bị lỗi, dữ liệu không tin được
    0 2 Pollution warning — kính bắt đầu bẩn, vẫn đo được
    0 4 Pollution error — kính bẩn nặng, phải lau

    Driver ghép cặp này vào info.sick_device_status ((word0<<8)|word1) và decode qua sick_error() / pollution_warning() / pollution_error(). Ngoài ra telegram còn mang input/output số (input_status/output_status) và tần số quét (scan_frequency_raw, đơn vị 1/100 Hz). Chưa verify trên TiM781S thật với kính bẩn — cần che/bôi bẩn kính để xác nhận giá trị 2/4.

  2. Query chủ động qua SOPAS (chưa implement): sRN SCdevicestate (0=busy, 1=ready, 2=error), sRN LCMstate (mức nhiễm bẩn), sRN DItype/sRN ODoprh (giờ vận hành). Cần cơ chế request/response xen giữa stream — xem mục Hướng mở rộng.

5. SICK nanoScan3 (UDP safety-data — block General System State)

Packet UDP của nanoScan3 gồm nhiều block, header trỏ tới từng block bằng cặp offset/size. Block General System State (offset tại header [32], size [34]) là block trạng thái an toàn; byte 0 của block (layout theo sick_safetyscanners, chưa verify phần cứng):

Bit Mask Ý nghĩa khi = 1
0 0x01 Run mode active (đang chạy bình thường)
1 0x02 Standby mode
2 0x04 Contamination warning — kính bẩn nhẹ
3 0x08 Contamination error — kính bẩn nặng, vùng an toàn không tin được
4 0x10 Reference contour status
5 0x20 Manipulation — nghi bị che/can thiệp cố ý

Driver đọc byte này vào info.nano_general_state, decode qua contamination_warning() / contamination_error() / manipulation(). Lưu ý: block này chỉ có mặt nếu được tick chọn trong cấu hình data output của Safety Designer — thiếu block thì trường giữ nullopt.

6. Slamtec RPLIDAR (serial — health qua SDK)

RPLIDAR không nhúng chẩn đoán trong stream điểm quét; thay vào đó SDK có lệnh getHealth() trả về status (0 = OK, 1 = Warning, 2 = Error) kèm error_code 16-bit. Driver (plugins/driver_rplidar) gọi health check một lần lúc open() — status Error thì open() trả DeviceError và không dùng thiết bị; Warning vẫn chạy nhưng để lại dấu.

  • Snapshot health nằm ở Diagnostics::rplidar_health_status / rplidar_error_code, decode qua rplidar_fault() / rplidar_warning().
  • Diagnostics::model (slamtec-0xNN từ device info) và Diagnostics::firmware (fw M.mm hw H) được tự nhận lúc open().
  • Chẩn đoán runtime chủ yếu là gián tiếp: recv_scan() timeout / mất kết nối serial (Timeout / DeviceDisconnected), giống mục transport dưới đây.

7. Chẩn đoán tầng transport (mọi driver)

Ngoài dữ liệu trên wire, bản thân driver cung cấp lớp chẩn đoán kết nối:

Tín hiệu API Ý nghĩa
Không mở được socket open()ErrorCode Lỗi cấu hình host (port bận, IP sai...)
Không có gói trong timeout_ms recv_scan() = false + last_error() == Timeout Đứt cáp, thiết bị mất nguồn, sai port
Lỗi recv giữa chừng last_error() == DeviceDisconnected Socket lỗi cứng
timestamp_ms nhảy lùi so sánh giữa 2 scan (Family A) Thiết bị vừa reset/reboot

Chiến lược giám sát khuyến nghị cho app: coi cảm biến healthy khi và chỉ khi recv_scan() thành công đều đặn get_diagnostics().has_fault() == false.

8. Kiểm tra sẵn sàng: is_ready() / wait_ready()

Chiến lược trên được gói sẵn trong hai hàm của LidarDriverInterface:

lidar->open();
if (!lidar->wait_ready(5000)) {          // bơm recv_scan() tới khi ready
    // timeout: xem last_error() (Timeout, DeviceDisconnected...)
}
// ... vòng lặp chính đang bơm recv_scan()/spin_once() ...
if (!lidar->is_ready()) { /* mất dữ liệu hoặc thiết bị báo fault */ }
  • is_ready(max_age_ms = 3000) = is_open() get_diagnostics().healthy() scan decode gần nhất chưa quá max_age_ms (truyền 0 để bỏ kiểm tra tuổi). Kiểm tra tuổi giúp phát hiện cảm biến chết giữa chừng — socket UDP vẫn "mở" và snapshot diagnostics vẫn "khỏe" dù thiết bị đã ngừng phát. Diagnostics chỉ được cập nhật bởi recv_scan()/spin_once(), nên phải có vòng lặp đang bơm dữ liệu thì is_ready() mới có nghĩa.
  • wait_ready(timeout_ms = 5000) = bơm recv_scan() (bỏ qua dữ liệu, không gọi callback) cho tới khi is_ready() hoặc hết giờ — dùng lúc khởi động, trước khi giao quyền bơm cho vòng lặp chính.
  • is_ready() không tính has_warning() (kính bẩn nhẹ vẫn đo được → vẫn ready); app muốn chặt hơn thì tự kiểm tra thêm !get_diagnostics().has_warning().

9. API

#include "lidar_manager.hpp"

xlidar::ScanResult r;
if (lidar->recv_scan(r, 1000)) {
    xlidar::Diagnostics d = lidar->get_diagnostics();

    if (!d.valid) {
        // chưa có scan nào được decode
    } else if (d.has_fault()) {
        // Family A: đọc từng bit
        if (d.voltage_fault())     /* điện áp bất thường */;
        if (d.temperature_fault()) /* nhiệt độ bất thường */;
        if (d.monitor_fault())     /* motor/giám sát bất thường */;
        printf("lidar fault: %s\n", xlidar::to_string(d).c_str());
    }

    // SICK: cảnh báo kính bẩn — chưa phải fault nhưng nên lên lịch lau
    if (d.has_warning()) {
        d.pollution_warning();      // TiM
        d.contamination_warning();  // nanoScan3
    }
    if (d.manipulation()) /* nanoScan3: nghi bị che/can thiệp */;

    // Family C raw (nullopt nếu không phải GS1-5)
    if (d.status_flags) printf("status_flags=0x%08X\n", *d.status_flags);
}
  • LidarDriverInterface::get_diagnostics() — snapshot từ vòng quét decode gần nhất; gọi từ cùng thread đang bơm recv_scan()/spin_once() (driver không khóa nội bộ).
  • decode_diagnostics(const ExtraInfo&) — hàm free, decode trực tiếp từ ScanResult::info nếu app muốn gắn chẩn đoán với đúng scan cụ thể.
  • to_string(Diagnostics) — chuỗi log 1 dòng: no data / ok / WARN: pollution / FAULT: voltage temperature.
  • has_fault() gộp mọi nguồn lỗi (OLEI byte lỗi, TiM device/pollution error, nano contamination error/manipulation); has_warning() gộp các mức cảnh báo kính bẩn.

10. Hướng mở rộng

  • SICK SOPAS query chủ động: sRN SCdevicestate (0=busy, 1=ready, 2=error), sRN LCMstate (mức nhiễm bẩn chi tiết) — cần cơ chế request/response xen giữa stream LMDscandata.
  • Family A bit 3-7: cần bảng tra từ OLEI hoặc thử nghiệm gây lỗi có chủ đích (hạ áp nguồn, chặn tản nhiệt) để xác minh.
  • GS1-5: cần thiết bị thật để xác minh toàn bộ Family C.
  • TiM pollution 2/4: cần thử che/bôi bẩn kính TiM781S thật để xác nhận.