update brand ESPE

This commit is contained in:
2026-07-07 10:38:42 +07:00
parent 1c347a4918
commit 917b4fe4c5
15 changed files with 741 additions and 263 deletions

405
README.md
View File

@@ -1,143 +1,145 @@
# Lidarlib
Thư viện C++17 cho lidar **OLEI** (UDP) **SICK** (TCP/UDP). Build bằng CMake
ra shared lib, hỗ trợ `find_package(lidarlib)`. Mọi driver dùng chung một
interface `lidarlib::Lidar` và một hàm khởi tạo duy nhất `lidarlib::make_lidar()`.
Thư viện C++17 thu nhận dữ liệu lidar 2D cho **OLEI** (UDP), **SICK**
(TCP/UDP) và **ESPE** (TCP/UDP)
- Tự nhận diện họ giao thức OLEI (Family A/B/C) theo từng gói tin
- Tự dò model (`MODEL_AUTO`) với Family B/C
- Chạy nhiều lidar song song (mỗi instance độc lập, an toàn đa luồng)
- Output chuẩn ROS `sensor_msgs/LaserScan` (radian, mét)
- Không có UI — tự viết giao diện trên API này
Mọi driver cùng implement một interface `lidarlib::Lidar`, khởi tạo qua một
factory duy nhất `lidarlib::make_lidar()`, output thống nhất theo định dạng
ROS `sensor_msgs/LaserScan`.
## Build
## Tính năng
- **Đa hãng, một API** — OLEI (Family A/B/C), SICK (TiM 5xx/7xx, nanoScan3)
và ESPE (LGA60) dùng chung interface: `open()` / `recv_scan()` / callback /
`close()`.
- **Tự nhận diện giao thức** — phân biệt họ giao thức OLEI theo frame ID từng
gói; chế độ `AUTO` tự dò model từ dữ liệu (Family B/C).
- **Chẩn đoán thiết bị** — đọc trạng thái tự chẩn đoán nhúng trong stream:
lỗi motor/điện áp/nhiệt độ (OLEI), kính bẩn/pollution (SICK TiM),
contamination/manipulation (nanoScan3).
- **Xử lý lỗi tường minh** — `ErrorCode` phân loại từ `errno` thật;
lifecycle an toàn với mọi thứ tự gọi `open()`/`close()`.
- **Đa luồng an toàn** — mỗi instance độc lập hoàn toàn, chạy mỗi lidar một
thread không cần khóa.
- **Cấu hình JSON** — khai báo danh sách lidar trong `config.json`,
load/save bằng API kèm sẵn.
## Cài đặt
Yêu cầu: Linux, CMake ≥ 3.10, trình dịch C++17. Không có dependency ngoài
(chỉ pthread).
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
```
Sinh ra `build/liblidarlib.so` (chỉ phụ thuộc pthread) và các binary demo
(`example`, `test_dual`, `sick_example`, `nanoscan_example`, `lidar_app`).
Tùy chọn: `-DLIDARLIB_BUILD_EXAMPLES=OFF` (tắt demo),
`-DBUILD_SHARED_LIBS=OFF` (static lib).
Cài đặt và dùng từ project khác:
```bash
cmake --install build --prefix "$HOME/.local" # hoặc sudo với /usr/local
```
Tùy chọn CMake: `-DLIDARLIB_BUILD_EXAMPLES=OFF` (tắt binary demo),
`-DBUILD_SHARED_LIBS=OFF` (build static).
Dùng từ project khác:
```cmake
find_package(lidarlib REQUIRED)
target_link_libraries(my_app PRIVATE lidarlib::lidarlib)
```
## Quick start
## Sử dụng
### Đọc scan
```cpp
#include "lidarlib/lidarlib.hpp" // toàn bộ API trong 1 include
#include "lidarlib/lidarlib.hpp" // toàn bộ API trong một include
lidarlib::LidarConfig c{"front", "192.168.1.10", 2368, "AUTO", false, "OLEI"};
std::unique_ptr<lidarlib::Lidar> lidar = lidarlib::make_lidar(c);
lidarlib::ErrorCode err = lidar->open();
if (err != lidarlib::ErrorCode::Ok) {
fprintf(stderr, "open that bai: %s\n", lidarlib::to_string(err));
if (lidar->open() != lidarlib::ErrorCode::Ok) {
fprintf(stderr, "open: %s\n", lidarlib::to_string(lidar->last_error()));
return 1;
}
lidarlib::ScanResult r;
if (!lidar->recv_scan(r, 1000)) {
// Timeout / DeviceDisconnected / NotOpen — xem lidar->last_error()
if (lidar->recv_scan(r, 1000)) {
// r.scan : LaserScan — điểm đo, format ROS
// r.info : ExtraInfo — metadata tuỳ model
} else {
// Timeout / DeviceDisconnected — xem lidar->last_error()
}
// r.scan : LaserScan — format sensor_msgs/LaserScan của ROS, chung mọi lidar
// r.info : ExtraInfo — thông tin thêm tuỳ family/model
printf("%zu diem, model=%s\n", r.scan.ranges.size(), r.info.detected_model.c_str());
```
Có thể khởi tạo driver trực tiếp thay vì qua `make_lidar()`:
Có thể khởi tạo driver trực tiếp không qua factory:
```cpp
lidarlib::Driver olei(lidarlib::MODEL_AUTO, "192.168.100.100", 2368);
lidarlib::SickDriver tim (lidarlib::MODEL_SICK_TIM571, "192.168.0.1", 2111);
lidarlib::NanoScanDriver nano(lidarlib::MODEL_SICK_NANOSCAN3, "0.0.0.0", 6060);
lidarlib::Driver olei(lidarlib::MODEL_AUTO, "192.168.100.100", 2368);
lidarlib::SickDriver tim (lidarlib::MODEL_SICK_TIM571, "192.168.0.1", 2111);
lidarlib::NanoScanDriver nano(lidarlib::MODEL_SICK_NANOSCAN3, "0.0.0.0", 6060);
lidarlib::EspeDriver espe(lidarlib::MODEL_ESPE_LGA60, "192.168.1.88", 8080);
```
Ngoài `recv_scan()` blocking còn có callback: `set_scan_callback()` +
`spin_once()` trong vòng lặp riêng.
### Chế độ callback
### Error handling & lifecycle
Thay cho `recv_scan()` blocking:
`open()` trả về `lidarlib::ErrorCode` (header `lidarlib/error.hpp`,
`to_string()` để log). Các code chính:
```cpp
lidar->set_scan_callback([](const lidarlib::ScanResult& r) { /* mỗi vòng quét */ });
while (running) lidar->spin_once();
```
| ErrorCode | Khi nào |
|---|---|
| `Ok` | Thành công |
| `AlreadyOpen` | Gọi `open()` khi đang mở — kết nối cũ giữ nguyên |
| `NotOpen` | Gọi `recv_scan()`/`spin_once()` khi chưa `open()` |
| `InvalidAddress` | Chuỗi IP không hợp lệ |
| `PortInUse` | Port local đã bị chiếm (bind `EADDRINUSE`/`EACCES`) |
| `BindFailed` / `SocketError` | Lỗi bind khác / không tạo được socket |
| `ConnectionRefused` / `ConnectionFailed` / `Timeout` | TCP connect (SICK TiM) bị từ chối / không tới được / quá 2s |
| `HandshakeFailed` | TCP nối được nhưng gửi `sEN LMDscandata 1` thất bại |
| `DeviceDisconnected` | Thiết bị đóng kết nối / lỗi recv giữa chừng |
### Chẩn đoán thiết bị
Trạng thái instance:
- `is_open()` — socket đang mở hay không.
- `last_error()` — kết quả của lần `open()`/`recv_scan()`/`spin_once()` gần
nhất (`recv_scan()` trả `false` thì gọi hàm này để biết `Timeout` hay
`DeviceDisconnected`).
- Lifecycle chịu lỗi mọi thứ tự gọi: `close()` trước `open()` hoặc `close()`
hai lần là no-op; `open()` hai lần trả `AlreadyOpen` và không đụng kết nối
đang chạy; sau `close()` có thể `open()` lại (state scan dở được reset).
### Chẩn đoán thiết bị (diagnostics)
Lidar OLEI nhúng thông tin tự chẩn đoán trong header gói dữ liệu (không có
kênh query riêng). API đọc ra qua `lidarlib/diagnostics.hpp` — nghiên cứu
chi tiết layout từng family xem [docs/diagnostics.md](docs/diagnostics.md).
Thiết bị nhúng thông tin tự chẩn đoán trong stream dữ liệu; driver decode
sẵn qua `get_diagnostics()` (chi tiết layout từng giao thức:
[docs/diagnostics.md](docs/diagnostics.md)):
```cpp
lidarlib::Diagnostics d = lidar->get_diagnostics();
if (!d.valid) {
// chưa decode được scan nào
// chưa decode được vòng quét nào
} else if (d.has_fault()) {
// OLEI Family A
d.monitor_fault(); // motor/giám sát bất thường
d.voltage_fault(); // điện áp ngoài dải
d.temperature_fault(); // nhiệt độ bất thường
d.monitor_fault(); // motor/giám sát bất thường
d.voltage_fault(); // điện áp ngoài dải
d.temperature_fault(); // nhiệt độ bất thường
// SICK
d.sick_error(); // TiM: device error
d.pollution_error(); // TiM: kính bẩn nặng
d.contamination_error(); // nanoScan3: kính bẩn nặng
d.manipulation(); // nanoScan3: nghi bị che/can thiệp
printf("lidar fault: %s\n", lidarlib::to_string(d).c_str());
printf("fault: %s\n", lidarlib::to_string(d).c_str());
} else if (d.has_warning()) {
// pollution_warning() / contamination_warning() — kính bẩn nhẹ, nên lau
}
```
- `get_diagnostics()` — snapshot từ vòng quét gần nhất; gọi cùng thread với
`recv_scan()`/`spin_once()`. `d.healthy()` = có data và không có fault.
- `decode_diagnostics(result.info)` — decode gắn với một scan cụ thể.
- Nguồn dữ liệu: OLEI Family A byte lỗi + `rotation_raw` + timestamp;
Family B không có gì trên wire; Family C `input/output/field_status`,
`status_flags` (raw, chưa verify); SICK TiM cặp device status trong
`LMDscandata`; nanoScan3 block General System State (cần bật trong
Safety Designer).
`d.healthy()` = đã có dữ liệu và không có fault. Khuyến nghị giám sát: cảm
biến khỏe khi và chỉ khi `recv_scan()` thành công đều đặn **và**
`get_diagnostics().has_fault() == false`.
## Cấu hình (config.json)
### Xử lý lỗi & lifecycle
`lidar_app` là app mẫu headless: đọc `config.json`, mở từng lidar bằng
`make_lidar()`, một thread mỗi con.
`open()` trả về `ErrorCode`; `last_error()` giữ kết quả của lần gọi gần nhất.
```bash
./build/lidar_app [my_config.json]
```
| ErrorCode | Ý nghĩa |
|---|---|
| `Ok` | Thành công |
| `AlreadyOpen` | `open()` khi đang mở — kết nối cũ giữ nguyên |
| `NotOpen` | `recv_scan()`/`spin_once()` khi chưa `open()` |
| `InvalidAddress` | Chuỗi IP không hợp lệ |
| `PortInUse` | Port local đã bị chiếm |
| `BindFailed` / `SocketError` | Lỗi bind khác / không tạo được socket |
| `ConnectionRefused` / `ConnectionFailed` / `Timeout` | TCP connect bị từ chối / không tới được / quá thời hạn |
| `HandshakeFailed` | TCP nối được nhưng lệnh start-stream thất bại |
| `DeviceDisconnected` | Thiết bị đóng kết nối / lỗi recv giữa chừng |
Lifecycle an toàn với mọi thứ tự gọi: `close()` là idempotent, `open()` lặp
trả `AlreadyOpen` và không đụng kết nối đang chạy, sau `close()` có thể
`open()` lại (state được reset).
### Cấu hình JSON
`lidar_app` (binary demo) đọc `config.json`, mở từng lidar một thread:
```json
{
@@ -145,157 +147,146 @@ if (!d.valid) {
{"name":"front", "ip":"192.168.100.100", "port":2368, "brand":"OLEI", "model":"AUTO", "inverted":false},
{"name":"rear", "ip":"192.168.100.100", "port":2369, "brand":"OLEI", "model":"AUTO", "inverted":true},
{"name":"sick1", "ip":"192.168.0.1", "port":2111, "brand":"SICK", "model":"SICK-TIM571"},
{"name":"nano1", "ip":"0.0.0.0", "port":6060, "brand":"SICK", "model":"SICK-nanoScan3"}
{"name":"nano1", "ip":"0.0.0.0", "port":6060, "brand":"SICK", "model":"SICK-nanoScan3"},
{"name":"espe1", "ip":"192.168.1.88", "port":8080, "brand":"ESPE", "model":"ESPE-LGA60"}
]
}
```
| Trường | Ý nghĩa |
|---|---|
| `brand` | `"OLEI"` (mặc định) hoặc `"SICK"` |
| `model` | Tên trong bảng model bên dưới; tên lạ → mặc định của hãng (`AUTO` / `SICK-TIM571`). Với SICK, `"SICK-nanoScan3"` → driver UDP, còn lại → driver TCP |
| `inverted` | Chỉ OLEI: `true` nếu lidar lắp úp ngược, driver tự đảo góc về hệ quy chiếu xe |
| `angle_min_deg` / `angle_max_deg` | Tuỳ chọn: remap tuyến tính góc output sang cửa sổ này (độ). Không cắt điểm nào, chỉ ghi lại `angle_min/max/increment`. Bỏ trống (±360) = tắt |
| `brand` | `"OLEI"` (mặc định), `"SICK"` hoặc `"ESPE"` |
| `model` | Tên trong bảng model bên dưới; tên lạ → mặc định của hãng |
| `inverted` | `true` nếu lidar lắp úp ngược driver tự đảo góc (mọi hãng) |
| `use_udp` | Chỉ ESPE: `true` để dùng transport UDP thay vì TCP |
| `angle_min_deg` / `angle_max_deg` | Tuỳ chọn: remap tuyến tính góc output sang cửa sổ này (không cắt điểm). Bỏ trống = tắt |
Đọc/ghi bằng `lidarlib::load_config(path)` / `lidarlib::save_config(path, cfg)`.
## Model
## Model hỗ trợ
### OLEI (`brand = "OLEI"`, UDP, port mặc định 2368)
### OLEI (UDP, port mặc định 2368)
| Constant | FOV (°) | Range (m) | Ghi chú |
| Constant | FOV (°) | Range (m) | Giao thức |
|---|---|---|---|
| `MODEL_VB` | -135…135 | 0.05…30 | 2D 270°, Family A |
| `MODEL_VF` | -180…180 | 0.05…30 | 2D 360°, Family A |
| `MODEL_LR1F` | -180…180 | 0.05…50 | Family A; 0° thô của máy chỉ về đuôi (offset +180°) |
| `MODEL_LR1FMI` | -180…180 | 0.05…30 | Family B, ~2400 điểm/vòng;thô chỉ về đuôi (offset +180°) |
| `MODEL_LR1BS5` | -180…180 | 0.05…30 | Family B |
| `MODEL_LR16F` | -135…135 | 0.05…30 | 3D 16-line |
| `MODEL_GS15` | -180…180 | 0.05…30 | Family C/V3 — **chưa verify phần cứng** |
| `MODEL_AUTO` | -180…180 | 0.05…30 | Không biết trước model; tự dò với Family B (chuỗi tên) và C (magic). Family A không mang tên model nên giữ FOV rộng |
| `MODEL_VB` | 135…135 | 0.05…30 | Family A |
| `MODEL_VF` | 180…180 | 0.05…30 | Family A |
| `MODEL_LR1F` | 180…180 | 0.05…50 | Family A (0° thiết bị hướng đuôi, offset +180°) |
| `MODEL_LR1FMI` | 180…180 | 0.05…30 | Family B, ~2400 điểm/vòng (hướng đuôi) |
| `MODEL_LR1BS5` | 180…180 | 0.05…30 | Family B |
| `MODEL_LR16F` | 135…135 | 0.05…30 | 3D 16-line |
| `MODEL_GS15` | 180…180 | 0.05…30 | Family C/V3 — chưa verify phần cứng |
| `MODEL_AUTO` | 180…180 | 0.05…30 | Tự dò model (Family B/C) |
Driver nhận diện họ giao thức theo Frame ID mỗi gói:
**Family A** `0xFAF0` (header 20B, 3B/điểm, CRC32) ·
**Family B** `0xFEF0` (header 40B kèm tên model ASCII, 8B/điểm) ·
**Family C/V3** `0xFEAC` (header 48B, 2/4B/điểm — port từ driver C#, chưa verify).
Driver nhận diện họ giao thức theo frame ID từng gói: **Family A** `0xFAF0`
(header 20 B, 3 B/điểm, CRC32) · **Family B** `0xFEF0` (header 40 B kèm tên
model ASCII, 8 B/điểm) · **Family C/V3** `0xFEAC` (header 48 B, 24 B/điểm).
Tên model đọc từ packet: `detected_model()` hoặc `result.info.detected_model`.
Tên model thật đọc từ packet xem qua `detected_model()` hoặc
`result.info.detected_model`.
### SICK (`brand = "SICK"`)
### SICK
| Constant | FOV (°) | Range (m) | Transport |
|---|---|---|---|
| `MODEL_SICK_TIM5XX` | -135…135 | 0.05…10 | TCP/SOPAS (CoLa-A), port 2111 |
| `MODEL_SICK_TIM571` | -135…135 | 0.05…25 | TCP/SOPAS, port 2111 |
| `MODEL_SICK_TIM7XX` | -135…135 | 0.05…25 | TCP/SOPAS, port 2111 |
| `MODEL_SICK_NANOSCAN3` | -137.5…137.5 | 0.05…40 | UDP safety-data, port 6060 |
| `MODEL_SICK_TIM5XX` | 135…135 | 0.05…10 | TCP/SOPAS (CoLa-A), port 2111 |
| `MODEL_SICK_TIM571` | 135…135 | 0.05…25 | TCP/SOPAS, port 2111 |
| `MODEL_SICK_TIM7XX` | 135…135 | 0.05…25 | TCP/SOPAS, port 2111 — verify trên TiM781S thật |
| `MODEL_SICK_NANOSCAN3` | 137.5…137.5 | 0.05…40 | UDP safety-data, port 6060 |
**TiM (`SickDriver`)**`open()` tự gửi `sEN LMDscandata 1` để bắt đầu stream.
Hệ góc trên dây đặt 90° = trước mặt nên preset có `angle_offset_deg = -90`,
output ra -135…135° với 0° = phía trước. **Đã verify trên TiM781S thật**
(811 điểm/scan, increment 0.333°, DIST1/RSSI1 đúng layout). Chưa verify:
encoder, kênh 8-bit, thông số TiM5xx/571.
- **TiM (`SickDriver`)** — `open()` tự gửi lệnh start-stream; góc output đã
quy về 0° = phía trước.
- **nanoScan3 (`NanoScanDriver`)** — receiver UDP thụ động; đích UDP phải
cấu hình sẵn trong SICK Safety Designer. Chưa verify phần cứng thật.
**nanoScan3 (`NanoScanDriver`)** — UDP receiver thụ động: chỉ bind cổng và
parse datagram; **đích UDP phải cấu hình sẵn trong SICK Safety Designer**
(driver không bắt tay CoLa2). Layout port từ `sick_safetyscanners` (Apache-2.0).
**Chưa verify phần cứng thật** — mới test bằng gói tổng hợp qua loopback.
### ESPE
## Output
| Constant | FOV (°) | Range (m) | Transport |
|---|---|---|---|
| `MODEL_ESPE_LGA60` | 160…160 | 0.05…50 | TCP (mặc định) hoặc UDP, port 8080 |
`ScanResult { LaserScan scan; ExtraInfo info; }` mỗi vòng quét:
- **LGA60 (`EspeDriver`)** — laser scanner FOV 320°, thiết bị quét
20°→340° với 0° hướng đuôi (offset 180° để output 0° = phía trước).
`open()` tự gửi lệnh start-capture `RAuto`; các tham số thiết bị (tốc độ
quay, độ phân giải 0.0250.5°, mức lọc nhiễu) lấy theo cấu hình đã nạp
bằng phần mềm Windows của hãng — driver không tự đổi. Frame dữ liệu
`HISN` (header big-endian, điểm đo little-endian: distance mm +
intensity); frame vùng `WSimu` (nếu thiết bị gửi) được đọc lấy mã lỗi.
Chuyển transport UDP qua tham số `use_udp` của constructor hoặc trường
`use_udp` trong config JSON. Mặc định của hãng: IP 192.168.1.88, port
8080. Port từ driver ROS gốc của hãng — chưa verify trên phần cứng thật.
- **`LaserScan`** — cùng field/đơn vị với ROS: `angle_min/max/increment` (rad,
đã unwrap liên tục, không giới hạn ±π), `ranges[]` (m), `intensities[]`
(0-255), `timestamp_ms` (đồng hồ thiết bị, 0 nếu family không có).
`range_min/max` lấy từ `ModelConfig` (đặt sẵn, không đo mỗi scan);
`time_increment/scan_time` luôn 0.
- **`ExtraInfo`** — field tuỳ family: `detected_model`, `error_status` (Family A),
`distance_scale_mm` (A/B), và các trường raw của Family C (chưa verify).
Field thiết bị không có giữ `std::nullopt`.
## Kiểu dữ liệu
### `ScanResult`
Kết quả một vòng quét: `{ LaserScan scan; ExtraInfo info; }`.
### `LaserScan`
Cùng field và đơn vị với ROS `sensor_msgs/LaserScan`:
| Field | Kiểu | Ý nghĩa |
|---|---|---|
| `angle_min` / `angle_max` | `float` | Góc điểm đầu/cuối (rad), unwrap liên tục |
| `angle_increment` | `float` | Bước góc (rad); góc điểm *i* = `angle_min + i·increment` |
| `ranges` | `vector<float>` | Khoảng cách (m), theo thứ tự quét |
| `intensities` | `vector<float>` | Cường độ phản xạ 0255 |
| `range_min` / `range_max` | `float` | Dải đo hợp lệ (m), lấy từ `ModelConfig` |
| `timestamp_ms` | `uint32_t` | Đồng hồ thiết bị (ms); 0 nếu giao thức không có |
| `time_increment` / `scan_time` | `float` | Luôn 0 (thiết bị không cung cấp) |
### `ExtraInfo`
Metadata tuỳ giao thức; trường thiết bị không có giữ `std::nullopt`:
| Field | Nguồn | Ý nghĩa |
|---|---|---|
| `detected_model` | mọi driver | Tên model thực đọc từ dữ liệu (hoặc tên cấu hình) |
| `error_status` | OLEI Family A | Byte lỗi thiết bị (xem Diagnostics) |
| `distance_scale_mm` | OLEI A/B | Hệ số mm/count của khoảng cách |
| `rotation_raw` | OLEI Family A | Tốc độ motor (raw) |
| `scan_frequency_raw`, `input_status`, `output_status`, `field_status`, `status_flags` | OLEI Family C, SICK TiM | Trạng thái I/O, field an toàn, cờ trạng thái (raw) |
| `sick_device_status` | SICK TiM | Cặp Device Status: 0 ok · 1 error · 2 pollution warning · 4 pollution error |
| `nano_general_state` | nanoScan3 | Byte 0 block General System State (bit `kNanoState*`) |
| `espe_error_status` | ESPE LGA60 | Từ lỗi thiết bị trong frame vùng `WSimu` (chỉ có khi host poll area data) |
### `Diagnostics`
Trạng thái tự chẩn đoán đã decode (`lidarlib/diagnostics.hpp`), trả về từ
`get_diagnostics()` hoặc `decode_diagnostics(result.info)`:
| API | Ý nghĩa |
|---|---|
| `valid` | Đã decode được ít nhất một vòng quét |
| `monitor_fault()` / `voltage_fault()` / `temperature_fault()` | OLEI Family A: motor / điện áp / nhiệt độ bất thường |
| `sick_error()` / `pollution_warning()` / `pollution_error()` | SICK TiM: lỗi thiết bị / kính bẩn nhẹ / kính bẩn nặng |
| `contamination_warning()` / `contamination_error()` / `manipulation()` | nanoScan3: kính bẩn / nghi bị can thiệp |
| `espe_fault()` | ESPE LGA60: từ lỗi thiết bị khác 0 (ý nghĩa bit chưa verify) |
| `has_fault()` | Gộp mọi nguồn lỗi |
| `has_warning()` | Gộp các cảnh báo kính bẩn (vẫn đo được) |
| `healthy()` | `valid && !has_fault()` |
| `to_string(d)` | Chuỗi log một dòng: `no data` / `ok` / `WARN: …` / `FAULT: …` |
### `ModelConfig` & `LidarConfig`
- `ModelConfig` — thông số một model: tên, FOV, dải đo, offset góc, cửa sổ
remap. Các preset `MODEL_*` khai báo sẵn trong header.
- `LidarConfig` — một entry cấu hình runtime: `{name, ip, port, model,
inverted, brand}`, dùng với `make_lidar()` và file JSON.
## Cấu trúc source
| File | Vai trò |
|---|---|
| `include/lidarlib/lidarlib.hpp` | Include tổng hợp toàn bộ API |
| `include/lidarlib/lidar.hpp` | Kiểu dữ liệu, interface `Lidar`, driver OLEI, preset `MODEL_*` |
| `include/lidarlib/sick_lidar.hpp` | `SickDriver`, `NanoScanDriver`, preset `MODEL_SICK_*` |
| `include/lidarlib/espe_lidar.hpp` | `EspeDriver`, preset `MODEL_ESPE_LGA60` |
| `include/lidarlib/diagnostics.hpp` | `Diagnostics`, bit lỗi, `decode_diagnostics()` |
| `include/lidarlib/error.hpp` | `enum class ErrorCode` + `to_string()` |
| `include/lidarlib/diagnostics.hpp` | `Diagnostics`, bit lỗi Family A, `decode_diagnostics()` |
| `include/lidarlib/lidar.hpp` | Data model, interface `Lidar`, driver OLEI, các `MODEL_*` OLEI |
| `include/lidarlib/sick_lidar.hpp` | `SickDriver`, `NanoScanDriver`, các `MODEL_SICK_*` |
| `include/lidarlib/config.hpp` | `LidarConfig`, load/save JSON, `make_lidar()` |
| `src/olei_lidar.cpp` | Parse Family A/B/C, CRC, gom scan |
| `src/olei_lidar.cpp` | Parse Family A/B/C, CRC, gom vòng quét |
| `src/sick_lidar.cpp` | Parse CoLa-A (TiM) + safety-data UDP (nanoScan3) |
| `src/espe_lidar.cpp` | Parse frame `HISN`/`WSimu` (LGA60), gom vòng quét |
| `src/lidar_config.cpp` | Bảng model/brand, config JSON, factory |
| `examples/` | Demo: 1 lidar, 2 lidar song song, SICK TiM, nanoScan3, app khung |
## Ghi chú
- Nếu port UDP đã bị app khác giữ (không bật `SO_REUSEPORT`), `open()` trả
`ErrorCode::PortInUse`. Kiểm tra: `ss -lunp | grep 2368`.
- `inverted` đã verify bằng sniff sống: `false` góc tăng dần, `true` góc giảm
dần cùng bước.
## Changelog
### 2026-07-07 — Diagnostics API cho SICK
- **`SickDriver` (TiM)**: decode cặp Device Status của `LMDscandata`
(0 ok / 1 error / 2 pollution warning / 4 pollution error) vào
`Diagnostics``sick_error()`, `pollution_warning()`, `pollution_error()`.
`info.sick_device_status` là trường mới; `info.error_status` **không còn**
nhận `status0` (trường đó thuộc Family A OLEI).
- **`NanoScanDriver`**: parse block General System State (offset header
`[32]/[34]`, byte 0) → `contamination_warning()`, `contamination_error()`,
`manipulation()`. Layout theo `sick_safetyscanners`, chưa verify phần cứng;
block phải được bật trong Safety Designer.
- **`Diagnostics` thêm `has_warning()`** (kính bẩn mức cảnh báo) và
`to_string()` in thêm `WARN: pollution` / `FAULT: ... contamination`.
- Test loopback: TiM server TCP giả (status `0 4` → pollution error) và gói
nanoScan3 tổng hợp (state `0x09` → contamination error, gói sạch →
`healthy()`); tài liệu tại [docs/diagnostics.md](docs/diagnostics.md) §4-5.
### 2026-07-07 — Diagnostics API
- **Header mới `lidarlib/diagnostics.hpp`**: struct `Diagnostics` (bit lỗi
Family A decode sẵn: `monitor/voltage/temperature_fault()`, `has_fault()`,
`healthy()`; trường raw Family C), hằng `kFaultMonitor/Voltage/Temperature`,
`to_string()` để log.
- **`Lidar::get_diagnostics()`**: snapshot chẩn đoán từ vòng quét decode gần
nhất; `valid = false` khi chưa có scan. Driver OLEI implement đầy đủ;
SICK tạm trả mặc định.
- **`decode_diagnostics(const ExtraInfo&)`**: decode gắn với một
`ScanResult` cụ thể.
- **Tài liệu nghiên cứu [docs/diagnostics.md](docs/diagnostics.md)**: layout
byte chẩn đoán từng family (A: byte lỗi `[5]` + rotation + timestamp;
B: không có; C: input/output/field/status raw), chẩn đoán tầng transport,
hướng mở rộng SICK.
- Đã test bằng gói Family A tổng hợp qua loopback (fault set → decode đúng
từng bit, vòng sạch → `healthy()`); bit map lấy theo tài liệu OLEI, chưa
tái tạo fault trên phần cứng thật.
### 2026-07-06 — Error handling & lifecycle API
- **Header mới `lidarlib/error.hpp`**: `enum class ErrorCode``Ok`,
`AlreadyOpen`, `NotOpen`, `SocketError`, `InvalidAddress`, `PortInUse`,
`BindFailed`, `ConnectionRefused`, `ConnectionFailed`, `HandshakeFailed`,
`Timeout`, `DeviceDisconnected` — kèm `to_string()` để log.
- **`open()` đổi chữ ký `bool``ErrorCode`** trên cả 3 driver (`Driver`,
`SickDriver`, `NanoScanDriver`). Lỗi phân loại từ `errno` thật: bind
`EADDRINUSE``PortInUse`, connect TCP bị từ chối → `ConnectionRefused`,
quá 2s → `Timeout`, gửi lệnh start-stream fail → `HandshakeFailed`,
IP sai format → `InvalidAddress`.
- **API trạng thái mới trên interface `Lidar`**:
- `is_open()` — socket đang mở hay không (cả 3 driver implement);
- `last_error()` — kết quả lần `open()`/`recv_scan()`/`spin_once()` gần
nhất; `recv_scan()` trả `false` thì gọi hàm này để biết `Timeout` hay
`DeviceDisconnected`.
- **Lifecycle chịu lỗi mọi thứ tự gọi**: `close()` trước `open()` hoặc gọi
hai lần là no-op; `open()` khi đang mở trả `AlreadyOpen` và không đụng kết
nối đang chạy; `recv_scan()`/`spin_once()` khi chưa mở trả `false` +
`NotOpen`; sau `close()` có thể `open()` lại (state scan dở được reset).
- **`NanoScanDriver` bỏ `SO_REUSEADDR`**: với UDP không có tác dụng (không có
TIME_WAIT) mà còn che mất lỗi trùng port — giờ `PortInUse` báo được thật.
- ⚠️ **Breaking change**: code cũ viết `if (!lidar->open())` bị đảo ngược
logic vì `ErrorCode::Ok == 0` — phải đổi thành
`if (lidar->open() != lidarlib::ErrorCode::Ok)`.
| `docs/diagnostics.md` | Nghiên cứu layout dữ liệu chẩn đoán từng giao thức |
| `examples/` | Demo: một lidar, hai lidar song song, SICK TiM, nanoScan3, app khung |