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>
14 KiB
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_statuslặp lại trong mỗi packet (~22°/packet), nhưng driver chỉ chốt giá trị theo vòng quét (lầnflush_scan()gần nhất) — đủ nhanh (10-20 Hz) và nhất quán vớiScanResult.
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::Timeouttừ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:
-
Nhúng trong stream (driver dùng đường này): telegram
sSN LMDscandatamang cặp Device Status ngay sau SerialNumber. Theo SICK Telegram Listing (8014631):Cặp giá trị Ý nghĩa 0 0OK 0 1Error — thiết bị lỗi, dữ liệu không tin được 0 2Pollution warning — kính bắt đầu bẩn, vẫn đo được 0 4Pollution 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. -
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-0xNNtừ device info) vàDiagnostics::firmware(fw M.mm hw H) được tự nhận lúcopen().- 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:
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ền0để 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ởirecv_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ơmrecv_scan()(bỏ qua dữ liệu, không gọi callback) cho tới khiis_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ínhhas_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ơmrecv_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::infonế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étissuestheo 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 streamLMDscandata. - 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.