# 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]` và `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 **và** `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`: ```cpp 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()` **và** `get_diagnostics().healthy()` **và** 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` | ```cpp #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.