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

252 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
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.