- 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>
234 lines
12 KiB
Markdown
234 lines
12 KiB
Markdown
# 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` (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 **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
|
||
|
||
```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
|
||
} 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.
|