Files
DriverLIdar/docs/diagnostics.md
loctv ef217bdca8 feat(config,diagnostics): explicit transport in DeviceConfig, vendor-neutral diagnostics
DeviceConfig now carries an optional transport (serial/udp/tcp) instead of
the ESPE-only use_udp bool. Plugins validate it in create_driver_instance:
a fixed-transport driver configured with the wrong transport fails open()
with InvalidConfig (via InvalidConfigDriver — the plugin ABI forbids
returning nullptr) rather than silently ignoring the setting. Selectable
drivers (ESPE) switch TCP/UDP through the same field. config.json
load/save round-trips "transport" for every transport, including serial,
and migrates legacy use_udp:true entries.

Diagnostics drops the per-vendor accessors (espe_fault, rplidar_fault,
monitor_fault, sick_error, pollution_*, contamination_*, manipulation) for
one common shape: a list of DiagnosticIssue{severity, code, detail} with
cross-vendor codes, plus a raw map of vendor passthrough values and
to_json() for hosts that prefer a string. Vendor bit decoding now lives in
one place (decode_diagnostics); has_fault/has_warning/healthy keep their
meaning, so is_ready()/wait_ready() are unchanged.

Also: README regains the model/protocol and ExtraInfo tables lost in the
lidarlib->xlidar refactor (verified against current code), and the empty
xlocd/ tree left by a stray sync run is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 09:44:41 +07:00

14 KiB
Raw Permalink 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::raw (key "olei.scan_frequency", "olei.input_status", "olei.output_status", "olei.field_status", "olei.status_flags", "olei.distance_ratio") thay vì decode sai thành issue. 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); decode_diagnostics() biến nó thành issue chung: bit error → {fault, device_error}, pollution warning → {warning, optics_dirty}, pollution error → {fault, optics_dirty} (raw giữ ở raw["sick.device_status"]). 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_diagnostics() biến nó thành issue chung: contamination warning → {warning, optics_dirty}, contamination error → {fault, optics_dirty}, manipulation → {fault, manipulation} (raw giữ ở raw["nano.general_state"]). 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 ở raw["rplidar.health_status"] / raw["rplidar.error_code"]; decode_diagnostics() sinh issue chung: health Error → {fault, device_error}, Warning → {warning, device_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

API chung cho mọi hãng — không có hàm decode riêng từng vendor. Mỗi vấn đề thiết bị là một DiagnosticIssue{severity, code, detail}; bảng map từ wire sang code chung:

Nguồn wire Issue (severity, code)
OLEI Family A bit monitor / voltage / temp fault motor / voltage / temperature
OLEI Family A bit 3-7 (reserved) ≠ 0 fault device_error
SICK TiM device error / pollution warning / pollution error fault device_error / warning optics_dirty / fault optics_dirty
nanoScan3 contamination warning / error / manipulation warning optics_dirty / fault optics_dirty / fault manipulation
ESPE fault word ≠ 0 fault device_error (detail kèm giá trị hex)
RPLIDAR health Warning / Error warning device_warning / fault device_error
#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
    }
    for (const xlidar::DiagnosticIssue& issue : d.issues) {
        printf("[%s] %s — %s\n", xlidar::to_string(issue.severity),
               issue.code.c_str(), issue.detail.c_str());
    }
    if (d.has_fault())   { /* dừng tin dữ liệu */ }
    if (d.has_warning()) { /* lên lịch bảo trì */ }

    // Giá trị thô của hãng (chỉ có khi wire mang nó), VD Family C raw:
    if (auto it = d.raw.find("olei.status_flags"); it != d.raw.end())
        printf("status_flags=0x%08X\n", it->second);

    printf("%s\n", xlidar::to_json(d).c_str());  // JSON cho REST/telemetry
}
  • 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: optics_dirty / FAULT: voltage temperature | WARN: optics_dirty.
  • to_json(Diagnostics) — chuỗi JSON đầy đủ (valid, model, firmware, healthy, issues[], raw{}) cho host nào muốn nhận string thay struct.
  • has_fault() / has_warning() quét issues theo severity; healthy() = valid && !has_fault().

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.