From 4225b189918d517c4374e202f7c10afc082d0859 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 03:46:24 +0000 Subject: [PATCH 01/14] Initial plan From 1b66e2c8b6e4bafb1aacd8eab3c2e72546aec323 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 03:59:19 +0000 Subject: [PATCH 02/14] Add sensor integration knowledge base (A, B, C directories) Agent-Logs-Url: https://github.com/YYCB/sensor_repository/sessions/9e4c25f0-f328-4484-b1be-26ba054a09bb Co-authored-by: YYCB <23326150+YYCB@users.noreply.github.com> --- A_camera_pipeline/A1_physical_link.md | 175 +++++++++++++++ A_camera_pipeline/A2_image_pipeline.md | 188 ++++++++++++++++ A_camera_pipeline/A3_encoding.md | 226 ++++++++++++++++++++ A_camera_pipeline/A4_transport.md | 277 ++++++++++++++++++++++++ A_camera_pipeline/A5_clock_sync.md | 231 ++++++++++++++++++++ A_camera_pipeline/A6_decode_render.md | 241 +++++++++++++++++++++ A_camera_pipeline/A7_diagnostics.md | 229 ++++++++++++++++++++ B_sensor_catalog/B1_camera.md | 207 ++++++++++++++++++ B_sensor_catalog/B2_mmwave_radar.md | 219 +++++++++++++++++++ B_sensor_catalog/B3_lidar.md | 256 ++++++++++++++++++++++ B_sensor_catalog/B4_imu_gnss.md | 243 +++++++++++++++++++++ B_sensor_catalog/B5_ultrasonic.md | 285 +++++++++++++++++++++++++ C_template/article_template.md | 118 ++++++++++ README.md | 88 +++++++- 14 files changed, 2982 insertions(+), 1 deletion(-) create mode 100644 A_camera_pipeline/A1_physical_link.md create mode 100644 A_camera_pipeline/A2_image_pipeline.md create mode 100644 A_camera_pipeline/A3_encoding.md create mode 100644 A_camera_pipeline/A4_transport.md create mode 100644 A_camera_pipeline/A5_clock_sync.md create mode 100644 A_camera_pipeline/A6_decode_render.md create mode 100644 A_camera_pipeline/A7_diagnostics.md create mode 100644 B_sensor_catalog/B1_camera.md create mode 100644 B_sensor_catalog/B2_mmwave_radar.md create mode 100644 B_sensor_catalog/B3_lidar.md create mode 100644 B_sensor_catalog/B4_imu_gnss.md create mode 100644 B_sensor_catalog/B5_ultrasonic.md create mode 100644 C_template/article_template.md diff --git a/A_camera_pipeline/A1_physical_link.md b/A_camera_pipeline/A1_physical_link.md new file mode 100644 index 0000000..182c8ea --- /dev/null +++ b/A_camera_pipeline/A1_physical_link.md @@ -0,0 +1,175 @@ +# A1. 物理与链路层(Camera → Host) + +## 1. 这篇文章要解决什么问题? + +选型相机接口并将相机可靠接入主机,涵盖带宽预算、驱动枚举、缓冲与丢帧策略。 + +--- + +## 2. 数据链路图 + +``` +[Camera Sensor] + │ RAW / YUV / MJPEG + ▼ +[物理接口层] + ┌──────────────────────────────────────────────────┐ + │ USB UVC │ MIPI CSI-2 │ GMSL2 │ GigE / ETH│ + └──────────────────────────────────────────────────┘ + │ + ▼ +[Host 驱动层] + Linux: V4L2 / UVC / 厂商 SDK / 内核 ISP Bridge + │ + ▼ +[设备节点 / API] + /dev/videoX · media controller · I2C 设备 + │ + ▼ +[缓冲队列] + MMAP / USERPTR / DMABUF → Ring Buffer + │ + ▼ +[上层应用] ← 帧数据(含 frame_id / timestamp) +``` + +--- + +## 3. 关键接口 / 协议 / 格式 + +| 接口 | 典型带宽 | 最大距离 | 主要用途 | +|------|----------|----------|----------| +| USB 2.0 UVC | 480 Mbps(实际 ~40 MB/s) | 5 m | 低成本 PC 摄像头 | +| USB 3.x UVC | 5 / 10 Gbps | 3 m(无中继) | 工业 USB 相机 | +| MIPI CSI-2 | 1–16 Gbps(Lane × 速率) | PCB 板级,< 30 cm | 移动/嵌入式(Jetson、RPi) | +| GMSL2 | 6 Gbps/lane | 15 m(同轴线) | 车载前装摄像头 | +| GigE Vision | 1 / 2.5 / 10 GbE | 100 m(Cat5e)| 工业机器视觉 | +| Ethernet RTSP/ONVIF | 受网络限制 | 不限(走 IP) | 监控 IPC、IP 相机 | + +--- + +## 4. 带宽预算 + +``` +带宽(bit/s) = 宽 × 高 × 帧率 × bit_depth × 通道数 + +示例:1920×1080 @ 30fps,10bit,RAW Bayer(1通道) + = 1920 × 1080 × 30 × 10 ≈ 622 Mbps(未压缩) + +YUV420(NV12)≈ 622 × (12/10) / 2 ≈ 373 Mbps +MJPEG 压缩后通常降至 50–150 Mbps(依质量因子) +``` + +**多路估算**:N 路相机带宽需求 = 单路 × N,需评估总线(USB Hub、PCIe、Ethernet Switch)的实际瓶颈。 + +--- + +## 5. 驱动与设备枚举 + +### 5.1 USB UVC +```bash +# 枚举 UVC 设备 +lsusb -t +v4l2-ctl --list-devices +v4l2-ctl -d /dev/video0 --list-formats-ext + +# 查看能力 +v4l2-ctl -d /dev/video0 --all +``` + +### 5.2 MIPI CSI-2(以 Jetson 为例) +```bash +# 内核驱动一般以 sensor driver + VI(Video Input)+ ISP 三层叠加 +# 检查媒体控制拓扑 +media-ctl -d /dev/media0 --print-topology + +# 设置格式(例如 RAW10 1920x1080) +media-ctl -d /dev/media0 \ + --set-v4l2 '"IMX477 10-001a":0 [fmt:SRGGB10_1X10/1920x1080]' +v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat=RG10 +``` + +### 5.3 GMSL2(以 MAX9296 Deserializer 为例) +- 需要厂商内核模块(`.ko`)及设备树(DTS)配置 I2C 映射; +- 典型拓扑:`Camera → Serializer(MAX9295) --同轴线--> Deserializer(MAX9296) --> SoC MIPI RX`; +- 调试时先确认 Link Lock(GPIO 状态或 I2C 寄存器读值)。 + +### 5.4 GigE Vision +```bash +# 使用 aravis(开源 GigE Vision 库) +arv-tool-0.8 detect # 自动发现网络上的相机 +arv-tool-0.8 -n features +``` + +--- + +## 6. 取流与缓冲 + +### 6.1 V4L2 缓冲类型 + +| 类型 | 说明 | 适用场景 | +|------|------|----------| +| `V4L2_MEMORY_MMAP` | 内核分配,用户 mmap | 通用,最简单 | +| `V4L2_MEMORY_USERPTR` | 用户分配物理连续内存 | 需要与其他子系统共享 | +| `V4L2_MEMORY_DMABUF` | 零拷贝,fd 共享 | GPU / ISP / 编码器互联 | + +### 6.2 DMABUF 零拷贝路径 +``` +V4L2 DMABUF fd → Export → V4L2 M2M Encoder Import + ↘ GPU Texture Import(OpenGL EGL) +``` + +### 6.3 丢帧策略 + +| 策略 | 说明 | 延迟特性 | +|------|------|----------| +| 环形覆盖(最新帧) | 丢弃最旧帧,保留最新 | 低延迟,不保证每帧 | +| 阻塞等待 | 队列满时生产者阻塞 | 零丢帧,延迟可能升高 | +| 有界丢弃 | 超过阈值丢帧并记日志 | 折中 | + +**推荐**:实时预览/算法流用**环形覆盖**;录制/存档流用**有界丢弃**并告警。 + +--- + +## 7. 关键参数与默认值 + +| 参数 | 推荐值 / 范围 | 说明 | +|------|--------------|------| +| 缓冲队列深度 | 3–6 | 太少丢帧,太多增加延迟 | +| MIPI Lane 数 | 2 / 4 | 依分辨率/帧率选择 | +| I2C 速率 | 100 / 400 kHz | 配置寄存器用 | +| GigE MTU | 8228 bytes(Jumbo Frame) | 降低 CPU 中断,建议开启 | +| USB 传输模式 | Isochronous | UVC 标准,保帧率 | + +--- + +## 8. 性能指标与验收标准 + +| 指标 | 目标值 | 检测方法 | +|------|--------|----------| +| 采集延迟(采样到应用拿帧) | < 2 帧周期(@30fps < 66 ms) | 硬件时间戳比对 | +| 丢帧率 | < 0.1%(稳态) | `v4l2-ctl --stream-count` | +| 驱动 CPU 占用 | < 5%(单路 1080p30) | `top` / `perf` | + +--- + +## 9. 常见问题与排查步骤(Checklist) + +- [ ] `lsusb` / `lspci` 能看到设备?→ 硬件/驱动问题 +- [ ] `/dev/videoX` 节点存在?→ `dmesg | grep video` +- [ ] `v4l2-ctl --list-formats-ext` 格式符合预期? +- [ ] 带宽是否超过物理接口上限?(重新计算) +- [ ] 多路 USB 时是否经过同一个 USB Host 控制器?(`lspci -tv`) +- [ ] MIPI 时钟是否匹配?(`media-ctl` 打印 pad 格式是否一致) +- [ ] GigE 相机 MTU 与交换机配置是否匹配? +- [ ] DMABUF 是否正确 import/export(dma_buf_fd 合法)? +- [ ] 时间戳单调递增?无时间跳变? + +--- + +## 10. 参考资料 + +- [Linux Kernel V4L2 Documentation](https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/v4l2.html) +- [Jetson MIPI CSI-2 Camera Developer Guide](https://developer.nvidia.com/embedded/jetson-partner-supported-cameras) +- [Aravis GigE Vision Library](https://github.com/AravisProject/aravis) +- [GMSL2 Automotive Camera Interface – Maxim/ADI](https://www.analog.com/en/product-category/gmsl-serdes.html) diff --git a/A_camera_pipeline/A2_image_pipeline.md b/A_camera_pipeline/A2_image_pipeline.md new file mode 100644 index 0000000..1a3f35c --- /dev/null +++ b/A_camera_pipeline/A2_image_pipeline.md @@ -0,0 +1,188 @@ +# A2. 图像链路(ISP / 预处理) + +## 1. 这篇文章要解决什么问题? + +将 RAW sensor 输出(或 YUV 直出相机)转换为算法/编码器可用的标准格式,同时完成白平衡、降噪、畸变校正、OSD 等预处理。 + +--- + +## 2. 数据链路图 + +``` +[Camera RAW Output] + (RGGB/BGGR/GRBG …) + │ + ▼ +[ISP(Image Signal Processor)] + ├─ 去马赛克(Demosaic) + ├─ 黑电平校正(BLC) + ├─ 镜头阴影校正(LSC) + ├─ 白平衡(AWB) + ├─ 色彩校正矩阵(CCM) + ├─ 降噪(NR:空域 + 时域) + ├─ 伽马 / Tone Mapping / HDR 合成 + └─ 输出:RGB888 / YUV420 / YUV422 + │ + ▼ +[颜色空间 & 格式转换] + RGB ↔ YUV(NV12 / I420 / NV21) + │ + ▼ +[叠加 & 几何变换] + ├─ OSD(时间戳、通道号、水印) + ├─ 裁剪(Crop)/ 缩放(Scale) + └─ 畸变校正(Undistort / Remap) + │ + ▼ +[编码器 / 算法消费] + H.264/H.265 编码器 or 检测/SLAM 算法 +``` + +--- + +## 3. 关键接口 / 协议 / 格式 + +| 阶段 | 典型格式 | 说明 | +|------|----------|------| +| RAW Bayer | RGGB10 / RGGB12 | 位宽取决于 sensor | +| ISP 输出 | NV12(YUV420 SP) | 编码器首选 | +| ISP 输出 | RGB888 / BGRA | 算法、OpenGL 纹理 | +| 中间缓冲 | DMABUF fd | 零拷贝跨模块传递 | + +--- + +## 4. ISP 处理流水线详解 + +### 4.1 去马赛克(Demosaic) + +- 算法:双线性(快,质量差)→ AHD/VCD(质量好,算力中等)→ 深度学习(最优)。 +- 嵌入式平台通常由硬件 ISP 完成,不需要软件实现。 + +### 4.2 白平衡(AWB) + +| 模式 | 说明 | +|------|------| +| 手动 | 固定 R/G/B 增益,适合受控环境 | +| 自动(灰世界假设) | 运行时调整,适合变光环境 | +| 一次性 AWB | 拍标准白板后锁定 | + +### 4.3 HDR 合成 + +- 多帧曝光合并(Short + Long Exposure Frames); +- 需要保证帧间运动补偿,否则出现鬼影; +- 输出通常为 16bit,需 Tone Mapping 压缩到 8bit 显示。 + +### 4.4 降噪 + +| 类型 | 方法 | 说明 | +|------|------|------| +| 空域 NR | Bilateral / NLM | 单帧处理,保边去噪 | +| 时域 NR | IIR / 3DNR | 跨帧累积,低光效果好 | +| 硬件 NR | ISP 内置 | 推荐优先使用 | + +--- + +## 5. 颜色空间与格式转换 + +### 5.1 RGB → YUV(BT.601) +``` +Y = 0.299 R + 0.587 G + 0.114 B +Cb = -0.169 R - 0.331 G + 0.500 B + 128 +Cr = 0.500 R - 0.419 G - 0.081 B + 128 +``` + +### 5.2 常用 YUV 格式 + +| 格式 | 别名 | 内存布局 | 适用 | +|------|------|----------|------| +| NV12 | YUV420 SP | YYYY…UV… | H.264/H.265 编码器 | +| I420 | YUV420P | YYY…U…V… | FFmpeg / x264 默认 | +| NV21 | YUV420 SP | YYYY…VU… | Android Camera2 | +| UYVY | YUV422 | UYVY交错 | 采集卡直出 | + +### 5.3 快速转换(libyuv / OpenCV / NPP) +```cpp +// libyuv RGB24 → NV12 +libyuv::RGB24ToNV12(src_rgb, src_stride, + dst_y, dst_stride_y, + dst_uv, dst_stride_uv, + width, height); + +// OpenCV BGR → YUV I420 +cv::cvtColor(bgr_mat, yuv_mat, cv::COLOR_BGR2YUV_I420); +``` + +--- + +## 6. 叠加与几何变换 + +### 6.1 OSD(On-Screen Display) +``` +推荐在编码前、ISP 输出后叠加,避免对 RAW 数据造成污染。 +内容:时间戳(精确到 ms)、通道编号、IP 地址、告警图标。 +``` + +### 6.2 畸变校正(Undistort) + +使用 OpenCV `cv::remap` 预计算 mapX / mapY: +```cpp +// 标定后获得 cameraMatrix, distCoeffs +cv::Mat map1, map2; +cv::initUndistortRectifyMap( + cameraMatrix, distCoeffs, + cv::Mat(), newCameraMatrix, + imageSize, CV_16SC2, map1, map2); + +// 逐帧执行(零拷贝 + GPU 加速) +cv::remap(src, dst, map1, map2, cv::INTER_LINEAR); +``` + +**注意**:仅算法输入流需要校正,录制/监控流可不校正以减少 CPU/GPU 负载。 + +### 6.3 缩放(Scale) +- 硬件缩放(VI/ISP Scaler)优先,减少 CPU 占用; +- 编码器内部缩放(如 NVENC 的 `--resize`)次选; +- 软件 `libyuv::ScaleUVPlane` 最后考虑。 + +--- + +## 7. 关键参数与默认值 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| ISP 输出格式 | NV12 | 编码器最佳输入 | +| 畸变校正 alpha | 0(保留所有有效像素) | 也可用 1 保留全部黑边 | +| OSD 字体大小 | 24–36 px | 视分辨率而定 | +| libyuv 缩放算法 | `kFilterBilinear` | 质量与速度均衡 | + +--- + +## 8. 性能指标与验收标准 + +| 指标 | 目标值 | 检测方法 | +|------|--------|----------| +| ISP 处理延迟 | < 5 ms(@1080p,硬件 ISP) | 硬件时间戳 | +| 颜色转换延迟 | < 1 ms(GPU / libyuv) | `perf stat` | +| CPU 占用(软件 ISP) | < 15%(单路 1080p30) | `htop` | +| PSNR(畸变校正后) | ≥ 38 dB | 对比参考图 | + +--- + +## 9. 常见问题与排查步骤(Checklist) + +- [ ] 图像色彩偏红/绿 → 检查 AWB 参数或 CCM 矩阵 +- [ ] 马赛克 / 花屏(RAW 场景)→ 确认 Bayer 模式(RGGB/BGGR) +- [ ] 图像黑边或裁剪异常 → 检查畸变校正 alpha 参数 +- [ ] OSD 时间戳不更新 → 检查 OSD 渲染线程是否死锁 +- [ ] HDR 合成出现鬼影 → 检查帧间对齐与运动补偿 +- [ ] GPU 纹理颜色错误 → 确认 NV12 stride 与宽度对齐(通常 64/128 字节对齐) +- [ ] 色彩空间转换精度问题 → 使用 BT.601 / BT.709 正确系数 + +--- + +## 10. 参考资料 + +- [libyuv 库](https://chromium.googlesource.com/libyuv/libyuv) +- [OpenCV 相机标定](https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html) +- [Jetson ISP / V4L2 ISP API](https://docs.nvidia.com/jetson/archives/r35.2.1/DeveloperGuide/text/SD/CameraDevelopment.html) +- [NVIDIA VPI(Vision Programming Interface)](https://docs.nvidia.com/vpi/) diff --git a/A_camera_pipeline/A3_encoding.md b/A_camera_pipeline/A3_encoding.md new file mode 100644 index 0000000..436b167 --- /dev/null +++ b/A_camera_pipeline/A3_encoding.md @@ -0,0 +1,226 @@ +# A3. 编码(Encode) + +## 1. 这篇文章要解决什么问题? + +将原始 YUV/RGB 帧压缩为 H.264 / H.265 码流,以降低存储/传输带宽,同时满足延迟、质量和 CPU/GPU 资源约束。 + +--- + +## 2. 数据链路图 + +``` +[原始帧:NV12 / I420 / RGB] + │ + ▼ +[编码器选择] + ┌──────────────────────────────────────────────┐ + │ 硬编(推荐) │ 软编(备选) │ + │ NVENC(NVIDIA) │ x264 / x265(libav) │ + │ V4L2 M2M(Jetson) │ OpenH264(Cisco) │ + │ MediaCodec(Android)│ libopenh264 │ + │ Intel QuickSync │ │ + └──────────────────────────────────────────────┘ + │ + ▼ +[码流输出] + AnnexB(裸流)/ AVCC(MP4 风格) + │ + ▼ +[封装(Mux)] + TS / FLV / MP4 / RTP Payload + │ + ▼ +[传输 / 存储] +``` + +--- + +## 3. 编码标准对比 + +| 特性 | H.264 (AVC) | H.265 (HEVC) | AV1 | +|------|-------------|--------------|-----| +| 压缩率 | 基准 | 同质量 ~40% 节省 | 同质量 ~50% 节省 | +| 硬件支持 | 极广 | 广(部分旧设备缺失) | 新一代(2022+) | +| 解码延迟 | 低 | 中 | 较高 | +| 专利费 | 有(H.264 专利池) | 有 | 免费 | +| 推荐用途 | 兼容性优先 | 带宽受限场景 | 未来趋势 | + +--- + +## 4. 关键编码参数 + +### 4.1 GOP(Group of Pictures) + +| 参数 | 说明 | 推荐值 | +|------|------|--------| +| `keyint` (IDR 间隔) | 关键帧间隔(帧数) | 30–60(1–2 秒@30fps) | +| `min-keyint` | 最小 IDR 间隔 | 同 `keyint`(固定场景)| +| `bframes` | B 帧数量 | 0(低延迟),2(高压缩) | +| `refs` | 参考帧数 | 1–3 | + +**低延迟模式**:`keyint=1`(每帧都是 I 帧)或设置 `tune=zerolatency`(x264/x265)。 + +### 4.2 码率控制 + +| 模式 | 说明 | 适用 | +|------|------|------| +| CBR(固定码率) | 输出码率稳定,带宽可预测 | 实时传输(RTP/RTMP) | +| VBR(可变码率) | 质量更优,码率波动 | 本地录制 | +| CQP(固定量化) | 固定质量,码率不可控 | 离线转码 | +| CRF(恒定质量因子) | x264/x265 专有,质量稳定 | 录制存档 | + +### 4.3 Profile / Level + +| Profile | B帧 | CABAC | 常见设备 | +|---------|-----|-------|---------| +| Baseline | ✗ | ✗ | 旧移动设备、WebRTC | +| Main | ✓ | ✓ | 通用 | +| High | ✓ | ✓ | 高清录制 | + +**Level** 决定最大分辨率×帧率(如 Level 4.1 支持 1080p60)。 + +--- + +## 5. 平台硬编代码示例 + +### 5.1 FFmpeg + NVENC +```bash +ffmpeg -f v4l2 -input_format nv12 -video_size 1920x1080 -framerate 30 \ + -i /dev/video0 \ + -c:v h264_nvenc \ + -preset llhq \ # low latency high quality + -rc cbr \ + -b:v 4M -maxrate 4M -bufsize 4M \ + -g 30 \ # keyint=30 + -bf 0 \ # 无 B 帧 + -an \ + -f rtsp rtsp://localhost:8554/camera0 +``` + +### 5.2 FFmpeg + V4L2 M2M(Jetson) +```bash +ffmpeg -f v4l2 -input_format nv12 -video_size 1920x1080 -framerate 30 \ + -i /dev/video0 \ + -c:v h264_v4l2m2m \ + -b:v 4M \ + -g 30 \ + -f rtp rtp://239.0.0.1:5004 +``` + +### 5.3 GStreamer + NVENC(Jetson nvv4l2h264enc) +```bash +gst-launch-1.0 v4l2src device=/dev/video0 ! \ + 'video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1' ! \ + nvv4l2h264enc bitrate=4000000 iframeinterval=30 \ + preset-level=1 control-rate=1 ! \ + h264parse ! \ + rtph264pay config-interval=1 ! \ + udpsink host=192.168.1.100 port=5004 +``` + +### 5.4 x264 软编(低延迟配置) +```bash +ffmpeg -f v4l2 -i /dev/video0 \ + -c:v libx264 \ + -preset ultrafast \ + -tune zerolatency \ + -x264opts "keyint=30:min-keyint=30:no-scenecut:bframes=0" \ + -b:v 2M \ + -f flv rtmp://localhost:1935/live/camera0 +``` + +--- + +## 6. AnnexB vs AVCC 格式 + +| 格式 | Start Code | 用途 | +|------|-----------|------| +| AnnexB | `00 00 00 01` 前缀 | 裸流(TS / RTP / 文件流) | +| AVCC | 4 字节 NALU 长度前缀 | MP4 / MOV / ISO BMFF | + +**切换方法(FFmpeg)**: +```bash +# AVCC → AnnexB +ffmpeg -i input.mp4 -c copy -bsf:v h264_mp4toannexb output.ts + +# AnnexB → AVCC +ffmpeg -i input.ts -c copy -bsf:v h264_annexb_to_mp4 output.mp4 +``` + +--- + +## 7. 端到端延迟分解 + +``` +[采集时刻] + │ 曝光 + 读出(~1/fps) + ▼ +[ISP 处理](~3–10 ms,硬件 ISP) + │ + ▼ +[编码排队](取决于 B 帧和 lookahead) + │ CBR 低延迟:< 1 frame + ▼ +[编码处理](NVENC ~2–5 ms) + │ + ▼ +[封包/Mux](< 1 ms) + │ + ▼ +[网络传输](局域网 < 1 ms,广域网变化大) + │ + ▼ +[解码](硬解 ~5 ms,软解 ~15 ms) + │ + ▼ +[渲染显示](~16 ms @60Hz) + +典型 LAN 端到端(硬编+硬解):30–80 ms +``` + +--- + +## 8. 关键参数与默认值 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| 编码格式 | H.264 | 兼容性最广 | +| Profile | High | 通用场景 | +| GOP / keyint | 30(1 秒@30fps) | 随机接入与压缩率平衡 | +| B 帧 | 0(低延迟),2(存档) | 低延迟必须为 0 | +| 码率(1080p30) | 4–8 Mbps CBR | 实时传输 | +| 编码器 | NVENC / V4L2 M2M | 优先硬编 | +| 像素格式 | NV12 | 硬编首选 | + +--- + +## 9. 性能指标与验收标准 + +| 指标 | 目标值 | 检测方法 | +|------|--------|----------| +| 编码延迟 | < 2 帧(@30fps < 66 ms) | 硬件时间戳 | +| CPU 占用(硬编) | < 5%(单路 1080p30) | `top` | +| GPU 占用(NVENC) | < 30% | `nvidia-smi` / `tegrastats` | +| VMAF / PSNR | VMAF > 80 | `ffmpeg -filter_complex vmaf` | +| 码率稳定性(CBR) | 偏差 < ±10% | 抓包统计 | + +--- + +## 10. 常见问题与排查步骤(Checklist) + +- [ ] 输出花屏/绿屏 → 确认输入像素格式(NV12 stride 是否 64 对齐) +- [ ] 首帧延迟高 → 检查 IDR 帧是否立即生成(`force_key_frames=expr:gte(t,0)`) +- [ ] 码率严重不稳 → 检查 buffer size 配置(CBR 要求 `bufsize = 2×bitrate`) +- [ ] 编码器报错 `CUDA error` → GPU 资源不足或驱动版本不匹配 +- [ ] V4L2 M2M 设备找不到 → `ls /dev/video*` 确认 M2M 节点,通常 `/dev/video1` 起 +- [ ] x264 CPU 100% → 改 `preset=ultrafast` 或切换硬编 +- [ ] SPS/PPS 丢失导致解码器无法解析 → 设置 `config-interval=-1`(GStreamer)或 `-vbsf dump_extra` + +--- + +## 11. 参考资料 + +- [FFmpeg H.264 编码指南](https://trac.ffmpeg.org/wiki/Encode/H.264) +- [NVENC 编程指南](https://docs.nvidia.com/video-technologies/video-codec-sdk/nvenc-video-encoder-api-prog-guide/) +- [GStreamer nvv4l2h264enc 插件文档](https://docs.nvidia.com/jetson/archives/r35.2.1/DeveloperGuide/text/SD/Multimedia/AcceleratedGstreamer.html) +- [x264 参数参考](https://code.videolan.org/videolan/x264) diff --git a/A_camera_pipeline/A4_transport.md b/A_camera_pipeline/A4_transport.md new file mode 100644 index 0000000..26fdbda --- /dev/null +++ b/A_camera_pipeline/A4_transport.md @@ -0,0 +1,277 @@ +# A4. 传输协议与工作流 + +## 1. 这篇文章要解决什么问题? + +梳理从编码器输出到消费端的四种主流传输路径(RTP/RTSP、WebRTC、HTTP-FLV/HLS、HTTP 上传),明确各路径的工作流向、适用场景与关键注意事项。 + +--- + +## 2. 传输协议总览 + +``` +[Encoder 码流输出] + │ + ├──► RTP/RTSP ──────► 局域网监控 / 机器人内网预览 + │ + ├──► WebRTC ─────────► 远程遥操作 / 实时视频对讲 + │ + ├──► HTTP-FLV / HLS ─► 大规模分发 / 可回放 + │ + └──► HTTP Upload ────► 事件上报 / 离线取证 / 数据回传 +``` + +--- + +## A4.1 RTP / RTSP + +### 工作流向 +``` +Camera/Encoder + │ 打包 RTP(H.264/H.265 RTP Payload) + ▼ +RTSP Server(会话控制:DESCRIBE/SETUP/PLAY/TEARDOWN) + │ UDP(默认)或 TCP(穿防火墙) + ▼ +Client(VLC / GStreamer / ffplay / 算法订阅者) + │ Jitter Buffer → 解封包 → 解码 + ▼ +消费(显示 / 算法) +``` + +### 常用实现 + +| 工具 | 角色 | 命令示例 | +|------|------|----------| +| GStreamer `rtsp-server` | 服务端 | 见下方示例 | +| MediaMTX(旧称 rtsp-simple-server) | 服务端 | 配置文件驱动 | +| FFmpeg | 推流客户端 | `ffmpeg ... -f rtsp rtsp://host/path` | +| VLC | 播放客户端 | `vlc rtsp://host:8554/camera0` | +| GStreamer `rtspsrc` | 拉流客户端 | 见下方示例 | + +```bash +# GStreamer RTSP 服务端(推荐 test-launch 快速验证) +./test-launch "( v4l2src device=/dev/video0 ! \ + video/x-raw,width=1920,height=1080,framerate=30/1 ! \ + nvv4l2h264enc bitrate=4000000 ! \ + rtph264pay name=pay0 pt=96 )" + +# 客户端拉流 +gst-launch-1.0 rtspsrc location=rtsp://192.168.1.10:8554/test latency=100 ! \ + rtph264depay ! h264parse ! avdec_h264 ! autovideosink +``` + +### 关注点 + +| 问题 | 说明 | +|------|------| +| UDP 丢包 | 使用 `rtspsrc latency=200`;严重时切 TCP(`protocols=tcp`) | +| Jitter Buffer | 值太小丢帧,太大增延迟;典型 100–200 ms | +| NAT 穿透差 | 内网可用,跨网需 TURN 或改 WebRTC | +| 客户端兼容性 | 部分浏览器不支持 RTSP,需转 WebRTC/HLS | + +--- + +## A4.2 WebRTC + +### 工作流向 +``` +Encoder(本地帧) + │ 输入帧 / 编码帧 + ▼ +WebRTC Stack + ├─ SRTP(媒体加密) + ├─ SCTP / DTLS(数据通道) + ├─ GCC / REMB / TWCC(拥塞控制) + └─ NACK / FEC(丢包恢复) + │ + ▼ ICE(STUN/TURN 协商) + │ + ▼ +Browser / App / Peer(解码渲染) +``` + +### 信令流程 +``` +Peer A Signaling Server Peer B + │── createOffer ─────────────► │ │ + │ │──── offer ─────────► │ + │ │◄─── answer ──────── │ + │◄─ answer ──────────────────── │ │ + │─────────────────────── ICE Candidates ────────────► │ + │◄──────────────────────────────────────── ICE ────── │ + │═══════════════════ SRTP/DTLS Media ════════════════► │ +``` + +### 关键库 / 框架 + +| 库 | 语言 | 特点 | +|----|------|------| +| [Pion WebRTC](https://github.com/pion/webrtc) | Go | 轻量,适合服务端 | +| [aiortc](https://github.com/aiortc/aiortc) | Python | 快速原型 | +| [GStreamer webrtcbin](https://gstreamer.freedesktop.org/documentation/webrtc/index.html) | C/GStreamer | 与 Pipeline 集成 | +| [libdatachannel](https://github.com/paullouisageneau/libdatachannel) | C++ | 嵌入式友好 | + +```bash +# GStreamer WebRTC 推流示例(需要 signaling server) +gst-launch-1.0 v4l2src ! \ + video/x-raw,width=1280,height=720,framerate=30/1 ! \ + videoconvert ! vp8enc ! rtpvp8pay ! \ + webrtcbin name=sendonly bundle-policy=max-bundle \ + stun-server=stun://stun.l.google.com:19302 +``` + +### 关注点 + +| 问题 | 说明 | +|------|------| +| 信令延迟 | WebSocket 信令尽量部署同区域 | +| STUN/TURN | 内网直连走 STUN,跨 NAT 必须 TURN;推荐 Coturn | +| 码率自适应 | GCC 算法在弱网下会主动降码率(可接受抖动) | +| 最低延迟 | 理想局域网 < 50 ms;广域网典型 100–300 ms | +| 浏览器兼容 | Chrome/Firefox/Safari 均支持 VP8/H.264 | + +--- + +## A4.3 HTTP-FLV / HLS / DASH + +### 工作流向 +``` +Encoder + │ H.264/H.265 码流 + ▼ +Muxer(FLV / TS segment / MP4 fragment) + │ RTMP 推流 或 直接写文件 + ▼ +Media Server(SRS / Nginx-RTMP / MediaMTX) + │ │ + ▼ ▼ +HTTP-FLV HLS(.m3u8 + .ts 切片) +(< 3s 延迟) (传统 3–10s;Low-Latency HLS < 1s) + │ │ + ▼ ▼ +Web Player 各类播放器(iOS / Android / PC) +``` + +### SRS 配置示例 +```nginx +# srs.conf 片段 +vhost __defaultVhost__ { + hls { + enabled on; + hls_fragment 1; # 切片时长(秒)—— LL-HLS + hls_window 5; # 窗口(保留切片数) + } + http_remux { + enabled on; + mount [vhost]/[app]/[stream].flv; + } +} +``` + +### 关注点 + +| 协议 | 典型延迟 | 主要缺点 | +|------|----------|----------| +| HTTP-FLV | 1–3 s | 依赖 Flash 时代协议(flv.js 可在浏览器播放) | +| HLS(传统) | 3–10 s | 切片时延高 | +| LL-HLS | < 1–2 s | 需服务端和客户端同时支持 | +| DASH | 2–8 s | 标准化好,CDN 支持广 | + +--- + +## A4.4 HTTP 上传 / REST 数据回传 + +### 工作流向 +``` +设备端编码(关键帧 / 事件片段) + │ HTTP POST(multipart/form-data 或 chunked) + ▼ +服务端(鉴权 → 存储 → 触发转码/审核) + │ + ▼ +下游消费(数据库 / 对象存储 / 流水线分析) +``` + +### 关键设计点 + +| 问题 | 推荐方案 | +|------|---------| +| 断点续传 | HTTP Range 请求 / TUS 协议 | +| 文件切片 | 每 10–30 s 一个 MP4 片段,避免单文件过大 | +| 鉴权 | JWT / API Key,HTTPS 强制 | +| 限流 | 设备端指数退避重试 | +| 存储成本 | 按事件上传(非全量),本地存储 + 周期同步 | + +```python +# 简单 HTTP 上传示例(Python requests) +import requests, pathlib + +def upload_clip(filepath: str, server_url: str, token: str): + with open(filepath, "rb") as f: + resp = requests.post( + f"{server_url}/api/v1/clips", + headers={"Authorization": f"Bearer {token}"}, + files={"file": (pathlib.Path(filepath).name, f, "video/mp4")}, + timeout=30, + ) + resp.raise_for_status() + return resp.json() +``` + +--- + +## 5. 协议选型矩阵 + +| 场景 | 推荐协议 | 次选 | +|------|---------|------| +| 局域网机器人实时预览 | RTP/RTSP | WebRTC | +| 远程遥操作(< 200ms) | WebRTC | — | +| 大规模直播分发 | HLS / HTTP-FLV | DASH | +| 离线事件取证/录制 | HTTP Upload | — | +| 多路录制回放 | HLS / MP4 | HTTP Upload | + +--- + +## 6. 关键参数与默认值 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| RTSP UDP 端口 | 554 / 8554 | 8554 无需 root | +| RTP jitter buffer | 100–200 ms | 局域网取小值 | +| HLS 切片时长 | 1–2 s(LL-HLS) | 传统用 4–6 s | +| WebRTC STUN | `stun.l.google.com:19302` | 默认公共 STUN | +| HTTP 超时 | 30 s | 上传大文件适当增加 | + +--- + +## 7. 性能指标与验收标准 + +| 指标 | 目标值 | 检测方法 | +|------|--------|----------| +| RTSP 端到端延迟(LAN) | < 300 ms | VLC 时间戳 | +| WebRTC 端到端延迟(LAN) | < 100 ms | `getStats()` | +| HLS 延迟(LL-HLS) | < 2 s | 播放器时间戳 | +| HTTP 上传成功率 | > 99.9% | 服务端日志 | + +--- + +## 8. 常见问题与排查步骤(Checklist) + +- [ ] RTSP 连不上 → 确认端口开放,`telnet host 8554` +- [ ] RTP 花屏 → 查 jitter buffer 是否太小;检查网络丢包 `ping -f` +- [ ] WebRTC 无法建立连接 → 检查 STUN/TURN;确认信令 WebSocket 正常 +- [ ] HLS 起播慢 → 增加 Preload hint(LL-HLS);检查 CDN 源站延迟 +- [ ] HTTP 上传超时 → 减小切片大小;添加断点续传逻辑 +- [ ] FLV 播放卡顿 → 检查服务端推流码率是否稳定;flv.js Worker 模式 + +--- + +## 9. 参考资料 + +- [RFC 7826 – RTSP 2.0](https://tools.ietf.org/html/rfc7826) +- [RFC 3550 – RTP](https://tools.ietf.org/html/rfc3550) +- [WebRTC 规范(W3C)](https://www.w3.org/TR/webrtc/) +- [SRS(Simple Real-time Server)](https://github.com/ossrs/srs) +- [MediaMTX(rtsp-simple-server)](https://github.com/bluenviron/mediamtx) +- [Apple HTTP Live Streaming(HLS)](https://developer.apple.com/documentation/http_live_streaming) +- [TUS 断点续传协议](https://tus.io/) diff --git a/A_camera_pipeline/A5_clock_sync.md b/A_camera_pipeline/A5_clock_sync.md new file mode 100644 index 0000000..0ed276e --- /dev/null +++ b/A_camera_pipeline/A5_clock_sync.md @@ -0,0 +1,231 @@ +# A5. 时钟同步与触发(GPIO / PTP) + +## 1. 这篇文章要解决什么问题? + +多传感器系统(相机、雷达、激光雷达等)需要统一时间基准,以实现帧对齐、数据融合和因果性保证。本文梳理 GPIO/TTL 硬触发和 PTP/IEEE 1588 软同步两种核心方案。 + +--- + +## 2. 数据链路图 + +### 2.1 GPIO 硬触发(多相机同步曝光) +``` +[主时钟源 / MCU / FPGA / GNSS PPS] + │ + │ GPIO / TTL 方波脉冲(上升沿触发) + │ + ├──► Camera 0 TRIGGER_IN → 同步曝光开始 + ├──► Camera 1 TRIGGER_IN → 同步曝光开始 + ├──► Camera 2 TRIGGER_IN → 同步曝光开始 + └──► LiDAR SYNC_IN → 旋转起始对齐 + ↓ + [所有传感器帧在同一时刻开始采样] +``` + +### 2.2 PTP/IEEE 1588(以太网时间同步) +``` +[GNSS 时间源 / GPS PPS + NMEA] ──► PTP Grandmaster + │ PTP 报文(Sync/Follow_Up/Delay_Req/Resp) + ─────┼───────────────────────── + 以太网交换机(支持 PTP 透传或 Boundary Clock) + ─────┼───────────────────────── + ┌───────────┼────────────────┐ + │ │ │ + [Camera A] [LiDAR B] [Radar C] + PTP Slave PTP Slave PTP Slave + │ │ │ + hw_timestamp hw_timestamp hw_timestamp + └───────────┴────────────────┘ + 对齐精度 < 1 µs(硬件 PTP) +``` + +--- + +## 3. 方案对比 + +| 方案 | 同步精度 | 适用距离 | 硬件要求 | 典型场景 | +|------|----------|----------|----------|---------| +| GPIO/TTL 硬触发 | < 1 µs | 短距离(线缆限制) | 触发引脚 | 多相机同步曝光 | +| GNSS PPS + GPIO | < 100 ns | — | GNSS 模块 + GPIO | 最高精度硬触发 | +| PTP/IEEE 1588 | < 1 µs(硬件);~10 µs(软件) | 网络范围 | PTP 网卡 / 交换机 | 以太网传感器集群 | +| GPS NTP | ~1–10 ms | — | GNSS 模块 | 粗粒度同步 | +| ROS Time Sync | ~1–5 ms | — | 无特殊硬件 | 软同步(精度要求低) | + +--- + +## 4. GPIO / TTL 硬触发 + +### 4.1 触发信号规格 + +| 参数 | 典型值 | +|------|--------| +| 信号电平 | 3.3 V 或 5 V TTL | +| 脉冲宽度 | 1–10 ms(相机要求不同) | +| 触发沿 | 上升沿(多数相机) | +| 最大频率 | ≤ 帧率(避免丢触发) | +| 信号阻抗 | 50 Ω 匹配(长线缆时) | + +### 4.2 典型连接(以 FLIR 相机为例) +``` +MCU GPIO_OUT ──── 限流电阻(100Ω) ──── Camera OPTO_IN + │ + 光耦隔离(±电气隔离) +``` + +### 4.3 Linux 侧读取触发时间戳 +```c +// V4L2 硬件时间戳(需驱动支持 V4L2_BUF_FLAG_TIMESTAMP_SOE) +struct v4l2_buffer buf; +buf.flags & V4L2_BUF_FLAG_TIMESTAMP_SOE; // Start of Exposure +struct timeval ts = buf.timestamp; // 硬件时间戳 +``` + +### 4.4 MCU 触发脉冲生成(以 STM32 为例) +```c +// TIM2 生成 30 Hz 触发脉冲 +void trigger_init(void) { + // 配置 Timer 输出比较,周期 33.3 ms,脉宽 5 ms + htim2.Init.Period = 33333 - 1; // 1 µs tick + HAL_TIM_OC_Start(&htim2, TIM_CHANNEL_1); +} +``` + +--- + +## 5. PTP / IEEE 1588 + +### 5.1 关键术语 + +| 术语 | 说明 | +|------|------| +| Grandmaster | 最优时钟源(通常连 GNSS) | +| Boundary Clock | 交换机上的时钟,隔离 PTP 域 | +| Transparent Clock | 交换机透传,修正驻留时间 | +| Hardware Timestamp | 网卡/PHY 打时间戳,精度 < 1 µs | +| Software Timestamp | OS 协议栈打时间戳,精度 ~10 µs | + +### 5.2 Linux PTP 配置(linuxptp) +```bash +# 安装 +sudo apt install linuxptp + +# 查看网卡 PTP 能力 +ethtool -T eth0 + +# 启动 PTP4L(硬件时间戳) +sudo ptp4l -i eth0 -H -m # -H: 硬件模式;-m: 打印到终端 + +# 同步系统时钟 +sudo phc2sys -s eth0 -c CLOCK_REALTIME -w -m + +# 查看同步状态 +sudo pmc -u -b 0 'GET TIME_STATUS_NP' +``` + +### 5.3 PTP 报文交换(E2E 模式) +``` +Master Slave + │── Sync (t1) ──────────────────► │ 记录 t2 + │── Follow_Up(t1) ───────────────► │ + │◄── Delay_Req (t3) ────────────── │ + │── Delay_Resp(t4) ──────────────► │ + +偏移 = [(t2-t1) - (t4-t3)] / 2 +传输延迟 = [(t2-t1) + (t4-t3)] / 2 +``` + +### 5.4 验证同步精度 +```bash +# 使用 ts2phc 工具(GNSS PPS 对准) +sudo ts2phc -f /etc/linuxptp/ts2phc-TC.cfg -s nmea -m + +# 查看偏移统计(ptp4l 输出) +# master offset -123 s2 freq -12345 path delay 456 +# 目标:|offset| < 1000 ns(硬件 PTP),< 10000 ns(软件 PTP) +``` + +--- + +## 6. 时间戳策略 + +### 6.1 时间戳类型对比 + +| 类型 | 精度 | 获取方式 | +|------|------|----------| +| 硬件时间戳(曝光起始) | < 1 µs | V4L2 `V4L2_BUF_FLAG_TIMESTAMP_SOE` | +| 硬件时间戳(DMA 完成) | < 10 µs | V4L2 `V4L2_BUF_FLAG_TIMESTAMP_EOF` | +| 软件时间戳(驱动入队) | ~100 µs | `clock_gettime(CLOCK_MONOTONIC)` | +| 软件时间戳(应用出队) | ~1 ms | 应用层 `gettimeofday` | + +**推荐**:优先使用硬件曝光起始时间戳(SOE),配合 PTP 时钟转换为统一时间域。 + +### 6.2 元数据字段规范 + +```json +{ + "frame_id": 12345, + "sensor_id": "camera_front_center", + "sensor_time_ns": 1700000000123456789, + "host_time_ns": 1700000000124000000, + "ptp_time_ns": 1700000000123500000, + "exposure_us": 8000, + "trigger_id": 12344 +} +``` + +--- + +## 7. 多传感器时间对齐流程 + +``` +1. 确定时间基准(Grandmaster / GNSS PPS) +2. 所有以太网传感器接入 PTP 域,配置 PTP Slave +3. GPIO 触发设备从 GNSS PPS 或 MCU(已对齐 PTP)产生脉冲 +4. 采集时记录 sensor_time + ptp_time(转换偏移) +5. 上层 ROS/中间件按 ptp_time 排序对齐 +6. 定期验证:记录 offset 分布,告警阈值 > 500 µs +``` + +--- + +## 8. 关键参数与默认值 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| PTP 同步间隔 | 125 ms(-3) | `logSyncInterval` | +| PTP 路径延迟测量间隔 | 1 s(0) | `logMinDelayReqInterval` | +| 触发脉冲宽度 | 5 ms | 多数相机要求 > 1 ms | +| 时间戳类型 | SOE(Start of Exposure) | 最接近真实采样时刻 | +| PTP 偏移告警阈值 | 500 µs | 超过告警并记录 | + +--- + +## 9. 性能指标与验收标准 + +| 指标 | 目标值 | 检测方法 | +|------|--------|----------| +| PTP 同步偏移(硬件) | < 1 µs | `ptp4l` offset 日志 | +| GPIO 触发抖动 | < 1 µs | 示波器测量 | +| 多相机帧时间差 | < 1/2 曝光时间 | 闪光灯测试法 | +| 传感器帧时间戳单调性 | 100% 单调 | 日志分析 | + +--- + +## 10. 常见问题与排查步骤(Checklist) + +- [ ] PTP offset 持续漂移 → 检查交换机是否支持 PTP 透传(`-E2E` 模式) +- [ ] 软件时间戳抖动大 → 改用硬件时间戳;检查系统负载 +- [ ] 多相机帧不同步 → 确认 GPIO 触发线连接;相机固件是否开启外触发模式 +- [ ] V4L2 时间戳为 0 → 内核驱动未支持 `V4L2_BUF_FLAG_TIMESTAMP_SOE` +- [ ] ptp4l 找不到 Grandmaster → 检查 PTP vlan 配置;确认交换机转发 PTP 多播 +- [ ] 时间戳回跳(非单调) → NTP 与 PTP 混用冲突,禁用 NTP 或使用 chrony + PTP + +--- + +## 11. 参考资料 + +- [linuxptp 项目](https://linuxptp.sourceforge.net/) +- [IEEE 1588-2019 标准](https://standards.ieee.org/ieee/1588/6825/) +- [V4L2 时间戳文档](https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/buffer.html#c.v4l2_buffer.timestamp) +- [FLIR 相机触发模式手册](https://www.flir.com/support-center/iis/machine-vision/application-note/configuring-synchronized-capture-with-multiple-cameras/) +- [ROS 时间同步最佳实践](https://wiki.ros.org/hector_slam/Tutorials/SettingUpForYourRobot) diff --git a/A_camera_pipeline/A6_decode_render.md b/A_camera_pipeline/A6_decode_render.md new file mode 100644 index 0000000..d301ee9 --- /dev/null +++ b/A_camera_pipeline/A6_decode_render.md @@ -0,0 +1,241 @@ +# A6. 解码、渲染与下游算法 + +## 1. 这篇文章要解决什么问题? + +将编码码流(H.264/H.265)解码为图像帧,并高效分发给显示(OpenGL/Vulkan/Web 播放器)和算法(检测/SLAM)两类消费者,同时保证零拷贝和低延迟。 + +--- + +## 2. 数据链路图 + +``` +[网络/存储 码流] + │ RTP / RTSP / HLS / 文件 + ▼ +[解复用(Demux)] + TS / FLV / MP4 → NAL Units + │ + ▼ +[解码器(Decoder)] + ┌────────────────────────────────┐ + │ 硬解:NVDEC / V4L2 M2M / VAAPI │ + │ 软解:FFmpeg avcodec / OpenH264 │ + └────────────────────────────────┘ + │ 解码帧:NV12 / YUV420 / RGBA + │ (DMABUF / CUDA 内存 / 共享内存) + │ + ├──────────────────────────────────────┐ + ▼ ▼ +[显示(Render)] [算法(Vision)] + OpenGL/EGL(纹理导入) 检测/跟踪/SLAM + GStreamer videosink 尽量零拷贝 + Web 播放器(Video Element) 定义清晰帧格式+元数据 +``` + +--- + +## 3. 解码器选择 + +### 3.1 平台硬件解码 + +| 平台 | API | 格式 | +|------|-----|------| +| NVIDIA(x86 / Jetson) | NVDEC / CUVID | H.264 / H.265 / AV1(40系+) | +| Intel | VAAPI / QSV | H.264 / H.265 / AV1 | +| Jetson(ARM) | V4L2 M2M / NVJPEG | H.264 / H.265 | +| Android | MediaCodec | H.264 / H.265 / VP9 | +| RK(Rockchip) | MPP(媒体处理平台) | H.264 / H.265 | + +### 3.2 FFmpeg 硬解命令 +```bash +# NVDEC(CUVID) +ffplay -vcodec h264_cuvid -rtsp_transport tcp rtsp://192.168.1.10:8554/test + +# VAAPI(Intel) +ffplay -hwaccel vaapi -hwaccel_output_format vaapi \ + -i rtsp://192.168.1.10:8554/test + +# V4L2 M2M(Jetson / RPi) +ffplay -vcodec h264_v4l2m2m -i rtsp://192.168.1.10:8554/test +``` + +### 3.3 GStreamer 硬解管道 +```bash +# Jetson nvv4l2decoder +gst-launch-1.0 rtspsrc location=rtsp://host:8554/test latency=100 ! \ + rtph264depay ! h264parse ! nvv4l2decoder ! \ + nvvidconv ! video/x-raw,format=BGRx ! \ + videoconvert ! video/x-raw,format=BGR ! \ + appsink name=sink + +# NVDEC(使用 nvh264dec) +gst-launch-1.0 rtspsrc location=rtsp://host:8554/test ! \ + rtph264depay ! h264parse ! nvh264dec ! \ + glimagesink +``` + +--- + +## 4. 缓冲模型与零拷贝 + +### 4.1 零拷贝路径(推荐) +``` +解码器 DMABUF fd + │ + ├──► EGL Image(OpenGL 纹理)── 显示(无拷贝) + │ + └──► CUDA cuImportExternalMemory── GPU 算法(无拷贝) +``` + +### 4.2 DMABUF → OpenGL 纹理(EGL) +```cpp +// 获取 DMABUF fd(来自 V4L2 或 GStreamer) +int dmabuf_fd = buf.m.fd; + +// 创建 EGLImage +EGLAttrib attrs[] = { + EGL_WIDTH, width, + EGL_HEIGHT, height, + EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_NV12, + EGL_DMA_BUF_PLANE0_FD_EXT, dmabuf_fd, + EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, + EGL_DMA_BUF_PLANE0_PITCH_EXT, stride, + // UV plane... + EGL_NONE +}; +EGLImage egl_image = eglCreateImage(display, EGL_NO_CONTEXT, + EGL_LINUX_DMA_BUF_EXT, nullptr, attrs); + +// 绑定纹理 +glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture_id); +glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, egl_image); +``` + +### 4.3 帧队列模型 + +| 场景 | 队列策略 | +|------|---------| +| 实时算法订阅 | 容量 1,覆盖旧帧(最新帧优先) | +| 显示 | 容量 2–3,允许短暂缓冲平滑抖动 | +| 录制 | 无限或大容量,丢帧时告警 | + +--- + +## 5. 渲染 + +### 5.1 GStreamer 显示 Sink + +```bash +# 本地 OpenGL 窗口 +gst-launch-1.0 ... ! glimagesink + +# Wayland +gst-launch-1.0 ... ! waylandsink + +# 无头(算法消费,不显示) +gst-launch-1.0 ... ! appsink emit-signals=true max-buffers=1 drop=true +``` + +### 5.2 Web 播放器(WebRTC / HLS) +```html + + + + + + + + +``` + +--- + +## 6. 下游算法接入 + +### 6.1 帧格式约定 + +```cpp +struct Frame { + uint64_t frame_id; + uint64_t timestamp_ns; // PTP 时间(纳秒) + uint32_t width, height; + PixelFormat format; // NV12 / BGR / RGBA + uint8_t* data[3]; // plane 指针 + int linesize[3]; // plane stride + int dmabuf_fd; // -1 表示普通内存 + // 可选元数据 + uint32_t exposure_us; + float gain_db; +}; +``` + +### 6.2 ROS 2 订阅示例 +```cpp +#include +#include + +void image_callback(const sensor_msgs::msg::Image::SharedPtr msg) { + // 使用 cv_bridge 转换(注意 encoding: "bgr8" / "nv12") + auto cv_img = cv_bridge::toCvShare(msg, "bgr8"); + cv::Mat frame = cv_img->image; + // 调用检测/SLAM 算法... +} +``` + +### 6.3 算法解耦建议 + +- 算法线程只读帧数据,不拥有帧生命周期(使用共享指针); +- 算法超时(> 2 帧周期)时,跳过当前帧,不阻塞采集线程; +- 通过 `frame_id` 和 `timestamp_ns` 与其他传感器数据对齐。 + +--- + +## 7. 关键参数与默认值 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| 解码器 | NVDEC / V4L2 M2M | 优先硬解 | +| 解码缓冲数 | 4–8 | 依平台 API | +| 帧队列(算法) | 容量 1,覆盖旧帧 | 保证实时性 | +| 渲染格式 | RGBA8(OpenGL)/ NV12(算法) | 按消费者选择 | +| 零拷贝 | DMABUF 优先 | 减少 PCIe/内存带宽 | + +--- + +## 8. 性能指标与验收标准 + +| 指标 | 目标值 | 检测方法 | +|------|--------|----------| +| 解码延迟(硬解) | < 5 ms(1080p) | 时间戳差值 | +| 显示帧率 | 与采集帧率一致(无丢帧) | `gst-launch` `dropped` 计数 | +| 算法帧率 | ≥ 算法设计帧率 | 算法内部计时 | +| 内存拷贝次数 | 0(零拷贝路径) | `perf mem` | +| GPU 内存占用 | < 200 MB(4路1080p) | `nvidia-smi` | + +--- + +## 9. 常见问题与排查步骤(Checklist) + +- [ ] 解码花屏 → 检查 SPS/PPS 是否在关键帧前送入;确认格式(AnnexB vs AVCC) +- [ ] 解码延迟高 → 关闭 B 帧(编码侧);使用低延迟 decode profile +- [ ] OpenGL 纹理颜色偏差 → GLSL Shader 中 NV12→RGB 转换系数是否正确 +- [ ] DMABUF 导入失败 → 检查 EGL 扩展是否支持 `EGL_EXT_image_dma_buf_import` +- [ ] 算法 CPU 占用高 → 确认是否绕过 DMABUF,发生了不必要的内存拷贝 +- [ ] 多路解码 GPU OOM → 减少并发解码流数;降低解码分辨率 + +--- + +## 10. 参考资料 + +- [FFmpeg 硬件加速文档](https://trac.ffmpeg.org/wiki/HWAccelIntro) +- [GStreamer NVDEC 插件](https://gstreamer.freedesktop.org/documentation/nvcodec/index.html) +- [EGL DMA-BUF 扩展规范](https://registry.khronos.org/EGL/extensions/EXT/EGL_EXT_image_dma_buf_import.txt) +- [NVIDIA Video Codec SDK(NVDEC)](https://developer.nvidia.com/nvidia-video-codec-sdk) +- [ROS 2 Image Transport](https://github.com/ros-perception/image_transport_plugins) diff --git a/A_camera_pipeline/A7_diagnostics.md b/A_camera_pipeline/A7_diagnostics.md new file mode 100644 index 0000000..9a734ea --- /dev/null +++ b/A_camera_pipeline/A7_diagnostics.md @@ -0,0 +1,229 @@ +# A7. 诊断与可观测性 + +## 1. 这篇文章要解决什么问题? + +建立贯穿采集→ISP→编码→传输→解码各阶段的可观测体系,快速定位延迟、丢帧、码率不稳等问题。 + +--- + +## 2. 可观测层次模型 + +``` +┌─────────────────────────────────────────────────────┐ +│ 应用层指标(fps / latency / drop rate / bitrate) │ ← 业务告警 +├─────────────────────────────────────────────────────┤ +│ 中间件日志(ROS topic stats / GStreamer pipeline) │ ← 调试 +├─────────────────────────────────────────────────────┤ +│ 内核 / 驱动事件(V4L2 seq / dmesg / perf) │ ← 底层诊断 +├─────────────────────────────────────────────────────┤ +│ 硬件指标(温度 / 功耗 / 编解码器占用) │ ← 稳定性评估 +└─────────────────────────────────────────────────────┘ +``` + +--- + +## 3. 各阶段关键监控指标 + +### 3.1 采集阶段 + +| 指标 | 说明 | 工具 | +|------|------|------| +| `fps_capture` | 实际采集帧率 | V4L2 `sequence` 字段差值 | +| `v4l2_sequence_gap` | V4L2 buffer sequence 跳变 = 丢帧 | 日志分析 | +| `queue_depth` | 缓冲队列深度 | 应用层统计 | +| `dma_timeout` | DMA 超时次数 | `dmesg` | + +```bash +# 检查 V4L2 实际帧率和丢帧 +v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=300 \ + --stream-to=/dev/null 2>&1 | grep fps +``` + +### 3.2 编码阶段 + +| 指标 | 说明 | 工具 | +|------|------|------| +| `encode_fps` | 编码帧率 | 编码器 API 统计 | +| `encode_latency_ms` | 编码耗时 | 帧时间戳差值 | +| `bitrate_kbps` | 实际码率 | 抓包统计 / 编码器回调 | +| `keyframe_interval` | IDR 间隔 | 码流解析 | +| `gpu_enc_util` | GPU 编码引擎占用 | `nvidia-smi` / `tegrastats` | + +```bash +# nvidia-smi 查看编码器占用 +nvidia-smi dmon -s u -d 1 | grep -E "Enc|Dec" + +# Jetson tegrastats +tegrastats --interval 500 +``` + +### 3.3 传输阶段 + +| 指标 | 说明 | 工具 | +|------|------|------| +| `rtp_packet_loss` | RTP 丢包率 | RTCP RR 报告 | +| `rtp_jitter_ms` | RTP 抖动 | RTCP RR 报告 | +| `network_bitrate` | 实际网络带宽占用 | `iftop` / `nethogs` | +| `rtcp_rtt_ms` | 往返时延 | RTCP SR/RR | + +```bash +# 抓取 RTP 包分析丢包 +tcpdump -i eth0 -w /tmp/rtp_cap.pcap udp port 5004 & +# 用 Wireshark 分析:Statistics → RTP Streams + +# 实时带宽监控 +iftop -i eth0 -P +``` + +### 3.4 解码阶段 + +| 指标 | 说明 | 工具 | +|------|------|------| +| `decode_fps` | 解码帧率 | 播放器统计 | +| `decode_latency_ms` | 解码耗时 | 时间戳差值 | +| `gpu_dec_util` | GPU 解码引擎占用 | `nvidia-smi` | +| `frame_drops` | 解码器丢帧 | 播放器 API | + +### 3.5 硬件指标 + +| 指标 | 告警阈值 | 工具 | +|------|----------|------| +| CPU 温度 | > 85°C | `sensors` / `cat /sys/class/thermal/thermal_zone*/temp` | +| GPU 温度 | > 90°C | `nvidia-smi` | +| 功耗 | 超 TDP | `tegrastats` / `nvidia-smi` | +| 内存占用 | > 80% | `free -h` | + +--- + +## 4. 日志规范 + +### 4.1 关键日志事件 + +``` +[CAPTURE ] frame_id=12345 seq=12345 ts_soe_ns=1700000123456789 queue_depth=2 +[ENCODE ] frame_id=12345 ts_in_ns=1700000123460000 ts_out_ns=1700000123464000 latency_ms=4 keyframe=0 +[TRANSMIT] frame_id=12345 rtp_ts=3600000 seq=4567 bytes=18234 +[DECODE ] frame_id=12345 ts_in_ns=1700000123565000 ts_out_ns=1700000123569000 latency_ms=4 +``` + +### 4.2 告警触发条件 + +| 事件 | 条件 | 级别 | +|------|------|------| +| 采集丢帧 | `v4l2_sequence` 跳变 ≥ 1 | WARN | +| 编码延迟高 | 单帧编码 > 2×帧周期 | WARN | +| RTP 丢包 | 连续丢包 > 3 帧 | ERROR | +| 码率偏差 | 实际码率偏离目标 > 20% | WARN | +| 温度过高 | CPU/GPU 温度超过阈值 | ERROR | + +--- + +## 5. 抓包与抓帧 + +### 5.1 网络抓包 +```bash +# 抓取指定摄像头的 RTP 流(端口 5004) +sudo tcpdump -i eth0 -w /tmp/camera0_$(date +%s).pcap \ + 'udp port 5004 or udp port 5005' -c 10000 + +# 使用 tshark 实时统计 RTP 丢包 +tshark -i eth0 -f "udp port 5004" \ + -T fields -e rtp.seq -e rtp.timestamp -e frame.time_epoch +``` + +### 5.2 保存关键帧(GStreamer) +```bash +# 触发保存 IDR 帧(每 100 帧存一次) +gst-launch-1.0 rtspsrc location=rtsp://host/test ! \ + rtph264depay ! h264parse ! tee name=t \ + t. ! queue ! avdec_h264 ! \ + videorate drop-only=true max-rate=1 ! \ + jpegenc ! \ + multifilesink location="/tmp/frame_%05d.jpg" \ + t. ! queue ! filesink location=/tmp/recording.ts +``` + +### 5.3 编码器统计(FFmpeg) +```bash +ffmpeg -i rtsp://host/test -vf "drawtext=text='%{pts\\:hms}':fontsize=24" \ + -c:v copy -f null - 2>&1 | grep -E "frame|fps|bitrate" +``` + +--- + +## 6. 故障注入(混沌测试) + +| 注入类型 | 工具 | 命令示例 | +|----------|------|----------| +| 网络限速 | `tc netem` | `tc qdisc add dev eth0 root tbf rate 2mbit burst 32kbit latency 400ms` | +| 网络丢包 | `tc netem` | `tc qdisc add dev eth0 root netem loss 5%` | +| 网络抖动 | `tc netem` | `tc qdisc add dev eth0 root netem delay 100ms 20ms` | +| CPU 降频 | `cpufreq-set` | `cpufreq-set -g powersave` | +| GPU 限频 | `nvidia-smi` | `nvidia-smi -pl 50`(限制功耗上限) | +| 磁盘 I/O 限速 | `cgroup blkio` | 通过 cgroup v2 配置 | + +```bash +# 模拟 5% 丢包 + 50ms 延迟(测试 WebRTC/RTSP 容错) +sudo tc qdisc add dev eth0 root netem loss 5% delay 50ms 10ms +# 恢复 +sudo tc qdisc del dev eth0 root +``` + +--- + +## 7. 监控面板建议(Grafana + Prometheus) + +```yaml +# prometheus.yml 采集目标(示意) +scrape_configs: + - job_name: 'sensor_metrics' + static_configs: + - targets: ['localhost:9101'] # 自定义 exporter + metrics_path: /metrics + +# 关键 Gauge/Counter 指标 +sensor_capture_fps{sensor="camera_front"} +sensor_encode_latency_ms{sensor="camera_front"} +sensor_rtp_packet_loss_ratio{sensor="camera_front"} +sensor_temperature_celsius{device="gpu"} +``` + +**推荐面板**: +- 实时 FPS 折线图(每路) +- 端到端延迟热力图 +- 码率 vs 目标码率(时间序列) +- 温度/功耗趋势 + +--- + +## 8. 关键参数与默认值 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| 日志级别(生产) | INFO | 关键事件必须记录 | +| 日志级别(调试) | DEBUG | 逐帧日志(性能影响大) | +| 监控采集间隔 | 1 s | Prometheus scrape | +| 丢帧告警窗口 | 10 s | 滑动窗口统计 | +| 抓包保留大小 | 500 MB(环形) | 避免磁盘溢出 | + +--- + +## 9. 常见问题与排查步骤(Checklist) + +- [ ] FPS 低于预期 → 先确认采集 FPS,再逐级检查编码/传输/解码 +- [ ] 延迟忽高忽低 → 查 `queue_depth` 是否有积压;检查网络 jitter +- [ ] 码率远低于设置 → 检查场景是否变化很少(VBR 节省带宽是正常的) +- [ ] 码率远高于设置 → CBR `bufsize` 配置错误;检查编码器参数 +- [ ] 日志中出现 sequence gap → 内核丢帧,检查采集线程实时性(降低 CPU 负载) +- [ ] Grafana 指标缺失 → 检查 exporter 是否正常;Prometheus 配置是否正确 +- [ ] 故障注入后恢复慢 → 检查 jitter buffer 大小;NACK/FEC 配置是否合理 + +--- + +## 10. 参考资料 + +- [Linux tc netem 文档](https://www.linux.org/docs/man8/tc-netem.html) +- [Prometheus + Grafana 最佳实践](https://prometheus.io/docs/practices/naming/) +- [GStreamer 调试与性能分析](https://gstreamer.freedesktop.org/documentation/tutorials/basic/debugging-tools.html) +- [FFmpeg 统计与分析](https://ffmpeg.org/ffprobe.html) +- [Wireshark RTP 分析](https://wiki.wireshark.org/RTP) diff --git a/B_sensor_catalog/B1_camera.md b/B_sensor_catalog/B1_camera.md new file mode 100644 index 0000000..e8a371b --- /dev/null +++ b/B_sensor_catalog/B1_camera.md @@ -0,0 +1,207 @@ +# B1. 相机(Camera)完整接入指南 + +## 1. 这篇文章要解决什么问题? + +覆盖相机从硬件选型到算法消费的完整接入路径,汇总 MIPI/GMSL/USB/GigE 接口差异、Linux 驱动调试、编码传输选型、标定和常见 Debug 方法。 + +--- + +## 2. 数据链路图 + +``` +[Camera Sensor] + │(MIPI CSI-2 / GMSL2 / USB / GigE) + ▼ +[Linux 驱动层] + V4L2 / UVC / 厂商 SDK / ISP Bridge + │ + ▼ +[ISP / 预处理] + 去马赛克 → AWB → NR → 格式转换(→ NV12) + │ + ▼ +[编码器] + NVENC / V4L2 M2M / x264 + │ + ├──► 传输(RTSP / WebRTC / HLS)──► 远端消费 + └──► 本地算法(检测/SLAM/标定) +``` + +--- + +## 3. 接口选型矩阵 + +| 接口 | 带宽上限 | 距离 | 成本 | 典型平台 | 推荐场景 | +|------|----------|------|------|---------|---------| +| USB 2.0 UVC | ~40 MB/s | 5 m | 低 | 通用 PC | 低帧率 / 低分辨率 | +| USB 3.x UVC | ~400 MB/s | 3 m | 低-中 | 通用 PC | 工业 USB 相机 | +| MIPI CSI-2 | 1–16 Gbps | 板级 | 低 | Jetson / RPi | 嵌入式平台 | +| GMSL2 | 6 Gbps | 15 m(同轴) | 高 | 车载 SoC | 车载 ADAS | +| GigE Vision | 1–10 GbE | 100 m | 中-高 | 工业 PC | 工业视觉 | +| Ethernet(RTSP/ONVIF) | 网络限制 | 无限(IP) | 低-中 | 任意 | 监控 IPC | + +--- + +## 4. Linux 驱动调试步骤 + +### 4.1 快速验证(USB UVC) +```bash +# 枚举设备 +v4l2-ctl --list-devices + +# 查看支持格式 +v4l2-ctl -d /dev/video0 --list-formats-ext + +# 取流预览(MJPEG → 本地播放) +ffplay -f v4l2 -input_format mjpeg -video_size 1920x1080 \ + -framerate 30 /dev/video0 +``` + +### 4.2 MIPI CSI-2(Jetson) +```bash +# 检查媒体控制拓扑 +media-ctl -d /dev/media0 --print-topology + +# 设置 sensor 格式 +media-ctl --set-v4l2 '"IMX477 10-001a":0 [fmt:SRGGB10_1X10/1920x1080]' + +# V4L2 取流 +v4l2-ctl -d /dev/video0 \ + --set-fmt-video=width=1920,height=1080,pixelformat=RG10 \ + --stream-mmap --stream-count=30 +``` + +### 4.3 GMSL2(MAX9295/9296) +```bash +# 检查 Link Lock 状态(I2C 寄存器) +i2cget -y 0 0x48 0x04 # MAX9296 Link Lock 寄存器(示例) + +# dmesg 检查初始化日志 +dmesg | grep -i "max9296\|gmsl\|link" +``` + +### 4.4 GigE Vision(Aravis) +```bash +arv-tool-0.8 detect +arv-tool-0.8 -n "Basler-acA1920" set Width=1920 Height=1080 +arv-tool-0.8 -n "Basler-acA1920" record --duration=5 output.avi +``` + +--- + +## 5. 编码与传输快速配置 + +### 5.1 RTSP 推流(GStreamer + Jetson) +```bash +gst-launch-1.0 v4l2src device=/dev/video0 ! \ + 'video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1' ! \ + nvv4l2h264enc bitrate=4000000 iframeinterval=30 ! \ + h264parse ! rtph264pay config-interval=1 ! \ + udpsink host=127.0.0.1 port=5004 +``` + +### 5.2 多路相机同时推流 +```bash +# 使用 MediaMTX 配置文件(mediamtx.yml) +paths: + camera0: + source: "v4l2:///dev/video0?video_size=1920x1080&framerate=30" + camera1: + source: "v4l2:///dev/video2?video_size=1920x1080&framerate=30" +``` + +--- + +## 6. 标定 + +### 6.1 内参标定(单目) +```bash +# 使用 ROS camera_calibration +rosrun camera_calibration cameracalibrator.py \ + --size 8x6 --square 0.025 \ + image:=/camera/image_raw camera:=/camera +``` + +**输出文件**(`camera.yaml`): +```yaml +camera_matrix: + data: [fx, 0, cx, 0, fy, cy, 0, 0, 1] +distortion_model: plumb_bob +distortion_coefficients: + data: [k1, k2, p1, p2, k3] +``` + +### 6.2 外参标定(多相机 / 相机-雷达) +- 工具:[Kalibr](https://github.com/ethz-asl/kalibr)(多相机 / IMU-Camera); +- 工具:[cam_lidar_calibration](https://github.com/acfr/cam_lidar_calibration)(相机-激光雷达); +- 标靶:棋盘格(≥ 8×6)或 AprilTag。 + +### 6.3 时间同步标定(Camera-IMU) +```bash +# Kalibr IMU-Camera 标定 +kalibr_calibrate_imu_camera \ + --target aprilgrid.yaml \ + --imu imu.yaml --imu-models calibrated \ + --cam camchain.yaml \ + --bag data.bag +``` + +--- + +## 7. 常见 Debug 场景 + +### 7.1 黑屏 / 无图像 +``` +1. dmesg | grep video → 确认驱动加载 +2. v4l2-ctl --list-devices → 确认节点存在 +3. 确认分辨率/格式匹配(list-formats-ext) +4. MIPI:media-ctl 拓扑是否正确连接? +5. GMSL:Link Lock 是否拉高?(i2cget 检查) +6. GigE:arv-tool detect 能看到相机?防火墙是否开放? +``` + +### 7.2 花屏 / 图像噪点 +``` +1. 带宽是否足够?(重新计算未压缩带宽) +2. USB:是否有 Hub?是否共享 USB Host 控制器? +3. MIPI:Lane 速率是否匹配?(dtsi 配置) +4. GigE:MTU 是否一致?丢包率 ping -f 检测 +5. 编码:输入格式/stride 是否对齐? +``` + +### 7.3 丢帧 +``` +1. v4l2 sequence gap → 采集线程 CPU 占用高,提高优先级(SCHED_FIFO) +2. 队列深度过小 → 增加缓冲 buffer 数 +3. 网络丢包 → 检查 switch/cable;启用 NACK/FEC +``` + +### 7.4 延迟高 +``` +1. 逐级分解延迟(A3 编码 + A4 传输参考章节) +2. 编码:关闭 B 帧,tune=zerolatency +3. 传输:使用 RTP/WebRTC 替代 HLS +4. 解码:确认使用硬解,无额外缓冲 +``` + +--- + +## 8. 性能指标与验收标准 + +| 指标 | 目标值 | +|------|--------| +| 采集帧率偏差 | < 1%(稳态) | +| 端到端延迟(LAN 直播) | < 150 ms | +| 多路丢帧率 | < 0.01% | +| 内参重投影误差 | < 0.5 px | +| 外参旋转误差 | < 0.5° | + +--- + +## 9. 参考资料 + +- [V4L2 API](https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/v4l2.html) +- [Jetson Camera Developer Guide](https://developer.nvidia.com/embedded/jetson-partner-supported-cameras) +- [Kalibr 多相机标定](https://github.com/ethz-asl/kalibr) +- [ROS camera_calibration](http://wiki.ros.org/camera_calibration) +- [MediaMTX(多协议媒体服务器)](https://github.com/bluenviron/mediamtx) diff --git a/B_sensor_catalog/B2_mmwave_radar.md b/B_sensor_catalog/B2_mmwave_radar.md new file mode 100644 index 0000000..5a1c1ae --- /dev/null +++ b/B_sensor_catalog/B2_mmwave_radar.md @@ -0,0 +1,219 @@ +# B2. 毫米波雷达(mmWave Radar)接入指南 + +## 1. 这篇文章要解决什么问题? + +完成毫米波雷达从硬件接入到点云/目标数据消费的全流程,包括 FMCW 基本原理、接口接入、协议解析、坐标系统一和标定。 + +--- + +## 2. 数据链路图 + +``` +[mmWave Radar 硬件] + (FMCW 天线阵列) + │ +[接口层] + CAN / Ethernet / UART / USB + │ +[驱动 / SDK 层] + 厂商驱动 or 自研协议解析器 + │ +[数据格式层] + ├─ 目标列表(Object List):ID、距离、速度、方位角、RCS + ├─ 点云(Point Cloud):[x, y, z, v_r, intensity] + └─ 栅格地图(Occupancy Grid)(部分型号) + │ +[后处理] + 坐标系转换(雷达系 → 车体系 → 世界系) + 时间戳对齐(PTP / CAN 时间戳) + │ +[上层应用] + 障碍物检测 / 目标追踪 / 自动驾驶感知融合 +``` + +--- + +## 3. FMCW 基本原理 + +### 3.1 关键参数 + +| 参数 | 说明 | 典型值 | +|------|------|--------| +| 工作频段 | 77 GHz(车载主流)/ 24 GHz | 76–81 GHz | +| 带宽(B) | 影响距离分辨率 | 1–4 GHz | +| 距离分辨率 | `c / (2B)` | ~5 cm(4 GHz 带宽) | +| 最大探测距离 | 依发射功率 | 近程 30 m / 远程 250 m | +| 速度分辨率 | `λ / (2 × N_chirps × T_chirp)` | ~0.1 m/s | +| 最大无模糊速度 | `λ / (4 × T_chirp)` | ±20–±80 m/s | +| 角度分辨率 | ~1–2°(水平,依天线数) | — | + +### 3.2 距离-速度测量原理(简化) +``` +发射:锯齿波调频信号(FMCW Chirp) +接收:与发射信号混频 → 差频(IF 信号) + 距离 ↔ IF 频率(Range FFT) + 速度 ↔ 相位差(Doppler FFT) + 角度 ↔ 天线相位差(Angle FFT) +``` + +--- + +## 4. 接口接入 + +### 4.1 CAN 接口 + +```bash +# 配置 CAN 接口(500 kbps) +sudo ip link set can0 type can bitrate 500000 +sudo ip link set can0 up + +# 抓取 CAN 报文 +candump can0 + +# 过滤特定 ID(如 0x200–0x20F 为目标列表) +candump can0,200:FF0 +``` + +**典型 CAN 报文结构(示意,依型号而定):** +``` +CAN ID: 0x200 DLC: 8 +Byte[0:1] 目标ID (uint16) +Byte[2:3] 距离 (uint16, 分辨率 0.01 m) +Byte[4:5] 相对速度 (int16, 分辨率 0.01 m/s) +Byte[6:7] 方位角 (int16, 分辨率 0.01°) +``` + +### 4.2 Ethernet(UDP) + +```python +import socket, struct + +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.bind(("0.0.0.0", 7778)) # 厂商指定端口 + +while True: + data, addr = sock.recvfrom(65535) + # 解析帧头 + header_fmt = ">HHI" # magic, frame_id, timestamp_ms + magic, frame_id, ts_ms = struct.unpack_from(header_fmt, data, 0) + # 解析目标列表... +``` + +### 4.3 UART / USB + +```bash +# 打开串口(115200,8N1) +stty -F /dev/ttyUSB0 115200 cs8 -cstopb -parenb +cat /dev/ttyUSB0 | hexdump -C + +# Python pyserial +import serial +ser = serial.Serial("/dev/ttyUSB0", 115200, timeout=1) +frame = ser.read(256) # 读取一帧(长度依协议) +``` + +--- + +## 5. 数据格式与坐标系 + +### 5.1 点云格式 + +```cpp +struct RadarPoint { + float x; // 前向(m) + float y; // 左向(m) + float z; // 上向(m) + float v_r; // 径向速度(m/s,正为远离) + float rcs; // 雷达截面积(dBsm) + uint8_t snr; // 信噪比 + uint64_t ts_ns; // 时间戳(ns) +}; +``` + +### 5.2 目标列表格式 + +```cpp +struct RadarObject { + uint16_t id; + float dist_m; // 距离 + float azimuth_deg; // 水平方位角(度) + float elevation_deg; // 垂直仰角(度,若支持) + float vel_ms; // 径向速度(m/s) + float rcs_dbsm; // RCS + uint8_t dynamic_state; // 0=静止, 1=移动, 2=驶离 +}; +``` + +### 5.3 坐标系约定 + +``` +雷达坐标系(FLU): + X → 正前方 + Y → 正左方 + Z → 正上方 + +转换到车体坐标系: + T_body_radar = [R | t](外参,需标定) +``` + +--- + +## 6. 误差与标定 + +### 6.1 外参标定(雷达 → 车体) + +- **方法 1**:使用角反射器(Trihedral Reflector)在标定场中测量; +- **方法 2**:与相机点云融合对齐(ICP); +- 精度目标:位置 < 5 cm,角度 < 0.5°。 + +### 6.2 速度偏差校正 + +```python +# 利用自车速度(来自轮速计/GNSS)校正雷达速度偏差 +# 理论:静止目标的雷达径向速度 = -v_ego * cos(azimuth) +def correct_velocity(v_r_measured, v_ego, azimuth_rad): + v_r_expected = -v_ego * math.cos(azimuth_rad) + bias = v_r_measured - v_r_expected # 速度偏差 + return v_r_measured - bias +``` + +### 6.3 时间延迟标定 + +- CAN:报文时间戳与 CAN 总线调度延迟(通常 < 1 ms); +- 以太网:使用 PTP 同步(见 A5); +- 方法:与 IMU 加速度计数据互相关估计延迟。 + +--- + +## 7. 性能指标与验收标准 + +| 指标 | 目标值 | +|------|--------| +| 探测距离(近程) | ≥ 0.2 m | +| 探测距离(远程) | ≥ 100 m(行人);≥ 200 m(车辆) | +| 距离精度 | ≤ 0.1 m(1σ) | +| 速度精度 | ≤ 0.1 m/s(1σ) | +| 方位角精度 | ≤ 1°(1σ) | +| 更新率 | ≥ 10 Hz | +| CAN 总线负载 | < 60% | + +--- + +## 8. 常见问题与排查步骤(Checklist) + +- [ ] 无数据输出 → 检查电源电压(典型 12 V);CAN 终端电阻(120 Ω)是否接好 +- [ ] 目标距离偏差大 → 检查安装高度/角度是否与标定一致 +- [ ] 速度方向反向 → 检查坐标系符号约定(进/离) +- [ ] 角度偏差 → 重新进行外参标定 +- [ ] 检测不到静止目标 → 多数 77 GHz 雷达默认过滤零速目标,检查静止目标模式配置 +- [ ] CAN 报文缺失 → `candump` 检查总线负载;是否有 Bus-Off? +- [ ] 幻象目标(Ghost) → 减少多路径反射(调整安装位置);使用滤波器 + +--- + +## 9. 参考资料 + +- [TI mmWave SDK 文档](https://www.ti.com/tool/MMWAVE-SDK) +- [Continental ARS 系列接口手册](https://www.continental-automotive.com/en-gl/Passenger-Cars/Safety/Advanced-Driver-Assistance-Systems/Radar-Sensors) +- [ROS radar_msgs 消息格式](https://github.com/ros-perception/radar_msgs) +- [FMCW 雷达原理(TI 应用笔记)](https://www.ti.com/lit/wp/spyy005a/spyy005a.pdf) diff --git a/B_sensor_catalog/B3_lidar.md b/B_sensor_catalog/B3_lidar.md new file mode 100644 index 0000000..c939dfd --- /dev/null +++ b/B_sensor_catalog/B3_lidar.md @@ -0,0 +1,256 @@ +# B3. 激光雷达(LiDAR)接入指南 + +## 1. 这篇文章要解决什么问题? + +完成激光雷达从 UDP 数据包到有效点云帧的全流程接入,包括网络配置、packet → frame 解析、点云格式、时间同步、外参标定和运动畸变补偿。 + +--- + +## 2. 数据链路图 + +``` +[LiDAR 硬件] + 旋转/固态,多线激光 + 探测器阵列 + │(UDP / Ethernet) +[网络层] + 专用 LAN / 交换机 + │ +[驱动 / SDK 层] + 厂商 ROS Driver / 自研 UDP 解析器 + │ +[点云帧组装] + Packet(~1248 字节/包)→ Frame(一圈 = 一帧,通常 10–20 Hz) + │ +[点云格式] + [x, y, z, intensity, ring, timestamp] + │ +[后处理] + ├─ 去畸变(Motion Compensation) + ├─ 外参变换(Sensor → Vehicle → World) + └─ 下采样 / 滤波(VoxelGrid / PassThrough) + │ +[上层应用] + SLAM / 障碍物检测 / 高精地图 +``` + +--- + +## 3. 网络配置 + +### 3.1 静态 IP 配置(推荐) +```bash +# 设置主机网卡 IP(与 LiDAR 同网段) +sudo ip addr add 192.168.1.100/24 dev eth1 +sudo ip link set eth1 up + +# 验证连通性 +ping 192.168.1.201 # LiDAR 默认 IP(各型号不同) +``` + +### 3.2 MTU 与网络优化 +```bash +# 开启 Jumbo Frame(推荐,降低 CPU 中断) +sudo ip link set eth1 mtu 9000 + +# 增大 UDP 接收缓冲区(防丢包) +sudo sysctl -w net.core.rmem_max=26214400 +sudo sysctl -w net.core.rmem_default=26214400 + +# 写入 /etc/sysctl.conf 持久化 +echo "net.core.rmem_max=26214400" | sudo tee -a /etc/sysctl.conf +``` + +### 3.3 中断亲和性(高点频 LiDAR) +```bash +# 查看网卡队列中断 +cat /proc/interrupts | grep eth1 + +# 绑定中断到指定 CPU +echo 4 > /proc/irq//smp_affinity_list +``` + +--- + +## 4. Packet → Frame 解析 + +### 4.1 典型 UDP 包结构(以 Velodyne VLP-16 为例) +``` +UDP Payload(1248 字节): + [0:1199] 12 个 Data Block,每块 100 字节 + Block: + [0:1] Block ID(0xFFEE) + [2:3] 方位角(0.01°单位) + [4:99] 32 个通道数据(distance × 2 + intensity × 1) + [1200:1203] GPS 时间戳(µs since top of hour) + [1204] 返回模式 + [1205] 产品 ID +``` + +### 4.2 帧组装逻辑 +```python +def assemble_frame(packets): + """ + 当方位角从 ~359° 回绕到 0° 时,认为一帧结束。 + """ + points = [] + prev_angle = None + for pkt in packets: + for block in pkt.blocks: + angle = block.azimuth + if prev_angle is not None and angle < prev_angle - 180: + yield points # 输出完整一帧 + points = [] + points.extend(decode_block(block)) + prev_angle = angle +``` + +### 4.3 XYZ 坐标计算 +```python +import math + +def polar_to_xyz(distance_m, azimuth_deg, elevation_deg): + az = math.radians(azimuth_deg) + el = math.radians(elevation_deg) + x = distance_m * math.cos(el) * math.sin(az) + y = distance_m * math.cos(el) * math.cos(az) + z = distance_m * math.sin(el) + return x, y, z +``` + +--- + +## 5. 点云格式 + +### 5.1 标准点云字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `x`, `y`, `z` | float32 | 3D 坐标(m,传感器坐标系) | +| `intensity` | float32 / uint8 | 反射强度(0–255) | +| `ring` | uint16 | 激光线束编号(0 起) | +| `timestamp` | float64 | 该点的硬件时间戳(s) | +| `distance` | float32 | 原始距离值(m) | +| `return_type` | uint8 | 单/双回波 | + +### 5.2 ROS PointCloud2 消息 +``` +sensor_msgs/PointCloud2 + header.stamp ← 帧起始时间戳(PTP/GNSS 对齐) + fields[]: + name="x" offset=0 datatype=7(FLOAT32) + name="y" offset=4 datatype=7 + name="z" offset=8 datatype=7 + name="intensity" offset=12 datatype=7 + name="ring" offset=16 datatype=4(UINT16) + name="timestamp" offset=20 datatype=8(FLOAT64) + point_step = 28 + row_step = point_step × width +``` + +--- + +## 6. 时间同步 + +### 6.1 PTP 同步(推荐) +```bash +# LiDAR 配置为 PTP Slave(通过 Web UI 或配置文件) +# 主机运行 ptp4l(见 A5 章节) +sudo ptp4l -i eth1 -H -m + +# 验证 LiDAR 与主机时钟偏差(< 1 µs 理想) +# 通过厂商 Web API 读取 LiDAR 时间偏移 +``` + +### 6.2 GPS/PPS 同步(无 PTP 时) +```bash +# 主机使用 gpsd + chrony 对齐 GNSS 时间 +sudo gpsd /dev/ttyS0 -F /var/run/gpsd.sock +# /etc/chrony.conf 中添加: +# refclock SHM 0 offset 0.1 delay 0.2 refid GPS +``` + +--- + +## 7. 运动畸变补偿(Motion Compensation) + +### 7.1 问题描述 +旋转 LiDAR 的一帧数据采集时间约为 100 ms(@10 Hz),期间车辆移动会导致点云畸变。 + +### 7.2 补偿方法 + +```python +def compensate_motion(points, T_start, T_end): + """ + 线性插值:假设帧内匀速运动 + T_start, T_end: 帧起止时的 6DOF 位姿(来自 IMU/里程计) + """ + for p in points: + alpha = (p.timestamp - T_start.time) / (T_end.time - T_start.time) + T_interp = interpolate_pose(T_start, T_end, alpha) + p.xyz = T_interp @ p.xyz # 变换到帧起始时刻的坐标系 + return points +``` + +**高精度方案**:使用 IMU 积分得到每个点的精确位姿(见 B4)。 + +--- + +## 8. 外参标定 + +### 8.1 LiDAR → Camera 外参 +```bash +# 使用 cam_lidar_calibration +rosrun cam_lidar_calibration run_optimiser \ + --camera_info /camera/camera_info \ + --lidar /velodyne_points \ + --board_dims 0.8 0.6 # 标定板尺寸(m) +``` + +### 8.2 多 LiDAR 外参对齐 +- 工具:[lidar_align](https://github.com/ethz-asl/lidar_align)(自动化 ICP) +- 方法:在静止场景下采集各 LiDAR 点云,最小化 ICP 残差 + +--- + +## 9. 关键参数与默认值 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| 帧率 | 10–20 Hz | 依分辨率要求 | +| UDP 接收缓冲 | 26 MB | `net.core.rmem_max` | +| MTU | 9000 | Jumbo Frame | +| 组帧方式 | 方位角回绕 | 最通用 | +| 时间同步 | PTP | 精度 < 1 µs | + +--- + +## 10. 性能指标与验收标准 + +| 指标 | 目标值 | +|------|--------| +| 点频 | ≥ 标称值(如 VLP-16:300,000 点/s) | +| 帧率偏差 | < 1%(稳态) | +| UDP 丢包率 | 0%(局域网直连) | +| PTP 时钟偏差 | < 1 µs | +| 外参标定残差(ICP) | < 3 cm | + +--- + +## 11. 常见问题与排查步骤(Checklist) + +- [ ] 无点云输出 → `tcpdump -i eth1 udp port 2368`(Velodyne 默认端口)确认 UDP 包到达 +- [ ] 点云丢失/不完整 → 检查 `rmem_max`;交换机是否有丢包 +- [ ] 点云畸变严重 → 运动补偿是否开启?IMU 时间同步是否正确? +- [ ] 强度值异常 → 检查目标材质反射率;多回波模式设置 +- [ ] 坐标系偏差 → 外参标定结果;确认旋转方向(FLU vs FRD) +- [ ] 帧率不稳 → CPU 处理瓶颈;增大接收缓冲;绑定中断到专用 CPU + +--- + +## 12. 参考资料 + +- [Velodyne VLP-16 用户手册](https://velodynelidar.com/products/puck/) +- [Livox Mid-360 SDK](https://github.com/Livox-SDK/Livox-SDK2) +- [ROS velodyne_driver](https://github.com/ros-drivers/velodyne) +- [lidar_align 外参标定](https://github.com/ethz-asl/lidar_align) +- [cam_lidar_calibration](https://github.com/acfr/cam_lidar_calibration) diff --git a/B_sensor_catalog/B4_imu_gnss.md b/B_sensor_catalog/B4_imu_gnss.md new file mode 100644 index 0000000..af3df79 --- /dev/null +++ b/B_sensor_catalog/B4_imu_gnss.md @@ -0,0 +1,243 @@ +# B4. IMU / GNSS 接入指南 + +## 1. 这篇文章要解决什么问题? + +完成 IMU 和 GNSS 的接入、时间同步、噪声模型建立、与相机/LiDAR 的外参标定,以及为 EKF/因子图融合提供合格的时间戳数据。 + +--- + +## 2. 数据链路图 + +``` +[IMU(加速度计 + 陀螺仪)] [GNSS 接收机] + SPI / I2C / UART / CAN UART / USB / Ethernet + │ │ +[驱动层] [NMEA / u-blox 协议解析] + Linux IIO / 厂商 SDK │ + │ │ +[数据字段] [数据字段] + ts_ns, ax, ay, az ts_ns, lat, lon, alt + wx, wy, wz, temp heading, speed, fix_quality + │ │ + └──────────────┬────────────────────────┘ + ▼ + [时间同步(PTP / PPS)] + │ + ▼ + [融合算法(EKF / 因子图)] + ├─ IMU 预积分(高频 ~200 Hz) + └─ GNSS 位置更新(低频 ~1–10 Hz) +``` + +--- + +## 3. IMU 接口与驱动 + +### 3.1 接口类型 + +| 接口 | 典型速率 | 适用 IMU | +|------|----------|---------| +| SPI | 10–50 MHz | 高速 MEMS(ICM-42688-P)| +| I2C | 100–400 kHz | 低速原型(MPU-6050)| +| UART | 115200–4M baud | 战术级(VectorNav)| +| CAN | 1 Mbps | 车载 IMU(Bosch SMI)| +| USB | 12 Mbps | 工业 IMU(Xsens MTi)| + +### 3.2 Linux IIO 驱动 +```bash +# 查看 IIO 设备 +ls /sys/bus/iio/devices/ +cat /sys/bus/iio/devices/iio:device0/name # 芯片名称 + +# 读取加速度(原始值需乘以 scale) +cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw +cat /sys/bus/iio/devices/iio:device0/in_accel_scale # m/s² per LSB + +# 配置采样率 +echo 200 > /sys/bus/iio/devices/iio:device0/sampling_frequency +``` + +### 3.3 UART IMU(VectorNav / Xsens) +```python +import serial, struct + +ser = serial.Serial("/dev/ttyUSB0", 921600, timeout=0.1) + +def parse_vnymr(line: bytes): + """解析 VectorNav VNYMR 报文""" + # $VNYMR,yaw,pitch,roll,MagX,MagY,MagZ,AccX,AccY,AccZ,GyrX,GyrY,GyrZ*checksum + fields = line.decode().strip().split(",") + yaw, pitch, roll = float(fields[1]), float(fields[2]), float(fields[3]) + ax, ay, az = float(fields[7]), float(fields[8]), float(fields[9]) + wx, wy, wz = float(fields[10]), float(fields[11]), float(fields[12].split("*")[0]) + return yaw, pitch, roll, ax, ay, az, wx, wy, wz +``` + +--- + +## 4. GNSS 接口与协议 + +### 4.1 NMEA 协议解析 +```python +import pynmea2, serial + +ser = serial.Serial("/dev/ttyACM0", 9600, timeout=1) +while True: + line = ser.readline().decode("ascii", errors="replace").strip() + if line.startswith("$G"): + try: + msg = pynmea2.parse(line) + if isinstance(msg, pynmea2.GGA): + print(f"lat={msg.latitude:.7f} lon={msg.longitude:.7f} " + f"alt={msg.altitude}m fix={msg.gps_qual}") + except pynmea2.ParseError: + pass +``` + +### 4.2 u-blox UBX 协议(高精度) +```bash +# 配置 u-blox M9N 输出 UBX-NAV-PVT(位置/速度/时间) +ubxtool -p CFG-MSG,0x01,0x07,1 /dev/ttyACM0 + +# 读取高精度时间戳 +ubxtool -p CFG-TP5 /dev/ttyACM0 # 配置 PPS 输出 +``` + +### 4.3 RTK GNSS(差分增强) +- 基站:搭建本地 NTRIP 基站 or 订阅公共 CORS 网络; +- 流动站:通过串口或以太网接收 RTCM3 差分数据; +- 精度:水平 1–2 cm(Fix 状态),垂直 2–3 cm。 + +--- + +## 5. 噪声模型与 Allan 方差 + +### 5.1 IMU 噪声参数 + +| 参数 | 符号 | 单位 | 典型值(MEMS) | +|------|------|------|--------------| +| 加速度计噪声密度 | σ_a | m/s²/√Hz | 0.001–0.01 | +| 陀螺仪噪声密度 | σ_g | rad/s/√Hz | 0.0001–0.001 | +| 加速度计随机游走 | σ_ba | m/s²/s^0.5 | 0.0001–0.001 | +| 陀螺仪随机游走 | σ_bg | rad/s/s^0.5 | 0.000001–0.00001 | + +### 5.2 Allan 方差标定 +```bash +# 使用 imu_utils(ROS)进行 Allan 方差分析 +# 静止采集 2–6 小时 IMU 数据,然后: +rosrun imu_utils imu_an \ + -imu_topic /imu/data_raw \ + -imu_frequency 200 \ + -dev_name xsens_mti \ + -ave_num 400 +``` + +--- + +## 6. 时间同步 + +### 6.1 PPS + NMEA 授时 +```bash +# GNSS 输出 PPS 信号接到主机 GPIO / DCD 引脚 +# 使用 chrony + PPS 驱动对齐系统时钟 +# /etc/chrony.conf: +refclock PPS /dev/pps0 lock GNSS refid PPS precision 1e-9 +refclock SHM 0 offset 0.5 delay 0.2 refid GNSS + +# 检查同步状态 +chronyc sources -v +``` + +### 6.2 IMU 硬件时间戳 +```bash +# 支持硬件时间戳的 IMU(如 Xsens MTi-670): +# 启用 SyncOut(PPS 同步输出)对齐到 GNSS PPS +# 在 ROS driver 配置中: +# syncOutMode: PPS +# syncInSkipFactor: 0 +``` + +### 6.3 时间戳质量要求(融合算法输入) + +| 要求 | 说明 | +|------|------| +| 单调递增 | 不允许时间回跳 | +| 时间戳精度 | IMU < 1 ms;GNSS < 100 µs(PPS 授时) | +| 连续性 | 允许短暂中断(< 0.5 s),需标记 gap | +| 时钟漂移 | < 50 ppm(需温度补偿 TCXO) | + +--- + +## 7. Camera-IMU 外参标定 + +```bash +# Kalibr 标定(棋盘格靶标 + 手持激励运动) +kalibr_calibrate_cameras \ + --target april_6x6.yaml \ + --bag calib.bag \ + --models pinhole-equi \ + --topics /camera/image_raw + +kalibr_calibrate_imu_camera \ + --target april_6x6.yaml \ + --imu imu.yaml \ + --imu-models calibrated \ + --cam camchain.yaml \ + --bag calib.bag +``` + +**输出**(`imu_camera_calibration.yaml`): +```yaml +cam0: + T_cam_imu: # 4×4 变换矩阵(IMU → Camera) + - [r00, r01, r02, tx] + - [r10, r11, r12, ty] + - [r20, r21, r22, tz] + - [ 0, 0, 0, 1] + timeshift_cam_imu: -0.005123 # 秒,正值表示 camera 时间滞后 +``` + +--- + +## 8. 关键参数与默认值 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| IMU 采样率 | 200–400 Hz | 融合算法通常需要 ≥ 100 Hz | +| GNSS 更新率 | 1–10 Hz | RTK 通常 10 Hz | +| 加速度计量程 | ±4 g(动态驾驶)| 激烈机动用 ±16 g | +| 陀螺仪量程 | ±250°/s(慢速);±2000°/s(高动态)| — | +| PPS 精度 | < 100 ns | 高精度 GNSS 授时 | + +--- + +## 9. 性能指标与验收标准 + +| 指标 | 目标值 | +|------|--------| +| IMU 时间戳抖动 | < 1 ms | +| GNSS 定位精度(RTK Fix) | 水平 < 2 cm | +| Camera-IMU 时间偏差 | < 1 ms(Kalibr 标定后) | +| EKF 融合输出频率 | ≥ 100 Hz | + +--- + +## 10. 常见问题与排查步骤(Checklist) + +- [ ] IMU 数据全零 → 检查 I2C/SPI 地址;`i2cdetect -y 1` 扫描总线 +- [ ] 加速度计静态偏置大 → 重新标定零偏(6 面静止标定法) +- [ ] 时间戳不单调 → 检查时钟源;使用硬件时间戳替代软件时间戳 +- [ ] GNSS 无定位(Fix) → 检查天线视野(仰角 > 15°);信噪比 SNR > 35 dBHz +- [ ] Camera-IMU 外参误差大 → 增加激励运动幅度;检查标靶质量 +- [ ] EKF 发散 → 检查噪声参数(Allan 方差);初始化阶段静止时间是否足够 + +--- + +## 11. 参考资料 + +- [Xsens MTi 系列文档](https://www.xsens.com/products) +- [VectorNav IMU 文档](https://www.vectornav.com/docs) +- [Kalibr IMU-Camera 标定](https://github.com/ethz-asl/kalibr) +- [imu_utils Allan 方差](https://github.com/gaowenliang/imu_utils) +- [u-blox ZED-F9P RTK 模块](https://www.u-blox.com/en/product/zed-f9p-module) +- [chrony PPS 配置](https://chrony.tuxfamily.org/doc/4.3/chrony.conf.html) diff --git a/B_sensor_catalog/B5_ultrasonic.md b/B_sensor_catalog/B5_ultrasonic.md new file mode 100644 index 0000000..75168c9 --- /dev/null +++ b/B_sensor_catalog/B5_ultrasonic.md @@ -0,0 +1,285 @@ +# B5. 超声波 / 麦克风阵列接入指南 + +## 1. 这篇文章要解决什么问题? + +完成超声波测距传感器和麦克风阵列的驱动接入、数据解析、同步策略及常见问题排查。 + +--- + +## 2. 数据链路图 + +### 2.1 超声波传感器 +``` +[超声波换能器] + 发射 40 kHz 脉冲 → 接收回波 + │ +[接口层] + GPIO(Trig/Echo)/ I2C / CAN / UART + │ +[驱动 / 解析层] + 飞行时间(ToF)= 回波时间 / 2 × 声速 + │ +[数据字段] + 距离(m)、时间戳(ns)、传感器ID + │ +[后处理] + 多传感器融合、盲区处理、温度补偿 + │ +[上层应用] + 泊车辅助、防碰撞、近距离感知 +``` + +### 2.2 麦克风阵列 +``` +[麦克风阵列] + PDM / I2S / USB 音频 + │ +[音频驱动层] + ALSA / PortAudio / libusb + │ +[数字信号处理] + 波束成形(Beamforming)→ 降噪(NS)→ 声源定向(DOA) + │ +[输出] + 增强语音、声源方向角(方位/仰角) + │ +[上层应用] + 语音唤醒 / 识别 / 声源追踪 +``` + +--- + +## 3. 超声波传感器 + +### 3.1 接口类型对比 + +| 接口 | 典型传感器 | 特点 | +|------|-----------|------| +| GPIO Trig/Echo | HC-SR04 | 最简单,需要精确计时 | +| I2C | MB1242 | 多传感器共享总线 | +| UART | MaxSonar EZ4 | 连续输出,TTL 电平 | +| CAN | 车载 USS 模块 | 多传感器总线,OEM 首选 | +| PWM | Maxbotix | 脉宽编码距离 | + +### 3.2 GPIO 接口(HC-SR04) + +**硬件连接**: +``` +VCC → 5V +GND → GND +TRIG → MCU GPIO_OUT(10 µs 脉冲触发) +ECHO → MCU GPIO_IN(回波宽度 = 飞行时间) +``` + +**Linux GPIO 读取(libgpiod)**: +```c +#include +#include + +struct gpiod_chip *chip = gpiod_chip_open("/dev/gpiochip0"); +struct gpiod_line *trig = gpiod_chip_get_line(chip, 23); +struct gpiod_line *echo = gpiod_chip_get_line(chip, 24); + +gpiod_line_request_output(trig, "ultrasonic", 0); +gpiod_line_request_input(echo, "ultrasonic"); + +// 触发:10 µs 高脉冲 +gpiod_line_set_value(trig, 1); +usleep(10); +gpiod_line_set_value(trig, 0); + +// 等待回波上升沿 +struct timespec t_start, t_end; +while (gpiod_line_get_value(echo) == 0); +clock_gettime(CLOCK_MONOTONIC, &t_start); +while (gpiod_line_get_value(echo) == 1); +clock_gettime(CLOCK_MONOTONIC, &t_end); + +double elapsed_us = (t_end.tv_nsec - t_start.tv_nsec) / 1000.0 + + (t_end.tv_sec - t_start.tv_sec) * 1e6; +// 声速 343 m/s(20°C),单程距离 +double distance_m = elapsed_us * 1e-6 * 343.0 / 2.0; +``` + +**注意**:用户态 GPIO 时序抖动较大(> 10 µs),推荐使用 MCU 或 FPGA 进行精确计时。 + +### 3.3 I2C 接口(MB1242) +```python +import smbus2, time + +bus = smbus2.SMBus(1) +ADDR = 0x70 # MB1242 默认地址 + +def read_distance_cm(): + bus.write_byte(ADDR, 0x51) # 触发测量 + time.sleep(0.08) # 等待 80 ms + high = bus.read_byte_data(ADDR, 0xE1) + low = bus.read_byte_data(ADDR, 0xE2) + return (high << 8 | low) # 单位:cm + +while True: + dist = read_distance_cm() + print(f"距离: {dist / 100:.2f} m") + time.sleep(0.1) +``` + +### 3.4 温度补偿 +``` +声速 c(m/s)= 331.5 + 0.607 × T(°C) + +// 修正后距离 +distance_m = elapsed_s × c / 2 +``` + +### 3.5 多传感器防串扰(时分复用) +```python +# 依次触发各传感器,间隔足够时间防止串扰 +SENSORS = [0, 1, 2, 3, 4, 5, 6, 7] # 8 路超声波 +INTERVAL_MS = 25 # 每路间隔 25 ms + +for sensor_id in SENSORS: + trigger(sensor_id) + time.sleep(INTERVAL_MS / 1000.0) + dist = read_echo(sensor_id) + publish(sensor_id, dist) +``` + +--- + +## 4. 超声波性能指标与验收标准 + +| 指标 | 典型值 | 说明 | +|------|--------|------| +| 量程 | 0.02–4 m(HC-SR04);0.2–15 m(车载 USS) | 依型号 | +| 盲区 | 0.02–0.3 m | 近距离无法测量 | +| 精度 | ±1–3 mm(近距);±1%(远距) | 温度补偿后 | +| 更新率 | 10–50 Hz | 依测量周期 | +| 视角 | 15°–40°(半角) | 依换能器设计 | +| 温度范围 | -40°C–85°C | 车规级 | + +--- + +## 5. 麦克风阵列 + +### 5.1 接口类型对比 + +| 接口 | 协议 | 特点 | +|------|------|------| +| I2S | 数字 PCM | 嵌入式平台首选(Jetson / RPi) | +| PDM | Pulse Density Modulation | 单线,需 PDM→PCM 转换 | +| USB | UAC 2.0 | 即插即用(ReSpeaker USB) | +| Ethernet | AVB / AES67 | 专业音频,远距离 | + +### 5.2 ALSA 配置与采集(I2S / USB) +```bash +# 查看音频设备 +aplay -l +arecord -l + +# 录制测试(16 kHz,立体声,PCM16) +arecord -D hw:1,0 -f S16_LE -r 16000 -c 2 -d 5 test.wav + +# 查看实时电平 +alsamixer +``` + +### 5.3 Python 音频采集(PortAudio / PyAudio) +```python +import pyaudio, numpy as np + +SAMPLE_RATE = 16000 +CHANNELS = 4 # 4 路麦克风阵列 +CHUNK_SIZE = 1024 +FORMAT = pyaudio.paInt16 + +pa = pyaudio.PyAudio() +stream = pa.open( + format=FORMAT, channels=CHANNELS, + rate=SAMPLE_RATE, input=True, + frames_per_buffer=CHUNK_SIZE +) + +while True: + raw = stream.read(CHUNK_SIZE, exception_on_overflow=False) + data = np.frombuffer(raw, dtype=np.int16) + # data shape: (CHUNK_SIZE × CHANNELS,) → reshape → (CHUNK_SIZE, CHANNELS) + frames = data.reshape(-1, CHANNELS) + # 送入波束成形 / DOA 算法 + process_frames(frames) +``` + +### 5.4 波束成形(Delay-and-Sum,简化示例) +```python +import numpy as np + +def delay_and_sum_beamform(frames, mic_positions, target_angle_deg, + sample_rate=16000, sound_speed=343.0): + """ + frames: (N_samples, N_mics) + mic_positions: (N_mics, 2) 坐标(m) + """ + angle_rad = np.deg2rad(target_angle_deg) + direction = np.array([np.cos(angle_rad), np.sin(angle_rad)]) + + delays_s = -mic_positions @ direction / sound_speed # 每个麦克风的延迟 + delays_samples = np.round(delays_s * sample_rate).astype(int) + + output = np.zeros(frames.shape[0]) + for i, d in enumerate(delays_samples): + output += np.roll(frames[:, i], d) + return output / frames.shape[1] +``` + +### 5.5 声源定向(DOA,以 SRP-PHAT 为例) +```bash +# 使用 ODAS(Open embeddeD Audition System) +# https://github.com/introlab/odas +# 配置 odas.cfg 指定麦克风位置 +odaslive -c odas.cfg # 输出实时 DOA 角度(JSON 流) +``` + +--- + +## 6. 时间同步 + +### 6.1 音视频同步 +- 音频硬件时钟(ALSA `DMA_CLOCK`)与 PTP 之间需要 `phc2sys` 对齐; +- 记录每个音频块的 `capture_timestamp`: + +```python +import time +timestamp_ns = time.clock_gettime_ns(time.CLOCK_MONOTONIC) +# 或使用 ALSA `snd_pcm_status_get_htstamp` 获取硬件时间戳 +``` + +### 6.2 多阵列同步 +- 使用 AVB(Audio Video Bridging)或 AES67 协议统一时钟; +- 简单方案:所有阵列接同一 USB Hub,利用 USB SOF 同步。 + +--- + +## 7. 常见问题与排查步骤(Checklist) + +**超声波**: +- [ ] 距离为 0 或最大值 → 检查 TRIG 脉冲宽度(需 ≥ 10 µs);ECHO 线是否浮空 +- [ ] 距离抖动大 → 添加中位数滤波;检查反射面是否平整 +- [ ] 多传感器串扰 → 确认时分复用间隔足够(≥ 20 ms) +- [ ] 近距盲区问题 → 更换量程更小的型号(如 VL53L1X TOF 激光测距) +- [ ] 温度变化导致偏差 → 添加温度传感器,动态修正声速 + +**麦克风阵列**: +- [ ] 无音频输入 → `arecord -l` 确认设备;检查权限(`audio` 组) +- [ ] 采集有噪声/爆音 → 降低麦克风增益;检查电源纹波 +- [ ] DOA 角度不准 → 重新标定麦克风位置;检查延迟补偿 +- [ ] 音视频不同步(> 200 ms) → 检查音频缓冲区大小;时间戳对齐策略 + +--- + +## 8. 参考资料 + +- [HC-SR04 数据手册](https://cdn.sparkfun.com/datasheets/Sensors/Proximity/HCSR04.pdf) +- [libgpiod 文档](https://libgpiod.readthedocs.io/) +- [ODAS 声源定向系统](https://github.com/introlab/odas) +- [ReSpeaker 麦克风阵列](https://wiki.seeedstudio.com/ReSpeaker_6-Mic_Circular_Array_kit_for_Raspberry_Pi/) +- [ROS ultrasonic_sensor 驱动](https://github.com/ros-drivers/ultrasonic_sensor) +- [AES67 音频网络标准](https://www.aes.org/publications/standards/search.cfm?docID=96) diff --git a/C_template/article_template.md b/C_template/article_template.md new file mode 100644 index 0000000..3a5287d --- /dev/null +++ b/C_template/article_template.md @@ -0,0 +1,118 @@ +# 统一贡献模板 + +> 复制此文件并填写各节内容。未确定的内容请保留占位符(`TODO`),不要删除节标题。 + +--- + +## 文件命名规范 + +``` +B_sensor_catalog/<分类><编号>_<英文简名>.md +示例:B2_mmwave_radar.md +``` + +--- + +## 1. 这篇文章要解决什么问题? + +> 用 2–4 句话描述背景与目标。 +> - 覆盖哪个传感器/模块? +> - 要解决接入流程中的哪个阶段? +> - 读完这篇文章,读者能独立完成哪些操作? + +TODO + +--- + +## 2. 数据链路图(从物理线到应用) + +> 使用 ASCII 或 Mermaid 图描述完整数据流,至少包含: +> 物理层 → 驱动层 → 数据格式层 → 后处理 → 上层应用 + +``` +[传感器硬件] + │ (接口类型) + ▼ +[驱动层] + │ + ▼ +[数据格式层] + │ + ▼ +[后处理/算法] + │ + ▼ +[上层应用] +``` + +--- + +## 3. 关键接口 / 协议 / 格式(列清单) + +> 列举本文涉及的所有接口、协议和数据格式,附简要说明。 + +| 接口 / 协议 / 格式 | 说明 | 参考章节 | +|-------------------|------|---------| +| TODO | TODO | TODO | + +--- + +## 4. 关键参数与默认值(表格) + +> 列出所有可配置参数,包含推荐值和说明。 + +| 参数名 | 推荐值 / 范围 | 单位 | 说明 | +|--------|--------------|------|------| +| TODO | TODO | TODO | TODO | + +--- + +## 5. 性能指标与验收标准 + +> 列出可量化的性能指标和对应的验收阈值,以及检测方法。 + +| 指标 | 目标值 | 检测方法 | +|------|--------|----------| +| TODO | TODO | TODO | + +--- + +## 6. 常见问题与排查步骤(Checklist) + +> 列举接入过程中最常见的问题和对应的排查步骤。 +> 使用 Checklist 格式,方便逐项排查。 + +- [ ] 问题 1:TODO → 排查步骤:TODO +- [ ] 问题 2:TODO → 排查步骤:TODO +- [ ] 问题 3:TODO → 排查步骤:TODO + +--- + +## 7. 参考资料 / 链接 / 抓包样例 / 配置片段 + +> 列出所有引用的文档、工具和外部链接。 + +- [文档名称](URL) +- [工具名称](URL) + +--- + +## 附录(可选) + +### A. 完整配置文件示例 + +```yaml +# TODO: 粘贴完整配置文件 +``` + +### B. 抓包 / 抓帧示例 + +```bash +# TODO: 相关命令示例 +``` + +### C. 相关术语 + +| 术语 | 解释 | +|------|------| +| TODO | TODO | diff --git a/README.md b/README.md index 3abfa84..0b8c46c 100644 --- a/README.md +++ b/README.md @@ -1 +1,87 @@ -# sensor_repository \ No newline at end of file +# 传感器接入知识库(Sensor Integration Repository) + +> 沉淀**接入流程、数据链路、编码传输、标定与同步、驱动与诊断、性能与稳定性**等可复用知识。 + +## 覆盖传感器类型 + +| 传感器 | 接口 | +|--------|------| +| 相机(Camera) | USB UVC、MIPI CSI-2、GMSL2、GigE Vision、Ethernet(RTSP/ONVIF) | +| 毫米波雷达(mmWave Radar) | CAN、Ethernet、UART | +| 激光雷达(LiDAR) | UDP/Ethernet | +| IMU / GNSS | SPI、I2C、UART、CAN | +| 超声波 / 麦克风阵列 | GPIO、I2C、USB | + +--- + +## 目录结构 + +``` +sensor_repository/ +├── README.md ← 本文件(导航入口) +│ +├── A_camera_pipeline/ ← 相机 → 编码 → 传输端到端流程 +│ ├── A1_physical_link.md ← 物理与链路层(Camera → Host) +│ ├── A2_image_pipeline.md ← 图像链路(ISP / 预处理) +│ ├── A3_encoding.md ← 编码(H.264 / H.265) +│ ├── A4_transport.md ← 传输协议(RTP/RTSP/WebRTC/HTTP) +│ ├── A5_clock_sync.md ← 时钟同步与触发(GPIO / PTP) +│ ├── A6_decode_render.md ← 解码、渲染与下游算法 +│ └── A7_diagnostics.md ← 诊断与可观测性 +│ +├── B_sensor_catalog/ ← 传感器分类知识库 +│ ├── B1_camera.md ← 相机全流程 +│ ├── B2_mmwave_radar.md ← 毫米波雷达 +│ ├── B3_lidar.md ← 激光雷达 +│ ├── B4_imu_gnss.md ← IMU / GNSS +│ └── B5_ultrasonic.md ← 超声波 / 麦克风阵列 +│ +└── C_template/ + └── article_template.md ← 统一贡献模板 +``` + +--- + +## 贡献方式 + +每篇内容建议按以下结构组织(详见 [C_template/article_template.md](C_template/article_template.md)): + +1. 这篇文章要解决什么问题? +2. 数据链路图(从物理线到应用) +3. 关键接口 / 协议 / 格式(列清单) +4. 关键参数与默认值(表格) +5. 性能指标与验收标准 +6. 常见问题与排查步骤(Checklist) +7. 参考资料 / 链接 / 抓包样例 / 配置片段 + +--- + +## 统一术语表 + +| 术语 | 说明 | +|------|------| +| MIPI CSI-2 | Mobile Industry Processor Interface Camera Serial Interface 2 | +| GMSL2 | Gigabit Multimedia Serial Link 2(车载长距离相机接口) | +| GigE Vision | 基于千兆以太网的工业相机标准 | +| H.264 / AVC | Advanced Video Coding | +| H.265 / HEVC | High Efficiency Video Coding | +| RTP | Real-time Transport Protocol | +| RTSP | Real Time Streaming Protocol | +| WebRTC | Web Real-Time Communication | +| HLS | HTTP Live Streaming | +| HTTP-FLV | HTTP + FLV 格式的流媒体 | +| PTP / IEEE 1588 | Precision Time Protocol(精确时间协议) | +| GPIO / TTL | 硬触发同步信号 | +| CAN | Controller Area Network | +| V4L2 | Video4Linux2(Linux 视频子系统) | +| DMABUF | DMA Buffer(零拷贝内存共享) | +| NVENC | NVIDIA 硬件视频编码器 | +| V4L2 M2M | V4L2 Memory-to-Memory(平台硬编码器接口) | + +--- + +## 快速导航 + +- **相机端到端流程** → [A_camera_pipeline/](A_camera_pipeline/) +- **各类传感器接入** → [B_sensor_catalog/](B_sensor_catalog/) +- **写文章用的模板** → [C_template/article_template.md](C_template/article_template.md) \ No newline at end of file From bc8d061672b50be9bf6a520297fb1af8fe5cd91a Mon Sep 17 00:00:00 2001 From: Y-ouch <792888359@qq.com> Date: Sat, 25 Apr 2026 15:06:17 +0800 Subject: [PATCH 03/14] Create Sensor HAL --- Sensor HAL | 2960 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2960 insertions(+) create mode 100644 Sensor HAL diff --git a/Sensor HAL b/Sensor HAL new file mode 100644 index 0000000..5eac412 --- /dev/null +++ b/Sensor HAL @@ -0,0 +1,2960 @@ +# Sensor HAL 详细设计文档 + +> 版本: V0.3.2 | 日期: 2026-04-28 | 状态: 草稿 +> 基线: V0.3.1 + 双厂商对比分析 (OrbbecSDK_ROS2 v2.7.6 vs realsense-ros 4.57.7) +> 版本序列: Notion V0.1 → V0.2 → V0.3 → V0.3.1 → **V0.3.2 (本版)** +> +> **V0.3.2 变更记录 (相对 V0.3.1):** +> - §2.1 新增: `StreamIndex{StreamType, int}` — 统一流标识符 (双厂共识) +> - §2.1 OptionRange → `OptionInfo` 升级: 新增 `OptionType` 枚举 + `enum_values` + `description` +> - §2.1 `ImageFrame` 新增 `TimestampDomain` 字段 + 帧元数据 (frame_number/actual_exposure/actual_gain) +> - §2.1 `ICameraHAL` 新增 `getSyncManager()` — 单设备硬件同步配置入口 +> - §2.1A `CameraIntrinsics` 新增 `DistortionModel` 枚举 (BrownConrady/KannalaBrandt4 鱼眼) +> - §2.1A 新增 `IMUCalibration` 结构体 (scale_bias矩阵, 噪声参数) +> - **§2.11 新增: `ISyncManager` 接口** — 单设备 7 种硬件同步模式 + SyncConfig 延迟参数 +> - §2.7 新增: `IDecoder/IDecoderFactory` — 平台解码器抽象 (libjpeg/Jetson NV/RK MPP) +> - §6.1 HALFactory 新增: `enableProcessLock()` — POSIX 共享内存互斥锁 (多进程安全) +> - §7.4 新增条目 15-19 +> - **§7.5 新增**: ParameterProvider 自动映射 / TF 光学帧四元数约定 / IMU 软件对齐 / 图像后处理归属 +> +> **V0.3.1 变更记录 (相对 V0.3):** +> - ICameraHAL 新增: getSupportedProfiles(), getExtrinsics(from,to), getIntrinsics(stream) +> - ICameraHAL 新增: 运行时硬件选项 (OptionRange/getOption/setOption) +> - ICameraHAL 新增: FrameSet 回调 (单设备内 Color+Depth 对齐帧组) +> - ICameraHAL 新增: StreamProfile / StreamType 枚举 +> - HALFactory 新增: DeviceInfo 结构 + enumerateDevices() + setDeviceChangedCallback() +> - **§2.10 ISyncCoordinator 层级修正: HAL → Module 层** (基于 RMOS V5 架构复核) +> - **§7 新增 上层需求清单**: Module / ArcRT Channel / rm_alg_foundation 14 项需求 +> +> **V0.3 变更记录 (相对 V0.2):** +> - 新增 §2.1A PixelEncoding 独立枚举 (24 种) +> - §2.6.1 OrbbecCameraHAL 映射升级为 API 代码级精度 +> - §2.6.2 GMSL 方案完全重写: 基于 OrbbecSDK + /dev/camsync 硬件触发 (非独立 V4L2) +> - §2.6.2 新增 IGmslTrigger 平台抽象层 (Orin NX / S100 / Sim) +> - §2.6.4 新增 UsbCameraHAL (V4L2) 独立映射 +> - §2.6.5 新增 SimCameraHAL 实现规范 +> - §2.6.6 新增 Adapter 对比总表 +> - §2.10 新增 多相机同步架构 (ISyncCoordinator + SyncPolicy) +> - CameraConfig 补充 depth_width/height, align_mode, ring_buffer_depth, color_encoding 等字段 +> - 修正 ImuCallback 签名不一致 (shared_ptr → const ref) +> - §3.4 修正 BlueSea CRC16 → STM32 CRC32 +> - §3 补充完整 LidarConfig 字段、BlueSea 6 种帧头格式、FanAssembler 算法 +> - §4 补充 YESENSE TLV 完整字节级协议、12+ DataID 缩放因子、Fletcher CRC +> - §5 从骨架升级为完整设计 (IAudioHAL 接口/数据结构/ALSA 映射/线程模型/CMake) + +--- + +## 目录 + +- §1 概述 +- §2 Camera HAL 详细设计 + - §2.1 ICameraHAL 接口 + - §2.1A PixelEncoding 独立枚举 (**V0.3 新增**) + - §2.2 数据结构 (**V0.3 补充**) + - §2.3 状态机 + - §2.4 ErrorCode 定义 + - §2.5 线程与并发模型 + - §2.6 Adapter→SDK 实现映射 + - §2.6.1 OrbbecCameraHAL (USB) — 代码级 API 映射 + - §2.6.2 OrbbecGmslCameraHAL (GMSL2) — OrbbecSDK + /dev/camsync + **IGmslTrigger 平台抽象** + - §2.6.3 RealsenseCameraHAL + - §2.6.4 UsbCameraHAL (V4L2 通用) (**V0.3 新增**) + - §2.6.5 SimCameraHAL (**V0.3 新增**) + - §2.6.6 Adapter 对比总表 (**V0.3 新增**) + - §2.7 CMake target 与文件布局 + - §2.8 异常与错误场景 + - §2.9 单测要点 + - **§2.10 多相机同步架构 (V0.3 新增)** + - **§2.11 ISyncManager — 单设备硬件同步接口 (V0.3.2 新增)** +- §3 LiDAR HAL 详细设计 +- §4 IMU HAL 详细设计 +- §5 Audio HAL 详细设计 +- §6 横切面: 工厂 / Sim / 性能 / Profile / 测试 +- **§7 上层需求清单 — 基于 realsense-ros 对标分析 (V0.3.1 新增)** + - §7.1 Module Library 层需求 (ISyncCoordinator / FilterPipeline / TF / 参数 / Lifecycle / Diagnostics) + - §7.2 ArcRT Channel 层需求 (跨品类 message_filter 时间对齐) + - §7.3 rm_alg_foundation 层需求 (深度滤波 / 点云生成 / 坐标变换) + - §7.4 层次归属总表 (14 项需求 × 优先级) + - **§7.5 新增 Module 层最佳实践 (V0.3.2 新增)**: ParameterProvider 自动映射 / TF 四元数约定 / IMU 软件对齐 / 图像后处理 + +--- + +## §1 概述 + +Sensor HAL 负责所有传感器设备的数据采集抽象。本文档为 Notion V0.3 版本,补全了 V0.2 中所有 TODO 章节,新增 GMSL 平台抽象层和多相机同步架构。 + +**接口继承体系:** +``` +IHardwareDevice (§4.0.5) + └── ISensorHAL (§4.1) + ├── ICameraHAL + ├── IImuHAL + ├── ILidarHAL + └── IAudioHAL +``` + +**子页与主文档关系:** +- Camera §2.1-§2.3: 以 Notion 子页 V0.2 为真源(本文摘要引用) +- Camera §2.4-§2.9: 本文为补充草稿,待同步回 Notion +- LiDAR §3 / IMU §4: 本文为首次详细设计,待同步回 Notion + +--- + +## §2 Camera HAL 详细设计 + +### §2.1-§2.3 接口/数据结构/状态机 (摘要) + +> 完整内容见 Notion Sensor HAL详设 V0.2 §2.1-§2.3。以下仅列关键要素供本地参考。 + +**ICameraHAL 核心方法:** +```cpp +class ICameraHAL : public ISensorHAL { +public: + virtual bool configure(const CameraConfig& config) = 0; + // open() / close() / isOpen() / health() 继承自 IHardwareDevice + virtual bool startStreaming() = 0; + virtual bool stopStreaming() = 0; + virtual bool getColorFrame(ImageFrame& out) = 0; + virtual bool getDepthFrame(ImageFrame& out) = 0; + virtual bool getIRFrame(ImageFrame& out) = 0; + virtual bool getPointCloud(PointCloud& out) = 0; + virtual CameraIntrinsics getColorIntrinsics() const = 0; + virtual CameraIntrinsics getDepthIntrinsics() const = 0; + virtual CameraExtrinsics getExtrinsics() const = 0; // [补充] + using FrameCallback = std::function)>; + virtual void setColorCallback(FrameCallback cb) = 0; + virtual void setDepthCallback(FrameCallback cb) = 0; + virtual void setIRCallback(FrameCallback cb) = 0; + + // ==== V0.3.1 新增: 基于 realsense-ros 对标分析 ==== + + // --- Profile 查询 --- + /// 查询设备支持的所有流配置 (分辨率/帧率/格式 组合) + virtual std::vector getSupportedProfiles() const = 0; + + // --- 扩展标定参数查询 --- + /// 获取任意两流之间的外参 (旋转+平移) + virtual CameraExtrinsics getExtrinsics(StreamType from, StreamType to) const = 0; + /// 获取指定流的内参 + virtual CameraIntrinsics getIntrinsics(StreamType stream) const = 0; + + // --- 运行时硬件选项 (曝光/增益/白平衡等) --- + // V0.3.2: OptionRange 升级为 OptionInfo (见 §2.1A), 接口签名同步更新 + /// 获取所有支持选项的完整描述列表 (含类型/范围/默认值/枚举表) + virtual std::vector getSupportedOptions() const = 0; + /// 获取单个选项完整描述 (V0.3.2 替代旧 getOptionRange) + virtual OptionInfo getOptionInfo(const std::string& name) const = 0; + virtual float getOption(const std::string& name) const = 0; + virtual bool setOption(const std::string& name, float value) = 0; + + // --- FrameSet 回调 (单设备内的 Color+Depth 对齐帧组) --- + struct FrameSet { + std::shared_ptr color; + std::shared_ptr depth; + std::shared_ptr ir; // nullable + uint64_t timestamp_ns; + }; + using FrameSetCallback = std::function; + virtual void setFrameSetCallback(FrameSetCallback cb) = 0; + + // ==== V0.3.2 新增: 基于双厂商对比分析 ==== + + // --- 单设备硬件同步配置入口 (单设备内 PRIMARY/SECONDARY/SOFTWARE_TRIGGER 等) --- + /// 获取单设备硬件同步管理器 (若设备不支持则返回 nullptr) + virtual std::shared_ptr getSyncManager() = 0; +}; +``` + +**StreamIndex / StreamProfile / StreamType (V0.3.1/V0.3.2):** +```cpp +/// 流类型枚举 (V0.3.2 扩展) +enum class StreamType : uint8_t { + COLOR, DEPTH, IR_LEFT, IR_RIGHT, FISHEYE, + GYRO, ACCEL, MOTION, // IMU 流 + LIDAR, LASER_SCAN, // LiDAR 流 + UNKNOWN = 0xFF, +}; + +/// 统一流标识符 — 区分同类型多路流 (V0.3.2 新增) +/// 双厂商均使用 stream_index_pair = pair 作为全局 key +/// index 用于区分: IR_LEFT(0)/IR_RIGHT(0), COLOR_LEFT(0)/COLOR_RIGHT(1) 等 +struct StreamIndex { + StreamType type = StreamType::UNKNOWN; + int index = 0; // 0 = 默认 (绝大多数单路流) + + bool operator==(const StreamIndex& o) const { return type == o.type && index == o.index; } + bool operator<(const StreamIndex& o) const { + return type < o.type || (type == o.type && index < o.index); + } +}; + +/// 时间戳域 (V0.3.2 新增) — 避免不同时钟域混用 +/// Orbbec 有 3 种: device(硬件时钟) / system(主机时钟) / global(SDK 对齐后) +enum class TimestampDomain : uint8_t { + Hardware, // 设备硬件时钟 (最准, 但需与主机对时) + System, // 主机系统时钟 (接收时打戳) + Global, // SDK 对齐后的统一时钟 (若 SDK 支持 PTP) +}; + +/// 单条流配置描述 (V0.3.2: stream 字段换为 StreamIndex) +struct StreamProfile { + StreamIndex stream; // 替换原 StreamType stream; index 默认 0 + int width; + int height; + int fps; + PixelEncoding format; + + bool exactMatch(const StreamProfile& o) const; + bool partialMatch(const StreamProfile& o) const; // 0 表示通配 +}; + +/// 硬件选项类型 (V0.3.2 新增) +enum class OptionType : uint8_t { Bool, Int, Float, Enum }; + +/// 硬件选项描述 (V0.3.2: OptionRange 升级为 OptionInfo) +/// 新增: OptionType / description / enum_values (离散选项用) +struct OptionInfo { + std::string name; + std::string description; // 人类可读说明 (来自 SDK option description) + OptionType type = OptionType::Float; + float min = 0.0f; + float max = 0.0f; + float step = 0.0f; + float default_value = 0.0f; + bool is_readonly = false; + std::map enum_values; // 仅 Enum 类型有效 (如 sync_mode→{FreeRun:0, Primary:1,...}) +}; + +/// ImageFrame 时间戳域字段 (V0.3.2 新增到原 ImageFrame 结构) +/// 在现有 ImageFrame 中补充以下字段: +// uint64_t timestamp_ns; // 已有 +// TimestampDomain timestamp_domain; // V0.3.2 新增: Hardware/System/Global +// uint64_t frame_number; // V0.3.2 新增: 设备帧序号 (单调递增) +// float actual_exposure_us; // V0.3.2 新增: 实际曝光时间 (μs, 0=未知) +// float actual_gain; // V0.3.2 新增: 实际增益 (dB, 0=未知) +// bool auto_exposure_enabled; // V0.3.2 新增: 当前 AE 状态 +``` + +**5 态状态机:** +``` +Closed ──configure()──→ Configured ──open()──→ Opened ──startStreaming()──→ Streaming + ↑ │ + └──────────────────────close()──────────────────────────────────────────────┘ + 任意状态 ──fault──→ Faulted ──reset()──→ Closed +``` + +--- + +### §2.1A PixelEncoding 独立枚举 (V0.3 新增) + +> **问题**: 主文档 V3.0 的 `ImageFrame::Encoding` 仅 8~12 值且嵌套在 struct 内。 +> Notion V0.2 定义了独立 `enum class PixelEncoding` 共 24 种。应统一采用 Notion 版本。 + +```cpp +// rm_hal_sensor/interface/include/rm_hal_sensor/pixel_encoding.hpp +#pragma once +#include + +namespace rm::hal::sensor { + +enum class PixelEncoding : uint8_t { + // === 彩色 (0x00~0x1F) === + RGB8 = 0x00, + BGR8 = 0x01, + RGBA8 = 0x02, + BGRA8 = 0x03, + YUYV = 0x04, // YUV422 packed + UYVY = 0x05, // YUV422 packed + NV12 = 0x06, // YUV420 semi-planar + NV21 = 0x07, // YUV420 semi-planar (Android) + I420 = 0x08, // YUV420 planar + M420 = 0x09, // YUV420 variant + + // === 灰度 (0x20~0x2F) === + MONO8 = 0x20, // 8-bit 灰度 (IR) + MONO16 = 0x21, // 16-bit 灰度 + + // === 深度 (0x30~0x3F) === + Z16 = 0x30, // 16-bit 深度 (mm) + Z32F = 0x31, // 32-bit float 深度 (m) + + // === 压缩 (0x40~0x4F) === + MJPEG = 0x40, + H264 = 0x41, + H265 = 0x42, + HEVC = H265, + + // === 红外 (0x50~0x5F) === + Y8 = 0x50, // 等同 MONO8,SDK 偏好名 + Y16 = 0x51, // 等同 MONO16 + + // === 特殊 (0xF0~0xFF) === + RAW16 = 0xF0, // Bayer 原始 16-bit + CUSTOM = 0xFF, +}; + +/// PixelEncoding → 可读字符串 +const char* pixelEncodingToString(PixelEncoding enc); +/// 每像素字节数 (压缩格式返回 0) +int bytesPerPixel(PixelEncoding enc); +/// 判断是否为压缩格式 +inline bool isCompressed(PixelEncoding enc) { + return enc >= PixelEncoding::MJPEG && enc <= PixelEncoding::H265; +} + +} // namespace rm::hal::sensor +``` + +**ImageFrame 相应修改**: 删除内嵌 `Encoding` enum,改用顶层 `PixelEncoding`: +```cpp +struct ImageFrame { + PixelEncoding encoding = PixelEncoding::BGR8; // 替换原嵌套 enum + // ... 其余字段不变 +}; +``` + +**CameraConfig 补充字段 (V0.3)**: + +**CameraIntrinsics 扩展 / DistortionModel / IMUCalibration (V0.3.2 新增):** +```cpp +// rm_hal_sensor/interface/include/rm_hal_sensor/calibration_types.hpp +namespace rm::hal::sensor { + +/// 畸变模型枚举 (V0.3.2 新增) +/// 两家厂商使用的畸变模型各有不同, HAL 统一标注 +enum class DistortionModel : uint8_t { + None, + BrownConrady, // 5/8 参数 plumb_bob / opencv (径向+切向), 两家均支持 + InverseBrownConrady, // RealSense D4xx 深度流使用 + KannalaBrandt4, // 鱼眼镜头 (4 参数), fisheye 模组 +}; + +/// 相机内参 (V0.3.2: 增加 distortion_model 字段) +struct CameraIntrinsics { + float fx = 0, fy = 0; // 焦距 (像素) + float cx = 0, cy = 0; // 主点 + int width = 0, height = 0; + DistortionModel distortion_model = DistortionModel::BrownConrady; + float distortion_coeffs[8] = {0}; // k1,k2,p1,p2,k3[,k4,k5,k6] or fisheye k1~k4 +}; + +/// IMU 标定参数 (V0.3.2 新增) +/// 来源: realsense-ros IMU calibration / Orbbec IMU params +/// 由 ICameraHAL::getIMUCalibration(stream_type) 返回 +struct IMUCalibration { + StreamType stream = StreamType::GYRO; // GYRO 或 ACCEL + float scale_bias[12] = {0}; // 3×4 矩阵 (3×3 scale + 3×1 bias), 行主序 + float noise_variances[3] = {0}; // 量测噪声方差 [x,y,z] (用于 EKF/UKF) + float bias_variances[3] = {0}; // 偏置随机游走方差 [x,y,z] + bool valid = false; // SDK 是否提供了有效标定 +}; + +/// 深度元数据 (V0.3.2 新增) +/// 由 ICameraHAL::getDepthMetadata() 返回 +struct DepthMetadata { + float depth_scale; // 1 LSB 对应的物理距离 (米), Orbbec 通常 0.001 + float depth_min_meters; // 有效量程下界 (米) + float depth_max_meters; // 有效量程上界 (米) +}; + +} // namespace rm::hal::sensor +``` + +**ICameraHAL 标定查询方法 (V0.3.2 增补, 在 getIntrinsics/getExtrinsics 之后):** +```cpp + // --- 扩展标定查询 (V0.3.2 增补) --- + /// 获取 IMU 传感器标定参数 (设备有内置 IMU 时有效) + virtual IMUCalibration getIMUCalibration(StreamType imu_stream) const = 0; + /// 获取深度量程元数据 (depth_scale / depth_min / depth_max) + virtual DepthMetadata getDepthMetadata() const = 0; + /// 加载用户自定义标定文件 (YAML, 覆盖出厂标定) + virtual bool loadUserCalibration(const std::string& yaml_path) = 0; + /// 导出当前标定到 YAML 字符串 + virtual std::string exportCalibration() const = 0; +``` + +```cpp +struct CameraConfig { + // ==== 原有字段 ==== + std::string device_id; + int width = 1280, height = 720; + int fps = 30; + bool enable_color = true; + bool enable_depth = true; + bool enable_ir = false; + std::string serial_number; + std::unordered_map extra_params; + + // ==== V0.3 新增字段 ==== + PixelEncoding color_encoding = PixelEncoding::BGR8; // 彩色流像素格式 + int depth_width = 640; // 深度流分辨率 (可与彩色不同) + int depth_height = 480; + int depth_fps = 30; + int ring_buffer_depth = 4; // 内部帧队列深度 + std::string align_mode; // "none" / "depth_to_color" / "color_to_depth" + std::string frame_aggregate_mode = "ANY"; // "full_frame"/"color_frame"/"ANY"/"disable" + + // GMSL 专用 + std::string usb_port; // GMSL 通道标识 "gmsl2-1" / "gmsl2-3" 等 + bool enable_gmsl_trigger = false; + int gmsl_trigger_fps = 3000; // GMSL 硬件触发频率 + std::string sync_mode = "free_run"; // "free_run"/"primary"/"secondary"/"hardware_triggering" + + // V4L2 通用 + std::string v4l2_node; // "/dev/video0" (仅 UsbCameraHAL 使用) +}; +``` + +--- + +### §2.4 ErrorCode 定义 + +> **补全 Notion V0.2 §2.4 TODO** + +HAL 层统一错误码。V3.4 各接口暂仍返回 `bool`,V3.5 规划切换为 `ErrorCode`。本节提前定义枚举,Driver 开发时可内部使用,待 V3.5 正式切换。 + +```cpp +// rm_hal_common/include/rm_hal_common/error_code.hpp +#pragma once +#include + +namespace rm::hal { + +enum class ErrorCode : int32_t { + // === 通用 (0~99) === + OK = 0, + UNKNOWN = 1, + NOT_IMPLEMENTED = 2, + + // === 设备生命周期 (100~199) === + DEVICE_NOT_FOUND = 100, // 设备未发现(枚举/序列号不匹配) + DEVICE_BUSY = 101, // 设备被其他进程占用 + DEVICE_DISCONNECTED = 102, // 运行中设备断开(热拔) + INVALID_STATE = 103, // 当前状态不允许该操作(如 Closed 态调 startStreaming) + ALREADY_OPEN = 104, // 重复 open() + NOT_OPEN = 105, // 未 open() 就调用数据方法 + + // === 配置 (200~299) === + INVALID_CONFIG = 200, // 配置参数校验失败 + UNSUPPORTED_FORMAT = 201, // 请求的 PixelEncoding 设备不支持 + UNSUPPORTED_RESOLUTION = 202, + UNSUPPORTED_FPS = 203, + + // === 数据/IO (300~399) === + TIMEOUT = 300, // 数据获取/指令发送超时 + IO_ERROR = 301, // 底层 IO 错误(串口/USB/网络/CAN) + FRAME_DROPPED = 302, // 帧丢失(ring buffer 溢出或 SDK 丢帧) + CRC_ERROR = 303, // 协议 CRC 校验失败 + BUFFER_OVERFLOW = 304, // 内部缓冲区溢出 + + // === SDK/驱动 (400~499) === + SDK_ERROR = 400, // 厂商 SDK 返回错误(详情在 error_msg) + SDK_NOT_INITIALIZED = 401, // SDK 未初始化 + FIRMWARE_MISMATCH = 402, // 固件版本不兼容 + + // === 权限/资源 (500~599) === + PERMISSION_DENIED = 500, // 设备权限不足(如 /dev/video* 无权限) + RESOURCE_EXHAUSTED = 501, // 系统资源耗尽(fd/memory/GPU) +}; + +/// 错误码转可读字符串 +const char* errorCodeToString(ErrorCode code); + +/// 扩展错误信息 +struct ErrorInfo { + ErrorCode code = ErrorCode::OK; + std::string message; // 人类可读描述 + std::string sdk_error_detail; // 厂商 SDK 原始错误(可选) +}; + +} // namespace rm::hal +``` + +**使用约定:** +- M1 阶段:Driver 内部使用 ErrorCode 做日志和诊断,对外接口仍返回 `bool` +- V3.5 切换:`bool open()` → `ErrorCode open()` 或 `ErrorInfo open()` +- Driver 实现需在 `health().error_msg` 中填充最后一次错误的文本描述 + +--- + +### §2.5 线程与并发模型 + +> **补全 Notion V0.2 §2.5 TODO** + +#### 2.5.1 线程角色 + +Camera HAL 实现内部涉及以下线程角色: + +| 线程 | 归属 | 职责 | 生命周期 | +|------|------|------|---------| +| **SDK 回调线程** | 厂商 SDK (OrbbecSDK/librealsense2) | 帧数据到达时触发回调 | SDK Pipeline start ~ stop | +| **解码线程** (可选) | HAL Driver 内部 | MJPEG/H264 → RGB 解码 | startStreaming ~ stopStreaming | +| **用户回调线程** | HAL Driver 内部 | 执行用户注册的 FrameCallback | startStreaming ~ stopStreaming | +| **调用者线程** | Module 层 | 调用 getColorFrame 等轮询方法 | 由调用者控制 | + +#### 2.5.2 数据流线程模型 + +``` +SDK 回调线程 ──push──→ [内部帧队列 (bounded)] ──pop──→ 用户回调线程 ──调用──→ FrameCallback + │ + └── 同时更新 ring buffer (供轮询) +``` + +**关键设计决策:** + +1. **SDK 回调线程不直接执行用户回调**: SDK 线程持有内部锁,直接回调可能导致死锁(用户回调内调 HAL 方法)。必须转移到独立的用户回调线程。 + +2. **内部帧队列有界**: 队列深度 = `CameraConfig::ring_buffer_depth`(默认 4)。队列满时丢弃最旧帧(非阻塞生产者),防止 SDK 回调线程被阻塞。 + +3. **回调与轮询互斥**: 同一流(color/depth/IR)上设置了 callback 时,`getXxxFrame()` 轮询方法返回 false(INVALID_STATE)。未设置 callback 时使用 ring buffer 轮询模式。 + +4. **shared_ptr 帧生命周期**: 回调传递 `shared_ptr`,接收方持有即可延长生命期。 + +#### 2.5.3 锁策略 + +| 锁 | 类型 | 保护对象 | 持有时机 | +|---|------|---------|---------| +| `state_mutex_` | `std::mutex` | 状态机转移 (configure/open/close/start/stop) | 生命周期方法调用期间 | +| `frame_queue_mutex_` | `std::mutex` | 内部帧队列 push/pop | 帧入队/出队时短暂持有 | +| `config_mutex_` | `std::mutex` | CameraConfig 读写 | configure() 和运行时参数查询 | + +**不需要锁的场景:** +- `health()`: 返回原子读取的 HealthStatus,无需加锁 +- `deviceId()`: 返回 configure() 时确定的不可变字符串 +- `isOpen()`: 读取 atomic + +#### 2.5.4 线程安全合同 + +- **ICameraHAL 实例不是线程安全的**: 调用者(Module 层)需确保不并发调用同一实例的方法 +- **例外**: `health()` / `deviceId()` / `isOpen()` 可从任意线程安全调用 +- **回调函数在专用线程执行**: 用户的 FrameCallback 在 HAL 内部的用户回调线程中被调用,不在 SDK 线程中 +- **回调函数内不得调用同一 HAL 实例的方法**: 避免重入死锁 + +#### 2.5.5 与 OrbbecSDK 线程模型的对齐 + +OrbbecSDK 的实际线程模型: +``` +SDK 内部 Pipeline 线程 ──onNewFrameSetCallback──→ SDK 回调线程 + │ │ + │ (颜色帧需要解码时) │ + └── colorFrameThread_(消费者线程) ── 独立线程做解码 +``` + +HAL 封装策略: +- OrbbecCameraHAL 在 `onNewFrameSetCallback` 中将 `shared_ptr` 转换为 `ImageFrame` +- 如果帧格式为 MJPEG 需要解码,在解码线程中完成后再入队 +- 转换后的 `ImageFrame` 入队到 HAL 内部队列 + +--- + +### §2.6 Adapter→SDK 实现映射 + +> **补全 Notion V0.2 §2.6 TODO** + +#### 2.6.1 OrbbecCameraHAL (USB) — Adapter 映射表 + +| ICameraHAL 方法 | OrbbecSDK API | 说明 | +|----------------|--------------|------| +| `configure(CameraConfig)` | `ob::Pipeline()` + `ob::Config::enableStream()` + `Config::setAlignMode()` + `Config::setFrameAggregateOutputMode()` | 创建 Pipeline,按 CameraConfig 中的分辨率/帧率/格式配置各流 | +| `open()` | `context->queryDeviceList()` + `deviceList->getDevice(idx)` | 按 device_id (序列号) 枚举并打开设备 | +| `close()` | `pipeline->stop()` + 释放 device/pipeline | 停止数据流并释放资源 | +| `startStreaming()` | `pipeline->start(config, callback)` | 启动 Pipeline,注册 `onNewFrameSetCallback` | +| `stopStreaming()` | `pipeline->stop()` | 停止 Pipeline | +| `getColorFrame()` | 从内部 ring buffer 取最新帧 | SDK 回调→转换→ring buffer,轮询方式 | +| `getDepthFrame()` | 同上 | depth 流 | +| `getIRFrame()` | 同上 | IR/IR_LEFT/IR_RIGHT 流 | +| `getPointCloud()` | `ob::PointCloudFilter::process(frameset)` | 使用 SDK 内置 PointCloudFilter | +| `getColorIntrinsics()` | `device->getCameraIntrinsics(OB_SENSOR_COLOR)` | 返回 fx/fy/cx/cy | +| `getDepthIntrinsics()` | `device->getCameraIntrinsics(OB_SENSOR_DEPTH)` | 同上 | +| `getExtrinsics()` | `device->getCalibrationCameraToCamera(src, dst)` | Rotation[9] + Translation[3] | +| `setColorCallback()` | 内部路由:帧入队后通知用户回调线程 | 见 §2.5 线程模型 | +| `health()` | 内部维护 `alive` / `data_rate_hz` / `error_msg` | alive = Pipeline 运行中 && 最近 2s 内有帧 | +| `deviceId()` | `device->getDeviceInfo()->serialNumber()` | 设备序列号 | + +**PixelEncoding 映射:** + +| PixelEncoding (HAL) | ob::OBFormat (SDK) | 说明 | +|---------------------|-------------------|------| +| RGB8 | OB_FORMAT_RGB | RGB 24bit | +| BGR8 | OB_FORMAT_BGR | OpenCV 默认 | +| YUYV | OB_FORMAT_YUYV | YUV422 | +| MJPEG | OB_FORMAT_MJPG | 压缩格式,需解码 | +| Z16 | OB_FORMAT_Y16 | 16bit 深度 | +| Y8 | OB_FORMAT_Y8 | 8bit 灰度 (IR) | +| NV21 | OB_FORMAT_NV21 | Android 常见 | +| H264 | OB_FORMAT_H264 | 压缩视频流 | + +#### 2.6.2 OrbbecGmslCameraHAL (Orin GMSL2) — Adapter 映射表 + +> **V0.3 关键修正**: 经 OrbbecSDK_ROS2 源码分析,Realman 使用的 Orbbec Gemini 330 GMSL 版本 +> **并非**独立 V4L2 驱动。GMSL 仅是物理传输层,SDK 抽象与 USB 版完全一致。 +> 唯一差异是需额外管理 `/dev/camsync` 硬件触发同步。因此 GmslCameraHAL 应 +> **继承/组合 OrbbecCameraHAL**,仅扩展 GMSL 触发逻辑。 + +##### 架构关系 + +``` +OrbbecCameraHAL (USB) ← §2.6.1 完整 SDK API 映射 + ↑ 继承 +OrbbecGmslCameraHAL (GMSL2) ← 本节: 仅扩展 trigger + 多机同步 + └── /dev/camsync 硬件触发 + └── 设备寻址: usb_port = "gmsl2-X" (替代 serial_number) +``` + +##### GMSL 相对于 USB 的差异项 + +| 差异项 | USB (§2.6.1) | GMSL2 (本节) | +|--------|-------------|-------------| +| 物理链路 | USB 3.0 | GMSL2 (Maxim SerDes) | +| 设备寻址 | `serial_number` | `usb_port: "gmsl2-{1,3,5,7}"` | +| SDK 接口 | OrbbecSDK `ob::Pipeline` | **同左** (完全一致) | +| 多机同步 | `sync_mode` 软件/主从 | `hardware_triggering` + `/dev/camsync` | +| 触发管理 | 无 | `openSocSyncPwmTrigger()` / `closeSocSyncPwmTrigger()` | +| 帧聚合 | 按需 | 建议 `full_frame` (4 机硬同步) | +| 时间域 | device / system | 建议 `global` (PTP) | + +##### /dev/camsync 硬件触发协议 + +```cpp +// 内核驱动 camsync: SoC PWM → GMSL2 SerDes → 各相机同步曝光 +#define DEVICE_PATH "/dev/camsync" + +struct cs_param_t { + uint8_t mode; // 1 = enable trigger, 0 = disable + uint16_t fps; // 触发频率,单位 0.01Hz (3000 = 30.00 Hz) +} __attribute__((packed)); + +// 启动触发 +int openSocSyncPwmTrigger(uint16_t fps) { + cs_param_t param = {1, fps}; + int fd = open(DEVICE_PATH, O_RDWR); + write(fd, ¶m, sizeof(param)); // 写入模式+频率 + cs_param_t rd; + read(fd, &rd, sizeof(rd)); // 回读确认 + return fd; // 保持 fd 打开 = 持续触发 +} + +// 停止触发 +void closeSocSyncPwmTrigger(int fd) { + close(fd); // 关闭 fd 即停止 PWM 触发 +} +``` + +##### ICameraHAL 方法映射 (增量) + +| ICameraHAL 方法 | OrbbecGmslCameraHAL 实现 | 说明 | +|----------------|------------------------|------| +| `configure()` | 父类 `OrbbecCameraHAL::configure()` + 保存 GMSL 参数 | `enable_gmsl_trigger` / `gmsl_trigger_fps` / `sync_mode` | +| `open()` | 按 `usb_port` (而非 serial_number) 查找设备 | `ctx->queryDeviceList()` 后按 port 匹配 | +| `startStreaming()` | 父类 `pipeline->start()` **之后** 调用 `startGmslTrigger()` | 先启动 SDK pipeline 再开 PWM | +| `stopStreaming()` | 先调用 `stopGmslTrigger()` **再** `pipeline->stop()` | 先停 PWM 再停 SDK | +| 其余方法 | 完全委托父类 | getColorFrame/getDepthFrame/getPointCloud 等无差异 | + +##### GMSL 专用参数 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `enable_gmsl_trigger` | bool | false | 是否启用 SoC PWM 硬件触发 | +| `gmsl_trigger_fps` | int | 3000 | 触发频率 (单位 0.01 Hz,3000=30Hz) | +| `usb_port` | string | "" | GMSL 通道标识,如 `"gmsl2-1"` | +| `sync_mode` | string | "hardware_triggering" | GMSL 多机场景应设为硬件触发 | +| `device_num` | int | 1 | 同步设备数量 (4 机时设 4) | + +##### Realman 平台 GMSL 实际配置 (验证参考) + +```yaml +# 4× Gemini 330 GMSL 相机 — 真机部署配置 +cameras: + head_depth_cam: + usb_port: "gmsl2-1" + sync_mode: "hardware_triggering" + device_num: 4 + enable_gmsl_trigger: true + gmsl_trigger_fps: 3000 # 30Hz + chest_depth_cam: + usb_port: "gmsl2-3" + sync_mode: "hardware_triggering" + device_num: 4 + left_arm_depth_cam: + usb_port: "gmsl2-5" + sync_mode: "hardware_triggering" + device_num: 4 + right_arm_depth_cam: + usb_port: "gmsl2-7" + sync_mode: "hardware_triggering" + device_num: 4 +# 注: 仅一台相机设 enable_gmsl_trigger=true (主触发源) +# 其余相机通过 GMSL2 硬件同步线跟随触发 +``` + +##### 多相机启动顺序 + +``` +1. 创建共享进程容器 (component_container_mt) +2. 依次加载 4 个 OrbbecGmslCameraHAL 实例,间隔 2s (避免 USB 枚举冲突) +3. 所有 pipeline->start() 完成后 +4. 主相机 (head) 执行 openSocSyncPwmTrigger(3000) +5. 所有相机开始同步出帧 (硬件触发 PWM) +``` + +##### IGmslTrigger 平台抽象层 (V0.3 新增) + +> **背景**: `/dev/camsync` 是 NVIDIA Jetson (Orin NX) 特有的内核驱动,负责 SoC PWM → GMSL2 +> SerDes 触发。未来 S100 平台 (不同 SoC) 的 GMSL 触发机制可能不同。为避免重复开发, +> 将触发逻辑抽象为 `IGmslTrigger` 接口,平台相关实现单独封装。 + +``` +┌──────────────────────────────────────┐ +│ OrbbecGmslCameraHAL │ ← OrbbecSDK 层 (平台无关) +│ ┌────────────────────────────────┐ │ +│ │ IGmslTrigger (interface) │ │ ← 触发抽象层 +│ └────────┬───────────┬───────────┘ │ +│ │ │ │ +│ ┌────────▼──┐ ┌──────▼──────┐ ┌────▼────────┐ +│ │ OrinNX │ │ S100 │ │ Sim │ +│ │ Trigger │ │ Trigger │ │ Trigger │ +│ │/dev/ │ │ (TBD: │ │ (software │ +│ │camsync │ │ vendor SDK │ │ timer) │ +│ │+cs_param_t│ │ / sysfs) │ │ │ +│ └───────────┘ └─────────────┘ └─────────────┘ +└──────────────────────────────────────┘ +``` + +```cpp +// rm_hal_sensor/interface/include/rm_hal_sensor/gmsl_trigger.hpp +#pragma once +#include +#include + +namespace rm::hal { + +/// GMSL 硬件触发抽象接口 — 隔离平台差异 +class IGmslTrigger { +public: + virtual ~IGmslTrigger() = default; + + /// 启动 PWM 同步触发 + /// @param fps_hundredths 触发频率, 单位 0.01Hz (3000 = 30.00 Hz) + /// @return 成功返回 true + virtual bool startTrigger(uint16_t fps_hundredths) = 0; + + /// 停止触发 + virtual void stopTrigger() = 0; + + /// 查询当前触发状态 + virtual bool isTriggering() const = 0; +}; + +/// Orin NX 实现: /dev/camsync + cs_param_t +class GmslTriggerOrinNX final : public IGmslTrigger { +public: + bool startTrigger(uint16_t fps_hundredths) override; + void stopTrigger() override; + bool isTriggering() const override; +private: + int fd_ = -1; // /dev/camsync 文件描述符 +}; + +/// S100 实现: 待定 (vendor SDK / sysfs / 其他触发路径) +class GmslTriggerS100 final : public IGmslTrigger { +public: + bool startTrigger(uint16_t fps_hundredths) override; // TODO: S100 触发协议 + void stopTrigger() override; + bool isTriggering() const override; +private: + // S100 平台特定资源 +}; + +/// Sim 实现: 纯软件定时器, 无硬件依赖 +class GmslTriggerSim final : public IGmslTrigger { +public: + bool startTrigger(uint16_t fps_hundredths) override; // 启动软件定时器 + void stopTrigger() override; + bool isTriggering() const override; +private: + bool running_ = false; +}; + +} // namespace rm::hal +``` + +> **设计决策**: OrbbecSDK 自身是平台无关的 (USB 和 GMSL 均通过同一 SDK 接口)。 +> 只有**触发机制**是平台相关的。因此: +> - 换平台 (Orin NX → S100): 只需实现新的 `IGmslTrigger` 子类,OrbbecSDK 调用层**零修改** +> - OrbbecGmslCameraHAL 通过构造函数注入 `std::unique_ptr` +> - HALFactory 根据 profile YAML 中的 `platform` 字段选择对应 Trigger 实现 + +##### 原设计 vs 实际 (勘误) + +| 原设计 (V0.1~V0.2) | 实际 (V0.3) | +|-------------------|------------| +| GMSL 走独立 V4L2 驱动 + MAX9296 deserializer | GMSL 走 OrbbecSDK (透明传输) | +| 需 libargus / NvMedia ISP | 不需要 (SDK 内部处理) | +| 每个 GMSL 相机对应 `/dev/video*` | 每个 GMSL 相机按 `usb_port` 寻址 | +| 帧同步依赖 V4L2 多路 DMA | 帧同步依赖 `/dev/camsync` PWM | +| GmslCameraHAL 完全独立实现 | OrbbecGmslCameraHAL **继承** OrbbecCameraHAL | +| GMSL 触发逻辑硬编码 Orin NX | 通过 IGmslTrigger 接口抽象, 支持多平台 | + +#### 2.6.3 RealsenseCameraHAL — Adapter 映射表 + +| ICameraHAL 方法 | librealsense2 API | 说明 | +|----------------|------------------|------| +| `configure()` | `rs2::config::enable_stream()` | 配置各流 | +| `open()` | `rs2::pipeline::start(config)` | RealSense 的 start = open + streaming | +| `startStreaming()` | 已在 open() 中完成,或重新 start() | | +| `getColorFrame()` | `pipeline.wait_for_frames().get_color_frame()` | | +| `getDepthFrame()` | `frameset.get_depth_frame()` | | +| `getPointCloud()` | `rs2::pointcloud::calculate(depth)` | | +| `getColorIntrinsics()` | `stream_profile.get_intrinsics()` | | +| `getExtrinsics()` | `stream_profile.get_extrinsics_to(other)` | | + +#### 2.6.4 UsbCameraHAL (V4L2 通用) — Adapter 映射表 (**V0.3 新增**) + +> 用于通用 USB UVC 摄像头 (无深度/IR 能力),基于 V4L2 直接驱动。 + +| ICameraHAL 方法 | V4L2 / usb_cam API | 说明 | +|----------------|-------------------|------| +| `configure()` | 保存分辨率/帧率/像素格式参数 | 不打开设备 | +| `open()` | `::open(dev_path, O_RDWR)` + `VIDIOC_S_FMT` + `VIDIOC_REQBUFS` + `mmap` | V4L2 初始化 | +| `startStreaming()` | `VIDIOC_STREAMON` + `VIDIOC_QBUF` (mmap buffers) | 开始捕获 | +| `stopStreaming()` | `VIDIOC_STREAMOFF` | | +| `getColorFrame()` | `VIDIOC_DQBUF` → 读取 buffer → `VIDIOC_QBUF` 回收 | 轮询模式 | +| `getDepthFrame()` | 返回 false | USB 通用摄像头无深度流 | +| `getIRFrame()` | 返回 false | 同上 | +| `getPointCloud()` | 返回 false | 无深度数据 | +| `getColorIntrinsics()` | 返回 `valid=false` 的零值默认内参,或从配置文件加载 | V4L2 无标定数据接口; 调用方需检查 `CameraIntrinsics::valid` 字段 | +| `close()` | `munmap` + `::close(fd)` | | +| `health()` | fd 有效 + 帧率统计 | alive = fd≥0 && 最近 2s 有帧 | + +**V4L2 像素格式映射:** + +| PixelEncoding (HAL) | V4L2 FourCC | 说明 | +|---------------------|-------------|------| +| YUYV | `V4L2_PIX_FMT_YUYV` | 最常见硬件输出 | +| MJPEG | `V4L2_PIX_FMT_MJPEG` | 压缩模式 (省带宽) | +| RGB8 | `V4L2_PIX_FMT_RGB24` | 部分设备不支持 | +| BGR8 | `V4L2_PIX_FMT_BGR24` | | +| MONO8 | `V4L2_PIX_FMT_GREY` | 灰度摄像头 | +| H264 | `V4L2_PIX_FMT_H264` | UVC 1.5+ | + +**格式转换能力 (可复用 usb_cam 存量代码):** +- YUYV → RGB8 (`yuyv2rgb`) +- MJPEG → RGB8 (`mjpeg2rgb`,依赖 libjpeg/libav) +- UYVY → RGB8 +- M420 → RGB8 + +#### 2.6.5 SimCameraHAL — 实现规范 (**V0.3 新增**) + +> 纯软件模拟,无硬件依赖。用于 CI 测试、算法开发、离线调试。 + +##### 行为规范 + +| 参数 | 规范 | +|------|------| +| 帧生成频率 | 按 `CameraConfig.fps` 生成,默认 30Hz | +| Color 帧内容 | 配置分辨率 BGR8,合成渐变图 + 帧号水印 | +| Depth 帧内容 | 同分辨率 Z16,值域 [500, 5000] mm 正弦波平面 | +| IR 帧内容 | 同分辨率 MONO8,固定灰度 128 | +| 时间戳 | `rm::hal::now_ns()` (系统单调时钟) | +| Intrinsics | 理想针孔: fx=fy=525, cx=w/2, cy=h/2, 畸变=0 | +| Extrinsics | 单位变换: R=I₃, T=0 | +| PointCloud | 根据合成深度 + 理想内参反投影生成 XYZ | +| 延迟模拟 | 可配置 0~5ms 随机抖动 | +| 丢帧模拟 | 可配置丢帧率 (0~1) | + +##### Sim 专用配置 (通过 `extra_params` 传入) + +```cpp +// CameraConfig.extra_params 中的 Sim 专用 key: +// "sim_jitter_ms" = "3" // 随机延迟上限 (ms) +// "sim_drop_rate" = "0.01" // 1% 丢帧率 +// "sim_pattern" = "gradient" // gradient / checkerboard / noise +``` + +#### 2.6.6 Camera Adapter 对比总表 (V0.3 新增) + +| 维度 | OrbbecCameraHAL (USB) | OrbbecGmslCameraHAL | RealsenseCameraHAL | UsbCameraHAL (V4L2) | SimCameraHAL | +|------|----------------------|--------------------|--------------------|---------------------|-------------| +| **SDK/API** | OrbbecSDK | OrbbecSDK + IGmslTrigger | librealsense2 | V4L2 ioctl | 无 (纯软件) | +| **物理链路** | USB 3.0 | GMSL2 (Maxim SerDes) | USB 3.0 | USB 2.0/3.0 | N/A | +| **同步方式** | SDK 软件同步 | /dev/camsync 硬件触发 | HW sync (GPIO) | 无 | 软件定时器 | +| **流类型** | Color + Depth + IR + PointCloud | 同左 | Color + Depth + IR + PointCloud | Color 仅 | Color + 合成 Depth | +| **设备寻址** | serial_number | usb_port (gmsl2-X) | serial_number | /dev/videoN | N/A | +| **平台依赖** | 无 | Orin NX (可扩展 S100) | 无 | Linux | 无 | +| **格式转换** | SDK 内部 | SDK 内部 | SDK 内部 | 需自行 (YUYV→RGB) | 合成 | +| **CMake target** | rm_hal_sensor_orbbec_usb | rm_hal_sensor_gmsl_orbbec_orin | rm_hal_sensor_realsense | rm_hal_sensor_usb_cam | rm_hal_sensor_camera_sim | +| **AI 开发就绪度** | 90% | 85% (需实现 IGmslTrigger) | 30% | 85% | 80% | + +--- + +### §2.7 CMake target 与文件布局 + +> **补全 Notion V0.2 §2.7 TODO** + +#### 2.7.1 Camera HAL CMake 结构 + +``` +src/hal/rm_hal_sensor/ +├── CMakeLists.txt # 顶层:add_subdirectory 各子目录 +├── interface/ +│ ├── CMakeLists.txt # INTERFACE library (纯头文件) +│ └── include/rm_hal_sensor/ +│ ├── sensor_hal_base.hpp +│ ├── camera_hal.hpp +│ ├── sensor_factory.hpp +│ └── ... +├── common/camera/ +│ ├── orbbec_usb/ +│ │ ├── CMakeLists.txt # target: rm_hal_sensor_orbbec_usb +│ │ ├── orbbec_camera_hal.hpp +│ │ └── orbbec_camera_hal.cpp +│ ├── realsense/ +│ │ ├── CMakeLists.txt # target: rm_hal_sensor_realsense +│ │ ├── realsense_camera_hal.hpp +│ │ └── realsense_camera_hal.cpp +│ └── usb_cam/ +│ ├── CMakeLists.txt # target: rm_hal_sensor_usb_cam +│ ├── usb_camera_hal.hpp +│ └── usb_camera_hal.cpp +├── orin/camera/ +│ └── gmsl_orbbec/ +│ ├── CMakeLists.txt # target: rm_hal_sensor_gmsl_orbbec_orin +│ ├── gmsl_camera_hal.hpp +│ └── gmsl_camera_hal.cpp +└── sim/camera/ + └── camera_sim/ + ├── CMakeLists.txt # target: rm_hal_sensor_camera_sim + └── sim_camera_hal.cpp +``` + +#### 2.7.2 CMakeLists.txt 模板 (叶子 Driver) + +```cmake +# src/hal/rm_hal_sensor/common/camera/orbbec_usb/CMakeLists.txt +project(rm_hal_sensor_orbbec_usb) + +find_package(OrbbecSDK REQUIRED) + +add_library(${PROJECT_NAME} STATIC + orbbec_camera_hal.cpp +) + +target_include_directories(${PROJECT_NAME} + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE ${OrbbecSDK_INCLUDE_DIRS} +) + +target_link_libraries(${PROJECT_NAME} + PUBLIC rm_hal_sensor_interface # HAL 接口头文件 + PRIVATE rm_hal_common # hal_clock / hal_types + PRIVATE OrbbecSDK::OrbbecSDK # 厂商 SDK +) + +# 禁止依赖中间件 +# target_link_libraries(${PROJECT_NAME} PRIVATE rclcpp) # ❌ 禁止 +``` + +#### 2.7.3 interface CMakeLists.txt + +```cmake +# src/hal/rm_hal_sensor/interface/CMakeLists.txt +project(rm_hal_sensor_interface) + +add_library(${PROJECT_NAME} INTERFACE) + +target_include_directories(${PROJECT_NAME} + INTERFACE + $ + $ +) + +target_link_libraries(${PROJECT_NAME} + INTERFACE rm_hal_common # IHardwareDevice + HealthStatus + +#### 2.7.4 IDecoder/IDecoderFactory — 平台解码器抽象 (**V0.3.2 新增**) + +> **来源**: Orbbec 在三个平台使用不同 MJPEG/H264 解码器,通过 CMake 编译选项选择。 +> 此为 HAL 内部实现细节(不暴露到 Module 层),但需在 CMake 中显式管理。 + +``` +MJPEG / H264 帧 + │ + ├── [Jetson 平台] USE_NV_HW_DECODER + │ └── JetsonNVDecoder (NvJPEGDecoder + NvBufferTransform) + │ + ├── [Rockchip 平台] USE_RK_HW_DECODER + │ └── RkMppDecoder (MPP + librga/libyuv) + │ + └── [通用 x86/ARM] 默认 + └── SoftwareDecoder (libjpeg-turbo tjDecompress2) +``` + +```cpp +// rm_hal_sensor/interface/include/rm_hal_sensor/decoder.hpp +namespace rm::hal::sensor { + +/// 压缩帧解码器接口 (HAL 内部使用, 不暴露到 Module 层) +class IDecoder { +public: + virtual ~IDecoder() = default; + /// 解码压缩数据 → 原始 BGR/RGB 数据 + virtual bool decode(const uint8_t* compressed, size_t size, + uint8_t* output_rgb, int width, int height) = 0; + virtual PixelEncoding outputFormat() const = 0; // BGR8 or RGB8 +}; + +/// 解码器工厂 (编译期平台选择) +class IDecoderFactory { +public: + static std::unique_ptr create(); + // 内部实现依据 CMake define 选择: + // #ifdef USE_NV_HW_DECODER → JetsonNVDecoder + // #elif USE_RK_HW_DECODER → RkMppDecoder + // #else → SoftwareDecoder (libjpeg-turbo) +}; + +} // namespace +``` + +```cmake +# rm_hal_sensor/common/camera/orbbec_usb/CMakeLists.txt (节选) +option(USE_RK_HW_DECODER "Enable Rockchip MPP hardware decoder" OFF) +option(USE_NV_HW_DECODER "Enable NVIDIA Jetson hardware decoder" OFF) + +if(USE_NV_HW_DECODER) + find_package(CUDA REQUIRED) + target_link_libraries(rm_hal_sensor_orbbec_usb PRIVATE nvjpeg nvbufsurface) + target_compile_definitions(rm_hal_sensor_orbbec_usb PRIVATE USE_NV_HW_DECODER) +elseif(USE_RK_HW_DECODER) + find_library(RKMPP_LIBRARY rockchip_mpp REQUIRED) + find_library(RKRGA_LIBRARY rga REQUIRED) + target_link_libraries(rm_hal_sensor_orbbec_usb PRIVATE ${RKMPP_LIBRARY} ${RKRGA_LIBRARY}) + target_compile_definitions(rm_hal_sensor_orbbec_usb PRIVATE USE_RK_HW_DECODER) +else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(TURBOJPEG REQUIRED libturbojpeg) + target_link_libraries(rm_hal_sensor_orbbec_usb PRIVATE ${TURBOJPEG_LIBRARIES}) +endif() +``` + +> **RMOS 平台对应**: Orin NX (机器人主板) → `USE_NV_HW_DECODER=ON`; S100 平台 → 待确认。 +> 此设计确保同一套 HAL 接口代码可跨平台编译, 仅底层解码实现不同。 +) +``` + +--- + +### §2.8 异常与错误场景 + +> **补全 Notion V0.2 §2.8 TODO** + +#### 2.8.1 场景清单 + +| 场景 | 触发条件 | HAL 行为 | Module 层响应建议 | +|------|---------|---------|------------------| +| **设备未找到** | open() 时序列号不匹配 | 返回 false + DEVICE_NOT_FOUND | 重试枚举或切换 sim | +| **设备被占用** | 另一个进程已 open 同一设备 | 返回 false + DEVICE_BUSY | 等待或终止冲突进程 | +| **USB 热拔** | 运行中 USB 断开 | 状态→Faulted,回调停止 | 监听 health().alive,触发重连 | +| **GMSL 链路故障** | deserializer 丢失同步 | error_msg 报告,帧率降零 | reset() 重试 | +| **SDK 崩溃** | 厂商 SDK 内部异常 | 捕获异常→Faulted + SDK_ERROR | reset(),必要时重启进程 | +| **帧超时** | 持续 N 秒无帧到达 | health().alive = false | Module 决定 reset() 或告警 | +| **格式不支持** | configure() 请求不支持的格式 | 返回 false + UNSUPPORTED_FORMAT | 降级到设备默认格式 | +| **Ring buffer 溢出** | 消费者慢于生产者 | 丢弃最旧帧,日志 WARN | 优化消费者或降帧率 | +| **内存不足** | 分配帧 buffer 失败 | 返回 false + RESOURCE_EXHAUSTED | 减少同时打开的相机数 | + +#### 2.8.2 故障恢复策略 + +``` +Module 检测 health().alive == false + ├── 第 1 次: reset() (= close + open) + ├── 第 2 次: 等待 3s + reset() + ├── 第 3 次: 销毁实例 + 重新 createCameraHAL + configure + open + └── 第 4 次: 放弃,上报系统级告警 +``` + +#### 2.8.3 超时参数 + +| 参数 | 默认值 | 说明 | +|------|-------|------| +| frame_timeout_ms | 3000 | 轮询 getXxxFrame 等待超时 | +| open_timeout_ms | 5000 | open() SDK 初始化超时 | +| health_alive_threshold_s | 2.0 | 超过此时间无帧则 alive=false | + +--- + +### §2.9 单测要点 + +> **补全 Notion V0.2 §2.9 TODO** + +#### 2.9.1 测试分层 + +| 层级 | 测试对象 | 方法 | 依赖 | +|------|---------|------|------| +| **接口合同测试** | ICameraHAL 接口语义 | SimCameraHAL 作为被测对象 | 无 SDK 依赖 | +| **Driver 单测** | OrbbecCameraHAL 等 | Mock SDK (gmock) | MockOrbbecSDK | +| **集成测试** | 真实设备 + Driver | 物理设备连接 | 真实 SDK + 设备 | + +#### 2.9.2 接口合同测试用例 + +```cpp +// test/test_camera_hal_contract.cpp +// 使用 SimCameraHAL 验证状态机语义 + +TEST(CameraHALContract, LifecycleHappyPath) { + auto cam = createCameraHAL("sim"); + CameraConfig cfg{.device_id = "sim_cam_0", .width = 640, .height = 480}; + ASSERT_TRUE(cam->configure(cfg)); // Closed → Configured + ASSERT_TRUE(cam->open()); // Configured → Opened + ASSERT_TRUE(cam->startStreaming()); // Opened → Streaming + ASSERT_TRUE(cam->stopStreaming()); // Streaming → Opened + ASSERT_TRUE(cam->close()); // Opened → Closed +} + +TEST(CameraHALContract, InvalidStateTransitions) { + auto cam = createCameraHAL("sim"); + ASSERT_FALSE(cam->open()); // Closed 态不能直接 open (未 configure) + ASSERT_FALSE(cam->startStreaming()); // Closed 态不能 startStreaming +} + +TEST(CameraHALContract, DoubleClose) { + auto cam = createCameraHAL("sim"); + // ... configure + open ... + ASSERT_TRUE(cam->close()); + ASSERT_TRUE(cam->close()); // 幂等: 对已关闭设备 close() 返回 true +} + +TEST(CameraHALContract, HealthWhenStreaming) { + auto cam = createCameraHAL("sim"); + // ... configure + open + startStreaming ... + auto h = cam->health(); + ASSERT_TRUE(h.alive); + ASSERT_GT(h.data_rate_hz, 0.0); +} + +TEST(CameraHALContract, CallbackMode) { + auto cam = createCameraHAL("sim"); + // ... configure + open ... + std::atomic count{0}; + cam->setColorCallback([&](auto frame) { count++; }); + cam->startStreaming(); + std::this_thread::sleep_for(200ms); + ASSERT_GT(count.load(), 0); +} +``` + +#### 2.9.3 Mock SDK 策略 + +```cpp +// 对 OrbbecSDK 的 Mock 核心: +class MockObPipeline { + void start(config, callback) { /* 启动模拟帧生成线程 */ } + void stop() { /* 停止线程 */ } +}; +// 通过编译期注入 (模板参数) 或链接期替换 (弱符号) +``` + +--- + +### §2.10 多相机同步架构 (V0.3 新增) + +> **⚠ 层级归属修正 (V0.3.1)**: +> 经 RMOS V5 架构文档复核,**ISyncCoordinator 属于 Module 层(HAL 编排层),而非 HAL 层本身**。 +> 理由: ISyncCoordinator 协调多个 ICameraHAL 实例的帧对齐,涉及跨设备编排逻辑,违反 +> HAL "单设备抽象" 原则。RMOS V5 架构明确指出 "多传感器时间戳对齐由 Channel 层 +> message_filter 支持",ISyncCoordinator 是此机制在 Module 层的具象化实现。 +> +> **保留本节原因**: 同步需求与 Camera HAL 设计强相关 (硬件触发、PTP 时钟、帧时间戳), +> 保留于此供参考。实际代码应放在 Module Library 层的 `rm_sensor_module` 包中。 +> 完整的上层需求清单见 **§7 上层需求清单**。 + +> **背景**: Realman 机器人当前使用 4× Orbbec Gemini 330 GMSL 相机,未来可能接入深云 (ShenYun) +> 等第三方深度相机。需要设计跨厂商的相机同步方案。 + +#### 2.10.1 同步层次模型 + +``` +┌───────────────────────────────────────────────────────────┐ +│ ISyncCoordinator │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ SyncGroup 0 │ │ SyncGroup 1 │ │ SyncGroup 2 │ │ +│ │ (Orbbec×4) │ │ (ShenYun×2) │ │ (Mixed) │ │ +│ │ HARDWARE_ONLY│ │ HARDWARE_ONLY│ │ PTP_ALIGNED │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ Time Base: PTP (IEEE 1588) or system monotonic clock │ +└───────────────────────────────────────────────────────────┘ +``` + +#### 2.10.2 SyncPolicy 枚举 + +```cpp +enum class SyncPolicy : uint8_t { + HARDWARE_ONLY, // 同厂商硬件触发 (Orbbec /dev/camsync, RealSense GPIO) + PTP_ALIGNED, // 跨厂商: PTP 时间基准 + 软件对齐 + BEST_EFFORT, // 尽力而为: 基于系统时钟对齐, 容忍 ±10ms 抖动 + NONE // 无同步要求 (各相机独立运行) +}; +``` + +#### 2.10.3 ISyncCoordinator 接口 + +```cpp +// rm_hal_sensor/interface/include/rm_hal_sensor/sync_coordinator.hpp +#pragma once +#include +#include +#include +#include + +namespace rm::hal { + +class ICameraHAL; // forward declaration + +/// 同步帧组: 同一时刻所有相机的帧集合 +struct SyncFrameSet { + uint64_t timestamp_ns; // 对齐后的统一时间戳 + std::vector color_frames; // 按相机索引排列 + std::vector depth_frames; + uint32_t missing_mask; // 位掩码: bit=1 表示该相机帧缺失 +}; + +using SyncCallback = std::function; + +/// 多相机同步协调器接口 +class ISyncCoordinator { +public: + virtual ~ISyncCoordinator() = default; + + /// 添加相机到同步组 + virtual void addCamera(std::shared_ptr camera, int group_id = 0) = 0; + + /// 设置同步策略 + virtual void setSyncPolicy(int group_id, SyncPolicy policy) = 0; + + /// 设置跨组对齐容忍度 (ns) + virtual void setAlignTolerance(uint64_t tolerance_ns) = 0; + + /// 注册同步帧回调 + virtual void setSyncCallback(SyncCallback cb) = 0; + + /// 启动同步采集 + virtual bool start() = 0; + + /// 停止 + virtual void stop() = 0; +}; + +} // namespace rm::hal +``` + +#### 2.10.4 同步策略说明 + +| 场景 | SyncPolicy | 实现方式 | 精度 | 限制 | +|------|-----------|---------|------|------| +| 4× Orbbec GMSL (当前) | HARDWARE_ONLY | `/dev/camsync` PWM 硬件触发 | < 100μs | 仅同厂商 GMSL | +| 2× RealSense | HARDWARE_ONLY | GPIO sync (inter_cam_sync_mode) | < 1ms | 需物理 GPIO 连接 | +| Orbbec + 深云 (跨厂商) | PTP_ALIGNED | PTP 时间基准 + 帧时间戳匹配 | 1~5ms | 需 PTP 硬件支持 | +| 低要求场景 | BEST_EFFORT | 系统时钟 + 滑动窗口匹配 | 5~15ms | 无硬件要求 | +| 单台相机 / 录制回放 | NONE | 各自独立 | N/A | 无同步 | + +#### 2.10.5 帧对齐算法 + +``` +对齐窗口: tolerance_ns (默认 5ms = 5,000,000 ns) + +每当任一相机回调新帧: + 1. 将帧存入环形缓冲 (按相机 ID) + 2. 取各缓冲中最新帧的时间戳 + 3. max_ts - min_ts ≤ tolerance_ns ? + → YES: 取出各缓冲最新帧, 组成 SyncFrameSet, 触发回调 + → NO: 丢弃最旧的帧, 设 missing_mask 对应位 + 4. 缓冲超时 (2× tolerance): 强制输出 partial SyncFrameSet +``` + +#### 2.10.6 PTP 时间同步 (跨厂商方案) + +> 当 `SyncPolicy == PTP_ALIGNED` 时,要求: + +1. **网络层**: 所有相机和主机在同一 PTP 域 (IEEE 1588v2) +2. **相机端**: 支持 PTP slave 模式 (Orbbec Gemini 330 支持 `global` 时间域) +3. **主机端**: `ptp4l` + `phc2sys` 服务将 PTP 时钟同步到系统时钟 +4. **对齐**: ISyncCoordinator 使用相机帧自带的 PTP 时间戳做帧对齐 + +> **深云相机集成**: 深云 SR 系列是否支持 PTP 需确认。若不支持,退回 `BEST_EFFORT` 策略 +> (基于系统时钟接收时间做粗对齐)。 + +#### 2.10.7 与现有设计的关系 + +- ISyncCoordinator **属于 Module 层**,不属于 HAL 层 (V0.3.1 修正) +- 实际代码位置: `rm_sensor_module/sync/` (Module Library 层) +- ISyncCoordinator 依赖 ICameraHAL 的帧回调 + 时间戳,但不侵入 HAL 接口 +- 单厂商 GMSL 场景可**直接走 `/dev/camsync` 硬件触发**,无需 ISyncCoordinator +- 跨厂商/跨品类同步 (Camera + LiDAR + IMU) 应使用 ArcRT Channel 层的 `message_filter` + (参见 §7.2 ArcRT Channel 层需求) + +--- + +### §2.11 ISyncManager — 单设备硬件同步接口 (**V0.3.2 新增**) + +> **层次归属**: ISyncManager **属于 HAL 层** (单设备内部硬件同步配置)。 +> 与 §2.10 的 ISyncCoordinator (Module 层, 跨设备编排) 严格区分: +> - **ISyncManager**: 单台相机内的同步模式配置 (PRIMARY/SECONDARY/SOFTWARE_TRIGGER 等), +> 直接映射 SDK API (`ob::Device::setMultiDeviceSyncConfig()` / +> `rs2::device::set_options()`), 属于 HAL。 +> - **ISyncCoordinator**: 编排多台相机互相等待的业务逻辑, 属于 Module 层。 +> +> 通过 `ICameraHAL::getSyncManager()` 访问; 不支持同步的设备返回 `nullptr`。 + +#### 2.11.1 SyncMode 枚举 (来自 Orbbec 7 种模式) + +```cpp +// rm_hal_sensor/interface/include/rm_hal_sensor/sync_manager.hpp +namespace rm::hal::sensor { + +/// 硬件同步模式 (V0.3.2 新增) +/// 对标 Orbbec OBMultiDeviceSyncMode; RealSense 有 inter_cam_sync_mode 类似概念 +enum class SyncMode : uint8_t { + FreeRun, // 自由运行 (默认, 无外部触发) + Standalone, // 独立模式 (忽略外部触发信号, 不向外输出) + Primary, // 主设备: 产生触发信号, 供从设备使用 + Secondary, // 从设备: 被动接收触发, 可能有掉帧 + SecondarySynced, // 从设备 (同步保证版): 接收触发后保证同步输出 + SoftwareTrigger, // 软件触发: 由代码调用 triggerOnce() 触发单次拍摄 + HardwareTrigger, // 硬件触发: 外部 GPIO/GMSL 信号 (与 /dev/camsync 配合) +}; + +/// 硬件同步精细配置参数 (V0.3.2 新增) +/// 对标 Orbbec 的细粒度时序参数; RealSense 仅部分支持 +struct SyncConfig { + SyncMode mode = SyncMode::FreeRun; + int depth_delay_us = 0; // 深度帧触发延迟 (μs) + int color_delay_us = 0; // 彩色帧触发延迟 (μs) + int trigger2image_delay_us = 0; // 触发信号到出图延迟 (μs) + int trigger_out_delay_us = 0; // 触发输出引脚延迟 (μs, Primary 模式) + bool trigger_out_enabled = false; // 是否向外输出触发信号 (Primary 模式) + int frames_per_trigger = 1; // 每次触发产生多少帧 (通常 1) +}; + +/// 单设备硬件同步管理器接口 (V0.3.2 新增) +/// 由 ICameraHAL::getSyncManager() 获取 +class ISyncManager { +public: + virtual ~ISyncManager() = default; + + /// 获取设备支持的同步模式列表 + virtual std::vector getSupportedSyncModes() const = 0; + + /// 设置同步配置 (运行时可调, 部分设备需重启流) + virtual bool setSyncConfig(const SyncConfig& config) = 0; + + /// 读取当前同步配置 + virtual SyncConfig getSyncConfig() const = 0; + + /// 软件触发一次 (仅 SyncMode::SoftwareTrigger 时有效) + /// 对应 Orbbec /camera/trigger_capture Service 的 HAL 侧实现 + virtual bool triggerOnce() = 0; + + /// 查询当前是否已完成同步 (Slave 模式下: 有没有收到主设备触发) + virtual bool isSynced() const = 0; +}; + +} // namespace rm::hal::sensor +``` + +#### 2.11.2 与 CameraConfig::sync_mode 的关系 + +V0.3 的 `CameraConfig.sync_mode` 是字符串 (`"free_run"` / `"primary"` / `"secondary"` / +`"hardware_triggering"`)。V0.3.2 推荐: +- **启动时**: 在 `configure()` 中将字符串映射为 `SyncMode` 枚举并调用 `getSyncManager()->setSyncConfig()` +- **运行时**: 通过 `getSyncManager()->setSyncConfig()` 动态修改 (如触发延迟调整) +- **软件触发**: Module 层将 ROS Service 调用转发为 `getSyncManager()->triggerOnce()` + +#### 2.11.3 OrbbecCameraHAL 实现映射 + +| ISyncManager 方法 | OrbbecSDK API | 说明 | +|------------------|--------------|------| +| `getSupportedSyncModes()` | `device->isPropertySupported(OB_STRUCT_MULTI_DEVICE_SYNC_CONFIG)` | 检查支持, 返回全部 7 种 | +| `setSyncConfig(cfg)` | `device->setStructuredData(OB_STRUCT_MULTI_DEVICE_SYNC_CONFIG, ...)` | 写入多设备同步配置结构体 | +| `getSyncConfig()` | `device->getStructuredData(OB_STRUCT_MULTI_DEVICE_SYNC_CONFIG)` | 读取当前配置 | +| `triggerOnce()` | `device->triggerCapture()` | 软件触发一次 | +| `isSynced()` | 检查帧 frameset `timeStampUs()` 有效且与主设备时间对齐 | HAL 内部实现 | + +#### 2.11.4 RMOS 机器人典型配置 + +```yaml +# Realman 4× GMSL 相机场景 +head_depth_cam: + sync_mode: "primary" # Primary: 产生触发信号 + trigger_out_enabled: true + trigger_out_delay_us: 0 + +chest_depth_cam: + sync_mode: "secondary" # Secondary: 接收 GMSL 硬件触发 + depth_delay_us: 0 + trigger2image_delay_us: 0 + +# 单机软件触发场景 (工业视觉 / 标定) +calibration_cam: + sync_mode: "software_trigger" # 调用 triggerOnce() 触发 + frames_per_trigger: 1 +``` + +--- + +## §3 LiDAR HAL 详细设计 + +> **补全 Notion V0.2 §3 TODO — 首次详细设计** + +### §3.1 ILidarHAL 完整接口 + +```cpp +// rm_hal_sensor/interface/include/rm_hal_sensor/lidar_hal.hpp +#pragma once +#include "rm_hal_sensor/sensor_hal_base.hpp" +#include +#include +#include +#include + +namespace rm::hal::sensor { + +/// LiDAR 扫描模式 +enum class LidarScanMode : int { + STANDARD = 0, // 标准扫描 + EXPRESS = 1, // 高速扫描 (降精度换帧率) + BOOST = 2, // 增强扫描 +}; + +struct LidarConfig { + std::string device_id; // "front_lidar" / "rear_lidar" + + // 连接参数 (BlueSea UDP) + std::string host; // LiDAR IP 地址 + int port = 6543; // UDP 数据端口 + + // 扫描范围 + double angle_min = -M_PI; // rad + double angle_max = M_PI; + double range_min = 0.1; // m + double range_max = 30.0; + + // 扫描参数 + int scan_frequency_hz = 10; // 期望扫描频率(部分 LiDAR 可调) + LidarScanMode scan_mode = LidarScanMode::STANDARD; + + // 滤波 + bool enable_intensity_filter = false; + float min_intensity = 0.0f; // 低于此强度的点被过滤 + + // ==== V0.3 新增: 协议参数 (来自 bluesea-ladar 存量) ==== + bool raw_bytes = true; // 使用原始字节解析 + bool with_checksum = false; // 启用 CRC32 校验 (STM32 变体) + bool with_intensity = false; // 数据带强度值 + bool output_360 = true; // 拼装 360° 后输出 (否则按扇形) + + int angle_resolution = 100; // 角度分辨率 (0.01° 单位), 100=1° + int rpm = 600; // 电机转速 (RPM) + + // 网络参数 + int recv_buf_size = 1024 * 1024; // UDP 接收缓冲区 (1MB) + int udp_timeout_ms = 5000; // 接收超时 + + // 去拖影 / 角度掩码 + int mask = 0; // 角度掩码 bitmask (扇区屏蔽) + int error_circle = 3; // 容错圈数 + bool with_deshadow = false; // 去拖影滤波 +}; + +struct LaserScanData { + uint64_t timestamp_ns = 0; // steady_clock, 整圈扫描起始时间 + + // 扫描几何 + double angle_min = 0; // rad + double angle_max = 0; + double angle_increment = 0; // rad/点 + double time_increment = 0; // 相邻点时间差 (s) + double scan_time = 0; // 整圈时间 (s) + + // 量程 + double range_min = 0; + double range_max = 0; + + // 数据 + std::vector ranges; // m, inf = 无效 + std::vector intensities; // 可选, 反射强度 +}; + +class ILidarHAL : public ISensorHAL { +public: + virtual bool configure(const LidarConfig& config) = 0; + + /// 轮询获取最新一圈扫描数据 + virtual bool getScan(LaserScanData& out) = 0; + + /// 回调模式: 每完成一圈触发 + using ScanCallback = std::function)>; + virtual void setScanCallback(ScanCallback cb) = 0; + + /// 运行时调整扫描频率 (如果设备支持) + virtual bool setScanFrequency(int hz) { return false; } +}; + +} // namespace rm::hal::sensor +``` + +### §3.2 LiDAR 状态机 + +``` +Closed ──configure()──→ Configured ──open()──→ Opened ──startReceiving*──→ Streaming + ↑ │ + └──────────────────────close()─────────────────────────────────────────────┘ + 任意状态 ──fault──→ Faulted ──reset()──→ Closed +``` + +*注: LiDAR 无独立的 `startStreaming()`,`open()` 成功后 UDP 数据即开始到达。Streaming 态由内部从首帧到达时自动切换。* + +**状态转移表:** + +| 当前状态 | 事件 | 下一状态 | 动作 | +|---------|------|---------|------| +| Closed | configure() | Configured | 保存配置 | +| Configured | open() | Opened | 创建 UDP socket,绑定端口 | +| Opened | 首帧到达 | Streaming | 更新 alive=true | +| Streaming | close() | Closed | 关闭 socket,释放缓冲区 | +| Streaming | UDP 超时 3s | Faulted | alive=false | +| Faulted | reset() | Closed | close() + 清理 | + +### §3.3 线程模型 + +``` +UDP 接收线程 (select/epoll) ──解析协议──→ [LaserScanData ring buffer] + │ + ├── getScan() 轮询:取最新 + └── ScanCallback:通知用户 +``` + +- **UDP 接收线程**: HAL 内部创建,`open()` 启动,`close()` 终止 +- **协议解析**: BlueSea 私有 UDP 协议 → 角度+距离+强度 → LaserScanData +- **Ring buffer**: 深度 2(LiDAR 帧率低,无需太深) + +### §3.4 BlueSea LiDAR Adapter→协议映射 (**V0.3 大幅扩展**) + +| ILidarHAL 方法 | BlueSea 协议操作 | 说明 | +|----------------|-----------------|------| +| `configure()` | 保存 host:port + 扫描参数 | | +| `open()` | `socket(AF_INET, SOCK_DGRAM)` + `bind()` + 启动接收线程 | | +| `close()` | 关闭 socket + join 接收线程 | | +| `getScan()` | 从 ring buffer 取最新 LaserScanData | FanAssembler 聚合后 | +| `health()` | alive = 最近 3s 内有完整扫描 | data_rate_hz = 实际扫描频率 | + +#### 3.4.1 BlueSea UDP 六种帧头格式 (V0.3 新增) + +BlueSea LiDAR 存在 **6 种帧头格式**,对应不同硬件版本和数据类型: + +##### HDR — 基础格式 (18B, magic = "LSXX") + +```c +struct HdrInfo { + unsigned char header[6]; // "LSXX" 前缀 + unsigned short angle; // 起始角度 (0.01°单位) + unsigned short span; // 角度范围 + unsigned short fbase; // 基础距离偏移 + unsigned short first; // 第一个数据点索引 + unsigned short last; // 最后一个数据点索引 + unsigned short fend; // 保留 +}; // sizeof = 18 +``` + +##### HDR2 — 扩展头 (24B, 含 CRC + 时间戳) + +```c +struct HdrInfo2 { + // HDR 基本字段 (18B) + unsigned char header[6]; + unsigned short angle, span, fbase, first, last, fend; + // 扩展字段 + unsigned short CRC; // CRC 校验 (由 STM32 CRC32 截短) + unsigned short timestamp_lo; // 时间戳低16位 + unsigned short timestamp_hi; // 时间戳高16位 +}; // sizeof = 24 +``` + +##### HDR3 — 含圈号 (26B) + +```c +struct HdrInfo3 { + HdrInfo2 base; // 24B + uint16_t circle_no; // 当前扫描圈序号 +}; // sizeof = 26 +``` + +##### HDR7 — 含强度标记 (28B) + +```c +struct HdrInfo7 { + // HDR2 字段 (24B) + unsigned char header[6]; + unsigned short angle, span, fbase, first, last, fend; + unsigned short CRC; + unsigned short timestamp_lo, timestamp_hi; + // 扩展 + unsigned short circle_no; + unsigned char with_intensity; // 1=每点附加强度字节 + unsigned char reserved; +}; // sizeof = 28 +// 数据区: with_intensity=1 每点=[distance(2B)+intensity(1B)]=3B; 否=2B +``` + +##### HDRAA / HDR99 — 特殊标记帧 (少见) + +```c +// 0xAA 或 0x99 作为首字节标识, 后续字节依固件版本而定 +// HAL 应能识别并跳过 +``` + +##### 帧头版本判别 + +```cpp +int identifyHeaderSize(const uint8_t* buf, bool with_checksum, bool with_intensity) { + if (buf[0] == 0xAA || buf[0] == 0x99) return -1; // 特殊帧, 跳过 + // "LS" prefix check + if (!(buf[0]=='L' && buf[1]=='S')) return -1; + // 按配置判定: + if (!with_checksum) return 18; // HDR + if (!with_intensity) return 24; // HDR2 (可能 26=HDR3) + return 28; // HDR7 +} +``` + +#### 3.4.2 数据点结构映射 + +```c +// BlueSea 原始数据点 +struct DataPoint { + uint16_t distance; // 单位 mm (或 0.25mm,取决于 fbase) + uint8_t intensity; // 0~255, 仅 with_intensity=1 时有效 +}; +``` + +**LaserScanData 字段映射:** + +| LaserScanData 字段 | 源值 | 计算方式 | +|-------------------|------|---------| +| `angle_min` | HDR.angle | `angle * 0.01 * π / 180` | +| `angle_max` | HDR.angle + HDR.span | `(angle + span) * 0.01 * π / 180` | +| `angle_increment` | N 点均分 span | `span * 0.01 * π / 180 / N` | +| `ranges[i]` | DataPoint.distance | `distance * 0.001` (mm→m) | +| `intensities[i]` | DataPoint.intensity | `intensity / 255.0f` | +| `timestamp_ns` | HDR2.timestamp_lo/hi | `((hi << 16) \| lo) * 1000` (μs→ns) | + +#### 3.4.3 FanAssembler — 360° 扇形聚合 (V0.3 新增) + +每个 UDP 包仅覆盖部分角度 (扇形)。需要 `FanAssembler` 拼装完整 360° 扫描: + +``` +UDP 包 (30°~45°) ──┐ +UDP 包 (45°~60°) ──┤──→ FanAssembler +UDP 包 (60°~75°) ──┤ ↓ span 累计 ≥ 360° +... ──┤ ↓ +UDP 包 (15°~30°) ──┘ → 发布一帧完整 LaserScanData +``` + +```cpp +class FanAssembler { + int total_points_; // 一圈总点数 (= 36000 / angle_resolution) + float resolution_deg_; // 角度分辨率 (°) + std::vector ranges_; + std::vector intensities_; + int total_span_ = 0; // 累计角度 (0.01° 单位) + +public: + /// 每收到一个 UDP 包调用。返回 true = 一帧就绪 + bool addFan(const HdrInfo& hdr, const DataPoint* pts, int count) { + int start_idx = hdr.angle / (resolution_deg_ * 100); + for (int i = 0; i < count; i++) { + int idx = (start_idx + i) % total_points_; + ranges_[idx] = pts[i].distance * 0.001f; + if (has_intensity_) + intensities_[idx] = pts[i].intensity / 255.0f; + } + total_span_ += hdr.span; + if (total_span_ >= 36000) { // ≥360.00° + total_span_ = 0; + return true; // 发布 + } + return false; + } + LaserScanData popScan(); // 构建 LaserScanData + reset buffers +}; +``` + +#### 3.4.4 CRC32 校验 (STM32 变体) (V0.3 勘误+新增) + +> **V0.3 勘误**: 原文档写"CRC16 校验"为 **错误**。BlueSea 实际使用 **STM32 CRC32**。 + +```cpp +// 多项式 0x04C11DB7, 无反转 (与 zlib CRC32 不同!) +uint32_t stm32_crc32(const uint8_t* data, size_t len) { + uint32_t crc = 0xFFFFFFFF; + size_t words = len / 4; + for (size_t i = 0; i < words; i++) { + uint32_t w = (data[i*4]<<24) | (data[i*4+1]<<16) | + (data[i*4+2]<<8) | data[i*4+3]; + crc ^= w; + for (int j = 0; j < 32; j++) + crc = (crc & 0x80000000) ? (crc<<1) ^ 0x04C11DB7 : crc<<1; + } + return crc; +} +// 校验流程: UDP 接收 → 解析帧头 → CRC32 比对 → 通过则解析数据点, 否则丢弃+计数 +``` + +#### 3.4.5 硬件型号配置预设 + +| 型号 | range_max | with_intensity | rpm | angle_resolution | 说明 | +|------|-----------|---------------|-----|-----------------|------| +| LDS-U50C-S | 50m | true | 600 | 36 (0.36°) | 标准 | +| LDS-U80C-S | 80m | true | 1200 | 18 (0.18°) | 高精度 | + +### §3.5 CMake target + +``` +src/hal/rm_hal_sensor/common/lidar/bluesea/ +├── CMakeLists.txt # target: rm_hal_sensor_bluesea +├── bluesea_lidar_hal.hpp +├── bluesea_lidar_hal.cpp +└── bluesea_protocol.hpp # BlueSea UDP 协议解析 +``` + +### §3.6 错误场景 + +| 场景 | HAL 行为 | +|------|---------| +| UDP 端口被占用 | open() 返回 false + IO_ERROR | +| 网络不通 | 持续无数据 → alive=false | +| 数据包 CRC 失败 | 丢弃该包,error_msg 计数 | +| 扫描频率异常 | health().data_rate_hz 偏差 >50% 时 WARN | + +### §3.7 单测要点 + +```cpp +TEST(LidarHALContract, LifecycleHappyPath) { /* sim 模式 */ } +TEST(LidarHALContract, ScanDataValid) { + // getScan 返回数据: ranges.size() > 0, angle_min < angle_max +} +TEST(LidarHALContract, CallbackReceivesData) { /* 类似 Camera */ } +``` + +--- + +## §4 IMU HAL 详细设计 + +> **补全 Notion V0.2 §4 TODO — 首次详细设计** + +### §4.1 IImuHAL 完整接口 + +```cpp +// rm_hal_sensor/interface/include/rm_hal_sensor/imu_hal.hpp +#pragma once +#include "rm_hal_sensor/sensor_hal_base.hpp" +#include +#include +#include + +namespace rm::hal::sensor { + +/// IMU 加速度计量程 +enum class AccelRange : int { + G2 = 2, // ±2g + G4 = 4, // ±4g + G8 = 8, // ±8g + G16 = 16, // ±16g +}; + +/// IMU 陀螺仪量程 +enum class GyroRange : int { + DPS250 = 250, // ±250 °/s + DPS500 = 500, + DPS1000 = 1000, + DPS2000 = 2000, +}; + +/// IMU 融合策略(多源数据对齐方式) +enum class ImuFusionMode : int { + NONE = 0, // 不融合,accel/gyro 独立输出 + COPY = 1, // 直接复制最近值(适合硬件已同步) + INTERPOLATION = 2, // 线性插值对齐时间戳(推荐) +}; + +struct ImuConfig { + std::string device_id; // "body_imu" / "chassis_imu" + + // 连接参数 (YESENSE 串口) + std::string port; // "/dev/yesenseIMU" + int baudrate = 460800; + + // 采样参数 + int output_rate_hz = 200; // 输出频率 + AccelRange accel_range = AccelRange::G8; + GyroRange gyro_range = GyroRange::DPS2000; + + // 融合策略 + ImuFusionMode fusion_mode = ImuFusionMode::INTERPOLATION; + + // 校正开关 + bool enable_accel_correction = true; // 加速度计数据校正 + bool enable_gyro_correction = true; // 陀螺仪数据校正 + bool enable_mag_correction = false; // 磁力计校正(如有) + + // 噪声参数(用于上层滤波器配置,如 EKF) + double accel_noise_density = 0.0001; // m/s²/√Hz + double gyro_noise_density = 0.0001; // rad/s/√Hz + double accel_random_walk = 0.0001; // m/s³/√Hz + double gyro_random_walk = 0.0001; // rad/s²/√Hz +}; + +struct ImuData { + uint64_t timestamp_ns = 0; // steady_clock + + // 加速度 (m/s²) + double accel_x = 0, accel_y = 0, accel_z = 0; + + // 角速度 (rad/s) + double gyro_x = 0, gyro_y = 0, gyro_z = 0; + + // 姿态四元数(如果设备 AHRS 输出) + double quat_w = 1, quat_x = 0, quat_y = 0, quat_z = 0; + bool has_orientation = false; + + // 磁力计 (μT,可选) + double mag_x = 0, mag_y = 0, mag_z = 0; + bool has_magnetometer = false; + + // 温度 (℃,可选) + double temperature = 0; + bool has_temperature = false; + + // 数据质量标志 + bool is_calibrated = true; // false = 原始未校正数据 + uint32_t sequence = 0; // 单调递增序列号 +}; + +/// IMU 设备信息(用于上层配置滤波器参数) +struct ImuDeviceInfo { + AccelRange accel_range; + GyroRange gyro_range; + double accel_noise_density; + double gyro_noise_density; + double accel_random_walk; + double gyro_random_walk; + double reference_temperature; // 校准参考温度 +}; + +class IImuHAL : public ISensorHAL { +public: + virtual bool configure(const ImuConfig& config) = 0; + + /// 轮询获取最新 IMU 数据 + virtual bool getData(ImuData& out) = 0; + + /// 回调模式: 每次新数据到达时触发 + using ImuCallback = std::function; // V0.3 修正: const ref 替代 shared_ptr + virtual void setDataCallback(ImuCallback cb) = 0; + + /// 获取设备噪声/校准信息 + virtual ImuDeviceInfo getDeviceInfo() const = 0; + + /// 重置姿态估计(如果设备支持 AHRS reset) + virtual bool resetOrientation() { return false; } +}; + +} // namespace rm::hal::sensor +``` + +### §4.2 IMU 状态机 + +``` +Closed ──configure()──→ Configured ──open()──→ Opened(=Streaming) + ↑ │ + └──────────────────────close()───────────────────────┘ + 任意状态 ──fault──→ Faulted ──reset()──→ Closed +``` + +*注: IMU 设备无独立 start/stop,open() 后串口即持续输出数据。* + +### §4.3 线程模型 + +``` +串口读取线程 (read + 协议解析) ──→ [ImuData ring buffer (depth=4)] + │ + ├── getData() 轮询 + └── ImuCallback 通知 +``` + +- **串口读取线程**: 以 `output_rate_hz` 频率 (200Hz) 读取 +- **协议解析**: YESENSE 私有串口协议 → 二进制解帧 → ImuData +- **融合处理**: 如果 `fusion_mode == INTERPOLATION`,在解析后对 accel/gyro 做线性插值时间对齐 + +### §4.4 YESENSE IMU Adapter→串口协议映射 (**V0.3 大幅扩展**) + +| IImuHAL 方法 | YESENSE 串口操作 | 说明 | +|-------------|-----------------|------| +| `configure()` | 保存串口参数 + 采样率 | | +| `open()` | `::open(port, O_RDWR)` + termios 460800/8N1 + 启动读取线程 | | +| `close()` | 关闭 fd + join 读取线程 | | +| `getData()` | ring buffer 取最新 | | +| `health()` | alive = 最近 1s 内有数据 | data_rate_hz = 实际输出频率 | +| `getDeviceInfo()` | 返回 configure 时设置的噪声参数 | | + +#### 4.4.1 YESENSE TLV 帧结构 (V0.3 新增) + +``` +┌──────────┬──────────┬────────┬────────┬─────────┬─────────┬────────┐ +│ HEADER_L │ HEADER_H │ TID_L │ TID_H │ LENGTH │ PAYLOAD │ CRC │ +│ 0x59 │ 0x53 │ 2 Byte │ │ 1 Byte │ N Byte │ 2 Byte │ +└──────────┴──────────┴────────┴────────┴─────────┴─────────┴────────┘ +``` +- **HEADER**: 固定 0x59 0x53 ("YS") +- **TID**: Transaction ID, 2 字节 LE +- **LENGTH**: Payload 长度 (0~255) +- **PAYLOAD**: 多个 TLV 三元组 `[DataID(1B) + Len(1B) + Value(Len B)]` 顺序排列 +- **CRC**: 2 字节 Fletcher checksum (ck1, ck2) +- **总帧长**: N + 7 字节 + +#### 4.4.2 DataID 与缩放因子 (V0.3 新增, 关键!) + +| DataID | 名称 | 字节 | 类型 | 缩放因子 | 输出单位 | HAL 目标 (SI) | +|--------|------|------|------|---------|---------|-------------| +| 0x01 | CHIP_TEMPERATURE | 2 | int16 | ×0.01 | °C | °C | +| 0x10 | ACCEL_RAW | 12 | int32×3 | ×0.000001 | g | → m/s² (×9.80665) | +| 0x11 | LINEAR_ACCEL | 12 | int32×3 | ×0.000001 | g | → m/s² (×9.80665) | +| 0x20 | ANGULAR_VELOCITY | 12 | int32×3 | ×0.000001 | dps | → rad/s (×π/180) | +| 0x30 | MAG_FIELD | 12 | int32×3 | ×0.001 | μT | → T (×1e-6) | +| 0x40 | EULER_ANGLES | 12 | int32×3 | ×0.000001 | deg | → rad (×π/180) | +| 0x41 | QUATERNION | 16 | int32×4 | ×0.000001 | 无量纲 | 无量纲 | +| 0x50 | UTC_TIME | 11 | struct | 见下方 | — | — | +| 0x60 | LOCATION | 12 | int32×3 | 经纬×1e-7, 高度×0.001 | deg, m | deg, m | +| 0x61 | SPEED_OVER_GROUND | 12 | int32×3 | ×0.001 | m/s | m/s | +| 0x70 | STATUS_WORD | 2 | uint16 | 位域 | — | — | +| 0x80 | SAMPLE_TIMESTAMP | 4 | uint32 | ×1 | ms | ms | + +**缩放常量 (HAL 内部使用):** + +```cpp +constexpr double ACCEL_SCALE = 0.000001; // int32 × 0.000001 = g +constexpr double GYRO_SCALE = 0.000001; // int32 × 0.000001 = dps +constexpr double EULER_SCALE = 0.000001; // int32 × 0.000001 = degree +constexpr double QUAT_SCALE = 0.000001; // int32 × 0.000001 +constexpr double MAG_SCALE = 0.001; // int32 × 0.001 = μT +constexpr double TEMP_SCALE = 0.01; // int16 × 0.01 = °C + +// g → m/s², dps → rad/s +constexpr double G_TO_MS2 = 9.80665; +constexpr double DPS_TO_RADS = M_PI / 180.0; +``` + +#### 4.4.3 Fletcher Checksum (V0.3 勘误+新增) + +> **V0.3 勘误**: 原文档写"CRC"不够精确。YESENSE 使用 **Fletcher checksum**。 + +```cpp +void fletcher_checksum(const uint8_t* data, size_t len, + uint8_t& ck1, uint8_t& ck2) { + ck1 = 0; ck2 = 0; + for (size_t i = 0; i < len; i++) { + ck1 += data[i]; + ck2 += ck1; + } // uint8_t 自然溢出截断 +} +// 校验范围: TID_L 到 PAYLOAD 末尾 (不含 HEADER 和 CRC 本身) +``` + +#### 4.4.4 帧同步状态机 (V0.3 新增) + +``` +SEEK_H1 ──0x59──→ SEEK_H2 ──0x53──→ READ_TID(2B) → READ_LEN(1B) + ↑ 非0x59 ↑ 非0x53 │ + └──────────────────┘ ↓ + READ_PAYLOAD(LEN B) → READ_CRC(2B) + │ + CRC OK → DISPATCH (解析 TLV) + CRC FAIL → 回退 SEEK_H1 +``` + +#### 4.4.5 ImuData 字段补全 (V0.3 新增) + +```cpp +struct ImuData { + // ==== 已有 ==== + uint64_t timestamp_ns; + double accel_x, accel_y, accel_z; // m/s² + double gyro_x, gyro_y, gyro_z; // rad/s + double quat_w, quat_x, quat_y, quat_z; + bool has_orientation; + double mag_x, mag_y, mag_z; + bool has_magnetometer; + double temperature; + bool has_temperature; + bool is_calibrated; + uint32_t sequence; + + // ==== V0.3 新增 ==== + double linear_accel_x, linear_accel_y, linear_accel_z; // 去重力 (DataID 0x11) + bool has_linear_accel = false; + double euler_roll, euler_pitch, euler_yaw; // rad (DataID 0x40, 已转换) + bool has_euler = false; + uint16_t status_word = 0; // DataID 0x70 + uint32_t sample_timestamp_ms = 0; // DataID 0x80 +}; +``` + +### §4.5 多 IMU 实例管理 + +系统中存在两个 IMU: +- `body_imu`: 安装在躯干,用于姿态估计 +- `chassis_imu`: 安装在底盘,用于里程计融合 + +**管理方式:** +- 工厂函数创建两个独立的 IImuHAL 实例 +- 各实例独占自己的串口 (`/dev/yesenseIMU_body` / `/dev/yesenseIMU_chassis`) +- 通过 `device_id` 区分 + +```yaml +# profiles/real_full.yaml +sensors: + body_imu: { type: yesense, port: /dev/yesenseIMU_body } + chassis_imu: { type: yesense, port: /dev/yesenseIMU_chassis } +``` + +### §4.6 错误场景 + +| 场景 | HAL 行为 | +|------|---------| +| 串口不存在 | open() 返回 false + DEVICE_NOT_FOUND | +| 串口权限不足 | open() 返回 false + PERMISSION_DENIED | +| 数据超时 | alive=false, error_msg="no data for 1s" | +| CRC 校验失败 | 丢弃该帧,统计错误率 | +| 波特率不匹配 | 持续 CRC 失败 → 自动尝试其他波特率(可选) | + +### §4.7 单测要点 + +```cpp +TEST(ImuHALContract, LifecycleHappyPath) { /* sim 模式 */ } +TEST(ImuHALContract, DataValid) { + // getData: accel 在合理范围 (±16g), gyro 在合理范围 (±2000°/s) +} +TEST(ImuHALContract, DataRate) { + // 200Hz ± 10% 容差 +} +TEST(ImuHALContract, FusionInterpolation) { + // 验证融合模式下 accel/gyro 时间戳一致 +} +TEST(ImuHALContract, MultiInstance) { + // 同时创建 body_imu + chassis_imu,互不干扰 +} +``` + +--- + +## §5 Audio HAL 详细设计 (**V0.3 从骨架升级为完整设计**) + +> 原 V0.2 仅预留骨架。V0.3 补全完整接口、数据结构、ALSA 映射、DOA 及线程模型。 +> 存量代码: `wheeltec_mic` (ReSpeaker 麦克风阵列) + `audio_encode` (ALSA 采集) + +### §5.1 IAudioHAL 完整接口 + +```cpp +// rm_hal_sensor/interface/include/rm_hal_sensor/audio_hal.hpp +#pragma once +#include "rm_hal_sensor/sensor_hal_base.hpp" +#include +#include +#include +#include + +namespace rm::hal::sensor { + +/// PCM 采样格式 +enum class AudioSampleFormat : uint8_t { + S16_LE = 0, // 16-bit signed LE (最常用, 语音识别) + S24_LE, // 24-bit signed + S32_LE, // 32-bit signed + F32_LE, // 32-bit float +}; + +/// 音频帧 — 一个 period 内的 PCM 采样 +struct AudioFrame { + const int16_t* data; // PCM 数据 (interleaved channels) + size_t sample_count; // 总采样 = frame_count × channels + size_t frame_count; // period 内帧数 + uint8_t channels; + uint32_t sample_rate; + AudioSampleFormat format; + uint64_t timestamp_ns; + uint32_t sequence; +}; + +/// DOA (Direction of Arrival) 声源定位结果 +struct DOAResult { + float azimuth_deg; // 水平方位角 (°), 0=正前方, 顺时针正 + float elevation_deg; // 仰角 (°), 0=水平 + float confidence; // 置信度 [0, 1] + uint64_t timestamp_ns; +}; + +/// 音频配置 +struct AudioConfig { + std::string device_type; // "respeaker_4mic" / "respeaker_6mic" / "alsa_generic" + std::string device_name; // ALSA 设备名 "plughw:2,0" / "default" + uint32_t sample_rate = 16000; // Hz (语音: 16000, 音乐: 48000) + uint8_t channels = 4; + AudioSampleFormat format = AudioSampleFormat::S16_LE; + size_t period_frames = 1024; // ALSA period + size_t buffer_frames = 4096; // ALSA buffer + bool enable_doa = true; +}; + +using AudioCallback = std::function; +using DOACallback = std::function; + +class IAudioHAL : public ISensorHAL { +public: + ~IAudioHAL() override = default; + + virtual bool configure(const AudioConfig& config) = 0; + + /// 开始采集 (ALSA 读取线程启动) + virtual bool startCapture() = 0; + virtual void stopCapture() = 0; + + /// 注册回调 + virtual void setAudioCallback(AudioCallback cb) = 0; + virtual void setDOACallback(DOACallback cb) = 0; + + /// 同步获取最近 DOA + virtual std::optional getLatestDOA() const = 0; +}; + +} // namespace rm::hal::sensor +``` + +### §5.2 Audio 状态机 + +``` +CREATED ──configure()──→ CONFIGURED ──open()──→ READY + ↓ + startCapture() → CAPTURING + ↓ ↑ + ← stopCapture() ← + ↓ + close() → CLOSED + +CAPTURING 内部子状态: + NORMAL — 正常采集 + OVERRUN — ALSA buffer overrun (自动恢复) + ERROR — 无法恢复 +``` + +### §5.3 ALSA PCM 映射 + +| AudioConfig 字段 | ALSA API | 说明 | +|-----------------|----------|------| +| `device_name` | `snd_pcm_open(&handle, name, SND_PCM_STREAM_CAPTURE, 0)` | | +| `sample_rate` | `snd_pcm_hw_params_set_rate_near()` | 可能被修正 | +| `channels` | `snd_pcm_hw_params_set_channels()` | | +| `format` | `snd_pcm_hw_params_set_format()` | | +| `period_frames` | `snd_pcm_hw_params_set_period_size_near()` | | +| `buffer_frames` | `snd_pcm_hw_params_set_buffer_size_near()` | | + +**采样格式映射:** + +| AudioSampleFormat | ALSA 常量 | 字节/样本 | +|-------------------|----------|----------| +| S16_LE | `SND_PCM_FORMAT_S16_LE` | 2 | +| S24_LE | `SND_PCM_FORMAT_S24_LE` | 3 | +| S32_LE | `SND_PCM_FORMAT_S32_LE` | 4 | +| F32_LE | `SND_PCM_FORMAT_FLOAT_LE` | 4 | + +### §5.4 线程模型 + +``` +┌────────────────────────┐ +│ ALSA 采集线程 │ ← snd_pcm_readi() 阻塞读取 +│ (captureLoop) │ → AudioFrame → audio_cb_ +│ │ → 若 enable_doa → DOA 估计 → doa_cb_ +└────────────────────────┘ + +多通道 interleaved 排列: + [ch0_s0, ch1_s0, ch2_s0, ch3_s0, ch0_s1, ch1_s1, ...] +``` + +**ALSA 采集循环核心:** + +```cpp +void captureLoop() { + std::vector buf(config_.period_frames * config_.channels); + while (running_) { + auto frames = snd_pcm_readi(pcm_, buf.data(), config_.period_frames); + if (frames < 0) { + frames = snd_pcm_recover(pcm_, frames, 0); // EPIPE=overrun 自动恢复 + if (frames < 0) { reportError(); break; } + continue; + } + AudioFrame af{buf.data(), size_t(frames)*config_.channels, + size_t(frames), config_.channels, + config_.sample_rate, config_.format, now_ns(), seq_++}; + if (audio_cb_) audio_cb_(af); + } +} +``` + +### §5.5 ReSpeaker DOA 声源定位 + +ReSpeaker 4-Mic / 6-Mic 通过 **USB HID** 暴露 DOA: + +| HID Register | 名称 | 说明 | +|-------------|------|------| +| 21 | DOAANGLE | 方向角 0~359° | +| 19 | SPEECHDETECTED | 语音检测 0/1 | +| 20 | VOICEACTIVITY | VAD 置信度 | + +``` +ReSpeaker DOA (0~359°, 正北=0 顺时针) + → HAL DOAResult.azimuth_deg (安装偏移校正后) +``` + +### §5.6 错误场景 + +| 场景 | ALSA 错误 | HAL 行为 | +|------|----------|---------| +| Buffer overrun | -EPIPE | `snd_pcm_prepare()` 恢复, 计数器++ | +| 设备断开 | -ENODEV | 状态→Faulted | +| 格式不支持 | configure 失败 | INVALID_CONFIG | +| 权限不足 | -EACCES | PERMISSION_DENIED | + +### §5.7 CMake target + +```cmake +# rm_hal_sensor/common/audio/CMakeLists.txt +find_package(ALSA REQUIRED) + +add_library(rm_hal_sensor_audio STATIC + alsa_audio_hal.cpp + respeaker_audio_hal.cpp + sim_audio_hal.cpp +) +target_link_libraries(rm_hal_sensor_audio + PUBLIC rm_hal_sensor_interface + PRIVATE ALSA::ALSA +) + +# ReSpeaker HID (可选) +find_package(PkgConfig) +pkg_check_modules(HIDAPI hidapi-libusb) +if(HIDAPI_FOUND) + target_sources(rm_hal_sensor_audio PRIVATE respeaker_hid.cpp) + target_link_libraries(rm_hal_sensor_audio PRIVATE ${HIDAPI_LIBRARIES}) + target_compile_definitions(rm_hal_sensor_audio PRIVATE HAS_HIDAPI) +endif() +``` + +### §5.8 单测要点 + +```cpp +TEST(AudioHALContract, LifecycleHappyPath) { /* SimAudioHAL */ } +TEST(AudioHALContract, CaptureCallbackReceivesData) { + // startCapture → audio_cb_ 被调用, frame_count > 0, channels == 4 +} +TEST(AudioHALContract, DOACallback) { + // enable_doa=true → doa_cb_ 被调用, azimuth 在 [0,360) +} +``` + +--- + +## §6 横切面: 工厂 / Sim / 性能 / Profile / 测试 (**V0.3 新增**) + +> 本节整合跨传感器类型的共性设计。 + +### §6.1 HALFactory 注册机制 + +**DeviceInfo 与设备发现 (V0.3.1 新增):** + +> 参考 realsense-ros 的设备枚举/热插拔机制。设备发现是 HAL 的基础能力,属于 HAL Interface 层。 + +```cpp +// rm_hal_sensor/interface/include/rm_hal_sensor/device_info.hpp +namespace rm::hal::sensor { + +struct DeviceInfo { + std::string type; // "orbbec" / "realsense" / "usb_cam" 等 (对应 Factory 注册名) + std::string serial_number; // 设备序列号 + std::string name; // 设备型号名 (如 "Gemini 330", "D435i") + std::string connection; // "usb" / "gmsl2" / "ethernet" / "sim" + std::string port; // 物理端口标识 ("gmsl2-1", "/dev/video0", ...) + std::string firmware_version; +}; + +/// 设备变更回调: added/removed 列表 +using DeviceChangedCallback = std::function& added, + const std::vector& removed)>; + +} // namespace rm::hal::sensor +``` + +**HALFactory (含设备枚举):** + +```cpp +// rm_hal_sensor/common/include/rm_hal_sensor/factory.hpp +namespace rm::hal::sensor { + +template +class HALFactory { +public: + using Creator = std::function()>; + using Enumerator = std::function()>; + + static HALFactory& instance() { + static HALFactory inst; + return inst; + } + + void registerType(const std::string& type_name, Creator creator) { + creators_[type_name] = std::move(creator); + } + + /// 注册设备枚举器 (各 Driver 提供静态发现函数) + void registerEnumerator(const std::string& type_name, Enumerator enumerator) { + enumerators_[type_name] = std::move(enumerator); + } + + std::unique_ptr create(const std::string& type_name) const { + auto it = creators_.find(type_name); + return it != creators_.end() ? it->second() : nullptr; + } + + /// 枚举所有已注册类型的可用设备 (V0.3.1 新增) + std::vector enumerateDevices() const { + std::vector all; + for (const auto& [name, enumer] : enumerators_) { + auto devs = enumer(); + all.insert(all.end(), devs.begin(), devs.end()); + } + return all; + } + + /// 设置热插拔回调 (由后台监视线程驱动) + void setDeviceChangedCallback(DeviceChangedCallback cb) { + device_changed_cb_ = std::move(cb); + } + + /// 启用多进程设备互斥锁 (V0.3.2 新增, 来自 Orbbec orb_device_lock 实践) + /// 使用 POSIX 共享内存 pthread_mutex (PROCESS_SHARED) 防止多进程同时抢占同一设备 + /// 典型场景: ROS2 组合容器内多个 HAL 实例 + 外部调试工具同时运行 + void enableProcessLock(const std::string& lock_name = "rmos_camera_hal") { + // 内部实现: shm_open(lock_name) → mmap → pthread_mutexattr_setpshared(SHARED) + process_lock_name_ = lock_name; + process_lock_enabled_ = true; + } + +private: + HALFactory() = default; + std::unordered_map creators_; + std::unordered_map enumerators_; + DeviceChangedCallback device_changed_cb_; + bool process_lock_enabled_ = false; // V0.3.2 新增 + std::string process_lock_name_; // V0.3.2 新增 +}; + +using CameraFactory = HALFactory; +using LidarFactory = HALFactory; +using ImuFactory = HALFactory; +using AudioFactory = HALFactory; + +// 便捷注册宏 +#define REGISTER_SENSOR_HAL(Factory, name, Impl) \ + static bool _reg_##Impl = []() { \ + Factory::instance().registerType(name, \ + []() { return std::make_unique(); }); \ + return true; \ + }(); + +} // namespace rm::hal::sensor +``` + +**注册示例:** +```cpp +REGISTER_SENSOR_HAL(CameraFactory, "orbbec", OrbbecCameraHAL) +REGISTER_SENSOR_HAL(CameraFactory, "usb_cam", UsbCameraHAL) +REGISTER_SENSOR_HAL(CameraFactory, "sim", SimCameraHAL) +REGISTER_SENSOR_HAL(LidarFactory, "bluesea", BlueseaLidarHAL) +REGISTER_SENSOR_HAL(ImuFactory, "yesense", YesenseImuHAL) +REGISTER_SENSOR_HAL(AudioFactory, "respeaker", RespeakerAudioHAL) +``` + +### §6.2 SimDriver 统一基类 + +```cpp +// rm_hal_sensor/sim/sim_base.hpp +class SimSensorBase { +protected: + std::mt19937 rng_; + + void initRng(const std::map& extra) { + auto it = extra.find("sim_seed"); + rng_.seed(it != extra.end() ? std::stoul(it->second) : std::random_device{}()); + } + + double gaussianNoise(double sigma) { + return std::normal_distribution(0.0, sigma)(rng_); + } + + bool shouldDrop(double rate) { + return rate > 0 && std::uniform_real_distribution<>(0,1)(rng_) < rate; + } +}; +``` + +**各 Sim 实现数据特征:** + +| 类型 | 频率 | 数据 | 噪声 | +|------|------|------|------| +| SimCamera | 30Hz | BGR8 渐变 + Z16 正弦深度 | 无 | +| SimLidar | 10Hz | 360° 正弦距离 2~8m | σ=0.01m | +| SimImu | 100Hz | 静止 (0,0,g) | accel σ=0.01, gyro σ=0.001 | +| SimAudio | 按配置 | 440Hz 正弦 + 噪声 | σ=100 (S16) | + +### §6.3 性能预算 + +**延迟目标:** + +| 接口 | 操作 | P99 延迟 | 吞吐 | +|------|------|---------|------| +| ICameraHAL | configure() | < 500ms | 一次性 | +| ICameraHAL | getColorFrame() | < 33ms | 30fps | +| ICameraHAL | getPointCloud() | < 50ms | 15fps | +| ILidarHAL | getScan() | < 100ms | 10~20Hz | +| IImuHAL | callback | < 1ms | 100~400Hz | +| IAudioHAL | callback | < 64ms | 连续流 | + +**内存预算:** + +| 传感器 | 单帧 | Ring Buffer | 总计 | +|--------|------|-------------|------| +| Camera Color 1280×720 | 2.76MB | ×4 | 11MB | +| Camera Depth 640×480 | 0.6MB | ×4 | 2.4MB | +| PointCloud XYZRGB | 7.37MB | ×2 | 15MB | +| LiDAR (3600点) | 29KB | ×2 | 58KB | +| IMU | 120B | 实时 | ~0 | +| Audio (1024×4ch×S16) | 8KB | ALSA 管理 | 32KB | + +### §6.4 Profile YAML 示例 + +**real_full.yaml (全传感器):** +```yaml +platform: orin +sensors: + camera: + type: "orbbec" + serial_number: "AY3A12100F7" + width: 1280 + height: 720 + fps: 30 + enable_color: true + enable_depth: true + align_mode: "depth_to_color" + lidar: + type: "bluesea" + ip: "192.168.1.200" + port: 6543 + with_checksum: true + with_intensity: true + imu: + type: "yesense" + serial_port: "/dev/ttyUSB0" + baud_rate: 460800 + audio: + type: "respeaker" + device_name: "plughw:2,0" + channels: 4 + enable_doa: true +``` + +**sim.yaml (全仿真):** +```yaml +platform: sim +sensors: + camera: { type: "sim", fps: 30, extra_params: { sim_seed: "42" } } + lidar: { type: "sim", extra_params: { sim_seed: "42" } } + imu: { type: "sim", sample_rate: 100 } + audio: { type: "sim", channels: 4 } +``` + +### §6.5 测试策略 + +**三层测试金字塔:** + +| 层级 | 范围 | 实现 | +|------|------|------| +| 单元测试 | 协议解析 / CRC / 缩放 / 格式转换 | GTest, CI 必过 | +| 契约测试 | Sim 实现行为验证 / 状态机 / 回调 | GTest + SimHAL, CI 必过 | +| 集成测试 | 真实硬件 | 手动 / nightly | + +**关键单元测试清单:** + +| 目标 | 用例 | +|------|------| +| PixelEncoding | 枚举唯一, bytesPerPixel, isCompressed | +| Fletcher CRC | 已知输入匹配, 空数据, 单字节 | +| STM32 CRC32 | 已知输入, 与硬件比对 | +| YESENSE TLV 解析 | 正常帧 / CRC 错误 / 不完整 / 连续帧 | +| BlueSea 帧头 | 6 种格式解析, CRC32 校验 | +| IMU 缩放 | DataID→SI 单位 (g→m/s², dps→rad/s) | +| FanAssembler | 单扇 / 多扇 / 跨圈 | +| 工厂注册 | 注册 / 创建 / 未注册返回 nullptr | + +--- + +## §7 上层需求清单 — 基于 realsense-ros 对标分析 (**V0.3.1 新增**) + +> **来源**: 对标 realsense-ros 4.57.7 分析后识别的 7 项差距中,3 项经 RMOS V5 架构复核 +> 后确认不属于 HAL 层,应在上层实现。本节将这些需求按 RMOS 架构层次归集,为后续 +> 开发提供明确依据。 +> +> **RMOS V5 层次 (相关部分)**: +> ``` +> ┌─ App ──────────────────────────────────────────────────┐ +> │ Module Library (rm_sensor_module, rm_vision_module...) │ +> │ ArcRT / ArcInfer (Channel, message_filter, Executor) │ +> │ rm_alg_foundation (纯 C++ 算法库) │ +> │ HAL (本文档) ← 仅此层属于 Sensor HAL 范畴 │ +> │ BSP / Hardware │ +> └────────────────────────────────────────────────────────┘ +> ``` + +### §7.1 Module Library 层需求 (rm_sensor_module) + +Module 层是 HAL 与上层 App 之间的桥梁。它持有 HAL 实例,负责将 HAL 原始数据转换为 +rm_interface 消息并发布到 ArcRT Channel。以下功能经架构复核确认属于此层: + +#### 7.1.1 ISyncCoordinator — 多相机帧同步编排 + +| 项目 | 说明 | +|------|------| +| **来源** | realsense-ros FrameAggregator + 我方 §2.10 设计 | +| **理由** | 编排多个 ICameraHAL 实例 = 跨设备业务逻辑, 违反 HAL "单设备抽象" | +| **代码位置** | `rm_sensor_module/sync/sync_coordinator.hpp` | +| **依赖 HAL** | ICameraHAL::setFrameSetCallback(), 帧时间戳 | +| **接口草案** | 见 §2.10.3 (已标注为 Module 层) | + +#### 7.1.2 Filter Pipeline — 后处理滤波管线 + +| 项目 | 说明 | +|------|------| +| **来源** | realsense-ros 的 `NamedFilter` 管线 (Decimation/Spatial/Temporal/HoleFilling/PointCloud) | +| **理由** | 后处理属于业务逻辑, HAL 只负责 "原始数据采集抽象" | +| **代码位置** | `rm_sensor_module/filter/` 或调用 `rm_alg_foundation` | +| **接口草案** | | + +```cpp +// rm_sensor_module/filter/filter_pipeline.hpp +namespace rm::sensor_module { + +/// 滤波器基类 (纯 C++, 不依赖中间件) +class IFilter { +public: + virtual ~IFilter() = default; + virtual std::string name() const = 0; + virtual bool process(const ImageFrame& in, ImageFrame& out) = 0; + virtual bool isEnabled() const = 0; + virtual void setEnabled(bool en) = 0; +}; + +/// 滤波管线: 有序执行一组 IFilter +class FilterPipeline { +public: + void addFilter(std::shared_ptr filter); + bool apply(const ImageFrame& in, ImageFrame& out); +private: + std::vector> filters_; +}; + +} // namespace rm::sensor_module +``` + +> **具体滤波器实现** (Decimation / SpatialSmooth / TemporalSmooth / HoleFilling) 的 +> 核心算法放在 `rm_alg_foundation` (见 §7.3),Module 层负责组织调用顺序和参数管理。 + +#### 7.1.3 TF 坐标树构建与发布 + +| 项目 | 说明 | +|------|------| +| **来源** | realsense-ros `BaseRealSenseNode::publishStaticTransforms()` | +| **理由** | TF2 是 ROS 概念, HAL 中 `#include ` 被严格禁止 (RMOS 铁律) | +| **代码位置** | `rm_sensor_module/tf/sensor_tf_publisher.hpp` | +| **依赖 HAL** | ICameraHAL::getExtrinsics(from, to), ICameraHAL::getIntrinsics(stream) | +| **实现要点** | | + +``` +HAL 提供 Module 层做 +───────────────────────────── ───────────────────────── +getExtrinsics(DEPTH, COLOR) → tf2::Transform depth→color +getExtrinsics(DEPTH, IR_LEFT) → tf2::Transform depth→ir +getIntrinsics(COLOR) → camera_info_manager 发布 CameraInfo +光学坐标系 (Z-forward) → 转换为 ROS 坐标系 (X-forward) +``` + +#### 7.1.4 ROS 参数服务映射 + +| 项目 | 说明 | +|------|------| +| **来源** | realsense-ros 的 `parameters.cpp` (150+ 动态参数) | +| **理由** | `rclcpp::Parameter` 是 ROS 概念, HAL 不感知参数服务器 | +| **代码位置** | `rm_sensor_module/param/sensor_param_bridge.hpp` | +| **依赖 HAL** | ICameraHAL::getSupportedOptions/getOption/setOption | +| **实现要点** | | + +``` +Module 启动时: + 1. 调用 hal->getSupportedOptions() 获取 vector (V0.3.2: 含类型/范围/枚举表) + 2. 对每个 opt: 根据 opt.type 选择 ROS 参数类型, 调用 declare_parameter() + (无需再单独调用 getOptionInfo, getSupportedOptions 已包含完整信息) + 3. 注册 on_set_parameters_callback + +参数变更时: + callback(param) → hal->setOption(name, value) + +周期查询: + timer → hal->getOption(name) → 若与 param 不一致则 set_parameter() +``` + +#### 7.1.5 设备重连与 Lifecycle 管理 + +| 项目 | 说明 | +|------|------| +| **来源** | realsense-ros `RealSenseNodeFactory::onDeviceEvent()` + Lifecycle 节点 | +| **理由** | Lifecycle (configure/activate/deactivate) 是 ROS 概念; 重连策略是 Module 层业务 | +| **代码位置** | `rm_sensor_module/lifecycle/sensor_lifecycle.hpp` | +| **依赖 HAL** | HALFactory::setDeviceChangedCallback(), ICameraHAL 状态机 | +| **实现要点** | | + +``` +设备热拔: + HALFactory::setDeviceChangedCallback() 通知 Module + → Module 调用 hal->close() + → 进入 Deactivated 状态 + → 启动重连定时器 (指数退避: 1s → 2s → 4s → max 30s) + +设备热插: + DeviceChangedCallback 报告 added + → 匹配已知 serial_number + → hal->configure() → hal->open() → hal->startStreaming() + → 恢复 Active 状态 +``` + +#### 7.1.6 Diagnostics 健康上报 + +| 项目 | 说明 | +|------|------| +| **来源** | realsense-ros 的 `diagnostic_updater` 集成 | +| **理由** | `diagnostic_updater` 是 ROS 包, HAL 不依赖 ROS | +| **代码位置** | `rm_sensor_module/diag/sensor_diagnostics.hpp` | +| **依赖 HAL** | IHardwareDevice::health(), 帧率统计, 错误码 | +| **实现要点** | 周期调用 health() → 组装 DiagnosticStatus → 发布到 /diagnostics | + +--- + +### §7.2 ArcRT Channel 层需求 + +ArcRT 是 RMOS 的通信中间件层。以下功能由 ArcRT 框架提供, Module 层调用: + +#### 7.2.1 跨品类多传感器时间戳对齐 (message_filter) + +| 项目 | 说明 | +|------|------| +| **来源** | realsense-ros 中 ROS message_filters 的 ApproximateTime/ExactTime 同步 | +| **RMOS 依据** | RMOS V5 架构 §横切关注点/时间同步: "多传感器时间戳对齐由 Channel 层 message_filter 支持" | +| **适用场景** | Camera Image + LiDAR PointCloud + IMU Data 联合时间对齐 | +| **与 §2.10 的区别** | §2.10 ISyncCoordinator = 同品类多相机 HAL 级帧对齐; 这里 = 跨品类消息级对齐 | +| **接口形式** | | + +``` +ArcRT Channel message_filter: + input: Channel + Channel + Channel + policy: ApproximateTime(tolerance=10ms) + output: SynchronizedCallback(image, cloud, imu) + +Module 层仅需: + 1. 将 HAL 数据转换为 rm_interface 消息发布到各 Channel + 2. 在消费端使用 message_filter 注册联合回调 +``` + +--- + +### §7.3 rm_alg_foundation 层需求 (纯 C++ 算法库) + +rm_alg_foundation 提供不依赖任何中间件的纯 C++ 算法。以下是从 realsense-ros 对标 +分析中识别的算法需求: + +#### 7.3.1 深度图后处理滤波器 + +| 滤波器 | realsense-ros 对标 | 算法描述 | 输入/输出 | +|--------|-------------------|---------|----------| +| **DecimationFilter** | rs2::decimation_filter | 均值下采样, 降低分辨率 (2×/4×/8×) | Z16 → Z16 (缩小) | +| **SpatialFilter** | rs2::spatial_filter | edge-preserving 空间域平滑 (双边滤波) | Z16 → Z16 | +| **TemporalFilter** | rs2::temporal_filter | 时域 IIR 平滑, 减少闪烁 | Z16 序列 → Z16 | +| **HoleFillingFilter** | rs2::hole_filling_filter | 填充深度空洞 (farest/nearest/left) | Z16 → Z16 | +| **ThresholdFilter** | rs2::threshold_filter | 距离范围截断 [min_dist, max_dist] | Z16 → Z16 | + +```cpp +// rm_alg_foundation/filter/depth_filters.hpp +namespace rm::alg { + +/// 通用深度图操作接口 +class IDepthFilter { +public: + virtual ~IDepthFilter() = default; + virtual void apply(const uint16_t* in, uint16_t* out, + int width, int height, float depth_scale) = 0; +}; + +class DecimationFilter : public IDepthFilter { /* factor: 2/4/8 */ }; +class SpatialFilter : public IDepthFilter { /* alpha, delta, iterations */ }; +class TemporalFilter : public IDepthFilter { /* alpha, delta */ }; +class HoleFillingFilter: public IDepthFilter { /* mode: farest/nearest/left */ }; +class ThresholdFilter : public IDepthFilter { /* min_m, max_m */ }; + +} // namespace rm::alg +``` + +#### 7.3.2 点云生成 (Depth + Intrinsics → 3D) + +| 项目 | 说明 | +|------|------| +| **对标** | rs2::pointcloud filter, realsense-ros PointCloudFilter | +| **算法** | 逐像素反投影: $Z = \text{depth}[u,v] \times \text{scale};\; X = (u - c_x) \cdot Z / f_x;\; Y = (v - c_y) \cdot Z / f_y$ | +| **输入** | Z16 深度图 + CameraIntrinsics (fx, fy, cx, cy) | +| **输出** | PointCloud (XYZ 或 XYZRGB, 若提供 Color + Extrinsics) | + +```cpp +// rm_alg_foundation/geometry/pointcloud_generator.hpp +namespace rm::alg { + +struct PointCloudXYZ { + std::vector points; // [x0,y0,z0, x1,y1,z1, ...] + int valid_count; +}; + +PointCloudXYZ depthToPointCloud( + const uint16_t* depth, int w, int h, + float fx, float fy, float cx, float cy, + float depth_scale, + float min_depth = 0.1f, + float max_depth = 10.0f); + +} // namespace rm::alg +``` + +#### 7.3.3 坐标系变换 (Optical ↔ ROS) + +| 项目 | 说明 | +|------|------| +| **对标** | realsense-ros 大量 optical→ROS 坐标变换 | +| **问题** | 相机光学坐标系 (X-right, Y-down, Z-forward) ≠ ROS (X-forward, Y-left, Z-up) | +| **位置** | `rm_alg_foundation/geometry/coord_transform.hpp` | + +```cpp +namespace rm::alg { + +struct Transform3D { + float rotation[9]; // 3×3 行主序 + float translation[3]; +}; + +/// 相机光学坐标系 → ROS 标准坐标系 +Transform3D opticalToRos(); + +/// 组合外参和坐标系变换 +Transform3D composeTransforms(const Transform3D& a, const Transform3D& b); + +} // namespace rm::alg +``` + +--- + +### §7.4 层次归属总表 + +> 本表汇总所有从 realsense-ros 对标分析中识别的差距及其 RMOS 层次归属。 + +| # | 功能 | RMOS 层次 | 实现位置 | HAL 依赖 | 优先级 | +|---|------|----------|---------|---------|--------| +| 1 | 设备发现 / 热插拔 | **HAL** | HALFactory + DeviceInfo | — | P0 | +| 2 | Profile 查询 | **HAL** | ICameraHAL::getSupportedProfiles() | — | P0 | +| 3 | 运行时硬件选项 | **HAL** | getSupportedOptions()/getOptionInfo()/getOption()/setOption() | — | P1 | +| 4 | 扩展标定参数 | **HAL** | getExtrinsics(from,to), getIntrinsics(stream) | — | P0 | +| 5 | 单设备 FrameSet | **HAL** | ICameraHAL::setFrameSetCallback() | — | P1 | +| 6 | 多相机帧同步 | **Module** | ISyncCoordinator (§2.10, §7.1.1) | FrameSetCallback | P1 | +| 7 | Filter Pipeline | **Module** + **rm_alg** | FilterPipeline + IDepthFilter (§7.1.2, §7.3.1) | 原始帧 | P2 | +| 8 | TF 坐标树 | **Module** | SensorTFPublisher (§7.1.3) | getExtrinsics/getIntrinsics | P1 | +| 9 | ROS 参数映射 | **Module** | SensorParamBridge (§7.1.4) | getOption/setOption | P2 | +| 10 | 设备重连/Lifecycle | **Module** | SensorLifecycle (§7.1.5) | DeviceChangedCallback | P1 | +| 11 | Diagnostics | **Module** | SensorDiagnostics (§7.1.6) | health() | P2 | +| 12 | 跨品类时间对齐 | **ArcRT Channel** | message_filter (§7.2.1) | — (消息级) | P1 | +| 13 | 点云生成 | **rm_alg** | depthToPointCloud (§7.3.2) | 深度图+内参 | P1 | +| 14 | 坐标系变换 | **rm_alg** | opticalToRos (§7.3.3) | — | P2 | +| 15 | StreamIndex 统一流索引 | **HAL** | `StreamIndex{StreamType,int}` (§2.1 V0.3.2) | — | P0 | +| 16 | OptionInfo 自描述选项 | **HAL** | `OptionInfo{type,enum_values,...}` (§2.1 V0.3.2) | — | P1 | +| 17 | TimestampDomain + 帧元数据 | **HAL** | `ImageFrame::timestamp_domain/frame_number/actual_*` | — | P1 | +| 18 | ISyncManager 单设备硬件同步 | **HAL** | ISyncManager (§2.11 V0.3.2) | — | P1 | +| 19 | IDecoder/IDecoderFactory | **HAL (内部)** | 平台解码器 (§2.7.4 V0.3.2) | — | P1 | +| 20 | DistortionModel + IMUCalibration | **HAL** | 标定扩展结构体 (§2.1A V0.3.2) | — | P2 | +| 21 | 进程锁 enableProcessLock | **HAL** | HALFactory::enableProcessLock (§6.1 V0.3.2) | — | P2 | +| 22 | ParameterProvider 自动映射 | **Module** | SensorParamBridge 增强 (§7.5.1 V0.3.2) | getOption/setOption | P2 | +| 23 | TF 光学帧四元数约定 | **Module** | SensorTFPublisher (§7.5.2 V0.3.2) | getExtrinsics | P1 | +| 24 | IMU 软件对齐策略 | **Module/ArcRT** | 插值对齐 (§7.5.3 V0.3.2) | ImuCallback | P2 | +| 25 | 图像翻转/去畸变后处理 | **rm_alg** | FlipFilter/UndistortFilter (§7.5.4 V0.3.2) | 原始帧 | P2 | + +> **优先级说明**: P0 = 基础能力, HAL 首版必须具备; P1 = 核心功能, 第一轮 Module 集成需要; +> P2 = 增强功能, 可后续迭代添加。 + +--- + +### §7.5 Module 层最佳实践 — 来自双厂商对比 (**V0.3.2 新增**) + +> **来源**: 对比分析 `/docs/hal_comparative_analysis/` 17 份文档后提炼的 Module 层实现指南。 +> 这些模式属于 Module Library 层 (`rm_sensor_module`), HAL 层**不实现**。 + +#### 7.5.1 ParameterProvider 自动映射 (来自 RealSense SensorParams) + +| 项目 | 说明 | +|------|------| +| **RealSense 做法** | `SensorParams::registerDynamicOptions(rs2::options)` 自动遍历 SDK 选项 → `declare_parameter()` | +| **Orbbec 做法** | 手工声明 100+ 参数 (逐一 `parameters_->setParam("xxx", ...)`) — 不推荐 | +| **RMOS 推荐** | 调用 `ICameraHAL::getSupportedOptions()` → 动态声明 ROS 参数 | + +```cpp +// rm_sensor_module/param/sensor_param_bridge.hpp +// 启动时自动注册所有 HAL 硬件选项为 ROS 参数 (动态) +void SensorParamBridge::registerDynamicOptions(ICameraHAL& hal, rclcpp::Node& node) { + auto options = hal.getSupportedOptions(); // 返回 vector (V0.3.2 新增) + for (const auto& opt : options) { + // 根据 OptionType 声明对应 ROS 类型 + if (opt.type == OptionType::Bool) + node.declare_parameter(opt.name, opt.default_value != 0.0f); + else if (opt.type == OptionType::Enum) + node.declare_parameter(opt.name, /* enum key for default */); + else + node.declare_parameter(opt.name, opt.default_value, + rcl_interfaces::build() + .description(opt.description) + .floating_point_range({rcl_build() + .from_value(opt.min).to_value(opt.max).step(opt.step)})); + + // 注册变更回调 → 写入 HAL + node.add_on_set_parameters_callback([&hal, name=opt.name](auto& params) { + for (auto& p : params) { + if (p.get_name() == name) + hal.setOption(name, static_cast(p.as_double())); + } + return rcl_interfaces::build().successful(true); + }); + } +} +``` + +**好处**: SDK 升级新增硬件选项时, Module 层**自动暴露**, 无需修改代码。 + +#### 7.5.2 TF 光学帧四元数约定 + +| 项目 | 说明 | +|------|------| +| **来源** | 两家厂商均遵循 ROS 光学坐标系约定 | +| **问题** | 相机光学坐标系 (Z-forward, X-right, Y-down) ≠ ROS (X-forward, Y-left, Z-up) | +| **位置** | `rm_sensor_module/tf/sensor_tf_publisher.hpp` | + +``` +光学坐标系 → ROS 坐标系的变换四元数: + x = -0.5, y = 0.5, z = -0.5, w = 0.5 + +验证: 将此四元数转为旋转矩阵: + [ 0 0 1 ] (光学 Z-forward → ROS X-forward) + [-1 0 0 ] (光学 X-right → ROS -Y → 左轴) + [ 0 -1 0 ] (光学 Y-down → ROS -Z → 下轴) +``` + +```cpp +// SensorTFPublisher 中使用此固定四元数 +static const geometry_msgs::msg::Quaternion kOpticalToRos = + []() { geometry_msgs::msg::Quaternion q; + q.x=-0.5; q.y=0.5; q.z=-0.5; q.w=0.5; return q; }(); + +// 对每个流: depth_link → depth_optical +tf_static_broadcaster_->sendTransform({ + .header = {.frame_id = "depth_link"}, + .child_frame_id = "depth_optical_frame", + .transform = {.rotation = kOpticalToRos}, // 纯旋转, 无平移 +}); +``` + +#### 7.5.3 IMU 软件对齐策略 (accel + gyro 不同采样率) + +| 项目 | 说明 | +|------|------| +| **问题** | Orbbec/RealSense 相机内置 IMU: accel 50Hz, gyro 200Hz (采样率不同) | +| **硬件同步方案** | Orbbec Gemini 330 支持 `enable_sync_output_accel_gyro=true` (设备级硬件对齐, 推荐) | +| **软件对齐方案** | RealSense `imu_callback_sync`: 线性插值 accel 到 gyro 时刻 | +| **RMOS Module 层推荐** | 优先使用 ISyncManager 开启硬件同步; 若不支持, 用 message_filter 软对齐 | + +``` +策略优先级: +1. [首选] ICameraHAL::setOption("enable_sync_output_accel_gyro", 1.0) + → HAL 回调直接返回已对齐的 accel+gyro 组合帧 + +2. [备选] ArcRT Channel message_filter::ApproximateTime + → 订阅独立 accel/gyro channel → 时间容忍 ±5ms 对齐 + → 输出: SynchronizedMsg + +3. [最后] Module 层软件插值 (仅当硬件不支持且 message_filter 不适用时) + → 缓存最近 N 个 accel + gyro 样本, 对 gyro 时刻做 accel 线性插值 + → 性能开销小, 但引入最大 1/accel_rate ≈ 20ms 误差 +``` + +**注意**: 相机内置 IMU (通过 ICameraHAL 获取) 与独立外部 IMU (IImuHAL, YESENSE) 的对齐 +应在 ArcRT Channel 层通过 message_filter 处理, 不在 Module 层实现。 + +#### 7.5.4 图像后处理归属 (翻转 / 镜像 / 去畸变) + +| 操作 | 来源 | RMOS 归属 | 接口位置 | +|------|------|----------|---------| +| 水平翻转/镜像 `cv::flip(img, ±1)` | 两家均有 `flip_stream[sip]` / `mirror_stream[sip]` | **rm_alg_foundation** | `rm_alg/image/flip_filter.hpp` | +| 旋转 90/180/270° `cv::rotate()` | 两家均有 `rotation_stream[sip]` | **rm_alg_foundation** | `rm_alg/image/rotate_filter.hpp` | +| 去畸变 `cv::undistort()` | Orbbec `enable_color_undistortion_` | **rm_alg_foundation** | `rm_alg/geometry/undistort_filter.hpp` | +| D2C 深度对齐 | Orbbec: HW/SW mode; RealSense: `rs2::align` | **HAL 内部** | `ICameraHAL::CameraConfig.align_mode` | +| 深度后处理滤波器 | 两家 SDK 均有 | **rm_alg_foundation** | `rm_alg/filter/depth_filters.hpp` (§7.3.1) | + +```cpp +// rm_alg_foundation/image/flip_filter.hpp — 示例接口草案 +namespace rm::alg { + +/// 图像几何变换滤波器 (纯 C++, 无中间件依赖) +struct ImageTransformConfig { + bool flip_vertical = false; // 垂直翻转 (cv::flip y=0) + bool mirror_horizontal = false; // 水平镜像 (cv::flip x=1) + int rotation_deg = 0; // 旋转度数: 0/90/180/270 +}; + +/// 应用几何变换到 BGR8/MONO8 图像 +bool applyImageTransform(const uint8_t* src, uint8_t* dst, + int width, int height, int channels, + const ImageTransformConfig& cfg); + +} // namespace rm::alg +``` + +**调用方式**: Module 层从 ROS 参数读取 transform 配置, 在收到 HAL 原始帧后调用 +`rm::alg::applyImageTransform()`, 再发布到 Topic。HAL 层不做此操作。 + +--- + +*本文档 V0.3.2, 2026-04-28。已整合双厂商对比分析精华 (OrbbecSDK_ROS2 v2.7.6 vs realsense-ros 4.57.7)。* From d228794b0b71ce500c18948b5a899cea7326cef2 Mon Sep 17 00:00:00 2001 From: Y-ouch <792888359@qq.com> Date: Sat, 25 Apr 2026 15:06:38 +0800 Subject: [PATCH 04/14] Rename Sensor HAL to Sensor HAL.md --- Sensor HAL => Sensor HAL.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sensor HAL => Sensor HAL.md (100%) diff --git a/Sensor HAL b/Sensor HAL.md similarity index 100% rename from Sensor HAL rename to Sensor HAL.md From 07fb22f450419128767dac5a586ea4f608d3f80a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 07:32:04 +0000 Subject: [PATCH 05/14] feat: add D_hal_design folder with updated design docs and C++ interface headers Agent-Logs-Url: https://github.com/YYCB/sensor_repository/sessions/00fcf984-0057-4dfc-b716-dfaf085e1a09 Co-authored-by: YYCB <23326150+YYCB@users.noreply.github.com> --- D_hal_design/README.md | 79 ++++++++++ D_hal_design/design/D0_overview.md | 116 +++++++++++++++ D_hal_design/design/D1_common.md | 114 +++++++++++++++ D_hal_design/design/D2_camera.md | 76 ++++++++++ D_hal_design/design/D3_lidar.md | 99 +++++++++++++ D_hal_design/design/D4_imu.md | 91 ++++++++++++ D_hal_design/design/D5_audio.md | 104 ++++++++++++++ .../include/rm_hal_audio/audio_hal.hpp | 58 ++++++++ .../include/rm_hal_audio/audio_types.hpp | 62 ++++++++ .../rm_hal_camera/calibration_types.hpp | 66 +++++++++ .../include/rm_hal_camera/camera_hal.hpp | 112 +++++++++++++++ .../include/rm_hal_camera/camera_types.hpp | 136 ++++++++++++++++++ .../include/rm_hal_camera/pixel_encoding.hpp | 57 ++++++++ .../include/rm_hal_camera/stream_type.hpp | 46 ++++++ .../include/rm_hal_camera/sync_manager.hpp | 61 ++++++++ .../include/rm_hal_common/error_code.hpp | 59 ++++++++ .../include/rm_hal_common/hal_factory.hpp | 110 ++++++++++++++ .../include/rm_hal_common/hardware_device.hpp | 46 ++++++ .../include/rm_hal_common/health_status.hpp | 18 +++ .../include/rm_hal_common/sensor_hal_base.hpp | 27 ++++ .../rm_hal_common/sensor_timestamp.hpp | 37 +++++ D_hal_design/include/rm_hal_imu/imu_hal.hpp | 58 ++++++++ D_hal_design/include/rm_hal_imu/imu_types.hpp | 116 +++++++++++++++ .../include/rm_hal_lidar/lidar_2d_hal.hpp | 50 +++++++ .../include/rm_hal_lidar/lidar_2d_types.hpp | 79 ++++++++++ .../include/rm_hal_lidar/lidar_3d_hal.hpp | 54 +++++++ .../include/rm_hal_lidar/lidar_3d_types.hpp | 68 +++++++++ 27 files changed, 1999 insertions(+) create mode 100644 D_hal_design/README.md create mode 100644 D_hal_design/design/D0_overview.md create mode 100644 D_hal_design/design/D1_common.md create mode 100644 D_hal_design/design/D2_camera.md create mode 100644 D_hal_design/design/D3_lidar.md create mode 100644 D_hal_design/design/D4_imu.md create mode 100644 D_hal_design/design/D5_audio.md create mode 100644 D_hal_design/include/rm_hal_audio/audio_hal.hpp create mode 100644 D_hal_design/include/rm_hal_audio/audio_types.hpp create mode 100644 D_hal_design/include/rm_hal_camera/calibration_types.hpp create mode 100644 D_hal_design/include/rm_hal_camera/camera_hal.hpp create mode 100644 D_hal_design/include/rm_hal_camera/camera_types.hpp create mode 100644 D_hal_design/include/rm_hal_camera/pixel_encoding.hpp create mode 100644 D_hal_design/include/rm_hal_camera/stream_type.hpp create mode 100644 D_hal_design/include/rm_hal_camera/sync_manager.hpp create mode 100644 D_hal_design/include/rm_hal_common/error_code.hpp create mode 100644 D_hal_design/include/rm_hal_common/hal_factory.hpp create mode 100644 D_hal_design/include/rm_hal_common/hardware_device.hpp create mode 100644 D_hal_design/include/rm_hal_common/health_status.hpp create mode 100644 D_hal_design/include/rm_hal_common/sensor_hal_base.hpp create mode 100644 D_hal_design/include/rm_hal_common/sensor_timestamp.hpp create mode 100644 D_hal_design/include/rm_hal_imu/imu_hal.hpp create mode 100644 D_hal_design/include/rm_hal_imu/imu_types.hpp create mode 100644 D_hal_design/include/rm_hal_lidar/lidar_2d_hal.hpp create mode 100644 D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp create mode 100644 D_hal_design/include/rm_hal_lidar/lidar_3d_hal.hpp create mode 100644 D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp diff --git a/D_hal_design/README.md b/D_hal_design/README.md new file mode 100644 index 0000000..cb7d175 --- /dev/null +++ b/D_hal_design/README.md @@ -0,0 +1,79 @@ +# D – Sensor HAL 更新设计与接口实现 + +本文件夹为 Sensor HAL 抽象层的**更新版详细设计文档**及 **C++ 接口头文件**实现。 + +## 与 `Sensor HAL.md` 的关系 + +`Sensor HAL.md`(V0.3.2)是本设计的输入基线。本文件夹在此基础上修正了 4 项缺口, +其余接口(ICameraHAL / ISyncManager / OptionInfo / BlueSea 协议 / YESENSE TLV 等) +**完全保留原设计**,不重复描述。 + +| # | 问题 | 修正 | +|---|------|------| +| 1 | `TimestampDomain` 仅限 Camera,LiDAR / IMU 时钟域缺失 | 提取为 `SensorTimestamp{ns, domain}` 置于 `rm_hal_common`,所有帧类型统一使用 | +| 2 | `ISensorHAL` 无 `reset()` 正式声明(仅在状态机图中出现) | 在 `ISensorHAL` 中声明 `virtual bool reset()`,默认实现调用 `close()` | +| 3 | `AudioFrame::data` 为裸指针 `const int16_t*`,异步回调存在悬空风险 | 改为 `shared_ptr>`,与 Camera 侧所有权模型一致 | +| 4 | `ILidarHAL` 混用 2D 扫描与 3D 点云语义 | 拆分为 `I2DLidarHAL`(BlueSea 2D)和 `I3DLidarHAL`(Velodyne / Livox 3D) | + +## 文件夹结构 + +``` +D_hal_design/ +├── README.md ← 本文件 +├── design/ ← 更新版详细设计文档 +│ ├── D0_overview.md ← 架构总览、层次划分、设计原则 +│ ├── D1_common.md ← rm_hal_common 公共类型设计 +│ ├── D2_camera.md ← Camera HAL 设计 +│ ├── D3_lidar.md ← LiDAR HAL 设计(2D/3D 拆分说明) +│ ├── D4_imu.md ← IMU HAL 设计 +│ └── D5_audio.md ← Audio HAL 设计(AudioFrame 修复说明) +└── include/ ← C++ 接口头文件(纯头文件,无 .cpp) + ├── rm_hal_common/ ← 跨传感器公共基础类型 + │ ├── sensor_timestamp.hpp ← SensorTimestamp + TimestampDomain ★ + │ ├── health_status.hpp ← HealthStatus + │ ├── error_code.hpp ← ErrorCode enum + ErrorInfo + │ ├── hardware_device.hpp ← IHardwareDevice 基类 + │ ├── sensor_hal_base.hpp ← ISensorHAL(含 reset())★ + │ └── hal_factory.hpp ← HALFactory + DeviceInfo + REGISTER_HAL + ├── rm_hal_camera/ ← Camera HAL 接口 + │ ├── stream_type.hpp ← StreamType enum + StreamIndex + │ ├── pixel_encoding.hpp ← PixelEncoding enum + │ ├── calibration_types.hpp ← DistortionModel / Intrinsics / Extrinsics / IMUCalibration + │ ├── camera_types.hpp ← ImageFrame / PointCloud / StreamProfile / OptionInfo / FrameSet / CameraConfig + │ ├── sync_manager.hpp ← SyncMode / SyncConfig / ISyncManager + │ └── camera_hal.hpp ← ICameraHAL(汇总入口) + ├── rm_hal_lidar/ ← LiDAR HAL 接口 + │ ├── lidar_2d_types.hpp ← Lidar2DConfig + LaserScanData + │ ├── lidar_2d_hal.hpp ← I2DLidarHAL(BlueSea 2D) + │ ├── lidar_3d_types.hpp ← Lidar3DConfig + PointXYZI + PointCloudXYZI ★ + │ └── lidar_3d_hal.hpp ← I3DLidarHAL(Velodyne / Livox 3D)★ + ├── rm_hal_imu/ ← IMU HAL 接口 + │ ├── imu_types.hpp ← AccelRange / GyroRange / ImuConfig / ImuData / ImuDeviceInfo + │ └── imu_hal.hpp ← IImuHAL + └── rm_hal_audio/ ← Audio HAL 接口 + ├── audio_types.hpp ← AudioSampleFormat / AudioFrame(已修复)/ DOAResult / AudioConfig + └── audio_hal.hpp ← IAudioHAL +``` + +★ = 相对 Sensor HAL.md V0.3.2 的新增 / 修改项 + +## 构建说明 + +所有文件为**纯头文件接口库**,不含实现代码(.cpp)。构建时只需将 `include/` 加入头文件搜索路径: + +```cmake +# CMakeLists.txt 片段 +add_library(rm_hal_sensor_interface INTERFACE) +target_include_directories(rm_hal_sensor_interface + INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_compile_features(rm_hal_sensor_interface INTERFACE cxx_std_17) +``` + +具体驱动(OrbbecCameraHAL、BlueseaLidar2DHAL 等)链接此 INTERFACE 库后即可实现对应接口。 + +## 设计原则 + +- **按同类传感器抽象**:Camera / LiDAR-2D / LiDAR-3D / IMU / Audio 各自独立接口,数据类型不强制统一 +- **统一生命周期**:所有 HAL 继承 `IHardwareDevice → ISensorHAL`,共享 `open/close/reset/health/deviceId` +- **统一时间域**:所有帧数据携带 `SensorTimestamp{ns, domain}`,上层融合时可判断时钟来源 +- **HAL 不依赖中间件**:禁止 `#include ` 或任何 ROS / ArcRT 头文件 diff --git a/D_hal_design/design/D0_overview.md b/D_hal_design/design/D0_overview.md new file mode 100644 index 0000000..045c55d --- /dev/null +++ b/D_hal_design/design/D0_overview.md @@ -0,0 +1,116 @@ +# D0 – Sensor HAL 架构总览 + +## 设计目标 + +Sensor HAL 为所有传感器设备提供统一的生命周期管理和类型安全的数据获取接口,同时保证: + +- **零中间件依赖**:HAL 层头文件不得 `#include` 任何 ROS / ArcRT / Qt 头文件 +- **按类型抽象**:不同物理特性的传感器使用独立接口与独立数据类型,不强制统一 +- **统一生命周期**:所有 HAL 类型共享 `IHardwareDevice → ISensorHAL` 基类 +- **统一时间域**:所有帧数据携带 `SensorTimestamp{ns, domain}` + +--- + +## RMOS 层次关系 + +``` +┌─ App / Motion Planning ────────────────────────────────────────┐ +│ Module Library (rm_sensor_module) │ +│ ISyncCoordinator — 多相机跨设备帧对齐 (Module 层,非 HAL) │ +│ FilterPipeline — 后处理滤波管线 (Module 层) │ +│ SensorTFPublisher — TF 坐标树发布 (Module 层) │ +├─ ArcRT Channel ────────────────────────────────────────────────┤ +│ message_filter — 跨品类时间戳对齐 (Channel 层) │ +├─ rm_alg_foundation ────────────────────────────────────────────┤ +│ depthToPointCloud / DecimationFilter / opticalToRos … │ +├─ Sensor HAL ← 本层 (D_hal_design/) ────────────────────────────┤ +│ ICameraHAL / I2DLidarHAL / I3DLidarHAL / IImuHAL / IAudioHAL│ +├─ BSP / SDK ────────────────────────────────────────────────────┤ +│ OrbbecSDK / librealsense2 / V4L2 / BlueSea UDP / YESENSE TLV │ +└────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 接口继承树 + +``` +IHardwareDevice (rm_hal_common/hardware_device.hpp) + │ open() / close() / isOpen() / deviceId() / health() + │ + └── ISensorHAL (rm_hal_common/sensor_hal_base.hpp) + │ + reset() ← 本文件夹新增的正式声明 + │ + ├── ICameraHAL (rm_hal_camera/camera_hal.hpp) + ├── I2DLidarHAL (rm_hal_lidar/lidar_2d_hal.hpp) + ├── I3DLidarHAL (rm_hal_lidar/lidar_3d_hal.hpp) ★ 新增 + ├── IImuHAL (rm_hal_imu/imu_hal.hpp) + └── IAudioHAL (rm_hal_audio/audio_hal.hpp) +``` + +--- + +## 四项修正(相对 Sensor HAL.md V0.3.2) + +| # | 原问题 | 修正方案 | 影响文件 | +|---|--------|---------|---------| +| 1 | `TimestampDomain` 仅 Camera 有,LiDAR / IMU 用裸 `uint64_t` | 提取 `SensorTimestamp{ns, domain}` 到 `rm_hal_common`,所有帧类型统一使用 | `sensor_timestamp.hpp`;所有帧结构体 | +| 2 | `reset()` 仅在状态机图中出现,无接口声明 | 在 `ISensorHAL` 中声明 `virtual bool reset()`,默认调用 `close()` | `sensor_hal_base.hpp` | +| 3 | `AudioFrame::data` 为裸 `const int16_t*`,异步回调存在悬空风险 | 改为 `shared_ptr>` | `audio_types.hpp` | +| 4 | `ILidarHAL` 混用 2D 扫描与 3D 点云 | 拆分为 `I2DLidarHAL`(BlueSea)和 `I3DLidarHAL`(Velodyne/Livox) | `lidar_2d_*.hpp`、`lidar_3d_*.hpp` | + +--- + +## 文件依赖图 + +``` +rm_hal_common/ + sensor_timestamp.hpp (无依赖) + health_status.hpp (无依赖) + error_code.hpp (无依赖) + hardware_device.hpp ─► health_status.hpp + sensor_hal_base.hpp ─► hardware_device.hpp + hal_factory.hpp (无 HAL 接口依赖; 纯模板) + +rm_hal_camera/ + stream_type.hpp (无依赖) + pixel_encoding.hpp (无依赖) + calibration_types.hpp ─► stream_type.hpp + camera_types.hpp ─► pixel_encoding.hpp, stream_type.hpp, sensor_timestamp.hpp + sync_manager.hpp (无依赖) + camera_hal.hpp ─► calibration_types.hpp, camera_types.hpp, sync_manager.hpp + sensor_hal_base.hpp, hal_factory.hpp + +rm_hal_lidar/ + lidar_2d_types.hpp ─► sensor_timestamp.hpp + lidar_2d_hal.hpp ─► lidar_2d_types.hpp, sensor_hal_base.hpp, hal_factory.hpp + lidar_3d_types.hpp ─► sensor_timestamp.hpp + lidar_3d_hal.hpp ─► lidar_3d_types.hpp, sensor_hal_base.hpp, hal_factory.hpp + +rm_hal_imu/ + imu_types.hpp ─► sensor_timestamp.hpp + imu_hal.hpp ─► imu_types.hpp, sensor_hal_base.hpp, hal_factory.hpp + +rm_hal_audio/ + audio_types.hpp ─► sensor_timestamp.hpp + audio_hal.hpp ─► audio_types.hpp, sensor_hal_base.hpp, hal_factory.hpp +``` + +--- + +## CMake 使用方式 + +```cmake +# 接口库(无 .cpp,仅头文件) +add_library(rm_hal_sensor_interface INTERFACE) +target_include_directories(rm_hal_sensor_interface + INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_compile_features(rm_hal_sensor_interface INTERFACE cxx_std_17) + +# 具体驱动示例 +add_library(rm_hal_sensor_orbbec_usb STATIC + orbbec_camera_hal.cpp) +target_link_libraries(rm_hal_sensor_orbbec_usb + PUBLIC rm_hal_sensor_interface + PRIVATE OrbbecSDK::OrbbecSDK) +``` diff --git a/D_hal_design/design/D1_common.md b/D_hal_design/design/D1_common.md new file mode 100644 index 0000000..11e3bed --- /dev/null +++ b/D_hal_design/design/D1_common.md @@ -0,0 +1,114 @@ +# D1 – rm_hal_common 公共基础类型 + +`rm_hal_common` 包含所有传感器类型共享的基础定义,不依赖任何 SDK 或中间件。 + +--- + +## 新增:SensorTimestamp(修正 #1) + +### 背景 + +`Sensor HAL.md V0.3.2` 中,`TimestampDomain`(Hardware / System / Global)仅定义在 +Camera 侧的 `ImageFrame` 里。`LaserScanData`、`ImuData`、`AudioFrame` 均使用裸 `uint64_t`, +既无法区分时钟来源,也无法在融合时判断是否可以直接做时间差运算。 + +### 修正 + +提取 `SensorTimestamp{uint64_t ns, TimestampDomain domain}` 到 `rm_hal_common/sensor_timestamp.hpp`, +所有传感器帧类型统一使用该结构体替换原有的 `uint64_t timestamp_ns`。 + +| 帧类型 | 典型 domain 值 | 说明 | +|--------|---------------|------| +| `ImageFrame` | Hardware(Orbbec / RealSense 硬件时间戳) | SDK 提供设备级时钟 | +| `LaserScanData` | System(UDP 接收时刻) 或 Hardware(HDR2 帧头有时间戳) | BlueSea HDR2/HDR3 携带 16 位时间戳,可升级为 Hardware | +| `PointCloudXYZI` | System 或 Hardware | 同上(Velodyne/Livox 协议提供设备时间戳) | +| `ImuData` | System(串口接收时刻) 或 Hardware(DataID 0x80) | YESENSE 提供 sample_timestamp_ms | +| `AudioFrame` | System(snd_pcm_readi 返回时刻) | ALSA 无硬件时间戳 | + +--- + +## 新增:ISensorHAL::reset()(修正 #2) + +### 背景 + +所有传感器的状态机图(Camera §2.3、LiDAR §3.2、IMU §4.2、Audio §5.2)都有 + +``` +Faulted ──reset()──► Closed +``` + +但任何接口类中都找不到 `reset()` 的声明,实现方容易遗漏。 + +### 修正 + +```cpp +class ISensorHAL : public IHardwareDevice { +public: + // Recover from Faulted → Closed. Default: calls close(). + virtual bool reset() { close(); return true; } +}; +``` + +具体实现可覆盖此方法做更深的清理(SDK 内部状态、内存池、环形缓冲区清空等)。 + +--- + +## HealthStatus + +原文档多处引用 `health()` 返回类型,但未明确定义结构体字段。完整定义: + +```cpp +struct HealthStatus { + bool alive = false; // 有数据输出 && 无持续故障 + double data_rate_hz = 0.0; // 实测输出频率 (Hz) + std::string error_msg; // 最后一次错误描述; 空 = 无错误 + uint32_t drop_count = 0; // 累计丢帧/包(自 open() 起) + uint32_t error_count = 0; // 累计协议/CRC 错误 +}; +``` + +--- + +## ErrorCode + +将 Sensor HAL.md §2.4 中分散描述的错误场景统一到 `ErrorCode` 枚举, +分为 5 组:General / DeviceLifecycle / Configuration / DataIO / SDK / Permissions。 + +当前阶段:驱动内部使用 `ErrorCode` 记录日志和设置 `health().error_msg`; +公开方法仍返回 `bool`(兼容现有调用方)。 +未来可迁移为 `ErrorCode open()` 以便调用方区分不同失败原因。 + +--- + +## HALFactory\ + +类型化工厂,通过字符串 key 注册和创建驱动实例。 + +```cpp +// 驱动注册(通常在 .cpp 静态初始化阶段): +REGISTER_HAL(CameraFactory, "orbbec", OrbbecCameraHAL) +REGISTER_HAL(CameraFactory, "sim", SimCameraHAL) +REGISTER_HAL(Lidar2DFactory,"bluesea", BlueseaLidar2DHAL) +REGISTER_HAL(Lidar3DFactory,"velodyne", VelodyneLidar3DHAL) +REGISTER_HAL(ImuFactory, "yesense", YesenseImuHAL) +REGISTER_HAL(AudioFactory, "respeaker",RespeakerAudioHAL) + +// 使用: +auto cam = CameraFactory::instance().create("orbbec"); +auto lidar = Lidar3DFactory::instance().create("velodyne"); +``` + +Factory 类型别名在各 HAL 头文件中定义(避免 hal_factory.hpp 循环依赖): + +```cpp +// camera_hal.hpp +using CameraFactory = rm::hal::HALFactory; +// lidar_2d_hal.hpp +using Lidar2DFactory = rm::hal::HALFactory; +// lidar_3d_hal.hpp +using Lidar3DFactory = rm::hal::HALFactory; +// imu_hal.hpp +using ImuFactory = rm::hal::HALFactory; +// audio_hal.hpp +using AudioFactory = rm::hal::HALFactory; +``` diff --git a/D_hal_design/design/D2_camera.md b/D_hal_design/design/D2_camera.md new file mode 100644 index 0000000..a44e4f5 --- /dev/null +++ b/D_hal_design/design/D2_camera.md @@ -0,0 +1,76 @@ +# D2 – Camera HAL 设计 + +Camera HAL 的设计与 `Sensor HAL.md V0.3.2 §2` 高度一致。本文件只描述变更点。 + +--- + +## 变更点 + +| 字段 | 原值 | 修改后 | +|------|------|--------| +| `ImageFrame::timestamp_ns` | `uint64_t` | `rm::hal::SensorTimestamp` | +| `PointCloud::timestamp_ns` | (无) | 新增 `rm::hal::SensorTimestamp timestamp` | + +其余接口(`ICameraHAL` / `ISyncManager` / `OptionInfo` / `StreamProfile` / +`CameraConfig` / `PixelEncoding` / 标定结构体)**完全保留原设计**。 + +--- + +## 头文件依赖顺序 + +``` +stream_type.hpp 无依赖 +pixel_encoding.hpp 无依赖 +calibration_types.hpp → stream_type.hpp +camera_types.hpp → pixel_encoding.hpp + stream_type.hpp + sensor_timestamp.hpp +sync_manager.hpp 无依赖 +camera_hal.hpp → calibration_types.hpp + camera_types.hpp + sync_manager.hpp + + sensor_hal_base.hpp + hal_factory.hpp +``` + +--- + +## ICameraHAL 完整方法表 + +| 类别 | 方法 | 说明 | +|------|------|------| +| 配置 | `configure(CameraConfig)` | 必须先于 open() 调用 | +| 生命周期 | `open / close / isOpen / reset` | 继承自 ISensorHAL | +| 流控 | `startStreaming / stopStreaming` | open 后调用 | +| 轮询 | `getColorFrame / getDepthFrame / getIRFrame / getPointCloud` | 注册回调后失效 | +| 回调 | `setColorCallback / setDepthCallback / setIRCallback / setFrameSetCallback` | HAL 内部线程调用 | +| Profile | `getSupportedProfiles()` | 枚举设备支持的分辨率/帧率 | +| 标定 | `getIntrinsics / getExtrinsics / getIMUCalibration / getDepthMetadata` | 读取出厂标定 | +| 标定 | `loadUserCalibration / exportCalibration` | 用户标定覆盖 | +| 硬件选项 | `getSupportedOptions / getOptionInfo / getOption / setOption` | 曝光/增益/白平衡等 | +| 同步 | `getSyncManager()` | 返回 ISyncManager(不支持则返回 nullptr) | + +--- + +## ISyncManager 层次说明 + +| 接口 | 层次 | 职责 | +|------|------|------| +| `ISyncManager` | HAL 层 | 单台设备的硬件同步模式配置(Primary / Secondary / SoftwareTrigger) | +| `ISyncCoordinator` | Module 层 | 编排多台相机互相等待的业务逻辑 | + +两者严格分离;`ISyncManager` 通过 `ICameraHAL::getSyncManager()` 获取。 + +--- + +## 线程安全合约 + +- `health()` / `isOpen()` / `deviceId()`:任意线程安全调用 +- 生命周期方法(`configure / open / close / startStreaming` 等):调用方不得并发 +- 回调在 HAL 内部专用线程执行;**回调内禁止回调同一 HAL 实例方法**(避免重入死锁) + +--- + +## 状态机 + +``` +Closed ──configure()──► Configured ──open()──► Opened ──startStreaming()──► Streaming + ▲ │ + └──────────────────────── close() ────────────────────────────────────────────┘ +任意状态 ──fault──► Faulted ──reset()──► Closed +``` diff --git a/D_hal_design/design/D3_lidar.md b/D_hal_design/design/D3_lidar.md new file mode 100644 index 0000000..2590022 --- /dev/null +++ b/D_hal_design/design/D3_lidar.md @@ -0,0 +1,99 @@ +# D3 – LiDAR HAL 设计(2D / 3D 拆分) + +## 设计决策:拆分 I2DLidarHAL 与 I3DLidarHAL + +`Sensor HAL.md V0.3.2 §3` 的 `ILidarHAL` 仅覆盖 2D 激光扫描(BlueSea UDP)。 +但仓库中同时存在 3D 旋转 LiDAR(Velodyne / Livox)的使用需求。 + +两类传感器的数据语义根本不同,使用统一接口会导致大量"不适用"的方法, +降低类型安全性和可读性: + +| 维度 | I2DLidarHAL(BlueSea) | I3DLidarHAL(Velodyne / Livox) | +|------|----------------------|--------------------------------| +| 输出类型 | `LaserScanData`(极坐标,ROS LaserScan 等效) | `PointCloudXYZI`(笛卡尔,每点含时间偏移) | +| 点密度 | ~360 点 / 圈 | 数万–数十万点 / 圈 | +| per-point 时间 | 无 | `time_offset_s`(运动补偿必需) | +| 协议 | BlueSea 私有 UDP,6 种帧头 | Velodyne PCAP UDP / Livox SDK | +| 连接 | UDP 单播 | UDP 广播 / SDK 管理 | + +--- + +## I2DLidarHAL(2D 激光扫描) + +**目标传感器**:BlueSea LDS-U50C-S、LDS-U80C-S + +**核心接口**: + +```cpp +class I2DLidarHAL : public ISensorHAL { + virtual bool configure(const Lidar2DConfig& config) = 0; + virtual bool getScan(LaserScanData& out) = 0; // 轮询 + using ScanCallback = std::function)>; + virtual void setScanCallback(ScanCallback cb) = 0; // 回调 + virtual bool setScanFrequency(int hz) { return false; } // 可选 +}; +``` + +**LaserScanData 时间域**: +- `domain = System`:UDP 接收时打戳(默认) +- `domain = Hardware`:BlueSea HDR2/HDR3 帧头中的 `timestamp_lo/hi` 字段可用时升级 + +**状态机**: +``` +Closed ──configure()──► Configured ──open()──► Streaming + ▲ │ + └─────────── close() ────────────────────────────┘ +任意状态 ──fault──► Faulted ──reset()──► Closed +``` +open() 后 UDP 数据即开始到达,无独立 startReceiving。 + +--- + +## I3DLidarHAL(3D 点云) + +**目标传感器**:Velodyne VLP-16、VLP-32C、HDL-64E;Livox Mid-360 等 + +**核心接口**: + +```cpp +class I3DLidarHAL : public ISensorHAL { + virtual bool configure(const Lidar3DConfig& config) = 0; + virtual bool getPointCloud(PointCloudXYZI& out) = 0; // 轮询 + using PointCloudCallback = std::function)>; + virtual void setPointCloudCallback(PointCloudCallback cb) = 0; // 回调 +}; +``` + +**PointXYZI 关键字段**: + +| 字段 | 含义 | +|------|------| +| `x, y, z` | 笛卡尔坐标(m),LiDAR 机体坐标系 | +| `intensity` | 归一化反射强度 [0,1] | +| `time_offset_s` | 相对 PointCloudXYZI::timestamp 的时间偏移(运动补偿输入) | +| `ring` | 激光环编号(Velodyne channel index,0-based) | + +**LidarReturnMode**:`Strongest / Last / Dual`(多回波支持) + +--- + +## 工厂别名与注册 + +```cpp +using Lidar2DFactory = rm::hal::HALFactory; +using Lidar3DFactory = rm::hal::HALFactory; + +// 驱动注册示例 +REGISTER_LIDAR2D_HAL("bluesea", BlueseaLidar2DHAL) +REGISTER_LIDAR3D_HAL("velodyne", VelodyneLidar3DHAL) +REGISTER_LIDAR3D_HAL("livox", LivoxLidar3DHAL) +REGISTER_LIDAR3D_HAL("sim", SimLidar3DHAL) +``` + +--- + +## 与原 ILidarHAL 的对比 + +原 `Sensor HAL.md §3` 的协议细节(BlueSea 6 种帧头、FanAssembler、STM32 CRC32) +完全保留在 `I2DLidarHAL` 对应的 `BlueseaLidar2DHAL` 实现中,接口层面不暴露。 +`I3DLidarHAL` 是本文件夹相对原设计的净增量。 diff --git a/D_hal_design/design/D4_imu.md b/D_hal_design/design/D4_imu.md new file mode 100644 index 0000000..b48d8f6 --- /dev/null +++ b/D_hal_design/design/D4_imu.md @@ -0,0 +1,91 @@ +# D4 – IMU HAL 设计 + +IMU HAL 与 `Sensor HAL.md V0.3.2 §4` 高度一致。本文件只描述变更点和补充说明。 + +--- + +## 变更点 + +| 字段 | 原值 | 修改后 | +|------|------|--------| +| `ImuData::timestamp_ns` | `uint64_t` | `rm::hal::SensorTimestamp timestamp` | + +默认 `domain = System`(串口接收时刻)。若设备 TLV 帧中的 DataID `0x80` +(SAMPLE_TIMESTAMP,单位 ms)可用,可将 domain 升级为 `Hardware` 以提供更准确的时钟。 + +其余一切(`ImuConfig` / `ImuData` 字段 / `ImuFusionMode` / `ImuDeviceInfo` / YESENSE TLV 协议映射)**完全保留原设计**。 + +--- + +## IImuHAL 方法表 + +| 方法 | 说明 | +|------|------| +| `configure(ImuConfig)` | 配置串口、采样率、量程、融合策略 | +| `open() / close() / reset()` | 继承自 ISensorHAL | +| `getData(ImuData& out)` | 轮询最新帧(ring buffer depth=4) | +| `setDataCallback(ImuCallback)` | 每个 TLV 帧到达时触发(~200 Hz) | +| `getDeviceInfo()` | 返回噪声参数,供 EKF / UKF 配置 | +| `resetOrientation()` | 重置 AHRS 姿态估计(可选,默认返回 false) | + +--- + +## ImuData 字段与 YESENSE DataID 映射 + +| 字段 | DataID | 原始单位 | 缩放因子 | SI 单位 | +|------|--------|---------|---------|---------| +| `accel_x/y/z` | 0x10 | g (int32) | ×1e-6 × 9.80665 | m/s² | +| `gyro_x/y/z` | 0x20 | dps (int32) | ×1e-6 × π/180 | rad/s | +| `quat_w/x/y/z` | 0x41 | 无 (int32) | ×1e-6 | 无量纲 | +| `linear_accel_x/y/z` | 0x11 | g (int32) | ×1e-6 × 9.80665 | m/s²(已去重力) | +| `euler_roll/pitch/yaw` | 0x40 | deg (int32) | ×1e-6 × π/180 | rad | +| `mag_x/y/z` | 0x30 | μT (int32) | ×1e-3 | μT | +| `temperature` | 0x01 | °C (int16) | ×0.01 | °C | +| `status_word` | 0x70 | — | — | 位域 | +| `sample_timestamp_ms` | 0x80 | ms (uint32) | ×1 | ms | + +--- + +## 状态机 + +``` +Closed ──configure()──► Configured ──open()──► Streaming + ▲ │ + └───────────── close() ──────────────────────────┘ +任意状态 ──fault──► Faulted ──reset()──► Closed +``` + +open() 后串口立即持续输出;无独立 startStreaming。 + +--- + +## 多实例管理 + +```yaml +sensors: + body_imu: { type: yesense, port: /dev/yesenseIMU_body, baudrate: 460800 } + chassis_imu: { type: yesense, port: /dev/yesenseIMU_chassis, baudrate: 460800 } +``` + +两个独立的 `IImuHAL` 实例各自持有串口 fd 和读取线程,互不干扰,通过 `device_id` 区分。 + +--- + +## GNSS 字段说明 + +YESENSE TLV DataID `0x60`(Location)和 `0x61`(Speed over Ground)当前未在 `ImuData` 中暴露。 + +若未来有独立 GNSS 接收机接入,应新增 `IGnssHAL` 接口而非在 `ImuData` 中堆砌字段。 + +--- + +## 线程模型 + +``` +串口读取线程 (read + TLV 解析) + │ + ├── getData() 轮询 → ring buffer (depth=4) + └── ImuCallback → 每帧触发 +``` + +回调在读取线程执行;**回调内禁止调用 open / close / configure**(避免死锁)。 diff --git a/D_hal_design/design/D5_audio.md b/D_hal_design/design/D5_audio.md new file mode 100644 index 0000000..12e8f8f --- /dev/null +++ b/D_hal_design/design/D5_audio.md @@ -0,0 +1,104 @@ +# D5 – Audio HAL 设计 + +Audio HAL 与 `Sensor HAL.md V0.3.2 §5` 保持一致,有一项关键修正。 + +--- + +## 修正:AudioFrame 所有权模型(修正 #3) + +### 原设计的问题 + +```cpp +// Sensor HAL.md V0.3.2 原版 ── 有安全漏洞 +struct AudioFrame { + const int16_t* data; // 裸指针,指向 ALSA mmap 内部缓冲区 + size_t sample_count; + // ... +}; +using AudioCallback = std::function; +``` + +`captureLoop()` 调用 `snd_pcm_readi()` 后,PCM 数据存在于内核分配的 ALSA buffer 中。 +`snd_pcm_readi()` 在下一次调用时可能立刻回收该 buffer。 +如果 `AudioCallback` 异步持有 `AudioFrame`(例如投递到队列,或在另一线程消费), +则 `data` 指针将成为悬空引用,引发 UB。 + +### 修正方案 + +```cpp +// D_hal_design 修正版 +struct AudioFrame { + std::shared_ptr> data; // 共享所有权 + size_t frame_count = 0; + uint8_t channels = 0; + uint32_t sample_rate = 0; + // ... +}; +``` + +HAL 内部 `captureLoop()` 在调用 ALSA 后**立即将数据复制**到一个堆分配的 +`vector`,包装为 `shared_ptr` 后填入 `AudioFrame`。 +每帧开销:一次 `new` + 一次 memcpy(1024 frames × 4 channels × 2 B = **8 KB**,可忽略)。 + +**与 Camera 侧对齐**:`ImageFrame` 已用 `shared_ptr` 传递, +`AudioFrame` 修正后遵循相同的所有权模型。 + +--- + +## IAudioHAL 方法表 + +| 方法 | 说明 | +|------|------| +| `configure(AudioConfig)` | 配置 ALSA 设备、采样率、通道数、DOA 开关 | +| `open() / close() / reset()` | 继承自 ISensorHAL | +| `startCapture()` | 启动 ALSA 采集线程 | +| `stopCapture()` | 停止采集线程 | +| `setAudioCallback(cb)` | 每个 ALSA period 触发(~64 ms at 16kHz/1024) | +| `setDOACallback(cb)` | 每次 DOA 估计更新触发(ReSpeaker HID 轮询) | +| `getLatestDOA()` | 同步获取最近 DOA(`std::optional`) | + +--- + +## ALSA PCM 映射 + +| `AudioConfig` 字段 | ALSA API | +|-------------------|----------| +| `device_name` | `snd_pcm_open(&handle, device_name, SND_PCM_STREAM_CAPTURE, 0)` | +| `sample_rate` | `snd_pcm_hw_params_set_rate_near()` | +| `channels` | `snd_pcm_hw_params_set_channels()` | +| `format` | `snd_pcm_hw_params_set_format()` | +| `period_frames` | `snd_pcm_hw_params_set_period_size_near()` | +| `buffer_frames` | `snd_pcm_hw_params_set_buffer_size_near()` | + +--- + +## ReSpeaker DOA + +ReSpeaker 4-Mic / 6-Mic 通过 USB HID 暴露 DOA 估计: + +| HID 寄存器 | 名称 | 说明 | +|-----------|------|------| +| 21 | DOAANGLE | 方位角 0~359°(正北 = 0,顺时针正) | +| 19 | SPEECHDETECTED | 语音检测 0/1 | +| 20 | VOICEACTIVITY | VAD 置信度 | + +`DOAResult.azimuth_deg` 在 HAL 内完成安装偏移校正后输出。 + +--- + +## 状态机 + +``` +Closed ──configure()──► Configured ──open()──► Ready ──startCapture()──► Capturing + ▲ │ + └──────────────────────── close() ─────────────────────────────────────────┘ +任意状态 ──fault──► Faulted ──reset()──► Closed +``` + +Capturing 内部子状态: + +| 子状态 | 条件 | HAL 行为 | +|--------|------|---------| +| NORMAL | 正常 | 持续调用 AudioCallback | +| OVERRUN | `snd_pcm_readi` 返回 -EPIPE | `snd_pcm_prepare()` 恢复,drop_count++ | +| ERROR | 无法恢复(-ENODEV 等) | 状态 → Faulted,停止采集线程 | diff --git a/D_hal_design/include/rm_hal_audio/audio_hal.hpp b/D_hal_design/include/rm_hal_audio/audio_hal.hpp new file mode 100644 index 0000000..dc44677 --- /dev/null +++ b/D_hal_design/include/rm_hal_audio/audio_hal.hpp @@ -0,0 +1,58 @@ +#pragma once +#include "rm_hal_audio/audio_types.hpp" +#include "rm_hal_common/hal_factory.hpp" +#include "rm_hal_common/sensor_hal_base.hpp" +#include +#include + +namespace rm::hal::sensor { + +/// HAL interface for microphone array audio capture. +/// +/// Target hardware: +/// ReSpeaker 4-Mic / 6-Mic (USB audio + USB HID DOA) +/// Generic ALSA microphone (no DOA capability) +/// +/// Lifecycle: +/// Closed ──configure()──► Configured ──open()──► Ready ──startCapture()──► Capturing +/// ▲ │ +/// └──────────────────────── close() ─────────────────────────────────────────┘ +/// Faulted ◄── fault ◄── (any state) Closed ◄── reset() ◄── Faulted +class IAudioHAL : public rm::hal::ISensorHAL { +public: + virtual bool configure(const AudioConfig& config) = 0; + // open() / close() / isOpen() / deviceId() / health() / reset() from ISensorHAL + + // ── Streaming control ───────────────────────────────────────────────────── + + /// Start the ALSA capture loop (launches internal thread). + virtual bool startCapture() = 0; + /// Stop the ALSA capture loop. + virtual void stopCapture() = 0; + + // ── Callback API ────────────────────────────────────────────────────────── + + /// Callback fired once per ALSA period (typically ~64 ms at 16 kHz / 1024 frames). + using AudioCallback = std::function; + virtual void setAudioCallback(AudioCallback cb) = 0; + + /// Callback fired whenever a new DOA estimate is computed (if enable_doa = true). + using DOACallback = std::function; + virtual void setDOACallback(DOACallback cb) = 0; + + /// Synchronous getter for the most recent DOA estimate. + /// Returns nullopt if enable_doa = false or no estimate is available yet. + virtual std::optional getLatestDOA() const = 0; +}; + +// ── Factory ─────────────────────────────────────────────────────────────────── + +using AudioFactory = rm::hal::HALFactory; + +/// Registration helper. Typical usage: +/// REGISTER_AUDIO_HAL("respeaker", RespeakerAudioHAL) +/// REGISTER_AUDIO_HAL("sim", SimAudioHAL) +#define REGISTER_AUDIO_HAL(type_name, Impl) \ + REGISTER_HAL(rm::hal::sensor::AudioFactory, type_name, Impl) + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_audio/audio_types.hpp b/D_hal_design/include/rm_hal_audio/audio_types.hpp new file mode 100644 index 0000000..1efea5c --- /dev/null +++ b/D_hal_design/include/rm_hal_audio/audio_types.hpp @@ -0,0 +1,62 @@ +#pragma once +#include "rm_hal_common/sensor_timestamp.hpp" +#include +#include +#include +#include + +namespace rm::hal::sensor { + +/// PCM sample format. +enum class AudioSampleFormat : uint8_t { + S16_LE = 0, ///< 16-bit signed little-endian (most common; speech recognition) + S24_LE = 1, ///< 24-bit signed LE + S32_LE = 2, ///< 32-bit signed LE + F32_LE = 3, ///< 32-bit float LE +}; + +/// One ALSA period of PCM audio samples. +/// +/// OWNERSHIP FIX vs Sensor HAL.md V0.3.2: +/// Original: `const int16_t* data` — raw pointer that aliases the ALSA mmap buffer. +/// Problem: If a callback holds AudioFrame past its call scope, the ALSA buffer +/// is returned to the kernel by snd_pcm_readi() causing a dangling reference. +/// Fix: `shared_ptr>` — the HAL copies data once before +/// enqueue, then creates a shared_ptr. Callbacks may safely extend lifetime +/// by holding their own copy of the shared_ptr. +/// Cost: One memcpy per period (~8 KB for 1024 frames × 4 channels × 2 B). +struct AudioFrame { + /// PCM samples: interleaved channels [ch0_s0, ch1_s0, …, ch0_s1, ch1_s1, …] + std::shared_ptr> data; + size_t frame_count = 0; ///< Samples per channel in this period + uint8_t channels = 0; + uint32_t sample_rate = 0; ///< Hz + AudioSampleFormat format = AudioSampleFormat::S16_LE; + rm::hal::SensorTimestamp timestamp; + uint32_t sequence = 0; + + /// Convenience: total number of samples across all channels. + size_t totalSamples() const noexcept { return frame_count * channels; } +}; + +/// Direction-of-arrival (DOA) result from a microphone array. +struct DOAResult { + float azimuth_deg = 0.f; ///< 0 = forward-facing direction, clockwise positive + float elevation_deg = 0.f; ///< 0 = horizontal plane + float confidence = 0.f; ///< [0, 1] + rm::hal::SensorTimestamp timestamp; +}; + +/// Audio capture configuration. +struct AudioConfig { + std::string device_type = "alsa_generic"; ///< "respeaker_4mic" | "respeaker_6mic" | "alsa_generic" + std::string device_name = "default"; ///< ALSA device string, e.g. "plughw:2,0" + uint32_t sample_rate = 16000; ///< Hz (16000 for speech; 48000 for music) + uint8_t channels = 4; + AudioSampleFormat format = AudioSampleFormat::S16_LE; + size_t period_frames = 1024; ///< ALSA period size in sample frames + size_t buffer_frames = 4096; ///< ALSA buffer size in sample frames + bool enable_doa = true; ///< Read DOA from ReSpeaker HID (register 21) +}; + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_camera/calibration_types.hpp b/D_hal_design/include/rm_hal_camera/calibration_types.hpp new file mode 100644 index 0000000..239c049 --- /dev/null +++ b/D_hal_design/include/rm_hal_camera/calibration_types.hpp @@ -0,0 +1,66 @@ +#pragma once +#include "rm_hal_camera/stream_type.hpp" +#include + +namespace rm::hal::sensor { + +// ── Distortion model ───────────────────────────────────────────────────────── + +/// Lens distortion model enumeration. +/// Both OrbbecSDK and librealsense2 expose this concept; HAL unifies the names. +enum class DistortionModel : uint8_t { + None, + BrownConrady, ///< 5- or 8-parameter radial + tangential (plumb_bob / opencv); both vendors + InverseBrownConrady, ///< RealSense D4xx depth-stream variant + KannalaBrandt4, ///< 4-parameter fisheye lens model +}; + +// ── Camera intrinsics ───────────────────────────────────────────────────────── + +/// Monocular camera intrinsic parameters for a single stream. +struct CameraIntrinsics { + float fx = 0.f, fy = 0.f; ///< Focal length in pixels + float cx = 0.f, cy = 0.f; ///< Principal point in pixels + int width = 0; + int height = 0; + DistortionModel distortion_model = DistortionModel::BrownConrady; + /// Distortion coefficients: k1,k2,p1,p2,k3[,k4,k5,k6] for BrownConrady, + /// or k1–k4 for KannalaBrandt4. Unused elements are 0. + float distortion_coeffs[8] = {}; + bool valid = false; ///< False when calibration data is unavailable +}; + +// ── Camera extrinsics ───────────────────────────────────────────────────────── + +/// Rigid-body transform from one camera stream coordinate frame to another. +struct CameraExtrinsics { + float rotation[9] = {}; ///< 3×3 row-major rotation matrix + float translation[3] = {}; ///< Translation vector in metres + bool valid = false; +}; + +// ── IMU calibration ─────────────────────────────────────────────────────────── + +/// Axis-level calibration parameters for a camera-embedded IMU stream. +/// Returned by ICameraHAL::getIMUCalibration(StreamType::GYRO | ACCEL). +struct IMUCalibration { + StreamType stream = StreamType::GYRO; ///< StreamType::GYRO or StreamType::ACCEL + /// 3×4 row-major matrix: [scale_3x3 | bias_3x1]. + /// Multiply raw int32 reading by this to get corrected SI output. + float scale_bias[12] = {}; + float noise_variances[3] = {}; ///< Measurement noise variance [x,y,z] + float bias_variances[3] = {}; ///< Bias random-walk variance [x,y,z] + bool valid = false; +}; + +// ── Depth metadata ──────────────────────────────────────────────────────────── + +/// Depth stream scale and valid range. +/// Returned by ICameraHAL::getDepthMetadata(). +struct DepthMetadata { + float depth_scale = 0.001f; ///< 1 LSB → metres (Orbbec default: 0.001) + float depth_min_meters = 0.1f; + float depth_max_meters = 10.0f; +}; + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_camera/camera_hal.hpp b/D_hal_design/include/rm_hal_camera/camera_hal.hpp new file mode 100644 index 0000000..6bcf9da --- /dev/null +++ b/D_hal_design/include/rm_hal_camera/camera_hal.hpp @@ -0,0 +1,112 @@ +#pragma once +#include "rm_hal_camera/calibration_types.hpp" +#include "rm_hal_camera/camera_types.hpp" +#include "rm_hal_camera/sync_manager.hpp" +#include "rm_hal_common/hal_factory.hpp" +#include "rm_hal_common/sensor_hal_base.hpp" +#include +#include +#include +#include + +namespace rm::hal::sensor { + +/// Primary HAL interface for colour / depth / IR cameras. +/// +/// Supported adapters (see Sensor HAL.md §2.6): +/// OrbbecCameraHAL (USB) — OrbbecSDK ob::Pipeline +/// OrbbecGmslCameraHAL — OrbbecSDK + IGmslTrigger (/dev/camsync) +/// RealsenseCameraHAL — librealsense2 rs2::pipeline +/// UsbCameraHAL (V4L2) — generic UVC via V4L2 ioctl +/// SimCameraHAL — synthetic data for CI / offline dev +/// +/// Lifecycle: +/// Closed ──configure()──► Configured ──open()──► Opened ──startStreaming()──► Streaming +/// ▲ │ +/// └──────────────────────── close() ────────────────────────────────────────────┘ +/// Faulted ◄── fault ◄── (any state) Closed ◄── reset() ◄── Faulted +class ICameraHAL : public rm::hal::ISensorHAL { +public: + // ── Configuration ──────────────────────────────────────────────────────── + + /// Configure the camera before opening. + /// May be called again after close() to reconfigure without re-creating the object. + virtual bool configure(const CameraConfig& config) = 0; + // open() / close() / isOpen() / deviceId() / health() / reset() from ISensorHAL + + // ── Streaming control ───────────────────────────────────────────────────── + + virtual bool startStreaming() = 0; + virtual bool stopStreaming() = 0; + + // ── Polling API ─────────────────────────────────────────────────────────── + // Active when no callback is registered for the corresponding stream. + // Returns false and sets health().error_msg if a callback is registered. + + virtual bool getColorFrame(ImageFrame& out) = 0; + virtual bool getDepthFrame(ImageFrame& out) = 0; + virtual bool getIRFrame (ImageFrame& out) = 0; + /// Get the latest point cloud (built by SDK PointCloudFilter or HAL). + virtual bool getPointCloud(PointCloud& out) = 0; + + // ── Callback API ────────────────────────────────────────────────────────── + // Setting a callback disables polling for the corresponding stream. + // Callbacks are invoked on a dedicated HAL-internal thread. + // IMPORTANT: do not call any method of the same ICameraHAL instance + // from inside a callback — doing so risks deadlock. + + using FrameCallback = std::function)>; + using FrameSetCallback = std::function; + + virtual void setColorCallback (FrameCallback cb) = 0; + virtual void setDepthCallback (FrameCallback cb) = 0; + virtual void setIRCallback (FrameCallback cb) = 0; + /// Aligned frame set (colour + depth + IR from a single device). + virtual void setFrameSetCallback(FrameSetCallback cb) = 0; + + // ── Profile discovery ───────────────────────────────────────────────────── + + /// Returns all stream configurations supported by the connected device. + virtual std::vector getSupportedProfiles() const = 0; + + // ── Calibration ─────────────────────────────────────────────────────────── + + virtual CameraIntrinsics getIntrinsics(StreamType stream) const = 0; + virtual CameraExtrinsics getExtrinsics(StreamType from, StreamType to) const = 0; + virtual IMUCalibration getIMUCalibration(StreamType imu_stream) const = 0; + virtual DepthMetadata getDepthMetadata() const = 0; + + /// Load user-supplied calibration from a YAML file, overriding factory calibration. + virtual bool loadUserCalibration(const std::string& yaml_path) = 0; + /// Export current calibration to a YAML string. + virtual std::string exportCalibration() const = 0; + + // ── Runtime hardware options ────────────────────────────────────────────── + + /// Get full descriptors for all options the device supports (exposure, gain, WB, …). + virtual std::vector getSupportedOptions() const = 0; + /// Get the full descriptor for a single option. + virtual OptionInfo getOptionInfo(const std::string& name) const = 0; + /// Get the current numeric value of an option. + virtual float getOption(const std::string& name) const = 0; + /// Set an option value at runtime. + virtual bool setOption(const std::string& name, float value) = 0; + + // ── Hardware synchronisation (single-device) ────────────────────────────── + + /// Get the single-device hardware sync manager. + /// Returns nullptr if the device does not support hardware sync. + virtual std::shared_ptr getSyncManager() = 0; +}; + +// ── Factory ─────────────────────────────────────────────────────────────────── + +using CameraFactory = rm::hal::HALFactory; + +/// Registration helper. Typical usage (in driver .cpp): +/// REGISTER_CAMERA_HAL("orbbec", OrbbecCameraHAL) +/// REGISTER_CAMERA_HAL("sim", SimCameraHAL) +#define REGISTER_CAMERA_HAL(type_name, Impl) \ + REGISTER_HAL(rm::hal::sensor::CameraFactory, type_name, Impl) + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_camera/camera_types.hpp b/D_hal_design/include/rm_hal_camera/camera_types.hpp new file mode 100644 index 0000000..477f944 --- /dev/null +++ b/D_hal_design/include/rm_hal_camera/camera_types.hpp @@ -0,0 +1,136 @@ +#pragma once +#include "rm_hal_camera/pixel_encoding.hpp" +#include "rm_hal_camera/stream_type.hpp" +#include "rm_hal_common/sensor_timestamp.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace rm::hal::sensor { + +// ── Image frame ─────────────────────────────────────────────────────────────── + +/// A single decoded image frame from a camera stream. +struct ImageFrame { + PixelEncoding encoding = PixelEncoding::BGR8; + int width = 0; + int height = 0; + int stride = 0; ///< Row stride in bytes (≥ width * bytesPerPixel) + std::vector data; ///< Raw pixel data + + rm::hal::SensorTimestamp timestamp; ///< Unified timestamp (domain = Hardware / System / Global) + uint64_t frame_number = 0; ///< Monotonic hardware frame counter + float actual_exposure_us = 0.f; ///< Actual exposure time in μs; 0 = unknown + float actual_gain = 0.f; ///< Actual gain in dB; 0 = unknown + bool auto_exposure_enabled = false; +}; + +// ── Point cloud (from ICameraHAL::getPointCloud) ───────────────────────────── + +/// Camera-derived point cloud (from depth + intrinsics via SDK PointCloudFilter). +/// For 3D LiDAR point clouds see rm_hal_lidar/lidar_3d_types.hpp. +struct PointCloud { + std::vector points; ///< Interleaved [x0,y0,z0, x1,y1,z1, …] in metres + std::vector colors; ///< Optional interleaved [r,g,b, …]; empty = XYZ only + int valid_count = 0; + rm::hal::SensorTimestamp timestamp; +}; + +// ── Stream profile ──────────────────────────────────────────────────────────── + +/// One stream configuration entry returned by getSupportedProfiles(). +struct StreamProfile { + StreamIndex stream; + int width = 0; + int height = 0; + int fps = 0; + PixelEncoding format = PixelEncoding::BGR8; + + bool operator==(const StreamProfile& o) const noexcept { + return stream == o.stream && width == o.width && height == o.height + && fps == o.fps && format == o.format; + } + + /// Returns true if all non-zero fields in `req` match this profile (0 = wildcard). + bool partialMatch(const StreamProfile& req) const noexcept; +}; + +// ── Hardware option descriptor ──────────────────────────────────────────────── + +/// Data type of a hardware option value. +enum class OptionType : uint8_t { Bool, Int, Float, Enum }; + +/// Full descriptor for a hardware option (exposure, gain, white-balance, sync-mode, …). +/// Returned by getSupportedOptions() and getOptionInfo(). +struct OptionInfo { + std::string name; + std::string description; ///< Human-readable description from SDK + OptionType type = OptionType::Float; + float min = 0.f; + float max = 0.f; + float step = 0.f; + float default_value = 0.f; + bool is_readonly = false; + /// Populated only for OptionType::Enum. + /// Key = enum label (e.g. "FreeRun"), Value = numeric representation. + std::map enum_values; +}; + +// ── FrameSet — single-device aligned frame group ────────────────────────────── + +/// A set of synchronised frames from a single camera device. +/// Delivered via ICameraHAL::setFrameSetCallback(). +struct FrameSet { + std::shared_ptr color; + std::shared_ptr depth; + std::shared_ptr ir; ///< Nullable — present only if IR stream enabled + rm::hal::SensorTimestamp timestamp; ///< Representative aligned timestamp +}; + +// ── Camera configuration ────────────────────────────────────────────────────── + +struct CameraConfig { + std::string device_id; + std::string serial_number; + + // ── Colour stream ───────────────────────────────────────────────────────── + int width = 1280; + int height = 720; + int fps = 30; + PixelEncoding color_encoding = PixelEncoding::BGR8; + bool enable_color = true; + + // ── Depth stream ────────────────────────────────────────────────────────── + int depth_width = 640; + int depth_height = 480; + int depth_fps = 30; + bool enable_depth = true; + + // ── IR stream ───────────────────────────────────────────────────────────── + bool enable_ir = false; + + // ── Streaming behaviour ─────────────────────────────────────────────────── + int ring_buffer_depth = 4; + std::string align_mode = "none"; ///< "none"|"depth_to_color"|"color_to_depth" + std::string frame_aggregate_mode = "ANY"; ///< "full_frame"|"color_frame"|"ANY"|"disable" + + // ── Synchronisation ─────────────────────────────────────────────────────── + std::string sync_mode = "free_run"; ///< "free_run"|"primary"|"secondary"|"hardware_triggering" + + // ── GMSL-specific ───────────────────────────────────────────────────────── + std::string usb_port; ///< GMSL channel id: "gmsl2-1", "gmsl2-3", … + bool enable_gmsl_trigger = false; + int gmsl_trigger_fps = 3000; ///< Units: 0.01 Hz; 3000 = 30.00 Hz + + // ── V4L2-specific ───────────────────────────────────────────────────────── + std::string v4l2_node; ///< e.g. "/dev/video0" + + // ── Driver-specific extra parameters ───────────────────────────────────── + std::unordered_map extra_params; +}; + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_camera/pixel_encoding.hpp b/D_hal_design/include/rm_hal_camera/pixel_encoding.hpp new file mode 100644 index 0000000..7e38c6b --- /dev/null +++ b/D_hal_design/include/rm_hal_camera/pixel_encoding.hpp @@ -0,0 +1,57 @@ +#pragma once +#include + +namespace rm::hal::sensor { + +/// Pixel encoding / format for an image frame. +/// Numeric values are stable — do not reorder or renumber existing entries. +enum class PixelEncoding : uint8_t { + // ── Colour (0x00–0x1F) ────────────────────────────────────────────────── + RGB8 = 0x00, ///< 24-bit RGB packed + BGR8 = 0x01, ///< 24-bit BGR packed (OpenCV default) + RGBA8 = 0x02, + BGRA8 = 0x03, + YUYV = 0x04, ///< YUV422 packed: Y0 U0 Y1 V0 + UYVY = 0x05, ///< YUV422 packed: U0 Y0 V0 Y1 + NV12 = 0x06, ///< YUV420 semi-planar (Y plane + interleaved UV plane) + NV21 = 0x07, ///< YUV420 semi-planar (Y plane + interleaved VU plane) + I420 = 0x08, ///< YUV420 fully planar + M420 = 0x09, ///< YUV420 variant + + // ── Grayscale (0x20–0x2F) ─────────────────────────────────────────────── + MONO8 = 0x20, ///< 8-bit luminance (IR, greyscale) + MONO16 = 0x21, ///< 16-bit luminance + + // ── Depth (0x30–0x3F) ─────────────────────────────────────────────────── + Z16 = 0x30, ///< 16-bit depth in mm (default Orbbec / RealSense depth format) + Z32F = 0x31, ///< 32-bit float depth in metres + + // ── Compressed (0x40–0x4F) ────────────────────────────────────────────── + MJPEG = 0x40, + H264 = 0x41, + H265 = 0x42, + HEVC = H265, ///< Alias for H265 + + // ── IR alias names used by some vendor SDKs (0x50–0x5F) ───────────────── + Y8 = 0x50, ///< Equivalent to MONO8 (OB_FORMAT_Y8) + Y16 = 0x51, ///< Equivalent to MONO16 (OB_FORMAT_Y16) + + // ── Raw / custom (0xF0–0xFF) ───────────────────────────────────────────── + RAW16 = 0xF0, ///< Bayer-pattern 16-bit raw + CUSTOM = 0xFF, +}; + +/// Returns the number of bytes per pixel for packed formats. +/// Returns 0 for compressed or variable-length formats (MJPEG, H264, H265). +int bytesPerPixel(PixelEncoding enc) noexcept; + +/// Returns true for formats that require a software or hardware decoder +/// before individual pixel access is possible. +inline bool isCompressed(PixelEncoding enc) noexcept { + return enc >= PixelEncoding::MJPEG && enc <= PixelEncoding::H265; +} + +/// Returns a short ASCII label (e.g. "BGR8", "Z16", "MJPEG"). Never returns nullptr. +const char* pixelEncodingToString(PixelEncoding enc) noexcept; + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_camera/stream_type.hpp b/D_hal_design/include/rm_hal_camera/stream_type.hpp new file mode 100644 index 0000000..28d993e --- /dev/null +++ b/D_hal_design/include/rm_hal_camera/stream_type.hpp @@ -0,0 +1,46 @@ +#pragma once +#include + +namespace rm::hal::sensor { + +/// Logical stream type within a device. +/// Used as the primary key in StreamIndex and as a parameter to +/// ICameraHAL::getIntrinsics() / getExtrinsics() / getIMUCalibration(). +enum class StreamType : uint8_t { + // ── Image streams ──────────────────────────────────────────────────────── + COLOR = 0x00, + DEPTH = 0x01, + IR_LEFT = 0x02, + IR_RIGHT = 0x03, + FISHEYE = 0x04, + + // ── IMU streams (camera-embedded IMU) ──────────────────────────────────── + GYRO = 0x10, ///< Gyroscope + ACCEL = 0x11, ///< Accelerometer + MOTION = 0x12, ///< Combined motion stream (some SDKs expose a single merged stream) + + // ── Distance streams ───────────────────────────────────────────────────── + LIDAR = 0x20, ///< 3D point cloud (used in I3DLidarHAL context) + LASER_SCAN = 0x21, ///< 2D laser scan (used in I2DLidarHAL context) + + UNKNOWN = 0xFF, +}; + +/// Identifies a specific stream instance within a device. +/// index = 0 for the default single stream. +/// index > 0 for additional streams of the same type +/// (e.g. IR_LEFT(0) / IR_RIGHT(0), COLOR(0) / COLOR(1) on a stereo camera). +struct StreamIndex { + StreamType type = StreamType::UNKNOWN; + int index = 0; + + bool operator==(const StreamIndex& o) const noexcept { + return type == o.type && index == o.index; + } + bool operator!=(const StreamIndex& o) const noexcept { return !(*this == o); } + bool operator<(const StreamIndex& o) const noexcept { + return type < o.type || (type == o.type && index < o.index); + } +}; + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_camera/sync_manager.hpp b/D_hal_design/include/rm_hal_camera/sync_manager.hpp new file mode 100644 index 0000000..a3a49ab --- /dev/null +++ b/D_hal_design/include/rm_hal_camera/sync_manager.hpp @@ -0,0 +1,61 @@ +#pragma once +#include +#include + +namespace rm::hal::sensor { + +/// Hardware synchronisation mode of a single camera device. +/// Maps to Orbbec OBMultiDeviceSyncMode and RealSense inter_cam_sync_mode. +enum class SyncMode : uint8_t { + FreeRun, ///< Default: device runs at its own internal rate + Standalone, ///< Ignores external trigger; does not output a trigger signal + Primary, ///< Generates a trigger signal to drive Secondary devices + Secondary, ///< Passively receives trigger from Primary; may have frame drops + SecondarySynced, ///< Secondary with guaranteed synchronised output + SoftwareTrigger, ///< Single-shot trigger via ISyncManager::triggerOnce() + HardwareTrigger, ///< External GPIO / GMSL PWM trigger (pairs with /dev/camsync) +}; + +/// Fine-grained timing parameters for hardware synchronisation. +/// Not all fields are honoured by every vendor SDK or device model. +struct SyncConfig { + SyncMode mode = SyncMode::FreeRun; + int depth_delay_us = 0; ///< Depth stream trigger delay (μs) + int color_delay_us = 0; ///< Colour stream trigger delay (μs) + int trigger2image_delay_us = 0; ///< Trigger-signal to first-pixel delay (μs) + int trigger_out_delay_us = 0; ///< Output trigger pin delay in Primary mode (μs) + bool trigger_out_enabled = false; ///< Whether to drive the output trigger pin (Primary only) + int frames_per_trigger = 1; ///< Frames generated per trigger pulse +}; + +/// Single-device hardware synchronisation manager. +/// +/// Obtained via ICameraHAL::getSyncManager(). +/// Returns nullptr if the device does not support hardware sync. +/// +/// Layer attribution: ISyncManager is HAL-layer (single device hardware config). +/// It is distinct from ISyncCoordinator (Module layer, multi-device orchestration). +class ISyncManager { +public: + virtual ~ISyncManager() = default; + + /// List the synchronisation modes supported by this device. + virtual std::vector getSupportedSyncModes() const = 0; + + /// Apply a synchronisation configuration. + /// Some devices require the stream to be stopped before changes take effect. + virtual bool setSyncConfig(const SyncConfig& config) = 0; + + /// Read the currently active synchronisation configuration. + virtual SyncConfig getSyncConfig() const = 0; + + /// Fire a software trigger — valid only in SyncMode::SoftwareTrigger. + /// Maps to Orbbec device->triggerCapture(). + virtual bool triggerOnce() = 0; + + /// Returns true when the device (in Secondary mode) has received a trigger + /// from the Primary and is producing synchronised output. + virtual bool isSynced() const = 0; +}; + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_common/error_code.hpp b/D_hal_design/include/rm_hal_common/error_code.hpp new file mode 100644 index 0000000..3b08fec --- /dev/null +++ b/D_hal_design/include/rm_hal_common/error_code.hpp @@ -0,0 +1,59 @@ +#pragma once +#include +#include + +namespace rm::hal { + +/// Unified HAL error codes. +/// +/// Current phase: driver implementations use ErrorCode internally for logging +/// and health().error_msg; public methods still return bool. +/// Future V3.5 migration: bool open() → ErrorCode open(). +enum class ErrorCode : int32_t { + // ── General (0–99) ────────────────────────────────────────────────────── + OK = 0, + UNKNOWN = 1, + NOT_IMPLEMENTED = 2, + + // ── Device lifecycle (100–199) ────────────────────────────────────────── + DEVICE_NOT_FOUND = 100, ///< Device not found during enumeration / open + DEVICE_BUSY = 101, ///< Device is already opened by another process + DEVICE_DISCONNECTED = 102, ///< Device disconnected while streaming (hot-unplug) + INVALID_STATE = 103, ///< Operation not allowed in current state + ALREADY_OPEN = 104, ///< open() called on an already-open device + NOT_OPEN = 105, ///< Data method called before open() + + // ── Configuration (200–299) ───────────────────────────────────────────── + INVALID_CONFIG = 200, ///< Configuration parameter validation failed + UNSUPPORTED_FORMAT = 201, ///< Requested PixelEncoding not supported by device + UNSUPPORTED_RESOLUTION = 202, + UNSUPPORTED_FPS = 203, + + // ── Data / IO (300–399) ───────────────────────────────────────────────── + TIMEOUT = 300, ///< Data acquisition / command timed out + IO_ERROR = 301, ///< Underlying IO error (serial / USB / network / CAN) + FRAME_DROPPED = 302, ///< Frame lost (ring buffer overflow or SDK drop) + CRC_ERROR = 303, ///< Protocol CRC / checksum validation failed + BUFFER_OVERFLOW = 304, ///< Internal buffer overflow + + // ── SDK / driver (400–499) ────────────────────────────────────────────── + SDK_ERROR = 400, ///< Vendor SDK returned an error (details in error_msg) + SDK_NOT_INITIALIZED = 401, + FIRMWARE_MISMATCH = 402, ///< Firmware version incompatible with SDK + + // ── Permissions / resources (500–599) ─────────────────────────────────── + PERMISSION_DENIED = 500, ///< Insufficient permissions (e.g. /dev/video* requires root) + RESOURCE_EXHAUSTED = 501, ///< System resource exhausted (fd / memory / GPU) +}; + +/// Returns a short human-readable label for an ErrorCode. Never returns nullptr. +const char* errorCodeToString(ErrorCode code) noexcept; + +/// Extended error information. +struct ErrorInfo { + ErrorCode code = ErrorCode::OK; + std::string message; ///< Human-readable description + std::string sdk_error_detail; ///< Raw vendor SDK error string (optional) +}; + +} // namespace rm::hal diff --git a/D_hal_design/include/rm_hal_common/hal_factory.hpp b/D_hal_design/include/rm_hal_common/hal_factory.hpp new file mode 100644 index 0000000..ce956e1 --- /dev/null +++ b/D_hal_design/include/rm_hal_common/hal_factory.hpp @@ -0,0 +1,110 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace rm::hal { + +/// Device discovery metadata; returned by HALFactory::enumerateDevices(). +struct DeviceInfo { + std::string type; ///< Factory registration name ("orbbec", "bluesea", "sim", …) + std::string serial_number; ///< Device serial number + std::string name; ///< Human-readable model name (e.g. "Gemini 330", "VLP-16") + std::string connection; ///< "usb" | "gmsl2" | "ethernet" | "serial" | "sim" + std::string port; ///< Physical port identifier ("gmsl2-1", "/dev/video0", …) + std::string firmware_version; +}; + +using DeviceChangedCallback = std::function& added, + const std::vector& removed)>; + +/// Type-parameterised HAL factory with hot-plug support. +/// +/// Specialise as CameraFactory / Lidar2DFactory / Lidar3DFactory / ImuFactory / AudioFactory +/// using the aliases defined at the bottom of each HAL header. +/// +/// Registration (typically at static-init time): +/// REGISTER_HAL(CameraFactory, "orbbec", OrbbecCameraHAL) +/// +/// Usage: +/// auto cam = CameraFactory::instance().create("orbbec"); +template +class HALFactory { +public: + using Creator = std::function()>; + using Enumerator = std::function()>; + + static HALFactory& instance() { + static HALFactory inst; + return inst; + } + + /// Register a driver constructor under a string key. + void registerType(const std::string& type_name, Creator creator) { + creators_[type_name] = std::move(creator); + } + + /// Register a static device enumerator for a driver type (optional). + void registerEnumerator(const std::string& type_name, Enumerator enumerator) { + enumerators_[type_name] = std::move(enumerator); + } + + /// Create a new driver instance for the given type key. + /// Returns nullptr if the key is not registered. + std::unique_ptr create(const std::string& type_name) const { + auto it = creators_.find(type_name); + return (it != creators_.end()) ? it->second() : nullptr; + } + + /// Enumerate all available devices across every registered driver type. + std::vector enumerateDevices() const { + std::vector all; + for (const auto& [name, fn] : enumerators_) { + auto devs = fn(); + all.insert(all.end(), devs.begin(), devs.end()); + } + return all; + } + + /// Register a callback for hot-plug device add / remove events. + void setDeviceChangedCallback(DeviceChangedCallback cb) { + device_changed_cb_ = std::move(cb); + } + + /// Enable POSIX shared-memory process mutex to prevent concurrent device + /// access from multiple processes (e.g. ROS composable container + debug tool). + /// Maps to Orbbec orb_device_lock pattern. + void enableProcessLock(const std::string& lock_name = "rmos_hal_lock") { + process_lock_name_ = lock_name; + process_lock_enabled_ = true; + } + +private: + HALFactory() = default; + HALFactory(const HALFactory&) = delete; + HALFactory& operator=(const HALFactory&) = delete; + + std::unordered_map creators_; + std::unordered_map enumerators_; + DeviceChangedCallback device_changed_cb_; + bool process_lock_enabled_ = false; + std::string process_lock_name_; +}; + +} // namespace rm::hal + +// ── Registration macro ──────────────────────────────────────────────────────── +// Usage (at namespace scope in a .cpp or header): +// REGISTER_HAL(rm::hal::sensor::CameraFactory, "orbbec", OrbbecCameraHAL) +// +// The macro creates a function-local static that triggers exactly once at +// program startup to register the driver with the factory singleton. +#define REGISTER_HAL(Factory, type_name, Impl) \ + static const bool _hal_reg_##Impl = []() { \ + Factory::instance().registerType( \ + type_name, []() { return std::make_unique(); }); \ + return true; \ + }() diff --git a/D_hal_design/include/rm_hal_common/hardware_device.hpp b/D_hal_design/include/rm_hal_common/hardware_device.hpp new file mode 100644 index 0000000..3865c29 --- /dev/null +++ b/D_hal_design/include/rm_hal_common/hardware_device.hpp @@ -0,0 +1,46 @@ +#pragma once +#include "rm_hal_common/health_status.hpp" +#include + +namespace rm::hal { + +/// Base interface for every hardware device managed by RMOS Sensor HAL. +/// +/// Lifecycle (all sensor types share this skeleton): +/// +/// Closed ──configure()──► Configured ──open()──► Opened ──[startX()]──► Streaming +/// ▲ │ +/// └──────────────────────── close() ──────────────────────────────────────┘ +/// Faulted ◄──── fault ◄──── (any state) +/// Closed ◄──── reset() ◄── Faulted +/// +/// Thread safety contract: +/// • open / close / configure / startX / stopX: caller must not overlap these calls. +/// • health() / isOpen() / deviceId(): safe to call concurrently from any thread. +class IHardwareDevice { +public: + virtual ~IHardwareDevice() = default; + + /// Activate the hardware device (open USB / serial / socket, start SDK, etc.). + /// Precondition: configure() has been called; device is in Configured or Closed state. + virtual bool open() = 0; + + /// Deactivate the device and release all associated resources. + /// Idempotent: calling close() on an already-closed device must return true. + virtual bool close() = 0; + + /// Returns true if the device is currently open (Opened or Streaming state). + /// Thread-safe. + virtual bool isOpen() const = 0; + + /// Unique device identifier (serial number, port path, or synthetic id for sim). + /// Returns the same string for the lifetime of the object. + /// Thread-safe. + virtual std::string deviceId() const = 0; + + /// Non-blocking health snapshot. + /// Thread-safe; never throws. + virtual HealthStatus health() const = 0; +}; + +} // namespace rm::hal diff --git a/D_hal_design/include/rm_hal_common/health_status.hpp b/D_hal_design/include/rm_hal_common/health_status.hpp new file mode 100644 index 0000000..7ebffd0 --- /dev/null +++ b/D_hal_design/include/rm_hal_common/health_status.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +namespace rm::hal { + +/// Runtime health snapshot of a HAL device. +/// Returned by IHardwareDevice::health(). +/// All fields are independently atomic/trivially readable — safe to call from any thread. +struct HealthStatus { + bool alive = false; ///< Device is open and actively delivering data + double data_rate_hz = 0.0; ///< Measured output rate (Hz); 0 when not streaming + std::string error_msg; ///< Description of the last error; empty = no error + uint32_t drop_count = 0; ///< Cumulative dropped frames / packets since open() + uint32_t error_count = 0; ///< Cumulative protocol / CRC / SDK errors since open() +}; + +} // namespace rm::hal diff --git a/D_hal_design/include/rm_hal_common/sensor_hal_base.hpp b/D_hal_design/include/rm_hal_common/sensor_hal_base.hpp new file mode 100644 index 0000000..fd41667 --- /dev/null +++ b/D_hal_design/include/rm_hal_common/sensor_hal_base.hpp @@ -0,0 +1,27 @@ +#pragma once +#include "rm_hal_common/hardware_device.hpp" + +namespace rm::hal { + +/// Common base for all sensor HAL interfaces. +/// +/// Adds reset() to the IHardwareDevice lifecycle. +/// reset() is the formal declaration corresponding to the +/// Faulted ──reset()──► Closed +/// transition that appears in every state-machine diagram in Sensor HAL.md but +/// was missing from any interface definition in V0.3.2. +/// +/// Default implementation: calls close() and returns true. +/// Concrete implementations may override to perform deeper cleanup +/// (e.g. reinitialise SDK internal state, flush buffers). +class ISensorHAL : public IHardwareDevice { +public: + /// Recover from Faulted state back to Closed. + /// Default: delegates to close(). + virtual bool reset() { + close(); + return true; + } +}; + +} // namespace rm::hal diff --git a/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp b/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp new file mode 100644 index 0000000..a5af0c1 --- /dev/null +++ b/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp @@ -0,0 +1,37 @@ +#pragma once +#include + +namespace rm::hal { + +/// Identifies the clock source of a sensor timestamp. +/// Use this when comparing timestamps across sensor types to decide whether +/// direct subtraction is valid or whether a PTP / clock-domain adjustment is needed. +enum class TimestampDomain : uint8_t { + Hardware, ///< Device hardware clock — highest accuracy; requires host PTP sync + System, ///< Host steady_clock stamped at packet / frame reception + Global, ///< SDK-aligned PTP clock, if the vendor SDK supports it (e.g. Orbbec "global" domain) +}; + +/// Unified timestamp carried by every sensor data frame. +/// +/// Replaces the bare uint64_t timestamp_ns fields that were spread across +/// LaserScanData, ImuData, AudioFrame etc. in Sensor HAL.md V0.3.2. +/// All frame structs use SensorTimestamp so the upper layer (ArcRT message_filter) +/// can inspect the domain before deciding whether timestamps are directly comparable. +struct SensorTimestamp { + uint64_t ns = 0; + TimestampDomain domain = TimestampDomain::System; + + bool operator<(const SensorTimestamp& o) const noexcept { return ns < o.ns; } + bool operator==(const SensorTimestamp& o) const noexcept { + return ns == o.ns && domain == o.domain; + } + bool operator!=(const SensorTimestamp& o) const noexcept { return !(*this == o); } + + /// Signed microsecond delta: (this - other). Only meaningful when both share the same domain. + int64_t deltaUs(const SensorTimestamp& o) const noexcept { + return static_cast(ns - o.ns) / 1000; + } +}; + +} // namespace rm::hal diff --git a/D_hal_design/include/rm_hal_imu/imu_hal.hpp b/D_hal_design/include/rm_hal_imu/imu_hal.hpp new file mode 100644 index 0000000..f3d3432 --- /dev/null +++ b/D_hal_design/include/rm_hal_imu/imu_hal.hpp @@ -0,0 +1,58 @@ +#pragma once +#include "rm_hal_imu/imu_types.hpp" +#include "rm_hal_common/hal_factory.hpp" +#include "rm_hal_common/sensor_hal_base.hpp" +#include + +namespace rm::hal::sensor { + +/// HAL interface for a 6-DOF inertial measurement unit. +/// +/// Target hardware: YESENSE series (TLV serial protocol, 460800 bps). +/// +/// Lifecycle: +/// Closed ──configure()──► Configured ──open()──► Streaming +/// (Serial data arrives immediately after open(); no explicit startStreaming.) +/// Faulted ◄── fault ◄── (any state) Closed ◄── reset() ◄── Faulted +/// +/// Multi-instance: +/// Create independent IImuHAL instances for each physical IMU. +/// Each holds its own file descriptor and internal thread. +/// Example: body_imu (/dev/yesenseIMU_body) +/// chassis_imu (/dev/yesenseIMU_chassis) +class IImuHAL : public rm::hal::ISensorHAL { +public: + virtual bool configure(const ImuConfig& config) = 0; + // open() / close() / isOpen() / deviceId() / health() / reset() from ISensorHAL + + // ── Polling API ─────────────────────────────────────────────────────────── + /// Get the most recently decoded IMU data. + /// Returns false if no data has been received yet or the device is not open. + virtual bool getData(ImuData& out) = 0; + + // ── Callback API ────────────────────────────────────────────────────────── + /// Callback invoked on every decoded TLV frame (~200 Hz). + /// Invoked on a HAL-internal serial-reader thread — keep the handler brief. + using ImuCallback = std::function; + virtual void setDataCallback(ImuCallback cb) = 0; + + // ── Device metadata ─────────────────────────────────────────────────────── + /// Returns device-level noise / calibration parameters for EKF / UKF config. + virtual ImuDeviceInfo getDeviceInfo() const = 0; + + /// Reset the on-device AHRS orientation estimate (if supported). + /// Default implementation returns false. + virtual bool resetOrientation() { return false; } +}; + +// ── Factory ─────────────────────────────────────────────────────────────────── + +using ImuFactory = rm::hal::HALFactory; + +/// Registration helper. Typical usage: +/// REGISTER_IMU_HAL("yesense", YesenseImuHAL) +/// REGISTER_IMU_HAL("sim", SimImuHAL) +#define REGISTER_IMU_HAL(type_name, Impl) \ + REGISTER_HAL(rm::hal::sensor::ImuFactory, type_name, Impl) + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_imu/imu_types.hpp b/D_hal_design/include/rm_hal_imu/imu_types.hpp new file mode 100644 index 0000000..331649e --- /dev/null +++ b/D_hal_design/include/rm_hal_imu/imu_types.hpp @@ -0,0 +1,116 @@ +#pragma once +#include "rm_hal_common/sensor_timestamp.hpp" +#include +#include + +namespace rm::hal::sensor { + +// ── Sensor range enums ──────────────────────────────────────────────────────── + +enum class AccelRange : int { + G2 = 2, ///< ±2 g + G4 = 4, ///< ±4 g + G8 = 8, ///< ±8 g + G16 = 16, ///< ±16 g +}; + +enum class GyroRange : int { + DPS250 = 250, ///< ±250 °/s + DPS500 = 500, + DPS1000 = 1000, + DPS2000 = 2000, +}; + +/// Fusion strategy used when accelerometer and gyroscope run at different rates. +enum class ImuFusionMode : int { + None = 0, ///< Accel and gyro delivered independently (no alignment) + Copy = 1, ///< Latest value is copied to matching timestamp + Interpolation = 2, ///< Linear interpolation to align timestamps (recommended) +}; + +// ── IMU configuration ───────────────────────────────────────────────────────── + +struct ImuConfig { + std::string device_id; + + // ── Serial port (YESENSE) ───────────────────────────────────────────────── + std::string port; + int baudrate = 460800; + + // ── Sampling ───────────────────────────────────────────────────────────── + int output_rate_hz = 200; + AccelRange accel_range = AccelRange::G8; + GyroRange gyro_range = GyroRange::DPS2000; + ImuFusionMode fusion_mode = ImuFusionMode::Interpolation; + + // ── Correction switches ─────────────────────────────────────────────────── + bool enable_accel_correction = true; + bool enable_gyro_correction = true; + bool enable_mag_correction = false; + + // ── Noise density parameters (for EKF / UKF) ───────────────────────────── + double accel_noise_density = 1e-4; ///< m/s²/√Hz + double gyro_noise_density = 1e-4; ///< rad/s/√Hz + double accel_random_walk = 1e-4; ///< m/s³/√Hz + double gyro_random_walk = 1e-4; ///< rad/s²/√Hz +}; + +// ── IMU data frame ──────────────────────────────────────────────────────────── + +/// One decoded IMU frame from the YESENSE TLV protocol. +/// +/// Timestamp change vs Sensor HAL.md V0.3.2: +/// uint64_t timestamp_ns → SensorTimestamp timestamp +/// domain = System (serial port reception time) unless the device provides +/// a hardware timestamp via DataID 0x80 (SAMPLE_TIMESTAMP), in which case +/// domain = Hardware. +struct ImuData { + rm::hal::SensorTimestamp timestamp; + + // ── Acceleration (m/s²) — DataID 0x10 ACCEL_RAW × 0.000001 × 9.80665 ────── + double accel_x = 0.0, accel_y = 0.0, accel_z = 0.0; + + // ── Angular velocity (rad/s) — DataID 0x20 × 0.000001 × π/180 ────────────── + double gyro_x = 0.0, gyro_y = 0.0, gyro_z = 0.0; + + // ── Orientation quaternion — DataID 0x41 × 0.000001 ────────────────────────── + double quat_w = 1.0, quat_x = 0.0, quat_y = 0.0, quat_z = 0.0; + bool has_orientation = false; + + // ── Linear acceleration without gravity — DataID 0x11 ──────────────────────── + double linear_accel_x = 0.0, linear_accel_y = 0.0, linear_accel_z = 0.0; + bool has_linear_accel = false; + + // ── Euler angles (rad) — DataID 0x40 × 0.000001 × π/180 ──────────────────── + double euler_roll = 0.0, euler_pitch = 0.0, euler_yaw = 0.0; + bool has_euler = false; + + // ── Magnetometer (μT) — DataID 0x30 × 0.001 ───────────────────────────────── + double mag_x = 0.0, mag_y = 0.0, mag_z = 0.0; + bool has_magnetometer = false; + + // ── Temperature (°C) — DataID 0x01 × 0.01 ─────────────────────────────────── + double temperature = 0.0; + bool has_temperature = false; + + // ── Data quality ────────────────────────────────────────────────────────────── + bool is_calibrated = true; + uint32_t sequence = 0; ///< Monotonic counter + uint16_t status_word = 0; ///< DataID 0x70 — device status bit field + uint32_t sample_timestamp_ms = 0; ///< DataID 0x80 — device-side sample time (ms) +}; + +// ── IMU device information ──────────────────────────────────────────────────── + +/// Noise and calibration metadata — used to configure an EKF / UKF. +struct ImuDeviceInfo { + AccelRange accel_range; + GyroRange gyro_range; + double accel_noise_density = 1e-4; + double gyro_noise_density = 1e-4; + double accel_random_walk = 1e-4; + double gyro_random_walk = 1e-4; + double reference_temperature_c = 25.0; ///< Temperature at calibration time +}; + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_lidar/lidar_2d_hal.hpp b/D_hal_design/include/rm_hal_lidar/lidar_2d_hal.hpp new file mode 100644 index 0000000..292f8d9 --- /dev/null +++ b/D_hal_design/include/rm_hal_lidar/lidar_2d_hal.hpp @@ -0,0 +1,50 @@ +#pragma once +#include "rm_hal_lidar/lidar_2d_types.hpp" +#include "rm_hal_common/hal_factory.hpp" +#include "rm_hal_common/sensor_hal_base.hpp" +#include +#include + +namespace rm::hal::sensor { + +/// HAL interface for 2D rotating laser scanners (BlueSea LDS-U50C-S / LDS-U80C-S). +/// +/// Output: LaserScanData — equivalent to ROS sensor_msgs/LaserScan. +/// Use I3DLidarHAL for 3D rotating/solid-state LiDAR (Velodyne, Livox). +/// +/// Lifecycle: +/// Closed ──configure()──► Configured ──open()──► Streaming +/// (UDP data arrives immediately after open(); no explicit startReceiving.) +/// Faulted ◄── fault ◄── (any state) Closed ◄── reset() ◄── Faulted +class I2DLidarHAL : public rm::hal::ISensorHAL { +public: + virtual bool configure(const Lidar2DConfig& config) = 0; + // open() / close() / isOpen() / deviceId() / health() / reset() from ISensorHAL + + // ── Polling API ─────────────────────────────────────────────────────────── + /// Get the most recently assembled 360° scan. + /// Returns false if no scan has been completed yet or the device is not open. + virtual bool getScan(LaserScanData& out) = 0; + + // ── Callback API ────────────────────────────────────────────────────────── + /// Callback fired once per complete 360° scan. + using ScanCallback = std::function)>; + virtual void setScanCallback(ScanCallback cb) = 0; + + // ── Runtime control ─────────────────────────────────────────────────────── + /// Adjust motor speed / scan frequency at runtime (device-dependent). + /// Default implementation returns false (not supported). + virtual bool setScanFrequency(int hz) { (void)hz; return false; } +}; + +// ── Factory ─────────────────────────────────────────────────────────────────── + +using Lidar2DFactory = rm::hal::HALFactory; + +/// Registration helper. Typical usage: +/// REGISTER_LIDAR2D_HAL("bluesea", BlueseaLidar2DHAL) +/// REGISTER_LIDAR2D_HAL("sim", SimLidar2DHAL) +#define REGISTER_LIDAR2D_HAL(type_name, Impl) \ + REGISTER_HAL(rm::hal::sensor::Lidar2DFactory, type_name, Impl) + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp b/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp new file mode 100644 index 0000000..0895319 --- /dev/null +++ b/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp @@ -0,0 +1,79 @@ +#pragma once +#include "rm_hal_common/sensor_timestamp.hpp" +#include +#include +#include + +namespace rm::hal::sensor { + +/// Scan mode hint for 2D LiDAR devices that support multiple modes. +enum class LidarScanMode : int { + Standard = 0, ///< Normal scan rate / resolution + Express = 1, ///< High speed at reduced resolution + Boost = 2, ///< Enhanced scan (device-specific) +}; + +/// Configuration for a 2D laser scanner (BlueSea UDP protocol). +struct Lidar2DConfig { + std::string device_id; + + // ── Network ─────────────────────────────────────────────────────────────── + std::string host; + int port = 6543; ///< BlueSea default UDP port + + // ── Scan geometry ───────────────────────────────────────────────────────── + double angle_min = -M_PI; ///< rad + double angle_max = M_PI; + double range_min = 0.1; ///< metres + double range_max = 30.0; ///< metres + + // ── Scan parameters ─────────────────────────────────────────────────────── + int scan_frequency_hz = 10; + LidarScanMode scan_mode = LidarScanMode::Standard; + int angle_resolution = 100; ///< 0.01° units; 100 = 1.00° + int rpm = 600; ///< Motor speed + + // ── BlueSea protocol flags ──────────────────────────────────────────────── + bool raw_bytes = true; ///< Use raw-byte frame parser + bool with_checksum = false; ///< Validate STM32 CRC32 (HDR2/HDR7 variants) + bool with_intensity = false; ///< Frames carry per-point intensity byte + bool output_360 = true; ///< Assemble full 360° before publishing + + // ── Network tuning ──────────────────────────────────────────────────────── + int recv_buf_size = 1024 * 1024; + int udp_timeout_ms = 5000; + + // ── Filtering ───────────────────────────────────────────────────────────── + bool enable_intensity_filter = false; + float min_intensity = 0.f; + int mask = 0; ///< Angular sector bitmask to suppress + int error_circle = 3; + bool with_deshadow = false; +}; + +/// One complete 360° laser scan — equivalent to ROS sensor_msgs/LaserScan. +/// +/// Timestamp change vs Sensor HAL.md V0.3.2: +/// uint64_t timestamp_ns → SensorTimestamp timestamp +/// domain = Hardware when the BlueSea HDR2 device timestamp is available, +/// domain = System otherwise (packet arrival time on steady_clock). +struct LaserScanData { + rm::hal::SensorTimestamp timestamp; ///< Start-of-scan timestamp + + // ── Geometry ────────────────────────────────────────────────────────────── + double angle_min = 0.0; ///< rad — start angle of this scan + double angle_max = 0.0; ///< rad — end angle + double angle_increment = 0.0; ///< rad per point + double time_increment = 0.0; ///< seconds between adjacent points + double scan_time = 0.0; ///< seconds for a full revolution + + // ── Range ───────────────────────────────────────────────────────────────── + double range_min = 0.0; ///< metres — valid range minimum + double range_max = 0.0; ///< metres — valid range maximum + + // ── Data ────────────────────────────────────────────────────────────────── + std::vector ranges; ///< metres; +inf = no return (< range_min or > range_max) + std::vector intensities; ///< Normalised [0,1]; empty if with_intensity = false +}; + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_lidar/lidar_3d_hal.hpp b/D_hal_design/include/rm_hal_lidar/lidar_3d_hal.hpp new file mode 100644 index 0000000..f3a07cd --- /dev/null +++ b/D_hal_design/include/rm_hal_lidar/lidar_3d_hal.hpp @@ -0,0 +1,54 @@ +#pragma once +#include "rm_hal_lidar/lidar_3d_types.hpp" +#include "rm_hal_common/hal_factory.hpp" +#include "rm_hal_common/sensor_hal_base.hpp" +#include +#include + +namespace rm::hal::sensor { + +/// HAL interface for 3D rotating or solid-state LiDAR sensors. +/// +/// Target hardware: Velodyne VLP-16, VLP-32C, HDL-64E; Livox Mid-360; etc. +/// +/// Rationale for separate interface (split from I2DLidarHAL): +/// 2D laser scanners (BlueSea) output polar range scan data (LaserScanData), +/// semantically equivalent to ROS sensor_msgs/LaserScan. +/// 3D LiDARs output dense Cartesian point clouds (PointCloudXYZI) with +/// per-point timing offsets (motion-distortion correction), ring IDs, +/// and a completely different network protocol. +/// A shared interface would mandate large numbers of "not applicable" methods, +/// reducing type safety and readability. +/// +/// Lifecycle: +/// Closed ──configure()──► Configured ──open()──► Streaming +/// (Data arrives immediately after open(); no explicit startReceiving.) +/// Faulted ◄── fault ◄── (any state) Closed ◄── reset() ◄── Faulted +class I3DLidarHAL : public rm::hal::ISensorHAL { +public: + virtual bool configure(const Lidar3DConfig& config) = 0; + // open() / close() / isOpen() / deviceId() / health() / reset() from ISensorHAL + + // ── Polling API ─────────────────────────────────────────────────────────── + /// Get the most recently assembled point cloud. + /// Returns false if no scan has completed yet or the device is not open. + virtual bool getPointCloud(PointCloudXYZI& out) = 0; + + // ── Callback API ────────────────────────────────────────────────────────── + /// Callback fired once per completed scan packet. + using PointCloudCallback = std::function)>; + virtual void setPointCloudCallback(PointCloudCallback cb) = 0; +}; + +// ── Factory ─────────────────────────────────────────────────────────────────── + +using Lidar3DFactory = rm::hal::HALFactory; + +/// Registration helper. Typical usage: +/// REGISTER_LIDAR3D_HAL("velodyne", VelodyneLidar3DHAL) +/// REGISTER_LIDAR3D_HAL("livox", LivoxLidar3DHAL) +/// REGISTER_LIDAR3D_HAL("sim", SimLidar3DHAL) +#define REGISTER_LIDAR3D_HAL(type_name, Impl) \ + REGISTER_HAL(rm::hal::sensor::Lidar3DFactory, type_name, Impl) + +} // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp b/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp new file mode 100644 index 0000000..9cc3488 --- /dev/null +++ b/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp @@ -0,0 +1,68 @@ +#pragma once +#include "rm_hal_common/sensor_timestamp.hpp" +#include +#include +#include + +namespace rm::hal::sensor { + +/// Multi-return mode — relevant for Velodyne and solid-state LiDAR +/// that can report more than one return per laser pulse. +enum class LidarReturnMode : uint8_t { + Strongest = 0, ///< Single return: highest-intensity return + Last = 1, ///< Single return: last-in-time return + Dual = 2, ///< Dual return: both strongest and last +}; + +/// One point in a 3D LiDAR scan. +/// +/// time_offset_s carries the per-point firing time relative to scan start, +/// enabling Motion Distortion Correction (MDC) in the processing pipeline. +struct PointXYZI { + float x = 0.f; ///< metres (right-hand, LiDAR frame) + float y = 0.f; ///< metres + float z = 0.f; ///< metres + float intensity = 0.f; ///< Normalised reflectivity [0, 1] + float time_offset_s = 0.f; ///< Seconds relative to PointCloudXYZI::timestamp + uint8_t ring = 0; ///< Laser ring / channel index (0-based) +}; + +/// A complete 3D LiDAR scan packet. +struct PointCloudXYZI { + std::vector points; + int valid_count = 0; + rm::hal::SensorTimestamp timestamp; ///< Start-of-scan timestamp + double scan_duration_s = 0.0; ///< Duration of this scan packet (s) + uint32_t sequence = 0; ///< Monotonic scan counter +}; + +/// Configuration for a 3D rotating or solid-state LiDAR. +struct Lidar3DConfig { + std::string device_id; + + // ── Network ─────────────────────────────────────────────────────────────── + std::string host; + int port = 2368; ///< Velodyne default data port; Livox: 56000 + + // ── Return mode ─────────────────────────────────────────────────────────── + LidarReturnMode return_mode = LidarReturnMode::Strongest; + + // ── Scan geometry ───────────────────────────────────────────────────────── + double range_min = 0.1; ///< metres + double range_max = 200.0; ///< metres (VLP-32C max: 200 m) + int target_fps = 10; ///< Desired scans per second (10 or 20) + + // ── Filtering ───────────────────────────────────────────────────────────── + float min_intensity = 0.f; ///< Discard points below this intensity + + // ── Network tuning ──────────────────────────────────────────────────────── + int recv_buf_size = 2 * 1024 * 1024; ///< UDP receive buffer (bytes) + int udp_timeout_ms = 2000; + + // ── Device model hint ───────────────────────────────────────────────────── + /// "VLP-16" | "VLP-32C" | "HDL-64E" | "Livox-Mid360" | "sim" | … + /// Used by the driver to select the correct packet decoder. + std::string model; +}; + +} // namespace rm::hal::sensor From 36c0db2f9ac572d748908120f62fecd7059df1f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 07:45:00 +0000 Subject: [PATCH 06/14] =?UTF-8?q?docs:=20add=20D6=5Fdesign=5Fpurpose.md=20?= =?UTF-8?q?=E2=80=94=20purpose,=20role,=20benefits,=20capabilities=20of=20?= =?UTF-8?q?Sensor=20HAL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/YYCB/sensor_repository/sessions/0ca1bf20-dbe2-4aff-bb55-726e16c8154f Co-authored-by: YYCB <23326150+YYCB@users.noreply.github.com> --- D_hal_design/design/D6_design_purpose.md | 242 +++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 D_hal_design/design/D6_design_purpose.md diff --git a/D_hal_design/design/D6_design_purpose.md b/D_hal_design/design/D6_design_purpose.md new file mode 100644 index 0000000..2c76ec9 --- /dev/null +++ b/D_hal_design/design/D6_design_purpose.md @@ -0,0 +1,242 @@ +# D6 – Sensor HAL 设计目的、作用、收益与能力说明 + +> **适读对象**:对本仓库 `D_hal_design/` 不熟悉的新成员; +> 在评估是否引入本接口层的团队负责人; +> 需要向上汇报时的参考材料。 + +--- + +## 一、设计目的 + +### 核心问题 + +RMOS 机器人系统同时接入 **五类异构传感器**: + +| 传感器 | 典型硬件 | 接入接口 | SDK / 协议 | +|--------|---------|---------|-----------| +| 相机(Camera) | Orbbec Gemini 330 / RealSense D435i / USB UVC | USB / GMSL2 | OrbbecSDK / librealsense2 / V4L2 | +| 2D 激光雷达 | BlueSea LDS-U50C-S | Ethernet UDP | BlueSea 私有帧协议 | +| 3D 激光雷达 | Velodyne VLP-16/32C、Livox Mid-360 | Ethernet UDP | Velodyne PCAP / Livox SDK | +| IMU | YESENSE 系列 | UART 串口 | YESENSE TLV 二进制协议 | +| 麦克风阵列 | ReSpeaker 4-Mic / 6-Mic | USB | ALSA / USB HID | + +如果各业务模块(感知、导航、语音)直接依赖厂商 SDK,会产生以下问题: + +1. **厂商绑定**:更换传感器型号 = 全局改代码,迁移成本高 +2. **时间戳不可信**:每个 SDK 用不同时钟域,跨传感器融合需额外适配 +3. **生命周期不一致**:open/close/reset 在各 SDK 里风格各异,上层必须写胶水代码 +4. **悬空指针风险**:异步回调直接传出 SDK 内部缓冲区的裸指针(如 ALSA mmap),回调延迟时 UB +5. **难以单元测试**:业务逻辑和硬件 I/O 耦合在一起,无法用 Sim 驱动替换 + +**本设计的目的就是用一个薄而稳定的接口层(HAL)解决上述所有问题。** + +--- + +## 二、在 RMOS 中的作用(层次定位) + +``` +┌─ 应用层 / Motion Planning ─────────────────────────────────────────────────────┐ +│ │ +│ Module Library (rm_sensor_module) │ +│ ┌────────────────────────────────────────────────────────────────────────────┐ │ +│ │ ISyncCoordinator FilterPipeline SensorTFPublisher SpeechManager │ │ +│ └────────────────────────────────────────────────────────────────────────────┘ │ +│ ▲ 只依赖 HAL 接口 │ +├─ ArcRT Channel(message_filter 跨品类时间戳对齐)────────────────────────────────┤ +├─ rm_alg_foundation(depthToPointCloud / DecimationFilter)──────────────────────┤ +│ │ +│ ┌── Sensor HAL 接口层(本文件夹,D_hal_design/) ────────────────────────────┐ │ +│ │ ICameraHAL I2DLidarHAL I3DLidarHAL IImuHAL IAudioHAL │ │ +│ │ 统一:open/close/reset/health/deviceId + SensorTimestamp │ │ +│ └─────────────────────────────────────────────────────────────────────────┘ │ +│ ▲ 实现层(驱动,不在本仓库) │ +├─ BSP / Vendor SDK ──────────────────────────────────────────────────────────────┤ +│ OrbbecSDK librealsense2 V4L2 BlueSea UDP YESENSE TLV ALSA │ +└────────────────────────────────────────────────────────────────────────────────┘ +``` + +**HAL 层的精确定位**: + +- **向下**:为各厂商 SDK / BSP 定义统一的实现合约(纯虚接口),驱动开发者只需继承并实现 +- **向上**:为业务模块提供稳定 API,模块代码 `#include` 的是 HAL 头文件,**永远不会看到 OrbbecSDK、librealsense2 或 ALSA 的类型** +- **横向**:提供跨传感器一致的时间戳(`SensorTimestamp`),使 ArcRT `message_filter` 可以对齐任意两路传感器的数据 + +--- + +## 三、带来的收益 + +### 3.1 对业务开发者 + +| 场景 | 无 HAL | 有 HAL | +|------|--------|--------| +| 更换相机型号(Orbbec → RealSense) | 改所有调用 `OBFrame::getTimestamp()` 的地方 | 只换驱动注册 key,业务代码不动 | +| 单元测试感知算法 | 必须接真实相机 | `CameraFactory::instance().create("sim")` 注入仿真帧 | +| 多传感器时间戳对齐 | 各类型时钟来源不同,自行判断 | 统一读 `frame.timestamp.ns` 与 `frame.timestamp.domain` | +| 传感器故障恢复 | 各 SDK 恢复流程不同 | 统一调 `hal->reset()`,内部细节透明 | +| 热插拔 | 轮询 SDK-specific 事件 | `HALFactory::setDeviceChangedCallback()` 统一注册 | + +### 3.2 对驱动开发者 + +- **明确合约**:接口头文件即设计文档,不需要口口相传哪些方法必须实现 +- **默认实现**:`reset()` 已提供 `close()` 降级默认,驱动可选择覆盖也可不管 +- **工厂注册一行搞定**:`REGISTER_CAMERA_HAL("orbbec", OrbbecCameraHAL)` 即可接入工厂体系 + +### 3.3 对系统整体 + +| 收益维度 | 具体描述 | +|---------|---------| +| **安全性** | `AudioFrame::data` 从裸指针改为 `shared_ptr`,消除了异步回调的悬空引用 UB | +| **可测试性** | 接口与 SDK 解耦,CI 可在无硬件环境运行全量感知单测 | +| **可维护性** | 单一变更点:传感器型号升级只影响对应驱动,不扩散到上层 | +| **时间精度** | 统一 `TimestampDomain`(Hardware/System/Global),融合算法可按需选择精度 | +| **扩展性** | 新增传感器类型(如 GNSS)只需新增 `IGnssHAL : ISensorHAL`,不影响已有接口 | + +--- + +## 四、提供的能力 + +### 4.1 统一生命周期管理 + +所有传感器共享相同的状态机骨架,调用方无需记忆每个 SDK 的差异: + +``` +Closed ──configure()──► Configured ──open()──► [Opened/Streaming] + ▲ │ + └──────────────────── close() ───────────────────────┘ +任意状态 ──fault──► Faulted ──reset()──► Closed +``` + +| 方法 | 来自 | 所有传感器通用 | +|------|------|:---:| +| `configure(Config)` | 各 HAL 接口 | ✓ | +| `open()` | `IHardwareDevice` | ✓ | +| `close()` | `IHardwareDevice` | ✓ | +| `reset()` | `ISensorHAL` ★ | ✓ | +| `isOpen()` | `IHardwareDevice` | ✓ | +| `deviceId()` | `IHardwareDevice` | ✓ | +| `health()` | `IHardwareDevice` | ✓ | + +### 4.2 统一健康监控 + +`HealthStatus` 结构体提供任意时刻非阻塞的健康快照: + +```cpp +HealthStatus h = hal->health(); +// h.alive — 设备是否正常出数据 +// h.data_rate_hz — 实测帧率 +// h.error_msg — 最新错误描述 +// h.drop_count — 累计丢帧数 +// h.error_count — 累计协议错误数 +``` + +诊断系统 / 运维看板可统一读取所有传感器的 `health()`,无需了解各 SDK 细节。 + +### 4.3 统一时间域(跨传感器数据融合的基础) + +```cpp +struct SensorTimestamp { + uint64_t ns; // 纳秒 + TimestampDomain domain; // Hardware / System / Global +}; +``` + +| 能力 | 描述 | +|------|------| +| 时钟域区分 | 上层 `message_filter` 可判断两帧是否来自同一时钟,决定是否直接做差 | +| 精度升级路径 | 驱动优先填 `Hardware`(设备自带时戳),回退时填 `System`,行为自描述 | +| 统一 API | 无论 Camera / LiDAR / IMU / Audio,读时间都是 `frame.timestamp.ns` | + +### 4.4 类型化传感器数据 + +每类传感器输出语义精确的独立数据类型,不强制统一: + +| 传感器 | 输出类型 | 关键字段 | +|--------|---------|---------| +| Camera | `ImageFrame` | 编码格式、分辨率、曝光、帧号、`SensorTimestamp` | +| Camera(对齐帧组) | `FrameSet` | color + depth + ir 三路对齐 | +| Camera(点云) | `PointCloud`(SDK 派生) | xyz + rgb,适用于深度+彩色融合 | +| 2D LiDAR | `LaserScanData` | 极坐标 ranges/intensities,等效 ROS LaserScan | +| 3D LiDAR | `PointCloudXYZI` | xyz + intensity + **time_offset**(运动补偿必需)+ ring | +| IMU | `ImuData` | 加速度、角速度、四元数、欧拉角、磁场、温度 | +| Audio | `AudioFrame` | shared_ptr PCM 数据(安全所有权) | +| Audio DOA | `DOAResult` | 方位角、仰角、置信度 | + +### 4.5 双模数据获取(轮询 + 回调) + +每个 HAL 接口同时支持两种获取模式,调用方按需选择: + +```cpp +// 方式 A:轮询(适合主循环同步消费) +LaserScanData scan; +lidar2d->getScan(scan); + +// 方式 B:回调(适合独立线程异步消费) +lidar2d->setScanCallback([](shared_ptr s) { + // HAL 内部线程调用;s 的生命周期由 shared_ptr 管理 +}); +``` + +### 4.6 硬件配置与标定(Camera) + +`ICameraHAL` 提供完整的运行时硬件控制与标定能力: + +- **流配置**:`getSupportedProfiles()` 枚举设备支持的分辨率/帧率/格式组合 +- **运行时选项**:`getOption / setOption`(曝光、增益、白平衡、深度后处理等) +- **标定读写**:`getIntrinsics / getExtrinsics / getIMUCalibration / loadUserCalibration` +- **硬件同步**:`getSyncManager()` → `ISyncManager`(Primary / Secondary / SoftwareTrigger) + +### 4.7 插件式驱动工厂(热插拔支持) + +```cpp +// 注册(驱动 .cpp 静态初始化,零侵入) +REGISTER_CAMERA_HAL("orbbec", OrbbecCameraHAL) +REGISTER_CAMERA_HAL("sim", SimCameraHAL) +REGISTER_LIDAR3D_HAL("velodyne", VelodyneLidar3DHAL) + +// 使用(配置驱动,业务代码无需 if/else) +auto hal = CameraFactory::instance().create(config["type"]); + +// 设备枚举 +auto devices = CameraFactory::instance().enumerateDevices(); + +// 热插拔回调 +CameraFactory::instance().setDeviceChangedCallback( + [](auto added, auto removed) { /* 处理插拔 */ }); +``` + +### 4.8 麦克风阵列 DOA 能力 + +`IAudioHAL` 不仅输出 PCM 音频流,还集成 ReSpeaker USB HID 的方向估计: + +```cpp +audio->setDOACallback([](const DOAResult& r) { + // r.azimuth_deg — 声源方位角(已校正安装偏移) + // r.confidence — 估计置信度 [0,1] +}); +auto latest = audio->getLatestDOA(); // 同步查询 +``` + +--- + +## 五、一句话总结 + +> **Sensor HAL(D_hal_design/)是 RMOS 传感器子系统的「标准插槽」——** +> 它用 20 个 C++17 纯头文件定义了相机、激光雷达、IMU、麦克风四类传感器的统一接入合约, +> 让业务代码与厂商 SDK 彻底解耦,让多传感器数据融合拥有可信的统一时间基准, +> 让传感器从「接一个改一次代码」变成「注册一行字符串、业务代码零改动」。 + +--- + +## 六、快速索引 + +| 想了解… | 看哪里 | +|---------|--------| +| 架构图 & 层次关系 | [D0_overview.md](D0_overview.md) | +| 时间戳统一方案 | [D1_common.md](D1_common.md) §SensorTimestamp | +| 相机接口完整方法 | [D2_camera.md](D2_camera.md) | +| 2D vs 3D LiDAR 拆分理由 | [D3_lidar.md](D3_lidar.md) | +| IMU TLV 字段映射 | [D4_imu.md](D4_imu.md) | +| AudioFrame 安全所有权说明 | [D5_audio.md](D5_audio.md) | +| C++ 头文件 | [../include/](../include/) | +| 传感器型号 & 接入规格 | [../../B_sensor_catalog/](../../B_sensor_catalog/) | +| 相机端到端流程 | [../../A_camera_pipeline/](../../A_camera_pipeline/) | From 661d7946d107c94d4301a6cb9d9a98f03e0c7c45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:52:07 +0000 Subject: [PATCH 07/14] =?UTF-8?q?fix:=20Fix-1~6+14+15=20=E2=80=94=20all=20?= =?UTF-8?q?6=20blocking=20defects=20resolved,=20plus=20Fix-10,14,15=20stro?= =?UTF-8?q?ng=20recs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/YYCB/sensor_repository/sessions/6928edf4-1502-4221-9872-306d97a8b7e7 Co-authored-by: YYCB <23326150+YYCB@users.noreply.github.com> --- .../include/rm_hal_audio/audio_types.hpp | 9 +++-- .../include/rm_hal_camera/camera_types.hpp | 40 ++++++++++++++++--- .../include/rm_hal_camera/pixel_encoding.hpp | 6 +-- .../include/rm_hal_common/hal_factory.hpp | 16 +++++++- .../include/rm_hal_common/sensor_hal_base.hpp | 5 +-- .../rm_hal_common/sensor_timestamp.hpp | 6 ++- 6 files changed, 64 insertions(+), 18 deletions(-) diff --git a/D_hal_design/include/rm_hal_audio/audio_types.hpp b/D_hal_design/include/rm_hal_audio/audio_types.hpp index 1efea5c..42c225c 100644 --- a/D_hal_design/include/rm_hal_audio/audio_types.hpp +++ b/D_hal_design/include/rm_hal_audio/audio_types.hpp @@ -21,13 +21,16 @@ enum class AudioSampleFormat : uint8_t { /// Original: `const int16_t* data` — raw pointer that aliases the ALSA mmap buffer. /// Problem: If a callback holds AudioFrame past its call scope, the ALSA buffer /// is returned to the kernel by snd_pcm_readi() causing a dangling reference. -/// Fix: `shared_ptr>` — the HAL copies data once before +/// Fix: `shared_ptr>` — the HAL copies raw bytes once before /// enqueue, then creates a shared_ptr. Callbacks may safely extend lifetime /// by holding their own copy of the shared_ptr. +/// Interpretation: callers must cast / reinterpret the byte buffer according to +/// AudioFrame::format (e.g. reinterpret_cast for S16_LE). /// Cost: One memcpy per period (~8 KB for 1024 frames × 4 channels × 2 B). struct AudioFrame { - /// PCM samples: interleaved channels [ch0_s0, ch1_s0, …, ch0_s1, ch1_s1, …] - std::shared_ptr> data; + /// PCM samples stored as raw bytes: interleaved channels [ch0_s0, ch1_s0, …] + /// Interpret according to AudioFrame::format. + std::shared_ptr> data; size_t frame_count = 0; ///< Samples per channel in this period uint8_t channels = 0; uint32_t sample_rate = 0; ///< Hz diff --git a/D_hal_design/include/rm_hal_camera/camera_types.hpp b/D_hal_design/include/rm_hal_camera/camera_types.hpp index 477f944..22ed706 100644 --- a/D_hal_design/include/rm_hal_camera/camera_types.hpp +++ b/D_hal_design/include/rm_hal_camera/camera_types.hpp @@ -1,6 +1,7 @@ #pragma once #include "rm_hal_camera/pixel_encoding.hpp" #include "rm_hal_camera/stream_type.hpp" +#include "rm_hal_camera/sync_manager.hpp" #include "rm_hal_common/sensor_timestamp.hpp" #include #include @@ -56,7 +57,14 @@ struct StreamProfile { } /// Returns true if all non-zero fields in `req` match this profile (0 = wildcard). - bool partialMatch(const StreamProfile& req) const noexcept; + bool partialMatch(const StreamProfile& req) const noexcept { + if (req.stream.type != StreamType::UNKNOWN && stream != req.stream) return false; + if (req.width != 0 && width != req.width) return false; + if (req.height != 0 && height != req.height) return false; + if (req.fps != 0 && fps != req.fps) return false; + if (req.format != PixelEncoding::BGR8 && format != req.format) return false; + return true; + } }; // ── Hardware option descriptor ──────────────────────────────────────────────── @@ -91,6 +99,24 @@ struct FrameSet { rm::hal::SensorTimestamp timestamp; ///< Representative aligned timestamp }; +// ── Camera configuration enumerations ──────────────────────────────────────── + +/// Depth-colour alignment mode applied by the SDK or HAL before frame delivery. +enum class AlignMode : uint8_t { + None, ///< No alignment — depth and colour are in their native resolution/FOV + DepthToColor, ///< Depth frame is warped to match the colour sensor FOV + ColorToDepth, ///< Colour frame is warped to match the depth sensor FOV +}; + +/// Frame aggregation policy for multi-stream capture. +/// Controls which stream combination triggers a FrameSet delivery. +enum class FrameAggregateMode : uint8_t { + FullFrame, ///< Deliver FrameSet only when ALL enabled streams have a new frame + ColorFrame, ///< Deliver whenever a new colour frame arrives (depth/IR may be stale) + Any, ///< Deliver on any new frame from any enabled stream + Disabled, ///< Do not aggregate; deliver each stream's frames independently +}; + // ── Camera configuration ────────────────────────────────────────────────────── struct CameraConfig { @@ -114,17 +140,19 @@ struct CameraConfig { bool enable_ir = false; // ── Streaming behaviour ─────────────────────────────────────────────────── - int ring_buffer_depth = 4; - std::string align_mode = "none"; ///< "none"|"depth_to_color"|"color_to_depth" - std::string frame_aggregate_mode = "ANY"; ///< "full_frame"|"color_frame"|"ANY"|"disable" + int ring_buffer_depth = 4; + AlignMode align_mode = AlignMode::None; + FrameAggregateMode frame_aggregate_mode = FrameAggregateMode::Any; // ── Synchronisation ─────────────────────────────────────────────────────── - std::string sync_mode = "free_run"; ///< "free_run"|"primary"|"secondary"|"hardware_triggering" + /// Initial hardware sync mode applied during open(). + /// Can be changed at runtime via ICameraHAL::getSyncManager()->setSyncConfig(). + SyncMode sync_mode = SyncMode::FreeRun; // ── GMSL-specific ───────────────────────────────────────────────────────── std::string usb_port; ///< GMSL channel id: "gmsl2-1", "gmsl2-3", … bool enable_gmsl_trigger = false; - int gmsl_trigger_fps = 3000; ///< Units: 0.01 Hz; 3000 = 30.00 Hz + float gmsl_trigger_fps_hz = 30.0f; ///< GMSL trigger frequency in Hz // ── V4L2-specific ───────────────────────────────────────────────────────── std::string v4l2_node; ///< e.g. "/dev/video0" diff --git a/D_hal_design/include/rm_hal_camera/pixel_encoding.hpp b/D_hal_design/include/rm_hal_camera/pixel_encoding.hpp index 7e38c6b..69113d1 100644 --- a/D_hal_design/include/rm_hal_camera/pixel_encoding.hpp +++ b/D_hal_design/include/rm_hal_camera/pixel_encoding.hpp @@ -32,9 +32,9 @@ enum class PixelEncoding : uint8_t { H265 = 0x42, HEVC = H265, ///< Alias for H265 - // ── IR alias names used by some vendor SDKs (0x50–0x5F) ───────────────── - Y8 = 0x50, ///< Equivalent to MONO8 (OB_FORMAT_Y8) - Y16 = 0x51, ///< Equivalent to MONO16 (OB_FORMAT_Y16) + // ── IR alias names used by some vendor SDKs ────────────────────────────── + Y8 = MONO8, ///< Alias for MONO8 (OB_FORMAT_Y8 maps to 8-bit luminance) + Y16 = MONO16, ///< Alias for MONO16 (OB_FORMAT_Y16 maps to 16-bit luminance) // ── Raw / custom (0xF0–0xFF) ───────────────────────────────────────────── RAW16 = 0xF0, ///< Bayer-pattern 16-bit raw diff --git a/D_hal_design/include/rm_hal_common/hal_factory.hpp b/D_hal_design/include/rm_hal_common/hal_factory.hpp index ce956e1..8588e6e 100644 --- a/D_hal_design/include/rm_hal_common/hal_factory.hpp +++ b/D_hal_design/include/rm_hal_common/hal_factory.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include #include #include @@ -31,6 +32,10 @@ using DeviceChangedCallback = std::function class HALFactory { public: @@ -44,23 +49,27 @@ class HALFactory { /// Register a driver constructor under a string key. void registerType(const std::string& type_name, Creator creator) { + std::lock_guard lock(mutex_); creators_[type_name] = std::move(creator); } /// Register a static device enumerator for a driver type (optional). void registerEnumerator(const std::string& type_name, Enumerator enumerator) { + std::lock_guard lock(mutex_); enumerators_[type_name] = std::move(enumerator); } /// Create a new driver instance for the given type key. /// Returns nullptr if the key is not registered. std::unique_ptr create(const std::string& type_name) const { + std::lock_guard lock(mutex_); auto it = creators_.find(type_name); return (it != creators_.end()) ? it->second() : nullptr; } /// Enumerate all available devices across every registered driver type. std::vector enumerateDevices() const { + std::lock_guard lock(mutex_); std::vector all; for (const auto& [name, fn] : enumerators_) { auto devs = fn(); @@ -70,14 +79,18 @@ class HALFactory { } /// Register a callback for hot-plug device add / remove events. + /// NOTE: Hot-plug event delivery is NOT YET IMPLEMENTED (reserved for V2.0). + /// The callback is stored but never invoked by this version of the factory. void setDeviceChangedCallback(DeviceChangedCallback cb) { + std::lock_guard lock(mutex_); device_changed_cb_ = std::move(cb); } /// Enable POSIX shared-memory process mutex to prevent concurrent device /// access from multiple processes (e.g. ROS composable container + debug tool). - /// Maps to Orbbec orb_device_lock pattern. + /// NOTE: NOT YET IMPLEMENTED (reserved for V2.0). void enableProcessLock(const std::string& lock_name = "rmos_hal_lock") { + std::lock_guard lock(mutex_); process_lock_name_ = lock_name; process_lock_enabled_ = true; } @@ -87,6 +100,7 @@ class HALFactory { HALFactory(const HALFactory&) = delete; HALFactory& operator=(const HALFactory&) = delete; + mutable std::mutex mutex_; std::unordered_map creators_; std::unordered_map enumerators_; DeviceChangedCallback device_changed_cb_; diff --git a/D_hal_design/include/rm_hal_common/sensor_hal_base.hpp b/D_hal_design/include/rm_hal_common/sensor_hal_base.hpp index fd41667..146d367 100644 --- a/D_hal_design/include/rm_hal_common/sensor_hal_base.hpp +++ b/D_hal_design/include/rm_hal_common/sensor_hal_base.hpp @@ -17,10 +17,9 @@ namespace rm::hal { class ISensorHAL : public IHardwareDevice { public: /// Recover from Faulted state back to Closed. - /// Default: delegates to close(). + /// Default: delegates to close() and propagates its return value. virtual bool reset() { - close(); - return true; + return close(); } }; diff --git a/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp b/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp index a5af0c1..9267f3d 100644 --- a/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp +++ b/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp @@ -28,9 +28,11 @@ struct SensorTimestamp { } bool operator!=(const SensorTimestamp& o) const noexcept { return !(*this == o); } - /// Signed microsecond delta: (this - other). Only meaningful when both share the same domain. + /// Signed microsecond delta: (this - other). + /// Only meaningful when both timestamps share the same domain; + /// calling across different domains produces an unspecified result. int64_t deltaUs(const SensorTimestamp& o) const noexcept { - return static_cast(ns - o.ns) / 1000; + return (static_cast(ns) - static_cast(o.ns)) / 1000; } }; From 20ba29355564df2d3f57b4663182fc3c55e0dfdd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:55:25 +0000 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20Fix-7~13=20=E2=80=94=20all=2015=20?= =?UTF-8?q?HAL=20interface=20defects=20resolved;=20full=20regression=20pas?= =?UTF-8?q?ses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/YYCB/sensor_repository/sessions/6928edf4-1502-4221-9872-306d97a8b7e7 Co-authored-by: YYCB <23326150+YYCB@users.noreply.github.com> --- .../include/rm_hal_audio/audio_hal.hpp | 6 ++- .../include/rm_hal_camera/camera_hal.hpp | 4 +- .../include/rm_hal_camera/stream_type.hpp | 10 ++--- .../include/rm_hal_common/sensor_hal_base.hpp | 38 +++++++++++++++---- D_hal_design/include/rm_hal_imu/imu_types.hpp | 26 +++++++++---- .../include/rm_hal_lidar/lidar_3d_types.hpp | 24 +++++++++--- 6 files changed, 79 insertions(+), 29 deletions(-) diff --git a/D_hal_design/include/rm_hal_audio/audio_hal.hpp b/D_hal_design/include/rm_hal_audio/audio_hal.hpp index dc44677..78c55d4 100644 --- a/D_hal_design/include/rm_hal_audio/audio_hal.hpp +++ b/D_hal_design/include/rm_hal_audio/audio_hal.hpp @@ -26,9 +26,11 @@ class IAudioHAL : public rm::hal::ISensorHAL { // ── Streaming control ───────────────────────────────────────────────────── /// Start the ALSA capture loop (launches internal thread). - virtual bool startCapture() = 0; + /// Overrides ISensorHAL::startStreaming(). + virtual bool startStreaming() override = 0; /// Stop the ALSA capture loop. - virtual void stopCapture() = 0; + /// Overrides ISensorHAL::stopStreaming(). + virtual bool stopStreaming() override = 0; // ── Callback API ────────────────────────────────────────────────────────── diff --git a/D_hal_design/include/rm_hal_camera/camera_hal.hpp b/D_hal_design/include/rm_hal_camera/camera_hal.hpp index 6bcf9da..bf12d4d 100644 --- a/D_hal_design/include/rm_hal_camera/camera_hal.hpp +++ b/D_hal_design/include/rm_hal_camera/camera_hal.hpp @@ -36,8 +36,8 @@ class ICameraHAL : public rm::hal::ISensorHAL { // ── Streaming control ───────────────────────────────────────────────────── - virtual bool startStreaming() = 0; - virtual bool stopStreaming() = 0; + virtual bool startStreaming() override = 0; + virtual bool stopStreaming() override = 0; // ── Polling API ─────────────────────────────────────────────────────────── // Active when no callback is registered for the corresponding stream. diff --git a/D_hal_design/include/rm_hal_camera/stream_type.hpp b/D_hal_design/include/rm_hal_camera/stream_type.hpp index 28d993e..21208b2 100644 --- a/D_hal_design/include/rm_hal_camera/stream_type.hpp +++ b/D_hal_design/include/rm_hal_camera/stream_type.hpp @@ -3,9 +3,13 @@ namespace rm::hal::sensor { -/// Logical stream type within a device. +/// Logical stream type within a camera or IMU device. /// Used as the primary key in StreamIndex and as a parameter to /// ICameraHAL::getIntrinsics() / getExtrinsics() / getIMUCalibration(). +/// +/// Note: LiDAR-specific stream types are NOT included here. +/// I2DLidarHAL / I3DLidarHAL use their own data types (LaserScanData / PointCloudXYZI) +/// and do not share this enumeration. enum class StreamType : uint8_t { // ── Image streams ──────────────────────────────────────────────────────── COLOR = 0x00, @@ -19,10 +23,6 @@ enum class StreamType : uint8_t { ACCEL = 0x11, ///< Accelerometer MOTION = 0x12, ///< Combined motion stream (some SDKs expose a single merged stream) - // ── Distance streams ───────────────────────────────────────────────────── - LIDAR = 0x20, ///< 3D point cloud (used in I3DLidarHAL context) - LASER_SCAN = 0x21, ///< 2D laser scan (used in I2DLidarHAL context) - UNKNOWN = 0xFF, }; diff --git a/D_hal_design/include/rm_hal_common/sensor_hal_base.hpp b/D_hal_design/include/rm_hal_common/sensor_hal_base.hpp index 146d367..c97d968 100644 --- a/D_hal_design/include/rm_hal_common/sensor_hal_base.hpp +++ b/D_hal_design/include/rm_hal_common/sensor_hal_base.hpp @@ -5,15 +5,27 @@ namespace rm::hal { /// Common base for all sensor HAL interfaces. /// -/// Adds reset() to the IHardwareDevice lifecycle. -/// reset() is the formal declaration corresponding to the -/// Faulted ──reset()──► Closed -/// transition that appears in every state-machine diagram in Sensor HAL.md but -/// was missing from any interface definition in V0.3.2. +/// Extends IHardwareDevice with two concepts: /// -/// Default implementation: calls close() and returns true. -/// Concrete implementations may override to perform deeper cleanup -/// (e.g. reinitialise SDK internal state, flush buffers). +/// 1. reset() — formal Faulted ──reset()──► Closed transition. +/// Default implementation: calls close() and propagates its return value. +/// Concrete implementations may override to perform deeper cleanup +/// (e.g. reinitialise SDK internal state, flush buffers). +/// +/// 2. startStreaming() / stopStreaming() — uniform streaming lifecycle. +/// All sensor types share the same lifecycle skeleton: +/// Closed ──configure()──► Configured ──open()──► Opened +/// ──startStreaming()──► Streaming +/// Streaming ──stopStreaming()──► Opened +/// (any state) ──close()──► Closed +/// +/// For devices where data flows immediately after open() (e.g. 2D/3D LiDAR, +/// IMU), the default no-op implementations are sufficient. +/// ICameraHAL and IAudioHAL override these with real start/stop logic. +/// +/// Thread safety contract (inherited): +/// • open / close / configure / startStreaming / stopStreaming: caller must not overlap. +/// • health() / isOpen() / deviceId(): safe to call concurrently from any thread. class ISensorHAL : public IHardwareDevice { public: /// Recover from Faulted state back to Closed. @@ -21,6 +33,16 @@ class ISensorHAL : public IHardwareDevice { virtual bool reset() { return close(); } + + /// Begin data delivery. + /// For devices where open() already starts data flow (LiDAR, IMU), the + /// default implementation is a no-op that returns true. + /// ICameraHAL and IAudioHAL override this to start their capture pipelines. + virtual bool startStreaming() { return true; } + + /// Stop data delivery without closing the device (returns to Opened state). + /// The default no-op is sufficient for devices that cannot pause mid-stream. + virtual bool stopStreaming() { return true; } }; } // namespace rm::hal diff --git a/D_hal_design/include/rm_hal_imu/imu_types.hpp b/D_hal_design/include/rm_hal_imu/imu_types.hpp index 331649e..7936d0a 100644 --- a/D_hal_design/include/rm_hal_imu/imu_types.hpp +++ b/D_hal_design/include/rm_hal_imu/imu_types.hpp @@ -48,11 +48,9 @@ struct ImuConfig { bool enable_gyro_correction = true; bool enable_mag_correction = false; - // ── Noise density parameters (for EKF / UKF) ───────────────────────────── - double accel_noise_density = 1e-4; ///< m/s²/√Hz - double gyro_noise_density = 1e-4; ///< rad/s/√Hz - double accel_random_walk = 1e-4; ///< m/s³/√Hz - double gyro_random_walk = 1e-4; ///< rad/s²/√Hz + // Note: noise density / random-walk parameters are not configured here. + // They are factory-calibrated values reported by IImuHAL::getDeviceInfo() + // after the device is opened. Use ImuDeviceInfo to initialise your EKF/UKF. }; // ── IMU data frame ──────────────────────────────────────────────────────────── @@ -96,8 +94,22 @@ struct ImuData { // ── Data quality ────────────────────────────────────────────────────────────── bool is_calibrated = true; uint32_t sequence = 0; ///< Monotonic counter - uint16_t status_word = 0; ///< DataID 0x70 — device status bit field - uint32_t sample_timestamp_ms = 0; ///< DataID 0x80 — device-side sample time (ms) + /// DataID 0x70 — YESENSE device status bit field (raw uint16). + /// Key bits (refer to YESENSE communication protocol §4.7 for the full table): + /// bit 0 : Gyro normal (1) / abnormal (0) + /// bit 1 : Accel normal (1) / abnormal (0) + /// bit 2 : Magnetometer normal (1) / abnormal (0) — only if has_magnetometer + /// bit 3 : Static state detected (1) + /// bit 4 : Initial alignment complete (1) + /// bit 8 : GPS valid (1) — for GNSS-aided variants + /// All other bits reserved; treat as 0. + uint16_t status_word = 0; + /// DataID 0x80 — device-side sample timestamp in milliseconds (wraps at ~49 days). + /// Relationship to SensorTimestamp::ns: when DataID 0x80 is present, + /// the HAL sets timestamp.domain = Hardware and converts this value to + /// timestamp.ns (after host–device clock alignment on open()). + /// If DataID 0x80 is absent, timestamp.domain = System (packet arrival time). + uint32_t sample_timestamp_ms = 0; }; // ── IMU device information ──────────────────────────────────────────────────── diff --git a/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp b/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp index 9cc3488..f063bdd 100644 --- a/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp +++ b/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp @@ -36,7 +36,20 @@ struct PointCloudXYZI { uint32_t sequence = 0; ///< Monotonic scan counter }; -/// Configuration for a 3D rotating or solid-state LiDAR. +/// Known 3D LiDAR hardware models. +/// Used by Lidar3DConfig::model to select the correct packet decoder. +/// Use LidarModel::Custom together with Lidar3DConfig::custom_model_name +/// for any device not listed here. +enum class LidarModel : uint8_t { + VLP_16 = 0, ///< Velodyne VLP-16 (Puck) + VLP_32C = 1, ///< Velodyne VLP-32C + HDL_64E = 2, ///< Velodyne HDL-64E + Livox_Mid360 = 3, ///< Livox Mid-360 + Sim = 4, ///< Synthetic / simulation source + Custom = 255, ///< Other device; see Lidar3DConfig::custom_model_name +}; + + struct Lidar3DConfig { std::string device_id; @@ -59,10 +72,11 @@ struct Lidar3DConfig { int recv_buf_size = 2 * 1024 * 1024; ///< UDP receive buffer (bytes) int udp_timeout_ms = 2000; - // ── Device model hint ───────────────────────────────────────────────────── - /// "VLP-16" | "VLP-32C" | "HDL-64E" | "Livox-Mid360" | "sim" | … - /// Used by the driver to select the correct packet decoder. - std::string model; + // ── Device model ───────────────────────────────────────────────────────── + /// Hardware model — selects the packet decoder inside the driver. + LidarModel model = LidarModel::VLP_16; + /// Optional free-form name used when model == LidarModel::Custom. + std::string custom_model_name; }; } // namespace rm::hal::sensor From e3cbbdd3126500614880e35af6cc98b3b1452a9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 09:13:28 +0000 Subject: [PATCH 09/14] fix: apply all 24 HAL interface defect fixes from expert review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical: - C-1: errorCodeToString / bytesPerPixel / pixelEncodingToString made inline - C-2: health_status comment fixed (std::string is NOT atomic) - C-3: partialMatch() BGR8 wildcard bug fixed via explicit match_any_format flag High: - H-1: angle_resolution int (0.01° units) -> float angle_resolution_deg (degrees) - H-2: DeviceInfo::connection string -> enum class ConnectionType - H-3: AudioConfig::device_type string -> enum class AudioDeviceType - H-4: getIntrinsics/getExtrinsics/getIMUCalibration unified to StreamIndex param - H-5: FrameSet::ir split into ir_left + ir_right for stereo IR support - H-6: error_circle fully documented - H-7: polling API getColorFrame/getDepthFrame/getIRFrame/getPointCloud add timeout_ms Medium: - M-1: CameraConfig::usb_port renamed to gmsl_port - M-3: ImuDeviceInfo gains model/serial_number/firmware_version - M-4: Lidar3DConfig::defaultsFor(LidarModel) factory method added - M-5: PointCloudXYZI::valid_count semantics documented - M-6: IMUCalibration::scale_bias formula clarified with corrected_SI = R*raw + b - M-7: DOAResult::elevation_deg documents 2D-array always-zero behaviour - M-8: std::hash specialisation added - M-9: ISyncManager::isSynced() behaviour documented for all SyncModes Low: - L-1: SensorTimestamp gains >, <=, >= operators and deltaNs() - L-5: ring_buffer_depth valid range [2,32] documented - L-6: DepthMetadata gains valid flag - L-8: enumerateDevices() snapshot-copies enumerator map before calling (deadlock fix) - L-9: audio_hal.hpp lifecycle comment startCapture -> startStreaming All 20 headers pass g++ -std=c++17 -Wall -Wextra -Wpedantic -fsyntax-only Agent-Logs-Url: https://github.com/YYCB/sensor_repository/sessions/7630daeb-0200-4ca2-b765-2680eaeb25af Co-authored-by: YYCB <23326150+YYCB@users.noreply.github.com> --- .../include/rm_hal_audio/audio_hal.hpp | 2 +- .../include/rm_hal_audio/audio_types.hpp | 14 ++++- .../rm_hal_camera/calibration_types.hpp | 23 ++++++-- .../include/rm_hal_camera/camera_hal.hpp | 25 +++++--- .../include/rm_hal_camera/camera_types.hpp | 29 ++++++++-- .../include/rm_hal_camera/pixel_encoding.hpp | 58 +++++++++++++++++-- .../include/rm_hal_camera/stream_type.hpp | 16 +++++ .../include/rm_hal_camera/sync_manager.hpp | 14 ++++- .../include/rm_hal_common/error_code.hpp | 29 +++++++++- .../include/rm_hal_common/hal_factory.hpp | 37 +++++++++--- .../include/rm_hal_common/health_status.hpp | 9 ++- .../rm_hal_common/sensor_timestamp.hpp | 11 +++- D_hal_design/include/rm_hal_imu/imu_types.hpp | 17 ++++-- .../include/rm_hal_lidar/lidar_2d_types.hpp | 9 ++- .../include/rm_hal_lidar/lidar_3d_types.hpp | 50 +++++++++++++++- 15 files changed, 298 insertions(+), 45 deletions(-) diff --git a/D_hal_design/include/rm_hal_audio/audio_hal.hpp b/D_hal_design/include/rm_hal_audio/audio_hal.hpp index 78c55d4..0153e69 100644 --- a/D_hal_design/include/rm_hal_audio/audio_hal.hpp +++ b/D_hal_design/include/rm_hal_audio/audio_hal.hpp @@ -14,7 +14,7 @@ namespace rm::hal::sensor { /// Generic ALSA microphone (no DOA capability) /// /// Lifecycle: -/// Closed ──configure()──► Configured ──open()──► Ready ──startCapture()──► Capturing +/// Closed ──configure()──► Configured ──open()──► Opened ──startStreaming()──► Streaming /// ▲ │ /// └──────────────────────── close() ─────────────────────────────────────────┘ /// Faulted ◄── fault ◄── (any state) Closed ◄── reset() ◄── Faulted diff --git a/D_hal_design/include/rm_hal_audio/audio_types.hpp b/D_hal_design/include/rm_hal_audio/audio_types.hpp index 42c225c..905f933 100644 --- a/D_hal_design/include/rm_hal_audio/audio_types.hpp +++ b/D_hal_design/include/rm_hal_audio/audio_types.hpp @@ -45,14 +45,24 @@ struct AudioFrame { /// Direction-of-arrival (DOA) result from a microphone array. struct DOAResult { float azimuth_deg = 0.f; ///< 0 = forward-facing direction, clockwise positive - float elevation_deg = 0.f; ///< 0 = horizontal plane + /// Elevation angle in degrees above the horizontal plane. + /// NOTE: 2D microphone arrays (ReSpeaker 4-Mic / 6-Mic circular) cannot + /// estimate elevation; this field is always 0.0 for those devices. + float elevation_deg = 0.f; float confidence = 0.f; ///< [0, 1] rm::hal::SensorTimestamp timestamp; }; +/// Identifies the microphone array / audio device driver type. +enum class AudioDeviceType : uint8_t { + ReSpeaker4Mic, ///< ReSpeaker 4-Mic USB array with HID DOA (register 21) + ReSpeaker6Mic, ///< ReSpeaker 6-Mic USB array with HID DOA + AlsaGeneric, ///< Any ALSA-compatible microphone; no DOA capability +}; + /// Audio capture configuration. struct AudioConfig { - std::string device_type = "alsa_generic"; ///< "respeaker_4mic" | "respeaker_6mic" | "alsa_generic" + AudioDeviceType device_type = AudioDeviceType::AlsaGeneric; std::string device_name = "default"; ///< ALSA device string, e.g. "plughw:2,0" uint32_t sample_rate = 16000; ///< Hz (16000 for speech; 48000 for music) uint8_t channels = 4; diff --git a/D_hal_design/include/rm_hal_camera/calibration_types.hpp b/D_hal_design/include/rm_hal_camera/calibration_types.hpp index 239c049..37a945b 100644 --- a/D_hal_design/include/rm_hal_camera/calibration_types.hpp +++ b/D_hal_design/include/rm_hal_camera/calibration_types.hpp @@ -42,14 +42,22 @@ struct CameraExtrinsics { // ── IMU calibration ─────────────────────────────────────────────────────────── /// Axis-level calibration parameters for a camera-embedded IMU stream. -/// Returned by ICameraHAL::getIMUCalibration(StreamType::GYRO | ACCEL). +/// Returned by ICameraHAL::getIMUCalibration(StreamIndex{StreamType::GYRO, 0}) +/// or ICameraHAL::getIMUCalibration(StreamIndex{StreamType::ACCEL, 0}). struct IMUCalibration { StreamType stream = StreamType::GYRO; ///< StreamType::GYRO or StreamType::ACCEL - /// 3×4 row-major matrix: [scale_3x3 | bias_3x1]. - /// Multiply raw int32 reading by this to get corrected SI output. + /// 3×4 row-major calibration matrix: [R_3x3 | b_3x1]. + /// + /// Correction formula: + /// corrected_SI = R * raw_int32 + b + /// + /// where R = scale_bias[0..8] (3×3 row-major scale / cross-axis matrix) + /// b = scale_bias[9..11] (3×1 bias vector, same SI units as output) + /// + /// For accelerometers: output is m/s². For gyroscopes: output is rad/s. float scale_bias[12] = {}; - float noise_variances[3] = {}; ///< Measurement noise variance [x,y,z] - float bias_variances[3] = {}; ///< Bias random-walk variance [x,y,z] + float noise_variances[3] = {}; ///< Measurement noise variance [x,y,z] (SI²/Hz) + float bias_variances[3] = {}; ///< Bias random-walk variance [x,y,z] (SI²·Hz) bool valid = false; }; @@ -61,6 +69,11 @@ struct DepthMetadata { float depth_scale = 0.001f; ///< 1 LSB → metres (Orbbec default: 0.001) float depth_min_meters = 0.1f; float depth_max_meters = 10.0f; + /// False when depth calibration data is unavailable (device not yet opened, + /// or the SDK did not expose scale information). When false, depth_scale + /// retains its default value 0.001 as a best-effort fallback — callers + /// should log a warning rather than treating the value as authoritative. + bool valid = false; }; } // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_camera/camera_hal.hpp b/D_hal_design/include/rm_hal_camera/camera_hal.hpp index bf12d4d..795e2f1 100644 --- a/D_hal_design/include/rm_hal_camera/camera_hal.hpp +++ b/D_hal_design/include/rm_hal_camera/camera_hal.hpp @@ -42,12 +42,19 @@ class ICameraHAL : public rm::hal::ISensorHAL { // ── Polling API ─────────────────────────────────────────────────────────── // Active when no callback is registered for the corresponding stream. // Returns false and sets health().error_msg if a callback is registered. - - virtual bool getColorFrame(ImageFrame& out) = 0; - virtual bool getDepthFrame(ImageFrame& out) = 0; - virtual bool getIRFrame (ImageFrame& out) = 0; + // + // timeout_ms controls blocking behaviour: + // 0 — non-blocking: return false immediately if no new frame is available. + // > 0 — block for up to timeout_ms milliseconds waiting for a new frame; + // return false on timeout. + // -1 — block indefinitely until a frame arrives (use with caution). + // The same timeout semantics apply to getDepthFrame, getIRFrame, and getPointCloud. + + virtual bool getColorFrame(ImageFrame& out, int timeout_ms = 0) = 0; + virtual bool getDepthFrame(ImageFrame& out, int timeout_ms = 0) = 0; + virtual bool getIRFrame (ImageFrame& out, int timeout_ms = 0) = 0; /// Get the latest point cloud (built by SDK PointCloudFilter or HAL). - virtual bool getPointCloud(PointCloud& out) = 0; + virtual bool getPointCloud(PointCloud& out, int timeout_ms = 0) = 0; // ── Callback API ────────────────────────────────────────────────────────── // Setting a callback disables polling for the corresponding stream. @@ -71,9 +78,11 @@ class ICameraHAL : public rm::hal::ISensorHAL { // ── Calibration ─────────────────────────────────────────────────────────── - virtual CameraIntrinsics getIntrinsics(StreamType stream) const = 0; - virtual CameraExtrinsics getExtrinsics(StreamType from, StreamType to) const = 0; - virtual IMUCalibration getIMUCalibration(StreamType imu_stream) const = 0; + virtual CameraIntrinsics getIntrinsics(StreamIndex stream) const = 0; + virtual CameraExtrinsics getExtrinsics(StreamIndex from, StreamIndex to) const = 0; + /// Retrieve calibration for a camera-embedded IMU stream. + /// Pass StreamIndex{StreamType::GYRO, 0} or StreamIndex{StreamType::ACCEL, 0}. + virtual IMUCalibration getIMUCalibration(StreamIndex imu_stream) const = 0; virtual DepthMetadata getDepthMetadata() const = 0; /// Load user-supplied calibration from a YAML file, overriding factory calibration. diff --git a/D_hal_design/include/rm_hal_camera/camera_types.hpp b/D_hal_design/include/rm_hal_camera/camera_types.hpp index 22ed706..ca23155 100644 --- a/D_hal_design/include/rm_hal_camera/camera_types.hpp +++ b/D_hal_design/include/rm_hal_camera/camera_types.hpp @@ -56,15 +56,28 @@ struct StreamProfile { && fps == o.fps && format == o.format; } - /// Returns true if all non-zero fields in `req` match this profile (0 = wildcard). + /// Returns true if all non-zero / non-wildcard fields in `req` match this profile. + /// + /// Wildcard rules: + /// stream.type == StreamType::UNKNOWN → any stream type matches + /// width == 0 → any width matches + /// height == 0 → any height matches + /// fps == 0 → any fps matches + /// match_any_format == true → any pixel format matches + /// + /// NOTE: To explicitly request BGR8 (rather than using it as a wildcard), + /// set req.format = PixelEncoding::BGR8 and req.match_any_format = false. bool partialMatch(const StreamProfile& req) const noexcept { if (req.stream.type != StreamType::UNKNOWN && stream != req.stream) return false; if (req.width != 0 && width != req.width) return false; if (req.height != 0 && height != req.height) return false; if (req.fps != 0 && fps != req.fps) return false; - if (req.format != PixelEncoding::BGR8 && format != req.format) return false; + if (!req.match_any_format && format != req.format) return false; return true; } + + /// When true, partialMatch() accepts any pixel format (format field is ignored). + bool match_any_format = false; }; // ── Hardware option descriptor ──────────────────────────────────────────────── @@ -92,10 +105,15 @@ struct OptionInfo { /// A set of synchronised frames from a single camera device. /// Delivered via ICameraHAL::setFrameSetCallback(). +/// +/// Stereo IR support: devices with two IR sensors (e.g. Orbbec Gemini 330) +/// populate both ir_left and ir_right. Single-IR devices set only ir_left; +/// ir_right remains nullptr. Callers should check each pointer before use. struct FrameSet { std::shared_ptr color; std::shared_ptr depth; - std::shared_ptr ir; ///< Nullable — present only if IR stream enabled + std::shared_ptr ir_left; ///< Left IR frame (or sole IR frame for single-IR devices) + std::shared_ptr ir_right; ///< Right IR frame; nullptr if device has only one IR sensor rm::hal::SensorTimestamp timestamp; ///< Representative aligned timestamp }; @@ -140,6 +158,9 @@ struct CameraConfig { bool enable_ir = false; // ── Streaming behaviour ─────────────────────────────────────────────────── + /// Number of frames held in the HAL-internal ring buffer per stream. + /// Valid range: [2, 32]. Values below 2 are clamped to 2 by the driver. + /// Larger values reduce frame-drop risk under CPU load at the cost of latency. int ring_buffer_depth = 4; AlignMode align_mode = AlignMode::None; FrameAggregateMode frame_aggregate_mode = FrameAggregateMode::Any; @@ -150,7 +171,7 @@ struct CameraConfig { SyncMode sync_mode = SyncMode::FreeRun; // ── GMSL-specific ───────────────────────────────────────────────────────── - std::string usb_port; ///< GMSL channel id: "gmsl2-1", "gmsl2-3", … + std::string gmsl_port; ///< GMSL channel identifier: "gmsl2-1", "gmsl2-3", … bool enable_gmsl_trigger = false; float gmsl_trigger_fps_hz = 30.0f; ///< GMSL trigger frequency in Hz diff --git a/D_hal_design/include/rm_hal_camera/pixel_encoding.hpp b/D_hal_design/include/rm_hal_camera/pixel_encoding.hpp index 69113d1..f217b2a 100644 --- a/D_hal_design/include/rm_hal_camera/pixel_encoding.hpp +++ b/D_hal_design/include/rm_hal_camera/pixel_encoding.hpp @@ -42,16 +42,66 @@ enum class PixelEncoding : uint8_t { }; /// Returns the number of bytes per pixel for packed formats. -/// Returns 0 for compressed or variable-length formats (MJPEG, H264, H265). -int bytesPerPixel(PixelEncoding enc) noexcept; +/// Returns 0 for compressed or variable-length formats (MJPEG, H264, H265), +/// and for planar / semi-planar YUV formats (NV12, NV21, I420, M420) where +/// stride calculation requires knowledge of the plane layout. +inline int bytesPerPixel(PixelEncoding enc) noexcept { + switch (enc) { + case PixelEncoding::RGB8: return 3; + case PixelEncoding::BGR8: return 3; + case PixelEncoding::RGBA8: return 4; + case PixelEncoding::BGRA8: return 4; + case PixelEncoding::YUYV: return 2; + case PixelEncoding::UYVY: return 2; + case PixelEncoding::NV12: return 0; // planar; use height * stride * 3/2 + case PixelEncoding::NV21: return 0; + case PixelEncoding::I420: return 0; + case PixelEncoding::M420: return 0; + case PixelEncoding::MONO8: return 1; + case PixelEncoding::MONO16: return 2; + case PixelEncoding::Z16: return 2; + case PixelEncoding::Z32F: return 4; + case PixelEncoding::RAW16: return 2; + case PixelEncoding::MJPEG: return 0; + case PixelEncoding::H264: return 0; + case PixelEncoding::H265: return 0; + case PixelEncoding::CUSTOM: return 0; + default: return 0; + } +} /// Returns true for formats that require a software or hardware decoder /// before individual pixel access is possible. inline bool isCompressed(PixelEncoding enc) noexcept { - return enc >= PixelEncoding::MJPEG && enc <= PixelEncoding::H265; + return enc == PixelEncoding::MJPEG + || enc == PixelEncoding::H264 + || enc == PixelEncoding::H265; // H265 == HEVC alias } /// Returns a short ASCII label (e.g. "BGR8", "Z16", "MJPEG"). Never returns nullptr. -const char* pixelEncodingToString(PixelEncoding enc) noexcept; +inline const char* pixelEncodingToString(PixelEncoding enc) noexcept { + switch (enc) { + case PixelEncoding::RGB8: return "RGB8"; + case PixelEncoding::BGR8: return "BGR8"; + case PixelEncoding::RGBA8: return "RGBA8"; + case PixelEncoding::BGRA8: return "BGRA8"; + case PixelEncoding::YUYV: return "YUYV"; + case PixelEncoding::UYVY: return "UYVY"; + case PixelEncoding::NV12: return "NV12"; + case PixelEncoding::NV21: return "NV21"; + case PixelEncoding::I420: return "I420"; + case PixelEncoding::M420: return "M420"; + case PixelEncoding::MONO8: return "MONO8"; + case PixelEncoding::MONO16: return "MONO16"; + case PixelEncoding::Z16: return "Z16"; + case PixelEncoding::Z32F: return "Z32F"; + case PixelEncoding::MJPEG: return "MJPEG"; + case PixelEncoding::H264: return "H264"; + case PixelEncoding::H265: return "H265"; + case PixelEncoding::RAW16: return "RAW16"; + case PixelEncoding::CUSTOM: return "CUSTOM"; + default: return "UNKNOWN"; + } +} } // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_camera/stream_type.hpp b/D_hal_design/include/rm_hal_camera/stream_type.hpp index 21208b2..8bcfcf7 100644 --- a/D_hal_design/include/rm_hal_camera/stream_type.hpp +++ b/D_hal_design/include/rm_hal_camera/stream_type.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include namespace rm::hal::sensor { @@ -44,3 +45,18 @@ struct StreamIndex { }; } // namespace rm::hal::sensor + +// ── std::hash specialisation ───────────────────────────────────────────────── +// Enables StreamIndex to be used as an unordered_map / unordered_set key. + +namespace std { +template<> +struct hash { + std::size_t operator()(const rm::hal::sensor::StreamIndex& s) const noexcept { + // Combine StreamType (uint8_t) and index (int) into a single hash. + std::size_t h = static_cast(s.type); + h ^= std::hash{}(s.index) + 0x9e3779b9u + (h << 6) + (h >> 2); + return h; + } +}; +} // namespace std diff --git a/D_hal_design/include/rm_hal_camera/sync_manager.hpp b/D_hal_design/include/rm_hal_camera/sync_manager.hpp index a3a49ab..26df768 100644 --- a/D_hal_design/include/rm_hal_camera/sync_manager.hpp +++ b/D_hal_design/include/rm_hal_camera/sync_manager.hpp @@ -53,8 +53,18 @@ class ISyncManager { /// Maps to Orbbec device->triggerCapture(). virtual bool triggerOnce() = 0; - /// Returns true when the device (in Secondary mode) has received a trigger - /// from the Primary and is producing synchronised output. + /// Returns true when the device is producing hardware-synchronised output. + /// + /// Mode-specific semantics: + /// Secondary / SecondarySynced — returns true when a valid trigger signal + /// has been received from the Primary and the output frame rate is + /// locked to the trigger. + /// Primary — returns true when the output trigger signal is being driven + /// (i.e. after setSyncConfig succeeded and the device is streaming). + /// FreeRun / Standalone — always returns false; these modes have no + /// external synchronisation dependency. + /// SoftwareTrigger / HardwareTrigger — returns true when the last + /// triggered capture completed successfully. virtual bool isSynced() const = 0; }; diff --git a/D_hal_design/include/rm_hal_common/error_code.hpp b/D_hal_design/include/rm_hal_common/error_code.hpp index 3b08fec..736962a 100644 --- a/D_hal_design/include/rm_hal_common/error_code.hpp +++ b/D_hal_design/include/rm_hal_common/error_code.hpp @@ -47,7 +47,34 @@ enum class ErrorCode : int32_t { }; /// Returns a short human-readable label for an ErrorCode. Never returns nullptr. -const char* errorCodeToString(ErrorCode code) noexcept; +inline const char* errorCodeToString(ErrorCode code) noexcept { + switch (code) { + case ErrorCode::OK: return "OK"; + case ErrorCode::UNKNOWN: return "UNKNOWN"; + case ErrorCode::NOT_IMPLEMENTED: return "NOT_IMPLEMENTED"; + case ErrorCode::DEVICE_NOT_FOUND: return "DEVICE_NOT_FOUND"; + case ErrorCode::DEVICE_BUSY: return "DEVICE_BUSY"; + case ErrorCode::DEVICE_DISCONNECTED: return "DEVICE_DISCONNECTED"; + case ErrorCode::INVALID_STATE: return "INVALID_STATE"; + case ErrorCode::ALREADY_OPEN: return "ALREADY_OPEN"; + case ErrorCode::NOT_OPEN: return "NOT_OPEN"; + case ErrorCode::INVALID_CONFIG: return "INVALID_CONFIG"; + case ErrorCode::UNSUPPORTED_FORMAT: return "UNSUPPORTED_FORMAT"; + case ErrorCode::UNSUPPORTED_RESOLUTION: return "UNSUPPORTED_RESOLUTION"; + case ErrorCode::UNSUPPORTED_FPS: return "UNSUPPORTED_FPS"; + case ErrorCode::TIMEOUT: return "TIMEOUT"; + case ErrorCode::IO_ERROR: return "IO_ERROR"; + case ErrorCode::FRAME_DROPPED: return "FRAME_DROPPED"; + case ErrorCode::CRC_ERROR: return "CRC_ERROR"; + case ErrorCode::BUFFER_OVERFLOW: return "BUFFER_OVERFLOW"; + case ErrorCode::SDK_ERROR: return "SDK_ERROR"; + case ErrorCode::SDK_NOT_INITIALIZED: return "SDK_NOT_INITIALIZED"; + case ErrorCode::FIRMWARE_MISMATCH: return "FIRMWARE_MISMATCH"; + case ErrorCode::PERMISSION_DENIED: return "PERMISSION_DENIED"; + case ErrorCode::RESOURCE_EXHAUSTED: return "RESOURCE_EXHAUSTED"; + default: return "UNKNOWN"; + } +} /// Extended error information. struct ErrorInfo { diff --git a/D_hal_design/include/rm_hal_common/hal_factory.hpp b/D_hal_design/include/rm_hal_common/hal_factory.hpp index 8588e6e..aa51fe5 100644 --- a/D_hal_design/include/rm_hal_common/hal_factory.hpp +++ b/D_hal_design/include/rm_hal_common/hal_factory.hpp @@ -9,13 +9,24 @@ namespace rm::hal { /// Device discovery metadata; returned by HALFactory::enumerateDevices(). +/// +/// Connection type of the physical link between the host and the device. +enum class ConnectionType : uint8_t { + USB, ///< USB 2.0 / 3.x + GMSL2, ///< GMSL2 (Gigabit Multimedia Serial Link 2, e.g. NVIDIA Jetson camera connector) + Ethernet, ///< Gigabit / 100BASE-TX (e.g. Velodyne, Livox, GMSL bridge) + Serial, ///< UART / RS-422 / RS-485 (e.g. YESENSE IMU, BlueSea serial variant) + Sim, ///< Synthetic / simulation source (no physical link) + Unknown, ///< Connection type could not be determined +}; + struct DeviceInfo { - std::string type; ///< Factory registration name ("orbbec", "bluesea", "sim", …) - std::string serial_number; ///< Device serial number - std::string name; ///< Human-readable model name (e.g. "Gemini 330", "VLP-16") - std::string connection; ///< "usb" | "gmsl2" | "ethernet" | "serial" | "sim" - std::string port; ///< Physical port identifier ("gmsl2-1", "/dev/video0", …) - std::string firmware_version; + std::string type; ///< Factory registration name ("orbbec", "bluesea", "sim", …) + std::string serial_number; ///< Device serial number + std::string name; ///< Human-readable model name (e.g. "Gemini 330", "VLP-16") + ConnectionType connection = ConnectionType::Unknown; + std::string port; ///< Physical port identifier ("gmsl2-1", "/dev/video0", …) + std::string firmware_version; }; using DeviceChangedCallback = std::function enumerateDevices() const { - std::lock_guard lock(mutex_); + std::unordered_map snapshot; + { + std::lock_guard lock(mutex_); + snapshot = enumerators_; + } std::vector all; - for (const auto& [name, fn] : enumerators_) { + for (const auto& [name, fn] : snapshot) { auto devs = fn(); all.insert(all.end(), devs.begin(), devs.end()); } diff --git a/D_hal_design/include/rm_hal_common/health_status.hpp b/D_hal_design/include/rm_hal_common/health_status.hpp index 7ebffd0..943b294 100644 --- a/D_hal_design/include/rm_hal_common/health_status.hpp +++ b/D_hal_design/include/rm_hal_common/health_status.hpp @@ -6,7 +6,14 @@ namespace rm::hal { /// Runtime health snapshot of a HAL device. /// Returned by IHardwareDevice::health(). -/// All fields are independently atomic/trivially readable — safe to call from any thread. +/// +/// Thread safety: health() itself must be callable from any thread (the virtual +/// method is declared thread-safe in IHardwareDevice). However, the individual +/// fields are NOT independently atomic — in particular, std::string error_msg +/// requires a mutex or copy-on-write scheme inside the implementation. +/// Callers receive a VALUE COPY of HealthStatus returned from health(); reading +/// that local copy is safe. Never read fields of a HealthStatus reference +/// obtained across threads without external synchronisation. struct HealthStatus { bool alive = false; ///< Device is open and actively delivering data double data_rate_hz = 0.0; ///< Measured output rate (Hz); 0 when not streaming diff --git a/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp b/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp index 9267f3d..df8011c 100644 --- a/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp +++ b/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp @@ -23,16 +23,25 @@ struct SensorTimestamp { TimestampDomain domain = TimestampDomain::System; bool operator<(const SensorTimestamp& o) const noexcept { return ns < o.ns; } + bool operator>(const SensorTimestamp& o) const noexcept { return o < *this; } + bool operator<=(const SensorTimestamp& o) const noexcept { return !(o < *this); } + bool operator>=(const SensorTimestamp& o) const noexcept { return !(*this < o); } bool operator==(const SensorTimestamp& o) const noexcept { return ns == o.ns && domain == o.domain; } bool operator!=(const SensorTimestamp& o) const noexcept { return !(*this == o); } + /// Signed nanosecond delta: (this - other). + /// Only meaningful when both timestamps share the same domain. + int64_t deltaNs(const SensorTimestamp& o) const noexcept { + return static_cast(ns) - static_cast(o.ns); + } + /// Signed microsecond delta: (this - other). /// Only meaningful when both timestamps share the same domain; /// calling across different domains produces an unspecified result. int64_t deltaUs(const SensorTimestamp& o) const noexcept { - return (static_cast(ns) - static_cast(o.ns)) / 1000; + return deltaNs(o) / 1000; } }; diff --git a/D_hal_design/include/rm_hal_imu/imu_types.hpp b/D_hal_design/include/rm_hal_imu/imu_types.hpp index 7936d0a..91d4d56 100644 --- a/D_hal_design/include/rm_hal_imu/imu_types.hpp +++ b/D_hal_design/include/rm_hal_imu/imu_types.hpp @@ -115,13 +115,22 @@ struct ImuData { // ── IMU device information ──────────────────────────────────────────────────── /// Noise and calibration metadata — used to configure an EKF / UKF. +/// Returned by IImuHAL::getDeviceInfo() after the device is opened. struct ImuDeviceInfo { + // ── Device identity ─────────────────────────────────────────────────────── + std::string model; ///< Device model string, e.g. "YIS300-IMU" + std::string serial_number; ///< Device serial number (from DataID 0xA0 if available) + std::string firmware_version; ///< Firmware version string (from DataID 0xA1 if available) + + // ── Active ranges (reflecting ImuConfig after open) ─────────────────────── AccelRange accel_range; GyroRange gyro_range; - double accel_noise_density = 1e-4; - double gyro_noise_density = 1e-4; - double accel_random_walk = 1e-4; - double gyro_random_walk = 1e-4; + + // ── Factory-calibrated noise parameters ────────────────────────────────── + double accel_noise_density = 1e-4; ///< m/s²/√Hz + double gyro_noise_density = 1e-4; ///< rad/s/√Hz + double accel_random_walk = 1e-4; ///< m/s²·√Hz (velocity random walk) + double gyro_random_walk = 1e-4; ///< rad/s·√Hz (angular random walk) double reference_temperature_c = 25.0; ///< Temperature at calibration time }; diff --git a/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp b/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp index 0895319..52a5151 100644 --- a/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp +++ b/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp @@ -30,7 +30,9 @@ struct Lidar2DConfig { // ── Scan parameters ─────────────────────────────────────────────────────── int scan_frequency_hz = 10; LidarScanMode scan_mode = LidarScanMode::Standard; - int angle_resolution = 100; ///< 0.01° units; 100 = 1.00° + /// Angular resolution in degrees between adjacent scan points. + /// Typical values: 1.0° (standard), 0.5° (high-res), 2.0° (low-res). + float angle_resolution_deg = 1.0f; int rpm = 600; ///< Motor speed // ── BlueSea protocol flags ──────────────────────────────────────────────── @@ -46,7 +48,10 @@ struct Lidar2DConfig { // ── Filtering ───────────────────────────────────────────────────────────── bool enable_intensity_filter = false; float min_intensity = 0.f; - int mask = 0; ///< Angular sector bitmask to suppress + int mask = 0; ///< Angular sector bitmask to suppress (1 bit = 1°; bit 0 = 0°) + /// Minimum number of consecutive valid points required to retain a scan + /// segment. Isolated returns within a gap of fewer than error_circle + /// points are treated as noise and discarded. Set to 0 to disable. int error_circle = 3; bool with_deshadow = false; }; diff --git a/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp b/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp index f063bdd..fe28b3d 100644 --- a/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp +++ b/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp @@ -28,9 +28,17 @@ struct PointXYZI { }; /// A complete 3D LiDAR scan packet. +/// +/// valid_count vs points.size(): +/// points always has exactly valid_count elements — there are no padding or +/// invalid entries in the vector. A point is included only when its range +/// is within [Lidar3DConfig::range_min, Lidar3DConfig::range_max] and its +/// intensity >= Lidar3DConfig::min_intensity. Callers may iterate over +/// points directly without checking valid_count, but valid_count is provided +/// as a convenience for logging and quick sanity checks. struct PointCloudXYZI { std::vector points; - int valid_count = 0; + int valid_count = 0; ///< Equal to points.size() after assembly rm::hal::SensorTimestamp timestamp; ///< Start-of-scan timestamp double scan_duration_s = 0.0; ///< Duration of this scan packet (s) uint32_t sequence = 0; ///< Monotonic scan counter @@ -55,7 +63,13 @@ struct Lidar3DConfig { // ── Network ─────────────────────────────────────────────────────────────── std::string host; - int port = 2368; ///< Velodyne default data port; Livox: 56000 + /// UDP data port. Default is the Velodyne value (2368). + /// IMPORTANT: different hardware uses different defaults — always use + /// defaultsFor(model) instead of constructing Lidar3DConfig directly when + /// targeting a specific hardware model. + /// Velodyne VLP-16 / VLP-32C / HDL-64E : 2368 + /// Livox Mid-360 : 56000 + int port = 2368; // ── Return mode ─────────────────────────────────────────────────────────── LidarReturnMode return_mode = LidarReturnMode::Strongest; @@ -77,6 +91,38 @@ struct Lidar3DConfig { LidarModel model = LidarModel::VLP_16; /// Optional free-form name used when model == LidarModel::Custom. std::string custom_model_name; + + /// Returns a Lidar3DConfig pre-populated with the correct defaults for + /// the specified hardware model (port, range_max, target_fps, etc.). + /// Always prefer this factory method over default-constructing Lidar3DConfig + /// when the target hardware is known at configuration time. + static Lidar3DConfig defaultsFor(LidarModel m) noexcept { + Lidar3DConfig cfg; + cfg.model = m; + switch (m) { + case LidarModel::VLP_16: + cfg.port = 2368; + cfg.range_max = 100.0; + break; + case LidarModel::VLP_32C: + cfg.port = 2368; + cfg.range_max = 200.0; + break; + case LidarModel::HDL_64E: + cfg.port = 2368; + cfg.range_max = 120.0; + break; + case LidarModel::Livox_Mid360: + cfg.port = 56000; + cfg.range_max = 70.0; + break; + case LidarModel::Sim: + case LidarModel::Custom: + default: + break; + } + return cfg; + } }; } // namespace rm::hal::sensor From cb618c5a0d56259349a763c0d0e661058c004d5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 09:16:58 +0000 Subject: [PATCH 10/14] fix: address code review feedback on interface clarifications - lidar_2d_types: clarify mask 32-bit limitation in comment - sync_manager: document SoftwareTrigger temporal behaviour of isSynced() - health_status: rephrase thread-safety comment (writer needs mutex, caller copy is safe) - calibration_types: add concrete scale_bias row-0 example in comment - camera_types: add MIN_RING_BUFFER_DEPTH constexpr - hal_factory: document O(N) snapshot overhead in enumerateDevices comment - lidar_3d_types: explain why valid_count is retained alongside points.size() Agent-Logs-Url: https://github.com/YYCB/sensor_repository/sessions/7630daeb-0200-4ca2-b765-2680eaeb25af Co-authored-by: YYCB <23326150+YYCB@users.noreply.github.com> --- .../include/rm_hal_camera/calibration_types.hpp | 4 ++++ D_hal_design/include/rm_hal_camera/camera_types.hpp | 6 ++++-- D_hal_design/include/rm_hal_camera/sync_manager.hpp | 10 ++++++++-- D_hal_design/include/rm_hal_common/hal_factory.hpp | 3 ++- D_hal_design/include/rm_hal_common/health_status.hpp | 12 +++++------- D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp | 8 +++++++- D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp | 6 ++++-- 7 files changed, 34 insertions(+), 15 deletions(-) diff --git a/D_hal_design/include/rm_hal_camera/calibration_types.hpp b/D_hal_design/include/rm_hal_camera/calibration_types.hpp index 37a945b..5c6291a 100644 --- a/D_hal_design/include/rm_hal_camera/calibration_types.hpp +++ b/D_hal_design/include/rm_hal_camera/calibration_types.hpp @@ -54,6 +54,10 @@ struct IMUCalibration { /// where R = scale_bias[0..8] (3×3 row-major scale / cross-axis matrix) /// b = scale_bias[9..11] (3×1 bias vector, same SI units as output) /// + /// Concrete example for gyro X-axis (row 0): + /// corrected_x = scale_bias[0]*raw_x + scale_bias[1]*raw_y + /// + scale_bias[2]*raw_z + scale_bias[9] + /// /// For accelerometers: output is m/s². For gyroscopes: output is rad/s. float scale_bias[12] = {}; float noise_variances[3] = {}; ///< Measurement noise variance [x,y,z] (SI²/Hz) diff --git a/D_hal_design/include/rm_hal_camera/camera_types.hpp b/D_hal_design/include/rm_hal_camera/camera_types.hpp index ca23155..71701fd 100644 --- a/D_hal_design/include/rm_hal_camera/camera_types.hpp +++ b/D_hal_design/include/rm_hal_camera/camera_types.hpp @@ -159,8 +159,10 @@ struct CameraConfig { // ── Streaming behaviour ─────────────────────────────────────────────────── /// Number of frames held in the HAL-internal ring buffer per stream. - /// Valid range: [2, 32]. Values below 2 are clamped to 2 by the driver. - /// Larger values reduce frame-drop risk under CPU load at the cost of latency. + /// Valid range: [MIN_RING_BUFFER_DEPTH, 32]. Values below the minimum are + /// clamped by the driver. Larger values reduce frame-drop risk under CPU + /// load at the cost of increased end-to-end latency. + static constexpr int MIN_RING_BUFFER_DEPTH = 2; int ring_buffer_depth = 4; AlignMode align_mode = AlignMode::None; FrameAggregateMode frame_aggregate_mode = FrameAggregateMode::Any; diff --git a/D_hal_design/include/rm_hal_camera/sync_manager.hpp b/D_hal_design/include/rm_hal_camera/sync_manager.hpp index 26df768..1b03f17 100644 --- a/D_hal_design/include/rm_hal_camera/sync_manager.hpp +++ b/D_hal_design/include/rm_hal_camera/sync_manager.hpp @@ -63,8 +63,14 @@ class ISyncManager { /// (i.e. after setSyncConfig succeeded and the device is streaming). /// FreeRun / Standalone — always returns false; these modes have no /// external synchronisation dependency. - /// SoftwareTrigger / HardwareTrigger — returns true when the last - /// triggered capture completed successfully. + /// SoftwareTrigger — returns true from the moment the triggered frame + /// becomes available (i.e. after the HAL has received and queued the + /// frame following the most recent triggerOnce() call). Returns false + /// immediately after triggerOnce() is called and before the resulting + /// frame arrives, and resets to false when the next triggerOnce() call + /// is made. + /// HardwareTrigger — returns true when at least one triggered frame has + /// been received since the last setSyncConfig() / open() call. virtual bool isSynced() const = 0; }; diff --git a/D_hal_design/include/rm_hal_common/hal_factory.hpp b/D_hal_design/include/rm_hal_common/hal_factory.hpp index aa51fe5..10c53ae 100644 --- a/D_hal_design/include/rm_hal_common/hal_factory.hpp +++ b/D_hal_design/include/rm_hal_common/hal_factory.hpp @@ -84,7 +84,8 @@ class HALFactory { /// internal mutex to avoid a potential deadlock when an enumerator /// re-enters registerType() or registerEnumerator() (e.g. during lazy /// hot-plug enumeration). The enumerator map is snapshot-copied under the - /// lock; individual enumerator calls run unlocked. + /// lock at O(N_types) cost; individual enumerator calls run unlocked. + /// For typical deployments (< 10 registered types) this overhead is negligible. std::vector enumerateDevices() const { std::unordered_map snapshot; { diff --git a/D_hal_design/include/rm_hal_common/health_status.hpp b/D_hal_design/include/rm_hal_common/health_status.hpp index 943b294..dc25233 100644 --- a/D_hal_design/include/rm_hal_common/health_status.hpp +++ b/D_hal_design/include/rm_hal_common/health_status.hpp @@ -7,13 +7,11 @@ namespace rm::hal { /// Runtime health snapshot of a HAL device. /// Returned by IHardwareDevice::health(). /// -/// Thread safety: health() itself must be callable from any thread (the virtual -/// method is declared thread-safe in IHardwareDevice). However, the individual -/// fields are NOT independently atomic — in particular, std::string error_msg -/// requires a mutex or copy-on-write scheme inside the implementation. -/// Callers receive a VALUE COPY of HealthStatus returned from health(); reading -/// that local copy is safe. Never read fields of a HealthStatus reference -/// obtained across threads without external synchronisation. +/// Thread safety: the health() method must use internal synchronisation (a +/// mutex or copy-on-write scheme) when writing error_msg, because std::string +/// is not atomically copyable. Callers receive a VALUE COPY of HealthStatus +/// returned from health(), and may read all fields of that local copy without +/// any external locking — the copy itself is thread-safe once received. struct HealthStatus { bool alive = false; ///< Device is open and actively delivering data double data_rate_hz = 0.0; ///< Measured output rate (Hz); 0 when not streaming diff --git a/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp b/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp index 52a5151..01c6f87 100644 --- a/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp +++ b/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp @@ -48,7 +48,13 @@ struct Lidar2DConfig { // ── Filtering ───────────────────────────────────────────────────────────── bool enable_intensity_filter = false; float min_intensity = 0.f; - int mask = 0; ///< Angular sector bitmask to suppress (1 bit = 1°; bit 0 = 0°) + /// Angular sector bitmask to suppress. + /// Each bit corresponds to one 1° sector: bit 0 = 0°–1°, bit 1 = 1°–2°, … + /// NOTE: int is 32 bits on all supported platforms, so only sectors 0°–31° + /// can be individually masked this way. For full 360° masking, use multiple + /// bitmask words or represent the suppressed range via angle_min / angle_max + /// in Lidar2DConfig. A value of 0 (default) disables sector suppression. + int mask = 0; /// Minimum number of consecutive valid points required to retain a scan /// segment. Isolated returns within a gap of fewer than error_circle /// points are treated as noise and discarded. Set to 0 to disable. diff --git a/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp b/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp index fe28b3d..ad50c50 100644 --- a/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp +++ b/D_hal_design/include/rm_hal_lidar/lidar_3d_types.hpp @@ -34,8 +34,10 @@ struct PointXYZI { /// invalid entries in the vector. A point is included only when its range /// is within [Lidar3DConfig::range_min, Lidar3DConfig::range_max] and its /// intensity >= Lidar3DConfig::min_intensity. Callers may iterate over -/// points directly without checking valid_count, but valid_count is provided -/// as a convenience for logging and quick sanity checks. +/// points directly without checking valid_count; the field is retained for +/// API consistency with other sensor types (e.g. PointCloud from camera HAL), +/// for logging convenience, and to support future partial-scan extensions +/// where points.size() may exceed valid_count. struct PointCloudXYZI { std::vector points; int valid_count = 0; ///< Equal to points.size() after assembly From e07c81f0bcf3c54728b499f68128374c397d918e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 09:54:55 +0000 Subject: [PATCH 11/14] fix: handle 3 leftover items + 3 new consistency issues found in audit Agent-Logs-Url: https://github.com/YYCB/sensor_repository/sessions/bacd96ca-f8a9-4129-8fe8-0723f7f1a9cc Co-authored-by: YYCB <23326150+YYCB@users.noreply.github.com> --- .../include/rm_hal_audio/audio_hal.hpp | 6 ++++++ .../include/rm_hal_audio/audio_types.hpp | 19 ++++++++++++++----- .../include/rm_hal_camera/camera_hal.hpp | 7 +++++++ .../include/rm_hal_camera/camera_types.hpp | 6 +++--- D_hal_design/include/rm_hal_imu/imu_types.hpp | 4 ++-- .../include/rm_hal_lidar/lidar_2d_types.hpp | 3 +++ 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/D_hal_design/include/rm_hal_audio/audio_hal.hpp b/D_hal_design/include/rm_hal_audio/audio_hal.hpp index 0153e69..c5223a8 100644 --- a/D_hal_design/include/rm_hal_audio/audio_hal.hpp +++ b/D_hal_design/include/rm_hal_audio/audio_hal.hpp @@ -45,6 +45,12 @@ class IAudioHAL : public rm::hal::ISensorHAL { /// Synchronous getter for the most recent DOA estimate. /// Returns nullopt if enable_doa = false or no estimate is available yet. virtual std::optional getLatestDOA() const = 0; + + /// Returns true when the device can estimate elevation angle (3D DOA). + /// ReSpeaker 4-Mic / 6-Mic circular arrays provide azimuth only and always + /// return false. Returns false if enable_doa = false or the device is not open. + /// When true, DOAResult::elevation_valid will be set in every delivered result. + virtual bool hasElevationCapability() const = 0; }; // ── Factory ─────────────────────────────────────────────────────────────────── diff --git a/D_hal_design/include/rm_hal_audio/audio_types.hpp b/D_hal_design/include/rm_hal_audio/audio_types.hpp index 905f933..7c11e48 100644 --- a/D_hal_design/include/rm_hal_audio/audio_types.hpp +++ b/D_hal_design/include/rm_hal_audio/audio_types.hpp @@ -44,12 +44,18 @@ struct AudioFrame { /// Direction-of-arrival (DOA) result from a microphone array. struct DOAResult { - float azimuth_deg = 0.f; ///< 0 = forward-facing direction, clockwise positive + float azimuth_deg = 0.f; ///< 0 = forward-facing direction, clockwise positive /// Elevation angle in degrees above the horizontal plane. + /// Valid only when elevation_valid == true. /// NOTE: 2D microphone arrays (ReSpeaker 4-Mic / 6-Mic circular) cannot - /// estimate elevation; this field is always 0.0 for those devices. - float elevation_deg = 0.f; - float confidence = 0.f; ///< [0, 1] + /// estimate elevation; elevation_deg is always 0.0 and elevation_valid is + /// always false for those devices. + float elevation_deg = 0.f; + /// True when the device can estimate elevation and this result carries a + /// meaningful elevation_deg value. Query IAudioHAL::hasElevationCapability() + /// to determine at device-open time whether elevation will ever be true. + bool elevation_valid = false; + float confidence = 0.f; ///< [0, 1] rm::hal::SensorTimestamp timestamp; }; @@ -69,7 +75,10 @@ struct AudioConfig { AudioSampleFormat format = AudioSampleFormat::S16_LE; size_t period_frames = 1024; ///< ALSA period size in sample frames size_t buffer_frames = 4096; ///< ALSA buffer size in sample frames - bool enable_doa = true; ///< Read DOA from ReSpeaker HID (register 21) + /// Read DOA from ReSpeaker HID (register 21). + /// Silently ignored — no DOA data is delivered — when device_type == AlsaGeneric, + /// because generic ALSA devices have no DOA hardware. + bool enable_doa = true; }; } // namespace rm::hal::sensor diff --git a/D_hal_design/include/rm_hal_camera/camera_hal.hpp b/D_hal_design/include/rm_hal_camera/camera_hal.hpp index 795e2f1..fd39521 100644 --- a/D_hal_design/include/rm_hal_camera/camera_hal.hpp +++ b/D_hal_design/include/rm_hal_camera/camera_hal.hpp @@ -52,6 +52,10 @@ class ICameraHAL : public rm::hal::ISensorHAL { virtual bool getColorFrame(ImageFrame& out, int timeout_ms = 0) = 0; virtual bool getDepthFrame(ImageFrame& out, int timeout_ms = 0) = 0; + /// Get the latest IR frame. + /// For single-IR devices this is the only IR stream. + /// For stereo-IR devices (e.g. Orbbec Gemini 330) this returns the IR_LEFT + /// frame only; use setFrameSetCallback() to receive ir_right simultaneously. virtual bool getIRFrame (ImageFrame& out, int timeout_ms = 0) = 0; /// Get the latest point cloud (built by SDK PointCloudFilter or HAL). virtual bool getPointCloud(PointCloud& out, int timeout_ms = 0) = 0; @@ -67,6 +71,9 @@ class ICameraHAL : public rm::hal::ISensorHAL { virtual void setColorCallback (FrameCallback cb) = 0; virtual void setDepthCallback (FrameCallback cb) = 0; + /// IR callback for single-IR devices, or for the IR_LEFT stream on stereo-IR + /// devices (e.g. Orbbec Gemini 330). To receive ir_right as well, register + /// setFrameSetCallback() which delivers FrameSet::ir_left and ir_right together. virtual void setIRCallback (FrameCallback cb) = 0; /// Aligned frame set (colour + depth + IR from a single device). virtual void setFrameSetCallback(FrameSetCallback cb) = 0; diff --git a/D_hal_design/include/rm_hal_camera/camera_types.hpp b/D_hal_design/include/rm_hal_camera/camera_types.hpp index 71701fd..e7e5095 100644 --- a/D_hal_design/include/rm_hal_camera/camera_types.hpp +++ b/D_hal_design/include/rm_hal_camera/camera_types.hpp @@ -51,6 +51,9 @@ struct StreamProfile { int fps = 0; PixelEncoding format = PixelEncoding::BGR8; + /// When true, partialMatch() accepts any pixel format (format field is ignored). + bool match_any_format = false; + bool operator==(const StreamProfile& o) const noexcept { return stream == o.stream && width == o.width && height == o.height && fps == o.fps && format == o.format; @@ -75,9 +78,6 @@ struct StreamProfile { if (!req.match_any_format && format != req.format) return false; return true; } - - /// When true, partialMatch() accepts any pixel format (format field is ignored). - bool match_any_format = false; }; // ── Hardware option descriptor ──────────────────────────────────────────────── diff --git a/D_hal_design/include/rm_hal_imu/imu_types.hpp b/D_hal_design/include/rm_hal_imu/imu_types.hpp index 91d4d56..c526a1a 100644 --- a/D_hal_design/include/rm_hal_imu/imu_types.hpp +++ b/D_hal_design/include/rm_hal_imu/imu_types.hpp @@ -123,8 +123,8 @@ struct ImuDeviceInfo { std::string firmware_version; ///< Firmware version string (from DataID 0xA1 if available) // ── Active ranges (reflecting ImuConfig after open) ─────────────────────── - AccelRange accel_range; - GyroRange gyro_range; + AccelRange accel_range = AccelRange::G8; ///< Matches ImuConfig default + GyroRange gyro_range = GyroRange::DPS2000; ///< Matches ImuConfig default // ── Factory-calibrated noise parameters ────────────────────────────────── double accel_noise_density = 1e-4; ///< m/s²/√Hz diff --git a/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp b/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp index 01c6f87..8168b22 100644 --- a/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp +++ b/D_hal_design/include/rm_hal_lidar/lidar_2d_types.hpp @@ -32,6 +32,9 @@ struct Lidar2DConfig { LidarScanMode scan_mode = LidarScanMode::Standard; /// Angular resolution in degrees between adjacent scan points. /// Typical values: 1.0° (standard), 0.5° (high-res), 2.0° (low-res). + /// Relationship to output: LaserScanData::angle_increment (radians) = + /// angle_resolution_deg * π / 180. The driver converts and populates + /// angle_increment in every delivered LaserScanData. float angle_resolution_deg = 1.0f; int rpm = 600; ///< Motor speed From 79662d4858b57621b8d0568d80e5ae44210037e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:06:05 +0000 Subject: [PATCH 12/14] build: add CMake build system, CTest unit tests, CI workflow Agent-Logs-Url: https://github.com/YYCB/sensor_repository/sessions/f509ddc3-cdf8-4905-a48d-0f9562f1e26a Co-authored-by: YYCB <23326150+YYCB@users.noreply.github.com> --- .github/workflows/ci.yml | 55 ++ .gitignore | 32 + CMakeLists.txt | 22 + D_hal_design/CMakeLists.txt | 38 ++ D_hal_design/tests/CMakeLists.txt | 16 + D_hal_design/tests/test_compile.cpp | 28 + D_hal_design/tests/test_types.cpp | 355 ++++++++++ build_rel/D_hal_design/Makefile | 200 ++++++ build_rel/D_hal_design/tests/Makefile | 284 ++++++++ build_rel/D_hal_design/tests/test_compile | Bin 0 -> 15784 bytes build_rel/D_hal_design/tests/test_types | Bin 0 -> 27896 bytes build_rel/DartConfiguration.tcl | 109 +++ build_rel/Makefile | 620 ++++++++++++++++++ build_rel/Testing/Temporary/CTestCostData.txt | 3 + build_rel/Testing/Temporary/LastTest.log | 61 ++ cmake/CompilerWarnings.cmake | 25 + scripts/build.sh | 25 + 17 files changed, 1873 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 D_hal_design/CMakeLists.txt create mode 100644 D_hal_design/tests/CMakeLists.txt create mode 100644 D_hal_design/tests/test_compile.cpp create mode 100644 D_hal_design/tests/test_types.cpp create mode 100644 build_rel/D_hal_design/Makefile create mode 100644 build_rel/D_hal_design/tests/Makefile create mode 100755 build_rel/D_hal_design/tests/test_compile create mode 100755 build_rel/D_hal_design/tests/test_types create mode 100644 build_rel/DartConfiguration.tcl create mode 100644 build_rel/Makefile create mode 100644 build_rel/Testing/Temporary/CTestCostData.txt create mode 100644 build_rel/Testing/Temporary/LastTest.log create mode 100644 cmake/CompilerWarnings.cmake create mode 100755 scripts/build.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ecdb5c4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + build-and-test: + name: ${{ matrix.name }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - name: "GCC 13 / Debug" + cc: gcc-13 + cxx: g++-13 + type: Debug + + - name: "GCC 13 / Release" + cc: gcc-13 + cxx: g++-13 + type: Release + + - name: "Clang 18 / Debug" + cc: clang-18 + cxx: clang++-18 + type: Debug + + steps: + - uses: actions/checkout@v4 + + - name: Install Clang 18 + if: startsWith(matrix.cxx, 'clang') + run: | + sudo apt-get update -qq + sudo apt-get install -y clang-18 + + - name: Configure + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + run: | + cmake -B build \ + -DCMAKE_BUILD_TYPE=${{ matrix.type }} \ + -DBUILD_TESTING=ON + + - name: Build + run: cmake --build build --parallel $(nproc) + + - name: Test + working-directory: build + run: ctest --output-on-failure --parallel $(nproc) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d4fa80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Build artifacts +build/ +out/ +.build/ + +# CMake generated +CMakeCache.txt +CMakeFiles/ +CMakeScripts/ +cmake_install.cmake +install_manifest.txt +CTestTestfile.cmake +_deps/ + +# Compiled objects +*.o +*.obj +*.a +*.so +*.so.* +*.dylib +*.dll +*.lib +*.exe + +# IDE / editor +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a409bca --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.22) + +project(rmos_sensor_hal + VERSION 0.4.0 + LANGUAGES CXX + DESCRIPTION "RMOS Sensor HAL — header-only C++17 interface library" +) + +# ── C++ standard ─────────────────────────────────────────────────────────────── +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# ── Compiler warnings (function used by test targets) ───────────────────────── +include(cmake/CompilerWarnings.cmake) + +# ── Testing ──────────────────────────────────────────────────────────────────── +# Enable with: cmake -DBUILD_TESTING=ON ... +include(CTest) + +# ── Sub-projects ─────────────────────────────────────────────────────────────── +add_subdirectory(D_hal_design) diff --git a/D_hal_design/CMakeLists.txt b/D_hal_design/CMakeLists.txt new file mode 100644 index 0000000..be10896 --- /dev/null +++ b/D_hal_design/CMakeLists.txt @@ -0,0 +1,38 @@ +# D_hal_design/CMakeLists.txt +# +# Defines the rm_hal_headers INTERFACE library. +# Consumers link with: target_link_libraries( PRIVATE rm_hal::headers) + +# ── INTERFACE library ────────────────────────────────────────────────────────── +add_library(rm_hal_headers INTERFACE) +add_library(rm_hal::headers ALIAS rm_hal_headers) + +target_include_directories(rm_hal_headers INTERFACE + $ + $ +) + +target_compile_features(rm_hal_headers INTERFACE cxx_std_17) + +# ── Install rules ────────────────────────────────────────────────────────────── +include(GNUInstallDirs) + +install(TARGETS rm_hal_headers + EXPORT rm_hal_targets +) + +install(DIRECTORY include/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.hpp" +) + +install(EXPORT rm_hal_targets + FILE rm_hal_headersTargets.cmake + NAMESPACE rm_hal:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/rm_hal_headers +) + +# ── Tests ────────────────────────────────────────────────────────────────────── +if(BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/D_hal_design/tests/CMakeLists.txt b/D_hal_design/tests/CMakeLists.txt new file mode 100644 index 0000000..2886a58 --- /dev/null +++ b/D_hal_design/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +# D_hal_design/tests/CMakeLists.txt + +# ── test_compile ─────────────────────────────────────────────────────────────── +# Includes every public header in a single translation unit. +# The test passes simply by compiling and linking without errors. +add_executable(test_compile test_compile.cpp) +target_link_libraries(test_compile PRIVATE rm_hal::headers) +target_enable_warnings(test_compile) +add_test(NAME hal.compile_check COMMAND test_compile) + +# ── test_types ───────────────────────────────────────────────────────────────── +# Runtime checks for types, free functions, and struct default values. +add_executable(test_types test_types.cpp) +target_link_libraries(test_types PRIVATE rm_hal::headers) +target_enable_warnings(test_types) +add_test(NAME hal.type_tests COMMAND test_types) diff --git a/D_hal_design/tests/test_compile.cpp b/D_hal_design/tests/test_compile.cpp new file mode 100644 index 0000000..3195fd0 --- /dev/null +++ b/D_hal_design/tests/test_compile.cpp @@ -0,0 +1,28 @@ +// test_compile.cpp +// +// Includes every public header in the rm_hal_headers interface library to verify +// they all compile cleanly together in a single translation unit. +// No runtime logic — the test passes if compilation and linking succeed. + +#include "rm_hal_audio/audio_hal.hpp" +#include "rm_hal_audio/audio_types.hpp" +#include "rm_hal_camera/calibration_types.hpp" +#include "rm_hal_camera/camera_hal.hpp" +#include "rm_hal_camera/camera_types.hpp" +#include "rm_hal_camera/pixel_encoding.hpp" +#include "rm_hal_camera/stream_type.hpp" +#include "rm_hal_camera/sync_manager.hpp" +#include "rm_hal_common/error_code.hpp" +#include "rm_hal_common/hal_factory.hpp" +#include "rm_hal_common/hardware_device.hpp" +#include "rm_hal_common/health_status.hpp" +#include "rm_hal_common/sensor_hal_base.hpp" +#include "rm_hal_common/sensor_timestamp.hpp" +#include "rm_hal_imu/imu_hal.hpp" +#include "rm_hal_imu/imu_types.hpp" +#include "rm_hal_lidar/lidar_2d_hal.hpp" +#include "rm_hal_lidar/lidar_2d_types.hpp" +#include "rm_hal_lidar/lidar_3d_hal.hpp" +#include "rm_hal_lidar/lidar_3d_types.hpp" + +int main() { return 0; } diff --git a/D_hal_design/tests/test_types.cpp b/D_hal_design/tests/test_types.cpp new file mode 100644 index 0000000..e28b8d6 --- /dev/null +++ b/D_hal_design/tests/test_types.cpp @@ -0,0 +1,355 @@ +// test_types.cpp +// +// Runtime unit tests for HAL types, free functions, and default values. +// Uses a custom CHECK macro so assertions run in both Debug and Release builds. + +#include "rm_hal_audio/audio_types.hpp" +#include "rm_hal_camera/camera_types.hpp" +#include "rm_hal_camera/pixel_encoding.hpp" +#include "rm_hal_camera/stream_type.hpp" +#include "rm_hal_common/error_code.hpp" +#include "rm_hal_common/health_status.hpp" +#include "rm_hal_common/sensor_timestamp.hpp" +#include "rm_hal_imu/imu_types.hpp" +#include "rm_hal_lidar/lidar_3d_types.hpp" + +#include +#include +#include +#include + +// ── helpers ──────────────────────────────────────────────────────────────────── +// CHECK always evaluates cond (unlike assert which is a no-op in Release builds). +#define CHECK(cond) do { \ + if (!(cond)) { \ + std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + std::exit(1); \ + } \ +} while (0) + +#define RUN(name) do { \ + std::puts(" " #name " ..."); \ + test_##name(); \ + std::puts(" " #name " OK"); \ +} while (0) + +// ── SensorTimestamp ──────────────────────────────────────────────────────────── +static void test_sensor_timestamp() +{ + using rm::hal::SensorTimestamp; + using rm::hal::TimestampDomain; + + // Default values + SensorTimestamp ts; + CHECK(ts.ns == 0); + CHECK(ts.domain == TimestampDomain::System); + + // Comparison operators + SensorTimestamp a, b; + a.ns = 1000; + b.ns = 2000; + CHECK(a < b); + CHECK(b > a); + CHECK(a <= b); + CHECK(b >= a); + CHECK(a != b); + + SensorTimestamp c; + c.ns = 1000; + c.domain = TimestampDomain::System; + CHECK(a == c); + + // deltaNs / deltaUs + CHECK(b.deltaNs(a) == 1000); + CHECK(a.deltaNs(b) == -1000); + CHECK(b.deltaUs(a) == 1); + CHECK(a.deltaUs(b) == -1); +} + +// ── HealthStatus ─────────────────────────────────────────────────────────────── +static void test_health_status() +{ + rm::hal::HealthStatus h; + CHECK(!h.alive); + CHECK(h.data_rate_hz == 0.0); + CHECK(h.drop_count == 0); + CHECK(h.error_count == 0); + CHECK(h.error_msg.empty()); +} + +// ── bytesPerPixel ────────────────────────────────────────────────────────────── +static void test_bytes_per_pixel() +{ + namespace pe = rm::hal::sensor; + using pe::PixelEncoding; + using pe::bytesPerPixel; + + CHECK(bytesPerPixel(PixelEncoding::RGB8) == 3); + CHECK(bytesPerPixel(PixelEncoding::BGR8) == 3); + CHECK(bytesPerPixel(PixelEncoding::RGBA8) == 4); + CHECK(bytesPerPixel(PixelEncoding::BGRA8) == 4); + CHECK(bytesPerPixel(PixelEncoding::YUYV) == 2); + CHECK(bytesPerPixel(PixelEncoding::UYVY) == 2); + CHECK(bytesPerPixel(PixelEncoding::NV12) == 0); // planar — no single bpp + CHECK(bytesPerPixel(PixelEncoding::NV21) == 0); + CHECK(bytesPerPixel(PixelEncoding::I420) == 0); + CHECK(bytesPerPixel(PixelEncoding::M420) == 0); + CHECK(bytesPerPixel(PixelEncoding::MONO8) == 1); + CHECK(bytesPerPixel(PixelEncoding::MONO16) == 2); + CHECK(bytesPerPixel(PixelEncoding::Z16) == 2); + CHECK(bytesPerPixel(PixelEncoding::Z32F) == 4); + CHECK(bytesPerPixel(PixelEncoding::RAW16) == 2); + CHECK(bytesPerPixel(PixelEncoding::MJPEG) == 0); + CHECK(bytesPerPixel(PixelEncoding::H264) == 0); + CHECK(bytesPerPixel(PixelEncoding::H265) == 0); + CHECK(bytesPerPixel(PixelEncoding::CUSTOM) == 0); + + // Aliases must resolve to same bpp as their canonical names + CHECK(bytesPerPixel(PixelEncoding::Y8) == bytesPerPixel(PixelEncoding::MONO8)); + CHECK(bytesPerPixel(PixelEncoding::Y16) == bytesPerPixel(PixelEncoding::MONO16)); + CHECK(bytesPerPixel(PixelEncoding::HEVC) == bytesPerPixel(PixelEncoding::H265)); +} + +// ── isCompressed ─────────────────────────────────────────────────────────────── +static void test_is_compressed() +{ + using rm::hal::sensor::PixelEncoding; + using rm::hal::sensor::isCompressed; + + CHECK( isCompressed(PixelEncoding::MJPEG)); + CHECK( isCompressed(PixelEncoding::H264)); + CHECK( isCompressed(PixelEncoding::H265)); + CHECK( isCompressed(PixelEncoding::HEVC)); // alias + + CHECK(!isCompressed(PixelEncoding::BGR8)); + CHECK(!isCompressed(PixelEncoding::RGB8)); + CHECK(!isCompressed(PixelEncoding::Z16)); + CHECK(!isCompressed(PixelEncoding::MONO8)); + CHECK(!isCompressed(PixelEncoding::NV12)); +} + +// ── pixelEncodingToString ────────────────────────────────────────────────────── +static void test_pixel_encoding_to_string() +{ + using rm::hal::sensor::PixelEncoding; + using rm::hal::sensor::pixelEncodingToString; + + CHECK(std::string(pixelEncodingToString(PixelEncoding::BGR8)) == "BGR8"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::RGB8)) == "RGB8"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::RGBA8)) == "RGBA8"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::BGRA8)) == "BGRA8"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::YUYV)) == "YUYV"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::MONO8)) == "MONO8"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::MONO16)) == "MONO16"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::Z16)) == "Z16"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::Z32F)) == "Z32F"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::MJPEG)) == "MJPEG"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::H264)) == "H264"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::H265)) == "H265"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::RAW16)) == "RAW16"); + CHECK(std::string(pixelEncodingToString(PixelEncoding::CUSTOM)) == "CUSTOM"); +} + +// ── errorCodeToString ────────────────────────────────────────────────────────── +static void test_error_code_to_string() +{ + using rm::hal::ErrorCode; + using rm::hal::errorCodeToString; + + CHECK(std::string(errorCodeToString(ErrorCode::OK)) == "OK"); + CHECK(std::string(errorCodeToString(ErrorCode::UNKNOWN)) == "UNKNOWN"); + CHECK(std::string(errorCodeToString(ErrorCode::NOT_IMPLEMENTED)) == "NOT_IMPLEMENTED"); + CHECK(std::string(errorCodeToString(ErrorCode::DEVICE_NOT_FOUND)) == "DEVICE_NOT_FOUND"); + CHECK(std::string(errorCodeToString(ErrorCode::DEVICE_BUSY)) == "DEVICE_BUSY"); + CHECK(std::string(errorCodeToString(ErrorCode::DEVICE_DISCONNECTED)) == "DEVICE_DISCONNECTED"); + CHECK(std::string(errorCodeToString(ErrorCode::INVALID_STATE)) == "INVALID_STATE"); + CHECK(std::string(errorCodeToString(ErrorCode::ALREADY_OPEN)) == "ALREADY_OPEN"); + CHECK(std::string(errorCodeToString(ErrorCode::NOT_OPEN)) == "NOT_OPEN"); + CHECK(std::string(errorCodeToString(ErrorCode::INVALID_CONFIG)) == "INVALID_CONFIG"); + CHECK(std::string(errorCodeToString(ErrorCode::UNSUPPORTED_FORMAT)) == "UNSUPPORTED_FORMAT"); + CHECK(std::string(errorCodeToString(ErrorCode::UNSUPPORTED_RESOLUTION)) == "UNSUPPORTED_RESOLUTION"); + CHECK(std::string(errorCodeToString(ErrorCode::UNSUPPORTED_FPS)) == "UNSUPPORTED_FPS"); + CHECK(std::string(errorCodeToString(ErrorCode::TIMEOUT)) == "TIMEOUT"); + CHECK(std::string(errorCodeToString(ErrorCode::IO_ERROR)) == "IO_ERROR"); + CHECK(std::string(errorCodeToString(ErrorCode::FRAME_DROPPED)) == "FRAME_DROPPED"); + CHECK(std::string(errorCodeToString(ErrorCode::CRC_ERROR)) == "CRC_ERROR"); + CHECK(std::string(errorCodeToString(ErrorCode::BUFFER_OVERFLOW)) == "BUFFER_OVERFLOW"); + CHECK(std::string(errorCodeToString(ErrorCode::SDK_ERROR)) == "SDK_ERROR"); + CHECK(std::string(errorCodeToString(ErrorCode::SDK_NOT_INITIALIZED)) == "SDK_NOT_INITIALIZED"); + CHECK(std::string(errorCodeToString(ErrorCode::FIRMWARE_MISMATCH)) == "FIRMWARE_MISMATCH"); + CHECK(std::string(errorCodeToString(ErrorCode::PERMISSION_DENIED)) == "PERMISSION_DENIED"); + CHECK(std::string(errorCodeToString(ErrorCode::RESOURCE_EXHAUSTED)) == "RESOURCE_EXHAUSTED"); + // Unknown code falls through to default "UNKNOWN" + CHECK(std::string(errorCodeToString(static_cast(9999))) == "UNKNOWN"); +} + +// ── Lidar3DConfig::defaultsFor ───────────────────────────────────────────────── +static void test_lidar3d_defaults() +{ + using rm::hal::sensor::LidarModel; + using rm::hal::sensor::Lidar3DConfig; + + auto vlp16 = Lidar3DConfig::defaultsFor(LidarModel::VLP_16); + CHECK(vlp16.model == LidarModel::VLP_16); + CHECK(vlp16.port == 2368); + CHECK(vlp16.range_max == 100.0); + + auto vlp32 = Lidar3DConfig::defaultsFor(LidarModel::VLP_32C); + CHECK(vlp32.port == 2368); + CHECK(vlp32.range_max == 200.0); + + auto hdl64 = Lidar3DConfig::defaultsFor(LidarModel::HDL_64E); + CHECK(hdl64.port == 2368); + CHECK(hdl64.range_max == 120.0); + + auto livox = Lidar3DConfig::defaultsFor(LidarModel::Livox_Mid360); + CHECK(livox.model == LidarModel::Livox_Mid360); + CHECK(livox.port == 56000); + CHECK(livox.range_max == 70.0); +} + +// ── StreamProfile::partialMatch ──────────────────────────────────────────────── +static void test_stream_profile_partial_match() +{ + using rm::hal::sensor::StreamProfile; + using rm::hal::sensor::StreamIndex; + using rm::hal::sensor::StreamType; + using rm::hal::sensor::PixelEncoding; + + // Device profile: 1280×720 @ 30fps BGR8 COLOR + StreamProfile device; + device.stream = StreamIndex{StreamType::COLOR, 0}; + device.width = 1280; + device.height = 720; + device.fps = 30; + device.format = PixelEncoding::BGR8; + + // Exact match + StreamProfile exact; + exact.stream = StreamIndex{StreamType::COLOR, 0}; + exact.width = 1280; + exact.height = 720; + exact.fps = 30; + exact.format = PixelEncoding::BGR8; + CHECK(device.partialMatch(exact)); + + // Wildcard stream type (UNKNOWN matches any) + StreamProfile wildStream; + wildStream.stream = StreamIndex{StreamType::UNKNOWN, 0}; + wildStream.width = 1280; + wildStream.height = 720; + wildStream.fps = 30; + wildStream.match_any_format = true; + CHECK(device.partialMatch(wildStream)); + + // Wildcard dimensions (0 = don't care) + StreamProfile wildDims; + wildDims.stream = StreamIndex{StreamType::COLOR, 0}; + wildDims.width = 0; // any width + wildDims.height = 0; // any height + wildDims.fps = 0; // any fps + wildDims.match_any_format = true; + CHECK(device.partialMatch(wildDims)); + + // Width mismatch + StreamProfile badWidth; + badWidth.stream = StreamIndex{StreamType::COLOR, 0}; + badWidth.width = 640; // different from 1280 + CHECK(!device.partialMatch(badWidth)); + + // Format mismatch (match_any_format = false by default) + StreamProfile badFormat; + badFormat.stream = StreamIndex{StreamType::COLOR, 0}; + badFormat.width = 1280; + badFormat.height = 720; + badFormat.fps = 30; + badFormat.format = PixelEncoding::RGB8; + CHECK(!device.partialMatch(badFormat)); +} + +// ── DOAResult defaults ───────────────────────────────────────────────────────── +static void test_doa_result_defaults() +{ + rm::hal::sensor::DOAResult doa; + CHECK(!doa.elevation_valid); + CHECK(doa.azimuth_deg == 0.f); + CHECK(doa.elevation_deg == 0.f); + CHECK(doa.confidence == 0.f); +} + +// ── ImuDeviceInfo defaults ───────────────────────────────────────────────────── +static void test_imu_device_info_defaults() +{ + rm::hal::sensor::ImuDeviceInfo info; + CHECK(info.accel_range == rm::hal::sensor::AccelRange::G8); + CHECK(info.gyro_range == rm::hal::sensor::GyroRange::DPS2000); +} + +// ── AudioFrame::totalSamples ─────────────────────────────────────────────────── +static void test_audio_frame_total_samples() +{ + rm::hal::sensor::AudioFrame af; + af.frame_count = 1024; + af.channels = 4; + CHECK(af.totalSamples() == 4096u); + + af.frame_count = 0; + CHECK(af.totalSamples() == 0u); +} + +// ── StreamIndex operators ────────────────────────────────────────────────────── +static void test_stream_index_operators() +{ + using rm::hal::sensor::StreamIndex; + using rm::hal::sensor::StreamType; + + StreamIndex color0{StreamType::COLOR, 0}; + StreamIndex color0b{StreamType::COLOR, 0}; + StreamIndex depth0{StreamType::DEPTH, 0}; + StreamIndex color1{StreamType::COLOR, 1}; + + CHECK(color0 == color0b); + CHECK(color0 != depth0); + CHECK(color0 < depth0); // COLOR(0x00) < DEPTH(0x01) + CHECK(color0 < color1); // same type, lower index + CHECK(!( depth0 < color0)); +} + +// ── std::hash ───────────────────────────────────────────────────── +static void test_stream_index_hash() +{ + using rm::hal::sensor::StreamIndex; + using rm::hal::sensor::StreamType; + + // Verify the hash specialisation allows StreamIndex in unordered containers. + std::unordered_set seen; + seen.insert(StreamIndex{StreamType::COLOR, 0}); + seen.insert(StreamIndex{StreamType::DEPTH, 0}); + seen.insert(StreamIndex{StreamType::IR_LEFT, 0}); + seen.insert(StreamIndex{StreamType::IR_RIGHT, 0}); + seen.insert(StreamIndex{StreamType::COLOR, 1}); + CHECK(seen.size() == 5u); + CHECK(seen.count(StreamIndex{StreamType::DEPTH, 0}) == 1u); + CHECK(seen.count(StreamIndex{StreamType::GYRO, 0}) == 0u); +} + +// ── main ─────────────────────────────────────────────────────────────────────── +int main() +{ + std::puts("=== rm_hal type tests ==="); + RUN(sensor_timestamp); + RUN(health_status); + RUN(bytes_per_pixel); + RUN(is_compressed); + RUN(pixel_encoding_to_string); + RUN(error_code_to_string); + RUN(lidar3d_defaults); + RUN(stream_profile_partial_match); + RUN(doa_result_defaults); + RUN(imu_device_info_defaults); + RUN(audio_frame_total_samples); + RUN(stream_index_operators); + RUN(stream_index_hash); + std::puts("=== All tests passed ==="); + return 0; +} diff --git a/build_rel/D_hal_design/Makefile b/build_rel/D_hal_design/Makefile new file mode 100644 index 0000000..c696a6f --- /dev/null +++ b/build_rel/D_hal_design/Makefile @@ -0,0 +1,200 @@ +# CMAKE generated file: DO NOT EDIT! +# Generated by "Unix Makefiles" Generator, CMake Version 3.31 + +# Default target executed when no arguments are given to make. +default_target: all +.PHONY : default_target + +# Allow only one "make -f Makefile2" at a time, but pass parallelism. +.NOTPARALLEL: + +#============================================================================= +# Special targets provided by cmake. + +# Disable implicit rules so canonical targets will work. +.SUFFIXES: + +# Disable VCS-based implicit rules. +% : %,v + +# Disable VCS-based implicit rules. +% : RCS/% + +# Disable VCS-based implicit rules. +% : RCS/%,v + +# Disable VCS-based implicit rules. +% : SCCS/s.% + +# Disable VCS-based implicit rules. +% : s.% + +.SUFFIXES: .hpux_make_needs_suffix_list + +# Command-line flag to silence nested $(MAKE). +$(VERBOSE)MAKESILENT = -s + +#Suppress display of executed commands. +$(VERBOSE).SILENT: + +# A target that is always out of date. +cmake_force: +.PHONY : cmake_force + +#============================================================================= +# Set environment variables for the build. + +# The shell in which to execute make rules. +SHELL = /bin/sh + +# The CMake executable. +CMAKE_COMMAND = /usr/local/bin/cmake + +# The command to remove a file. +RM = /usr/local/bin/cmake -E rm -f + +# Escaping for special characters. +EQUALS = = + +# The top-level source directory on which CMake was run. +CMAKE_SOURCE_DIR = /home/runner/work/sensor_repository/sensor_repository + +# The top-level build directory on which CMake was run. +CMAKE_BINARY_DIR = /home/runner/work/sensor_repository/sensor_repository/build_rel + +#============================================================================= +# Targets provided globally by CMake. + +# Special rule for the target test +test: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running tests..." + /usr/local/bin/ctest --force-new-ctest-process $(ARGS) +.PHONY : test + +# Special rule for the target test +test/fast: test +.PHONY : test/fast + +# Special rule for the target edit_cache +edit_cache: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake cache editor..." + /usr/local/bin/ccmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) +.PHONY : edit_cache + +# Special rule for the target edit_cache +edit_cache/fast: edit_cache +.PHONY : edit_cache/fast + +# Special rule for the target rebuild_cache +rebuild_cache: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..." + /usr/local/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) +.PHONY : rebuild_cache + +# Special rule for the target rebuild_cache +rebuild_cache/fast: rebuild_cache +.PHONY : rebuild_cache/fast + +# Special rule for the target list_install_components +list_install_components: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Available install components are: \"Unspecified\"" +.PHONY : list_install_components + +# Special rule for the target list_install_components +list_install_components/fast: list_install_components +.PHONY : list_install_components/fast + +# Special rule for the target install +install: preinstall + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Install the project..." + /usr/local/bin/cmake -P cmake_install.cmake +.PHONY : install + +# Special rule for the target install +install/fast: preinstall/fast + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Install the project..." + /usr/local/bin/cmake -P cmake_install.cmake +.PHONY : install/fast + +# Special rule for the target install/local +install/local: preinstall + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing only the local directory..." + /usr/local/bin/cmake -DCMAKE_INSTALL_LOCAL_ONLY=1 -P cmake_install.cmake +.PHONY : install/local + +# Special rule for the target install/local +install/local/fast: preinstall/fast + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing only the local directory..." + /usr/local/bin/cmake -DCMAKE_INSTALL_LOCAL_ONLY=1 -P cmake_install.cmake +.PHONY : install/local/fast + +# Special rule for the target install/strip +install/strip: preinstall + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing the project stripped..." + /usr/local/bin/cmake -DCMAKE_INSTALL_DO_STRIP=1 -P cmake_install.cmake +.PHONY : install/strip + +# Special rule for the target install/strip +install/strip/fast: preinstall/fast + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing the project stripped..." + /usr/local/bin/cmake -DCMAKE_INSTALL_DO_STRIP=1 -P cmake_install.cmake +.PHONY : install/strip/fast + +# The main all target +all: cmake_check_build_system + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -E cmake_progress_start /home/runner/work/sensor_repository/sensor_repository/build_rel/CMakeFiles /home/runner/work/sensor_repository/sensor_repository/build_rel/D_hal_design//CMakeFiles/progress.marks + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/all + $(CMAKE_COMMAND) -E cmake_progress_start /home/runner/work/sensor_repository/sensor_repository/build_rel/CMakeFiles 0 +.PHONY : all + +# The main clean target +clean: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/clean +.PHONY : clean + +# The main clean target +clean/fast: clean +.PHONY : clean/fast + +# Prepare targets for installation. +preinstall: all + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/preinstall +.PHONY : preinstall + +# Prepare targets for installation. +preinstall/fast: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/preinstall +.PHONY : preinstall/fast + +# clear depends +depend: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1 +.PHONY : depend + +# Help Target +help: + @echo "The following are some of the valid targets for this Makefile:" + @echo "... all (the default if no target is provided)" + @echo "... clean" + @echo "... depend" + @echo "... edit_cache" + @echo "... install" + @echo "... install/local" + @echo "... install/strip" + @echo "... list_install_components" + @echo "... rebuild_cache" + @echo "... test" +.PHONY : help + + + +#============================================================================= +# Special targets to cleanup operation of make. + +# Special rule to run CMake to check the build system integrity. +# No rule that depends on this can have commands that come from listfiles +# because they might be regenerated. +cmake_check_build_system: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0 +.PHONY : cmake_check_build_system + diff --git a/build_rel/D_hal_design/tests/Makefile b/build_rel/D_hal_design/tests/Makefile new file mode 100644 index 0000000..17d3812 --- /dev/null +++ b/build_rel/D_hal_design/tests/Makefile @@ -0,0 +1,284 @@ +# CMAKE generated file: DO NOT EDIT! +# Generated by "Unix Makefiles" Generator, CMake Version 3.31 + +# Default target executed when no arguments are given to make. +default_target: all +.PHONY : default_target + +# Allow only one "make -f Makefile2" at a time, but pass parallelism. +.NOTPARALLEL: + +#============================================================================= +# Special targets provided by cmake. + +# Disable implicit rules so canonical targets will work. +.SUFFIXES: + +# Disable VCS-based implicit rules. +% : %,v + +# Disable VCS-based implicit rules. +% : RCS/% + +# Disable VCS-based implicit rules. +% : RCS/%,v + +# Disable VCS-based implicit rules. +% : SCCS/s.% + +# Disable VCS-based implicit rules. +% : s.% + +.SUFFIXES: .hpux_make_needs_suffix_list + +# Command-line flag to silence nested $(MAKE). +$(VERBOSE)MAKESILENT = -s + +#Suppress display of executed commands. +$(VERBOSE).SILENT: + +# A target that is always out of date. +cmake_force: +.PHONY : cmake_force + +#============================================================================= +# Set environment variables for the build. + +# The shell in which to execute make rules. +SHELL = /bin/sh + +# The CMake executable. +CMAKE_COMMAND = /usr/local/bin/cmake + +# The command to remove a file. +RM = /usr/local/bin/cmake -E rm -f + +# Escaping for special characters. +EQUALS = = + +# The top-level source directory on which CMake was run. +CMAKE_SOURCE_DIR = /home/runner/work/sensor_repository/sensor_repository + +# The top-level build directory on which CMake was run. +CMAKE_BINARY_DIR = /home/runner/work/sensor_repository/sensor_repository/build_rel + +#============================================================================= +# Targets provided globally by CMake. + +# Special rule for the target test +test: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running tests..." + /usr/local/bin/ctest --force-new-ctest-process $(ARGS) +.PHONY : test + +# Special rule for the target test +test/fast: test +.PHONY : test/fast + +# Special rule for the target edit_cache +edit_cache: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake cache editor..." + /usr/local/bin/ccmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) +.PHONY : edit_cache + +# Special rule for the target edit_cache +edit_cache/fast: edit_cache +.PHONY : edit_cache/fast + +# Special rule for the target rebuild_cache +rebuild_cache: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..." + /usr/local/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) +.PHONY : rebuild_cache + +# Special rule for the target rebuild_cache +rebuild_cache/fast: rebuild_cache +.PHONY : rebuild_cache/fast + +# Special rule for the target list_install_components +list_install_components: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Available install components are: \"Unspecified\"" +.PHONY : list_install_components + +# Special rule for the target list_install_components +list_install_components/fast: list_install_components +.PHONY : list_install_components/fast + +# Special rule for the target install +install: preinstall + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Install the project..." + /usr/local/bin/cmake -P cmake_install.cmake +.PHONY : install + +# Special rule for the target install +install/fast: preinstall/fast + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Install the project..." + /usr/local/bin/cmake -P cmake_install.cmake +.PHONY : install/fast + +# Special rule for the target install/local +install/local: preinstall + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing only the local directory..." + /usr/local/bin/cmake -DCMAKE_INSTALL_LOCAL_ONLY=1 -P cmake_install.cmake +.PHONY : install/local + +# Special rule for the target install/local +install/local/fast: preinstall/fast + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing only the local directory..." + /usr/local/bin/cmake -DCMAKE_INSTALL_LOCAL_ONLY=1 -P cmake_install.cmake +.PHONY : install/local/fast + +# Special rule for the target install/strip +install/strip: preinstall + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing the project stripped..." + /usr/local/bin/cmake -DCMAKE_INSTALL_DO_STRIP=1 -P cmake_install.cmake +.PHONY : install/strip + +# Special rule for the target install/strip +install/strip/fast: preinstall/fast + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing the project stripped..." + /usr/local/bin/cmake -DCMAKE_INSTALL_DO_STRIP=1 -P cmake_install.cmake +.PHONY : install/strip/fast + +# The main all target +all: cmake_check_build_system + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -E cmake_progress_start /home/runner/work/sensor_repository/sensor_repository/build_rel/CMakeFiles /home/runner/work/sensor_repository/sensor_repository/build_rel/D_hal_design/tests//CMakeFiles/progress.marks + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/all + $(CMAKE_COMMAND) -E cmake_progress_start /home/runner/work/sensor_repository/sensor_repository/build_rel/CMakeFiles 0 +.PHONY : all + +# The main clean target +clean: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/clean +.PHONY : clean + +# The main clean target +clean/fast: clean +.PHONY : clean/fast + +# Prepare targets for installation. +preinstall: all + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/preinstall +.PHONY : preinstall + +# Prepare targets for installation. +preinstall/fast: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/preinstall +.PHONY : preinstall/fast + +# clear depends +depend: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1 +.PHONY : depend + +# Convenience name for target. +D_hal_design/tests/CMakeFiles/test_compile.dir/rule: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/CMakeFiles/test_compile.dir/rule +.PHONY : D_hal_design/tests/CMakeFiles/test_compile.dir/rule + +# Convenience name for target. +test_compile: D_hal_design/tests/CMakeFiles/test_compile.dir/rule +.PHONY : test_compile + +# fast build rule for target. +test_compile/fast: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_compile.dir/build.make D_hal_design/tests/CMakeFiles/test_compile.dir/build +.PHONY : test_compile/fast + +# Convenience name for target. +D_hal_design/tests/CMakeFiles/test_types.dir/rule: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/CMakeFiles/test_types.dir/rule +.PHONY : D_hal_design/tests/CMakeFiles/test_types.dir/rule + +# Convenience name for target. +test_types: D_hal_design/tests/CMakeFiles/test_types.dir/rule +.PHONY : test_types + +# fast build rule for target. +test_types/fast: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_types.dir/build.make D_hal_design/tests/CMakeFiles/test_types.dir/build +.PHONY : test_types/fast + +test_compile.o: test_compile.cpp.o +.PHONY : test_compile.o + +# target to build an object file +test_compile.cpp.o: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_compile.dir/build.make D_hal_design/tests/CMakeFiles/test_compile.dir/test_compile.cpp.o +.PHONY : test_compile.cpp.o + +test_compile.i: test_compile.cpp.i +.PHONY : test_compile.i + +# target to preprocess a source file +test_compile.cpp.i: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_compile.dir/build.make D_hal_design/tests/CMakeFiles/test_compile.dir/test_compile.cpp.i +.PHONY : test_compile.cpp.i + +test_compile.s: test_compile.cpp.s +.PHONY : test_compile.s + +# target to generate assembly for a file +test_compile.cpp.s: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_compile.dir/build.make D_hal_design/tests/CMakeFiles/test_compile.dir/test_compile.cpp.s +.PHONY : test_compile.cpp.s + +test_types.o: test_types.cpp.o +.PHONY : test_types.o + +# target to build an object file +test_types.cpp.o: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_types.dir/build.make D_hal_design/tests/CMakeFiles/test_types.dir/test_types.cpp.o +.PHONY : test_types.cpp.o + +test_types.i: test_types.cpp.i +.PHONY : test_types.i + +# target to preprocess a source file +test_types.cpp.i: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_types.dir/build.make D_hal_design/tests/CMakeFiles/test_types.dir/test_types.cpp.i +.PHONY : test_types.cpp.i + +test_types.s: test_types.cpp.s +.PHONY : test_types.s + +# target to generate assembly for a file +test_types.cpp.s: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_types.dir/build.make D_hal_design/tests/CMakeFiles/test_types.dir/test_types.cpp.s +.PHONY : test_types.cpp.s + +# Help Target +help: + @echo "The following are some of the valid targets for this Makefile:" + @echo "... all (the default if no target is provided)" + @echo "... clean" + @echo "... depend" + @echo "... edit_cache" + @echo "... install" + @echo "... install/local" + @echo "... install/strip" + @echo "... list_install_components" + @echo "... rebuild_cache" + @echo "... test" + @echo "... test_compile" + @echo "... test_types" + @echo "... test_compile.o" + @echo "... test_compile.i" + @echo "... test_compile.s" + @echo "... test_types.o" + @echo "... test_types.i" + @echo "... test_types.s" +.PHONY : help + + + +#============================================================================= +# Special targets to cleanup operation of make. + +# Special rule to run CMake to check the build system integrity. +# No rule that depends on this can have commands that come from listfiles +# because they might be regenerated. +cmake_check_build_system: + cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0 +.PHONY : cmake_check_build_system + diff --git a/build_rel/D_hal_design/tests/test_compile b/build_rel/D_hal_design/tests/test_compile new file mode 100755 index 0000000000000000000000000000000000000000..853cb36b2b295adaa571b2f1a0aa78362d58997d GIT binary patch literal 15784 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 +Test time = 0.00 sec +---------------------------------------------------------- +Test Passed. +"hal.compile_check" end time: Apr 25 10:05 UTC +"hal.compile_check" time elapsed: 00:00:00 +---------------------------------------------------------- + +2/2 Testing: hal.type_tests +2/2 Test: hal.type_tests +Command: "/home/runner/work/sensor_repository/sensor_repository/build_rel/D_hal_design/tests/test_types" +Directory: /home/runner/work/sensor_repository/sensor_repository/build_rel/D_hal_design/tests +"hal.type_tests" start time: Apr 25 10:05 UTC +Output: +---------------------------------------------------------- +=== rm_hal type tests === + sensor_timestamp ... + sensor_timestamp OK + health_status ... + health_status OK + bytes_per_pixel ... + bytes_per_pixel OK + is_compressed ... + is_compressed OK + pixel_encoding_to_string ... + pixel_encoding_to_string OK + error_code_to_string ... + error_code_to_string OK + lidar3d_defaults ... + lidar3d_defaults OK + stream_profile_partial_match ... + stream_profile_partial_match OK + doa_result_defaults ... + doa_result_defaults OK + imu_device_info_defaults ... + imu_device_info_defaults OK + audio_frame_total_samples ... + audio_frame_total_samples OK + stream_index_operators ... + stream_index_operators OK + stream_index_hash ... + stream_index_hash OK +=== All tests passed === + +Test time = 0.00 sec +---------------------------------------------------------- +Test Passed. +"hal.type_tests" end time: Apr 25 10:05 UTC +"hal.type_tests" time elapsed: 00:00:00 +---------------------------------------------------------- + +End testing: Apr 25 10:05 UTC diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake new file mode 100644 index 0000000..7fd2af9 --- /dev/null +++ b/cmake/CompilerWarnings.cmake @@ -0,0 +1,25 @@ +# cmake/CompilerWarnings.cmake +# +# Provides target_enable_warnings() — applies a strict but +# portable warning set to non-INTERFACE executable / library targets. +# +# Usage (in any CMakeLists.txt after including this file from the root): +# add_executable(my_test ...) +# target_enable_warnings(my_test) + +function(target_enable_warnings target) + target_compile_options(${target} PRIVATE + $<$: + -Wall + -Wextra + -Wpedantic + -Wshadow + -Wnull-dereference + -Wcast-align + > + $<$: + /W4 + /permissive- + > + ) +endfunction() diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..f023af8 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# scripts/build.sh — Local one-shot configure, build, and test. +# +# Usage: +# ./scripts/build.sh # Debug build in ./build +# ./scripts/build.sh Release # Release build +# ./scripts/build.sh Debug out # Custom build type and directory + +set -euo pipefail + +BUILD_TYPE="${1:-Debug}" +BUILD_DIR="${2:-build}" + +echo "==> Configure (type=${BUILD_TYPE}, dir=${BUILD_DIR})" +cmake -B "${BUILD_DIR}" \ + -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ + -DBUILD_TESTING=ON + +echo "==> Build" +cmake --build "${BUILD_DIR}" --parallel + +echo "==> Test" +ctest --test-dir "${BUILD_DIR}" --output-on-failure --parallel + +echo "==> Done ✓" From 6cbc5334fe92ca799ee504de391dce1a3f0a420a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:06:26 +0000 Subject: [PATCH 13/14] fix: remove accidentally committed build_rel/ artifacts, extend .gitignore Agent-Logs-Url: https://github.com/YYCB/sensor_repository/sessions/f509ddc3-cdf8-4905-a48d-0f9562f1e26a Co-authored-by: YYCB <23326150+YYCB@users.noreply.github.com> --- .gitignore | 1 + build_rel/D_hal_design/Makefile | 200 ------ build_rel/D_hal_design/tests/Makefile | 284 -------- build_rel/D_hal_design/tests/test_compile | Bin 15784 -> 0 bytes build_rel/D_hal_design/tests/test_types | Bin 27896 -> 0 bytes build_rel/DartConfiguration.tcl | 109 --- build_rel/Makefile | 620 ------------------ build_rel/Testing/Temporary/CTestCostData.txt | 3 - build_rel/Testing/Temporary/LastTest.log | 61 -- 9 files changed, 1 insertion(+), 1277 deletions(-) delete mode 100644 build_rel/D_hal_design/Makefile delete mode 100644 build_rel/D_hal_design/tests/Makefile delete mode 100755 build_rel/D_hal_design/tests/test_compile delete mode 100755 build_rel/D_hal_design/tests/test_types delete mode 100644 build_rel/DartConfiguration.tcl delete mode 100644 build_rel/Makefile delete mode 100644 build_rel/Testing/Temporary/CTestCostData.txt delete mode 100644 build_rel/Testing/Temporary/LastTest.log diff --git a/.gitignore b/.gitignore index 3d4fa80..ab6dd2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Build artifacts build/ +build_*/ out/ .build/ diff --git a/build_rel/D_hal_design/Makefile b/build_rel/D_hal_design/Makefile deleted file mode 100644 index c696a6f..0000000 --- a/build_rel/D_hal_design/Makefile +++ /dev/null @@ -1,200 +0,0 @@ -# CMAKE generated file: DO NOT EDIT! -# Generated by "Unix Makefiles" Generator, CMake Version 3.31 - -# Default target executed when no arguments are given to make. -default_target: all -.PHONY : default_target - -# Allow only one "make -f Makefile2" at a time, but pass parallelism. -.NOTPARALLEL: - -#============================================================================= -# Special targets provided by cmake. - -# Disable implicit rules so canonical targets will work. -.SUFFIXES: - -# Disable VCS-based implicit rules. -% : %,v - -# Disable VCS-based implicit rules. -% : RCS/% - -# Disable VCS-based implicit rules. -% : RCS/%,v - -# Disable VCS-based implicit rules. -% : SCCS/s.% - -# Disable VCS-based implicit rules. -% : s.% - -.SUFFIXES: .hpux_make_needs_suffix_list - -# Command-line flag to silence nested $(MAKE). -$(VERBOSE)MAKESILENT = -s - -#Suppress display of executed commands. -$(VERBOSE).SILENT: - -# A target that is always out of date. -cmake_force: -.PHONY : cmake_force - -#============================================================================= -# Set environment variables for the build. - -# The shell in which to execute make rules. -SHELL = /bin/sh - -# The CMake executable. -CMAKE_COMMAND = /usr/local/bin/cmake - -# The command to remove a file. -RM = /usr/local/bin/cmake -E rm -f - -# Escaping for special characters. -EQUALS = = - -# The top-level source directory on which CMake was run. -CMAKE_SOURCE_DIR = /home/runner/work/sensor_repository/sensor_repository - -# The top-level build directory on which CMake was run. -CMAKE_BINARY_DIR = /home/runner/work/sensor_repository/sensor_repository/build_rel - -#============================================================================= -# Targets provided globally by CMake. - -# Special rule for the target test -test: - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running tests..." - /usr/local/bin/ctest --force-new-ctest-process $(ARGS) -.PHONY : test - -# Special rule for the target test -test/fast: test -.PHONY : test/fast - -# Special rule for the target edit_cache -edit_cache: - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake cache editor..." - /usr/local/bin/ccmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) -.PHONY : edit_cache - -# Special rule for the target edit_cache -edit_cache/fast: edit_cache -.PHONY : edit_cache/fast - -# Special rule for the target rebuild_cache -rebuild_cache: - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..." - /usr/local/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) -.PHONY : rebuild_cache - -# Special rule for the target rebuild_cache -rebuild_cache/fast: rebuild_cache -.PHONY : rebuild_cache/fast - -# Special rule for the target list_install_components -list_install_components: - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Available install components are: \"Unspecified\"" -.PHONY : list_install_components - -# Special rule for the target list_install_components -list_install_components/fast: list_install_components -.PHONY : list_install_components/fast - -# Special rule for the target install -install: preinstall - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Install the project..." - /usr/local/bin/cmake -P cmake_install.cmake -.PHONY : install - -# Special rule for the target install -install/fast: preinstall/fast - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Install the project..." - /usr/local/bin/cmake -P cmake_install.cmake -.PHONY : install/fast - -# Special rule for the target install/local -install/local: preinstall - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing only the local directory..." - /usr/local/bin/cmake -DCMAKE_INSTALL_LOCAL_ONLY=1 -P cmake_install.cmake -.PHONY : install/local - -# Special rule for the target install/local -install/local/fast: preinstall/fast - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing only the local directory..." - /usr/local/bin/cmake -DCMAKE_INSTALL_LOCAL_ONLY=1 -P cmake_install.cmake -.PHONY : install/local/fast - -# Special rule for the target install/strip -install/strip: preinstall - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing the project stripped..." - /usr/local/bin/cmake -DCMAKE_INSTALL_DO_STRIP=1 -P cmake_install.cmake -.PHONY : install/strip - -# Special rule for the target install/strip -install/strip/fast: preinstall/fast - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing the project stripped..." - /usr/local/bin/cmake -DCMAKE_INSTALL_DO_STRIP=1 -P cmake_install.cmake -.PHONY : install/strip/fast - -# The main all target -all: cmake_check_build_system - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -E cmake_progress_start /home/runner/work/sensor_repository/sensor_repository/build_rel/CMakeFiles /home/runner/work/sensor_repository/sensor_repository/build_rel/D_hal_design//CMakeFiles/progress.marks - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/all - $(CMAKE_COMMAND) -E cmake_progress_start /home/runner/work/sensor_repository/sensor_repository/build_rel/CMakeFiles 0 -.PHONY : all - -# The main clean target -clean: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/clean -.PHONY : clean - -# The main clean target -clean/fast: clean -.PHONY : clean/fast - -# Prepare targets for installation. -preinstall: all - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/preinstall -.PHONY : preinstall - -# Prepare targets for installation. -preinstall/fast: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/preinstall -.PHONY : preinstall/fast - -# clear depends -depend: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1 -.PHONY : depend - -# Help Target -help: - @echo "The following are some of the valid targets for this Makefile:" - @echo "... all (the default if no target is provided)" - @echo "... clean" - @echo "... depend" - @echo "... edit_cache" - @echo "... install" - @echo "... install/local" - @echo "... install/strip" - @echo "... list_install_components" - @echo "... rebuild_cache" - @echo "... test" -.PHONY : help - - - -#============================================================================= -# Special targets to cleanup operation of make. - -# Special rule to run CMake to check the build system integrity. -# No rule that depends on this can have commands that come from listfiles -# because they might be regenerated. -cmake_check_build_system: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0 -.PHONY : cmake_check_build_system - diff --git a/build_rel/D_hal_design/tests/Makefile b/build_rel/D_hal_design/tests/Makefile deleted file mode 100644 index 17d3812..0000000 --- a/build_rel/D_hal_design/tests/Makefile +++ /dev/null @@ -1,284 +0,0 @@ -# CMAKE generated file: DO NOT EDIT! -# Generated by "Unix Makefiles" Generator, CMake Version 3.31 - -# Default target executed when no arguments are given to make. -default_target: all -.PHONY : default_target - -# Allow only one "make -f Makefile2" at a time, but pass parallelism. -.NOTPARALLEL: - -#============================================================================= -# Special targets provided by cmake. - -# Disable implicit rules so canonical targets will work. -.SUFFIXES: - -# Disable VCS-based implicit rules. -% : %,v - -# Disable VCS-based implicit rules. -% : RCS/% - -# Disable VCS-based implicit rules. -% : RCS/%,v - -# Disable VCS-based implicit rules. -% : SCCS/s.% - -# Disable VCS-based implicit rules. -% : s.% - -.SUFFIXES: .hpux_make_needs_suffix_list - -# Command-line flag to silence nested $(MAKE). -$(VERBOSE)MAKESILENT = -s - -#Suppress display of executed commands. -$(VERBOSE).SILENT: - -# A target that is always out of date. -cmake_force: -.PHONY : cmake_force - -#============================================================================= -# Set environment variables for the build. - -# The shell in which to execute make rules. -SHELL = /bin/sh - -# The CMake executable. -CMAKE_COMMAND = /usr/local/bin/cmake - -# The command to remove a file. -RM = /usr/local/bin/cmake -E rm -f - -# Escaping for special characters. -EQUALS = = - -# The top-level source directory on which CMake was run. -CMAKE_SOURCE_DIR = /home/runner/work/sensor_repository/sensor_repository - -# The top-level build directory on which CMake was run. -CMAKE_BINARY_DIR = /home/runner/work/sensor_repository/sensor_repository/build_rel - -#============================================================================= -# Targets provided globally by CMake. - -# Special rule for the target test -test: - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running tests..." - /usr/local/bin/ctest --force-new-ctest-process $(ARGS) -.PHONY : test - -# Special rule for the target test -test/fast: test -.PHONY : test/fast - -# Special rule for the target edit_cache -edit_cache: - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake cache editor..." - /usr/local/bin/ccmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) -.PHONY : edit_cache - -# Special rule for the target edit_cache -edit_cache/fast: edit_cache -.PHONY : edit_cache/fast - -# Special rule for the target rebuild_cache -rebuild_cache: - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..." - /usr/local/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) -.PHONY : rebuild_cache - -# Special rule for the target rebuild_cache -rebuild_cache/fast: rebuild_cache -.PHONY : rebuild_cache/fast - -# Special rule for the target list_install_components -list_install_components: - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Available install components are: \"Unspecified\"" -.PHONY : list_install_components - -# Special rule for the target list_install_components -list_install_components/fast: list_install_components -.PHONY : list_install_components/fast - -# Special rule for the target install -install: preinstall - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Install the project..." - /usr/local/bin/cmake -P cmake_install.cmake -.PHONY : install - -# Special rule for the target install -install/fast: preinstall/fast - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Install the project..." - /usr/local/bin/cmake -P cmake_install.cmake -.PHONY : install/fast - -# Special rule for the target install/local -install/local: preinstall - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing only the local directory..." - /usr/local/bin/cmake -DCMAKE_INSTALL_LOCAL_ONLY=1 -P cmake_install.cmake -.PHONY : install/local - -# Special rule for the target install/local -install/local/fast: preinstall/fast - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing only the local directory..." - /usr/local/bin/cmake -DCMAKE_INSTALL_LOCAL_ONLY=1 -P cmake_install.cmake -.PHONY : install/local/fast - -# Special rule for the target install/strip -install/strip: preinstall - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing the project stripped..." - /usr/local/bin/cmake -DCMAKE_INSTALL_DO_STRIP=1 -P cmake_install.cmake -.PHONY : install/strip - -# Special rule for the target install/strip -install/strip/fast: preinstall/fast - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Installing the project stripped..." - /usr/local/bin/cmake -DCMAKE_INSTALL_DO_STRIP=1 -P cmake_install.cmake -.PHONY : install/strip/fast - -# The main all target -all: cmake_check_build_system - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -E cmake_progress_start /home/runner/work/sensor_repository/sensor_repository/build_rel/CMakeFiles /home/runner/work/sensor_repository/sensor_repository/build_rel/D_hal_design/tests//CMakeFiles/progress.marks - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/all - $(CMAKE_COMMAND) -E cmake_progress_start /home/runner/work/sensor_repository/sensor_repository/build_rel/CMakeFiles 0 -.PHONY : all - -# The main clean target -clean: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/clean -.PHONY : clean - -# The main clean target -clean/fast: clean -.PHONY : clean/fast - -# Prepare targets for installation. -preinstall: all - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/preinstall -.PHONY : preinstall - -# Prepare targets for installation. -preinstall/fast: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/preinstall -.PHONY : preinstall/fast - -# clear depends -depend: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1 -.PHONY : depend - -# Convenience name for target. -D_hal_design/tests/CMakeFiles/test_compile.dir/rule: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/CMakeFiles/test_compile.dir/rule -.PHONY : D_hal_design/tests/CMakeFiles/test_compile.dir/rule - -# Convenience name for target. -test_compile: D_hal_design/tests/CMakeFiles/test_compile.dir/rule -.PHONY : test_compile - -# fast build rule for target. -test_compile/fast: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_compile.dir/build.make D_hal_design/tests/CMakeFiles/test_compile.dir/build -.PHONY : test_compile/fast - -# Convenience name for target. -D_hal_design/tests/CMakeFiles/test_types.dir/rule: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 D_hal_design/tests/CMakeFiles/test_types.dir/rule -.PHONY : D_hal_design/tests/CMakeFiles/test_types.dir/rule - -# Convenience name for target. -test_types: D_hal_design/tests/CMakeFiles/test_types.dir/rule -.PHONY : test_types - -# fast build rule for target. -test_types/fast: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_types.dir/build.make D_hal_design/tests/CMakeFiles/test_types.dir/build -.PHONY : test_types/fast - -test_compile.o: test_compile.cpp.o -.PHONY : test_compile.o - -# target to build an object file -test_compile.cpp.o: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_compile.dir/build.make D_hal_design/tests/CMakeFiles/test_compile.dir/test_compile.cpp.o -.PHONY : test_compile.cpp.o - -test_compile.i: test_compile.cpp.i -.PHONY : test_compile.i - -# target to preprocess a source file -test_compile.cpp.i: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_compile.dir/build.make D_hal_design/tests/CMakeFiles/test_compile.dir/test_compile.cpp.i -.PHONY : test_compile.cpp.i - -test_compile.s: test_compile.cpp.s -.PHONY : test_compile.s - -# target to generate assembly for a file -test_compile.cpp.s: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_compile.dir/build.make D_hal_design/tests/CMakeFiles/test_compile.dir/test_compile.cpp.s -.PHONY : test_compile.cpp.s - -test_types.o: test_types.cpp.o -.PHONY : test_types.o - -# target to build an object file -test_types.cpp.o: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_types.dir/build.make D_hal_design/tests/CMakeFiles/test_types.dir/test_types.cpp.o -.PHONY : test_types.cpp.o - -test_types.i: test_types.cpp.i -.PHONY : test_types.i - -# target to preprocess a source file -test_types.cpp.i: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_types.dir/build.make D_hal_design/tests/CMakeFiles/test_types.dir/test_types.cpp.i -.PHONY : test_types.cpp.i - -test_types.s: test_types.cpp.s -.PHONY : test_types.s - -# target to generate assembly for a file -test_types.cpp.s: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(MAKE) $(MAKESILENT) -f D_hal_design/tests/CMakeFiles/test_types.dir/build.make D_hal_design/tests/CMakeFiles/test_types.dir/test_types.cpp.s -.PHONY : test_types.cpp.s - -# Help Target -help: - @echo "The following are some of the valid targets for this Makefile:" - @echo "... all (the default if no target is provided)" - @echo "... clean" - @echo "... depend" - @echo "... edit_cache" - @echo "... install" - @echo "... install/local" - @echo "... install/strip" - @echo "... list_install_components" - @echo "... rebuild_cache" - @echo "... test" - @echo "... test_compile" - @echo "... test_types" - @echo "... test_compile.o" - @echo "... test_compile.i" - @echo "... test_compile.s" - @echo "... test_types.o" - @echo "... test_types.i" - @echo "... test_types.s" -.PHONY : help - - - -#============================================================================= -# Special targets to cleanup operation of make. - -# Special rule to run CMake to check the build system integrity. -# No rule that depends on this can have commands that come from listfiles -# because they might be regenerated. -cmake_check_build_system: - cd /home/runner/work/sensor_repository/sensor_repository/build_rel && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0 -.PHONY : cmake_check_build_system - diff --git a/build_rel/D_hal_design/tests/test_compile b/build_rel/D_hal_design/tests/test_compile deleted file mode 100755 index 853cb36b2b295adaa571b2f1a0aa78362d58997d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15784 zcmeHOU2I%O6`r*d;?gvB(wMkOXjY}IQmAisZO2NWZr1TnuEF`iaf(`guCBe;_DcI> zcJH=!k%}Q8VzrTqkf2CZLgnWL3AI(F_Nir97XGEy+tFByW2I-+i( z?+>f}>H}nN(VQvI7@$&O2U{uGkS?M+oD+3!bDRDU{2pyZOQ;zYc;V?YS}=#cmWCA{iTJ~!3hcPO9j&gY8Nh3p|_8|b>B}{Url^#@E5;-s*p^glCF39`Arm@n16xf?j@JpY(`?wjAHFt^YX&m`Q( zxPMYR4ZkY)H~1B)uq|WC!Nb0K%L-}(>t{GZ$0N4Ng&kW%i~Yhg?M*Y5eBw_VZ!svmxe z==vQ$r0@0v8SP)c<3+M&+7ApHeY$pujIU{bt}p$@*z%PxxXZ7(=l{5JW^yE5OaIcn zIB=y-sdJj7Z_POHCw`e2_u|&CG8|nEJE<$9oTskbP`}oGV2LZfBuM`EYyZO&40o-$ z%Pa0ne>vv9wA$(>F1uIO!rfFsr&Q2Zzh?RgJ@;>EfQwc2W9QuS1ApTm+V1k};db}Y zfp^HbehYQ;dX~N}Z+(O8#OF!dXrJ|edWZ^1)regqT147pBVZ$7BVZ$7BVZ$7BVZ$7 zBVZ$7BVZ$7Bk+G10e=6L*w^}rgVZ;Ef3$z4UjH27-Guy${vN{b5snkm&wc$4lRzaN zo=}N}j>Nw0+u9zd7g+pmQ@^VxzdPAsc$GK5f+nBJ)M!-hEM!-hEM!-hEM!-hEM!-hEM!-hk zKZ*eMD`I~l_8wx-=62K~`xkE#`yScnc#GKB2f0sd>~GvFHugj6--N`W{Oz@RiCvzS zO^n!^ILALeDVaU3N?n!m4;ors5WG>aOR!TAfW475+5d>&j45dd{iun6Hhb!X`fe2) ze*dhS{!*MYwI;USgiF5kCRkq&;z&P#wY?(O^RC$FmsJ111MCm~FKfiU)DuHPA5R`U zH&rc$)nvNY>2*@w{Z(!DJkrzWr23rnT~hGxn&fzFQGeyvQWsU!(bd|G{0M69-C67E>ecu z#eD?+yW0O|h5G{hX8q^$0twoQ{ag~B>)7miFKd6A?XmIxm6q*PTaxQ~&b>yTg!+)e zeSp$c$~4>m-ANaogi{1Fn%|>TCPGp+O?-#i(}){Nj}X7BrCs590sda`kJs}lT|d0m z@d4s@w(L=umnfNKp8r=UX&AZ9)Pux#&^UHw;AwD}_+2z!XJkI;|C^$sFNr_K9jb4L ze@6WA{CJFb?oZtR3F4CwNAOwV@l1`->Sf}0wa`;6Hb2$b>2jEMO0<*RpUQcmKdZu^ z5_;36!h9|tIMeg60tn;3^%xX-sr^O$q{el)UZeU z?uYL`HF$Dth*Wfd!5eX<)E%Bs-ih(k#|Ou~)1#wjM<%_=!Q3vWl*kzPzbQMYM7-fzB4- ziulk$(~|@nk1sU3&4(6rnCJ*kWw8+YQ-opJ;2d(fBAsBES5C1M2F`4;>dco*^FcXW zj8Ie6934N&WrZ3%KGx0CLc7oTl{w{P7mL({!LV#f9t_HrT&cLh@TjaDhsRJk z^Lg{-%$6t<1`G7f6M;NSI%!kf+N1LT;M6!vo_`P?bZIuRwDu zdI!9}Jg+n8)}d#wN%3=ISZ7GF_C=oYBlO{W8T!r6N3ky_{QWnUQhfX^s*j(O;^%xc z;kg8Ej51L@{{8`za1@N|KSTUGw3a~sv3>%geXO72{r5@FYbWTjz5+fj?Q$PE&d_6B zlp~J+WJ8bj8t?*@MHAN%O-zO4wh!eoL7h^uanF>pYw*jQhg|{(-dZ5k1zGtKCFWf}gS0 zZSZeW0na_O-&hCo_EkzXx#4*JbAk@z_+#SO1$wLtYrK%Al+oYoU(f@2-5Aw31Sz9I zJE#FV*lUzw4(($d)gk)0zg!OFs+9=l#J#~oFJWx(&_@wt-*BQ#Mk;irV|*X;97TWd l{Nvv(=sRT_=|Zo`{ss+6Nz9}*Yjgc)4;#Vum>_1Ve*shM2?89(0pdu`Qag}h#gg+x~Q zWec^Fi4dk~ODR*BPKGWp(-!h)AT?dUAz}Rk6Sk6}Ux$=cPFTX4lom9 z|2JPhzFe@*J->U-x%;{IZ8z7sy$uBxiy}jz@^yu{4S&axI+1WFAR-`jN{uodzc%Gm zWit38hVk?|PLRsOo`PIz6?h>i>6KGt9C`|e6-i|YiIQHv&^eu_NUD@^p7bVBRPbgx zTVy1uOwZ(J=XMkm4IR&MNwf8+ZIni#w6Hn+GNG5DqCLw5mF-Dw zLVq>Fo|&48t(f(Cgr1qYV3UHRlm~^jH}t@f0iQdj(MN*mHh0tT|DboKM zk=@)X^2^r`ifn&DslO@A>kV}+S}?CSFt;}pjt|Tos97|3(SoX2q^g>wQ@L#TqcOFt zsY5|`j1nfz6>Q+Vv`1+(MLNkJ6L@KA%K5nc@=8c+Td6l6U7 zj6C?QdF*_Y2fry#dCtmX|Lr{T4SDb<^5pw+9^91&zc-KlMS0|R<-s@S!Qae-ug{Y& zjUOBS%;Ek#c0PdMc>M_RaqXOw2cM39Io+Ho_{qR$U}V}ld7|_?il(jYi-fh9?u+W0 zrf8n_MlBGG2G@pSdNA7FSl=572itvJy+NKfe#+`VZ_AZ^inh84_rTQZrsZuqgaSd`7wWCHYc0`GUr>t%dwj7Ttv}Km@~?L+&>Eo?3<%iW*GHub zucP0lHXUX34-7aQj_NL7EaXS^qoMFxj~~T%_&H0aZV z?pCtIsuK;edLEfYhaDyZp;&(;7Ie!73Wfu+d8j38WlQu%)}oNXXfzUSS?*V~j_|q= zL|cQgcwbQIkLxiM30>&FT=Vx_u63hbl;A)}S5Pms8xp(wQ9-?%lEJ~LQk|*G`+T7= z#L(qYQLXL}b;vh@N?)+g-`7th7StKnzg~eVr2S`~O+I)Lv1|RXMVx~;KgXBV*K6~u z?8-8)XKB4wT{XXIK_+g*97c4$5v{IT$YocqgpCDNj+z{{+M(31TREj|@*i}u%uZ79-`9fSB=8&A%EoZKu zra*2bFbWn}Y!d06;Pn$j$0)GYDX^KL5OzezD7}Ke`P;W(*_fhSDfs&sKS{Y>@bQyK zzeu?eJOvBN>kw(g_Z<2@xM|9*0=t6oQBf=qYQhq@F@}mO=aO? zOUF_VWZ~sm@`tkU6SMHLPR@}3r1f3~>HlcIK+=7%fe;TP@0;B%e@BS zEm=72BV<^ag|q#yNa)PM**-$xJz2Qy14`=8!ZAQHVIT{inn4w1Ll%w!kqMi#@Zt=r zC|j~{v4v-;w`SobS@K)6@X{>&t}I;c0V#P~7Cs|Oek2ReUI$0B@Z+-NAJ4+YB@#>B zorQ~`nWc3!db9=pHe2As(pjIXgKt~a<)t`$29gGgC~bH_DzE) z=QtKMcydf*T?S7MX{^=Y$q|h$HF$DBW7P&vj%Vz2gC~bGcC5jZqZunOcycgfAAFvv zFFBU6R}G#V%Gh%TPmW~lVS^_JGIno(o7i!{_ zR6V*&O}Z+q0rl*n*_A-!RyFZn>es+iizpF7v*5CcZX%i-s@M!MlYEloC*#NHj}CQs z$q&xPJrDKzw{K+{NuGn6;3gcS>hL8MW$JKmg*9~&0m=G@gLpO7TxH z7Sv%^MZY?Hbw%f+u8I~E%T>{-KIW=ei5wJ)uxW<0esDt-8wf^7-N1C01jx?aJbpRF z@8Iz`#c$#9Ybh?|H&Ogrj=%R0h({4mEy2`6p{}MxA5Ub3?V!Z6X)KYI*u@inN`f_% zScpjs&7TwS4?J;%%%05SFHpRc$A3sNg*?vW4}Qj)Vh_pe=kZ^V%qu*;m+eR%$=wfQlGHS(ga!wSnQR`8J#)blF$1df30%~MR z9YxUMR!U~Olho}PU)&<$mJB|WgBN7*-W=!4{uQ(OO% zPCvwQU4$5ve|aV;djai#l%_=*j*IA8*hb&};4CODhbZDdq4@a}KWy5}Qm@}d)50-& z@tU2mdLcxeBgJ#_<_-YF>L z9ZY`VSw!Z1t|ktsJKjBC-EpW;wLGdm`?)?72HsJ*0c(0MpZ8Lq3cg{4 z#*<)lIYzySdFsYbN{4P>dHh*T9P}hUN*($Wnmtiep(Y+t2M?95J0HK6c(t>1dr`&a z`ZCz4P?N{0iF)g!MFsF{Fl9KkC0NVLFJ9xk#JOfCHPN&CPgWCeI-a9`z6T{&6NMhw zB^wFYO6>C{-t@w6K95ZI|2Xv*$~)KMezfV+*yE<%QQ?U&rMa6Wrsl(jOd53>oVH03KI%hMCs>468 z*upe#<`kAj1;33b_%fl>Svtd2q0-E@B84V&nKALf*=hRK)g8l=N@Qmxx|!Doe2d`gMEX|2ZxQ?` zXz7fhiao+!SlHVp()Tj?p^5`Yr^ROj<8Q9mEz<31ZPM=nk7DnJ7^~(`#S4hCVq1|! z0mUw(C{-^7%IZ5*aR^ZhVU>oQUCOBj)+hbsRDGD2Oi_L!Q))5KCGkjV6gKLH-YFd#gjv}6H`swNX5?GAyxvX@ zz0w%jSWkA<@U?p`b^y+i1ldb2QI=9Mru%4DFns){vy7d@3RvPhiMKz*;`b7Mu<)mF z}xYg_`Ag34E9FGeuvnNU>7p>+hE6HSUgD{ zdlKgeEdgUbjwBdDbsuZy$UKJJM9313yvXV^Ovu?Bxr!k-5^^F(PGd-tkSQE_3>S3H zksAp47~9rvWBaiM>m7RDW6R8Vd!8A8+@Z`2R@XA>q(UWEcmvN%(SX#f7aIy&429#R z!W(dKL|Q0O)kZZi~YQsEY^aH65G&``L{PQRz0W z@Coj8i#ik;3bls9gHnN(bQI}74Tb07Vni!h4TWK;KrS8%cN+?K7z!_8WQtb0L@M0C z75>psxWZ8QsiAPTRJedETxKZL847Vj;nR0TrKfU*<%UA3q2MwUo{$Pp!oi>p=NJnA zO^%e$>EjHATcrYdB(W>F50WA}hB1uVV!@9<5E}}8g~`D?;o$9T-vUQ^Lm4@@e<4%J zC4YY&$nf-QY0Vuz{scV9kQa+F*y1k!7{pQ^y@QesPY+_>?;QChl$neF9QRS2Blik23cEfR!%3j3L_y5iWi^TL$hSq?x9Jzy#AV*d+ z_CI?3qrZM&{nvizO*UJ-xc0W}QFpvLS+(pYX2-q)#2n9g9J{=Uy`F>btH~+e z6V zwLAyj1EYvFCO%aiWcuLy-rASrr+5+v>uUZKf6!gKtT;a6NuGp~Zv1`e#x&~r5ZXye zt=x~Xp6n=Blencfe2q;_>~xGifNEga7kZMD>B?74x+~R0QAO%15>EVXXtZ?Wt0a+( zD;EqGzpExKxcV&}+Q%?bBqUL;4!X-N>_TLW_6=%MD?{$_hF?+Bpo!25YT`-JGAt%q zMs@5^6OVfmPpJpjSX{TuUZgx|&xLd-w&t^HQg_lkS_~ z;^j5+pozV>uoouS{l;VmF1y#@?>H5G<|y616ur}0{16pIwS0>HQFpwBPUo@r(CNH$ z$cUOa<#+0^8_Jfk7ct&a2gZyWIYayS^$*?Oz_rhx)#P=!nK`(D+OYO99XBO-e7N6| zx(j-0@+zA;TyLku>1q-YO1X)poQs4qN;py^M2OqVJ=}4h*b@5JP^F=)6Cx*H= zxfW$Ss3ty4{VqkF{;*@zQ~QEm%sidA-@>}KapUXyJ~ep_8VGIm->F|h{NgnmN1d0H z-aML<0kbbB5(X6;4f?4@8Q^HqpX{!4YB%MI!LlEN53<)Nj!)F#nUA0elvIGy9&?uy z14`o<_xW~NvCTG&iThueTWWHhRULc`&2tDhmUg^Zh@Khihm9O*7k^&BCbF9NQ@mPD zHeaL7FYeb1?s(Q#Zb&ht%X6rF47Dy5A-5 zvn8idzHlr~jIaxfaj(j2d6L+Akc6bQfC>A{g7j_-)KeGAZ9d0b!UoPtBp&M*7 z)FiU7lA~LtCg2kn$?AF%J27=UJ6=Nrz6{2*V?TV|%c7zmdWMT$@FvR}F<7>HlWWSn zi4)aCBM8b~tLR0`6UDnciQO<9zmormw)rDryulau>!C>4)*X%X*~0PO zURx+;3rBR@m3U1rAQ}Ph*6#>$p#bn)V)%UWyZ z%&{$5VmqCprwcptdLn(ndC_<{9E{Fe7l~dzFBS~PB2m0Yi8mEPdL+7j9K@yd_SovC)EtTb?s4U-+*l}hVatccmwu45>*kN&A zq6yns)v;=YXm^SpalIOwo0?_4So{dfL%NPdqC}h_OB1fw>WmS@PCo+u`PB`g6sr+E z@^-nP#l1{chvG-rE^76n1;PwPkGMQgUTD}^c*J&Eofl&Wf*If8rZkG>%3@q(vTP4bvg-pDBE>By1b5oPM z-l(&%mz^>JyPl>MPOrzMwY59j-J*m!*x}>q3+wCjwz{3JRa$e4TXt?XG~xE64#~15 z*$LZ`bwcYkc%(zf19D0JCaQ`;6Ivd@ZtF^hM zCF2cpFmCZn_iue`eNMqL`~>YR?PzFlw`$ER+^r4X=8Hr%P3Q#LtIf4Mr(79+g5@GT z<}I5%?H+i5)tOcsCuIWVYw)x-UgT_bYmJ^ZOp1C{)c7!bg5_;-x58o@=7Q#OH+eFp zJq(|qU7AK6t?>Nrm8uhKVaD*`31s<;#}7WC(eYu9-xt%*%dC-KudGFwlbLC3-Q%TH z6q}9h&b6S}o@;tUe0*f9s;Z)NY~q8yKCM3*=??VcclwdPGN7F<)PQZ;%VvZ28kE zlUOhquJT9XVZ9R987R0X90(3v#pCVR{x4qaa<{ar=h*Dmu&WJ6oR#ZK=q+2-+MH3d z#}&FvQ5u{cugx|)ws>}6G00SfE)M9&SwjN&rCqhccEs%0u}Hy&RG~ek(n$M3g|>6_ zvru9CD1~o=6tN{y*d#GF8j5*GAOTw8WjuQ=R^%eXR#+u_F=V!Bam&c?)d_i<*N_~@ zaQ!(j+_@qgup-^6lH&uFj2BZ(9!Saf5f(Ff2Ia8Xs~k4(l*1>CvILit(LTBuw9%!X zja>@j8gmJaSAHF-<6AvUo4$TF{>FhZs%*BNps!c&p-VcF+_|Qg>jpr71G)#a9qB5b4_pm8 z0J?=Jo|(w=7Vwyq1cZi)-BK_9FOsp%`c?WXOe_#3VSwDf#?GKm7y-| z-M*5thLZ9NN~f)}ZcxrYsrIb-6{nLp>0g4sn*T%{2@rvB9e;M5ywwgsa&JviWlMm{Pp~ikIkaz!JuEhMfx`t zx=U=sMN3O6Z=B>Tv2U8}DybQq;zHZ)wM;Fkah2FH9F~^YASwre4eIyduN>p@EYPCw z;-{o#aLT61H%=NZx~cG61?)(UJ%HOG(}sR2S_g);o#>lwmckPX(K+>^bC6t)zhcyH z-_z+d^;OYI{FH373>I#pk))(=JJB+uHfDdBhZvkG&fC8nZEyb@{z-}q{ z%yOWJ_#zuv&>0RIi0Q&-EssrBHcZCL0hD+q5|4(XEpW62j<&$j7C71hM_b@;rUm5r z67oC=zQW@*NVW@*D?Yw)G4XWf28Ciy_yHY=<6{jL%Fp36b2h{p&f{HA7HB_7L7q(^ z&$pnxIR%-HmlGqzZ=?Mtg4{_A(4G=O5M_@@pUPo;zRV-o^07es9SV5GGA9T%#inss zsImPeAv1+EE_qnM`=2aq5QZhbMPw|?eL%?JRdyC+{&?Y?1u2K`L|FaM?hL|vW{lq| z6!AI><7GMVN;2cSL^{56$VnIG|GyD=?wZ-4oi^bJb%M4C+9_zip!Du1g)M?^6?B`R zqk`@hbg!TX1jXS=ER+dq6VxtfouDm(b_&Y1zWlGXGglVp=q$EXc68weTioWDj{|S) za~H)KTYYWyf+{k+_&}?0O9tT}zN1`>BW^uxm5W7w|{;=xjo94JIbSQc#A| zg+2mZgHX61+we|wc6dtQdrkN&0+)VC>U<2`f)ivaJ2@i# zm8FRA6BOx3q&$7#K;@KvPT~ugJTeu2N#gm+-^S!;Dl^?u(xBtj*9Up(O?kebl%@|~ zEV%kI&#P+~eqzQCN%`*rKON=YHisi0iG**$5fWZk$$>6`-wC`7UbVba0D{~t#f*Fcj&$yPN+?GdvBoDqn z4}JpL|5)^xJa6<&k-=wql`fMd=hO|H+Q&RjF3e-6D-V8u z9{h8%k9lhAc|u;=mlE`?*?8@+3OLP| z4Rg5UX+og`>o%KT!m-gk9}0A@XgoVzdGM?A;I{*}F^joBBf!U-zkkSM=c7FMGIZ^D z_E+S=`+>9iR`T++$zrwnqq?Iig0tv+T_H{PtySm_a@Lz*u@J6JB}d-8PJptrh~rR94vz-1OPneiS1uoDY$n4eu!aT8sq zlN3Eo)zQBBcuKN}KNN8|vYwr}Q9K+vNX5lLg_&~>Yc)?G7}jx6q8n)NiZ~wB=@BN@ zrZ1qo?m}gu=BUvcBRKic7hW60vzS;9UIT=!g&NLEbn&NuJTbshWzsmuD6%9R=$I+F zJQva1Wp{hK9PH2Sb~_f~FvCzd7Q}&q@o?yhc#sN%|0!4091j8_{w6#&@t|e`Bb!>7 zVe>VzCjx5I?+Zn_*aD5Tv=EM>q~{i7m1j%hU2b|;q}{HyE^l{eRiRka$9faZcC@ws zcUl|ss5hX{8InAP$s5z-_)yoc7>9^zdY|T}V?^nc%0NV0+Z*Yk*G1_;l;(>MDD+^i zH>d{#)P3VgXgHHHr1{ucpqK>F^$LGp9fxKq9aP%uZq86Zi18(LSsk$cb zG~dgfV7lrf;qK5{mxGQg4F-I=k7U2%b8Vr&H|UE}jq!vXFN5Ix+BhB{qvojiI49lK zu>()}!KD~$rc-b0jDt%>qC9Ms>*1hOI{XxmWpQe24#dx5&4uXXR(?2Z4vrZVi@>vo z130tSbx~C`*j<(PwB3AX%yNse9yI+>U^?EFp2>@2V>7wp>{e=zzjAVKw#lmMT5+Ik z#t6=WWe4qX>7(*}n@ZqeMK|)t7CM4-h;9NUj z`Kem%pgzKjBiL);pk}Qr?!P>!$7t?;ag2MX+nwdeU_THoA&Z67Xin?ClOhH`FuiB zae1A2K4C7u5Bj*I&(vQ&zmRmRFidSkZ722RbA%AE!YqCHJVa9Y`~xB^nA`sfp!94( zmR~+kkhDhV;r1O1Qcluqfzq=I$;;;zk{%FxRDLR_EWdyiI+BRmRO-v;Cz48inZMMR z_5Z%mUn&gB=P{D*6%{6ZdIn|I|5sonBJF4HkBR$a;?SQlOZ9-2{_70g; z1{KK}dZ$UhUp%*vRLV;el9zOwNq>XTm$XjQKcD^sCjC)yvrE#B%!4~_LFOy%J!I0C z_eUj_&jF>px&0my`m+D!eP&5(OwV!5`9E#am-pEvrT)nd=KOz;L~0_L{~jlomb8ur zZB~%uM?!BPlC59v<|&e1C{JTq{VQBtRkon4f_!%&&ztoq{Pi@U4jrZ`_ukj=GB`t??w!YFSWLr3u z^ITexdXk?7Wx_J~ck+~Sp)boR -Test time = 0.00 sec ----------------------------------------------------------- -Test Passed. -"hal.compile_check" end time: Apr 25 10:05 UTC -"hal.compile_check" time elapsed: 00:00:00 ----------------------------------------------------------- - -2/2 Testing: hal.type_tests -2/2 Test: hal.type_tests -Command: "/home/runner/work/sensor_repository/sensor_repository/build_rel/D_hal_design/tests/test_types" -Directory: /home/runner/work/sensor_repository/sensor_repository/build_rel/D_hal_design/tests -"hal.type_tests" start time: Apr 25 10:05 UTC -Output: ----------------------------------------------------------- -=== rm_hal type tests === - sensor_timestamp ... - sensor_timestamp OK - health_status ... - health_status OK - bytes_per_pixel ... - bytes_per_pixel OK - is_compressed ... - is_compressed OK - pixel_encoding_to_string ... - pixel_encoding_to_string OK - error_code_to_string ... - error_code_to_string OK - lidar3d_defaults ... - lidar3d_defaults OK - stream_profile_partial_match ... - stream_profile_partial_match OK - doa_result_defaults ... - doa_result_defaults OK - imu_device_info_defaults ... - imu_device_info_defaults OK - audio_frame_total_samples ... - audio_frame_total_samples OK - stream_index_operators ... - stream_index_operators OK - stream_index_hash ... - stream_index_hash OK -=== All tests passed === - -Test time = 0.00 sec ----------------------------------------------------------- -Test Passed. -"hal.type_tests" end time: Apr 25 10:05 UTC -"hal.type_tests" time elapsed: 00:00:00 ----------------------------------------------------------- - -End testing: Apr 25 10:05 UTC From 9cdfb2efa5220e0fc9da61ba882780ca88ee3656 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:25:14 +0000 Subject: [PATCH 14/14] fix: SensorTimestamp strict-weak-ordering, REGISTER_HAL macro, CI permissions; expand tests to 26 groups Agent-Logs-Url: https://github.com/YYCB/sensor_repository/sessions/c97005c9-a948-47e6-806b-e2fbe3a02a6e Co-authored-by: YYCB <23326150+YYCB@users.noreply.github.com> --- .github/workflows/ci.yml | 3 + .../include/rm_hal_common/hal_factory.hpp | 16 +- .../rm_hal_common/sensor_timestamp.hpp | 14 +- D_hal_design/tests/test_types.cpp | 359 +++++++++++++++++- 4 files changed, 385 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecdb5c4..2ed1bdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: ["**"] +permissions: + contents: read + jobs: build-and-test: name: ${{ matrix.name }} diff --git a/D_hal_design/include/rm_hal_common/hal_factory.hpp b/D_hal_design/include/rm_hal_common/hal_factory.hpp index 10c53ae..345c3c4 100644 --- a/D_hal_design/include/rm_hal_common/hal_factory.hpp +++ b/D_hal_design/include/rm_hal_common/hal_factory.hpp @@ -138,9 +138,15 @@ class HALFactory { // // The macro creates a function-local static that triggers exactly once at // program startup to register the driver with the factory singleton. -#define REGISTER_HAL(Factory, type_name, Impl) \ - static const bool _hal_reg_##Impl = []() { \ - Factory::instance().registerType( \ - type_name, []() { return std::make_unique(); }); \ - return true; \ +// +// Variable naming: the static is named _hal_reg___ so +// that the same Impl class can be safely registered in multiple factories +// (e.g. a SimHAL registered in both CameraFactory and Lidar3DFactory), and +// so that including the same registration header twice compiles cleanly — +// the line-number suffix makes each instantiation unique. +#define REGISTER_HAL(Factory, type_name, Impl) \ + static const bool _hal_reg_##Factory##_##Impl##_##__LINE__ = []() { \ + Factory::instance().registerType( \ + type_name, []() { return std::make_unique(); }); \ + return true; \ }() diff --git a/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp b/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp index df8011c..41cb1b5 100644 --- a/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp +++ b/D_hal_design/include/rm_hal_common/sensor_timestamp.hpp @@ -22,7 +22,19 @@ struct SensorTimestamp { uint64_t ns = 0; TimestampDomain domain = TimestampDomain::System; - bool operator<(const SensorTimestamp& o) const noexcept { return ns < o.ns; } + bool operator<(const SensorTimestamp& o) const noexcept { + // Compare ns first; break ties by domain to remain consistent with + // operator==, which requires both ns and domain to match. + // Rationale: operator== already compares both fields, so operator< must + // also use both fields to satisfy strict-weak-ordering: if neither + // a(const SensorTimestamp& o) const noexcept { return o < *this; } bool operator<=(const SensorTimestamp& o) const noexcept { return !(o < *this); } bool operator>=(const SensorTimestamp& o) const noexcept { return !(*this < o); } diff --git a/D_hal_design/tests/test_types.cpp b/D_hal_design/tests/test_types.cpp index e28b8d6..2d680d6 100644 --- a/D_hal_design/tests/test_types.cpp +++ b/D_hal_design/tests/test_types.cpp @@ -4,17 +4,23 @@ // Uses a custom CHECK macro so assertions run in both Debug and Release builds. #include "rm_hal_audio/audio_types.hpp" +#include "rm_hal_camera/calibration_types.hpp" #include "rm_hal_camera/camera_types.hpp" #include "rm_hal_camera/pixel_encoding.hpp" #include "rm_hal_camera/stream_type.hpp" +#include "rm_hal_camera/sync_manager.hpp" #include "rm_hal_common/error_code.hpp" +#include "rm_hal_common/hal_factory.hpp" #include "rm_hal_common/health_status.hpp" #include "rm_hal_common/sensor_timestamp.hpp" #include "rm_hal_imu/imu_types.hpp" +#include "rm_hal_lidar/lidar_2d_types.hpp" #include "rm_hal_lidar/lidar_3d_types.hpp" #include #include +#include +#include #include #include @@ -44,7 +50,7 @@ static void test_sensor_timestamp() CHECK(ts.ns == 0); CHECK(ts.domain == TimestampDomain::System); - // Comparison operators + // Comparison operators — same domain SensorTimestamp a, b; a.ns = 1000; b.ns = 2000; @@ -59,6 +65,22 @@ static void test_sensor_timestamp() c.domain = TimestampDomain::System; CHECK(a == c); + // Cross-domain ordering consistency (Bug #1 fix validation): + // Two timestamps with the same ns but different domains must NOT be + // considered both "equivalent" under < AND "not equal" under ==. + // After the fix, operator< includes domain as a tie-breaker. + SensorTimestamp hw, sys; + hw.ns = 5000; hw.domain = TimestampDomain::Hardware; + sys.ns = 5000; sys.domain = TimestampDomain::System; + CHECK(hw != sys); // different domains → not equal + // Exactly one ordering must hold (strict weak ordering) + CHECK((hw < sys) != (sys < hw)); // exactly one is less-than + // Transitive: a < hw, hw < b, then a < b + SensorTimestamp a2; a2.ns = 4999; a2.domain = TimestampDomain::System; + SensorTimestamp b2; b2.ns = 5001; b2.domain = TimestampDomain::System; + CHECK(a2 < hw); + CHECK(hw < b2); + // deltaNs / deltaUs CHECK(b.deltaNs(a) == 1000); CHECK(a.deltaNs(b) == -1000); @@ -333,6 +355,327 @@ static void test_stream_index_hash() CHECK(seen.count(StreamIndex{StreamType::GYRO, 0}) == 0u); } +// ── SensorTimestamp in std::set (strict-weak-ordering validation) ────────────── +static void test_sensor_timestamp_ordered_set() +{ + using rm::hal::SensorTimestamp; + using rm::hal::TimestampDomain; + + // std::set requires strict weak ordering. After Bug #1 fix, inserting + // timestamps with the same ns but different domains must produce 2 distinct + // elements (not silently collapse to 1 as it would with the old ordering). + std::set s; + + SensorTimestamp hw; hw.ns = 1000; hw.domain = TimestampDomain::Hardware; + SensorTimestamp sys; sys.ns = 1000; sys.domain = TimestampDomain::System; + SensorTimestamp gl; gl.ns = 1000; gl.domain = TimestampDomain::Global; + + s.insert(hw); + s.insert(sys); + s.insert(gl); + CHECK(s.size() == 3u); // all three are distinct elements + + // Iterating in sorted order should reflect the domain tie-break ordering. + // Hardware < System < Global (enum values 0, 1, 2). + auto it = s.begin(); + CHECK(it->domain == TimestampDomain::Hardware); ++it; + CHECK(it->domain == TimestampDomain::System); ++it; + CHECK(it->domain == TimestampDomain::Global); +} + +// ── HALFactory register / create / enumerate ─────────────────────────────────── + +// A minimal concrete interface used only in this test to avoid depending on +// any real sensor HAL interface (those are pure-virtual and cannot be instantiated). +struct ITestDevice { virtual ~ITestDevice() = default; virtual int id() const = 0; }; +struct AlphaDevice : ITestDevice { int id() const override { return 1; } }; +struct BetaDevice : ITestDevice { int id() const override { return 2; } }; + +static void test_hal_factory_register_create() +{ + using rm::hal::HALFactory; + + // Use a local factory type to isolate this test from real sensor factories. + HALFactory& fac = HALFactory::instance(); + + fac.registerType("alpha", []() { return std::make_unique(); }); + fac.registerType("beta", []() { return std::make_unique(); }); + + // Create registered types + auto a = fac.create("alpha"); + auto b = fac.create("beta"); + CHECK(a != nullptr); + CHECK(b != nullptr); + CHECK(a->id() == 1); + CHECK(b->id() == 2); + + // Unknown type returns nullptr (no exception) + auto unknown = fac.create("gamma"); + CHECK(unknown == nullptr); + + // Re-registering with a new creator replaces the old one + fac.registerType("alpha", []() { return std::make_unique(); }); + auto a2 = fac.create("alpha"); + CHECK(a2 != nullptr); + CHECK(a2->id() == 2); // now returns BetaDevice +} + +static void test_hal_factory_enumerate() +{ + using rm::hal::HALFactory; + using rm::hal::DeviceInfo; + + HALFactory& fac = HALFactory::instance(); + + // Register an enumerator that returns one DeviceInfo + fac.registerEnumerator("alpha", []() { + DeviceInfo d; + d.type = "alpha"; + d.serial_number = "SN0001"; + d.name = "Alpha Device"; + return std::vector{d}; + }); + + auto devs = fac.enumerateDevices(); + CHECK(!devs.empty()); + bool found = false; + for (const auto& d : devs) { + if (d.serial_number == "SN0001") { found = true; break; } + } + CHECK(found); +} + +// ── Calibration type defaults ────────────────────────────────────────────────── +static void test_calibration_defaults() +{ + using rm::hal::sensor::CameraIntrinsics; + using rm::hal::sensor::CameraExtrinsics; + using rm::hal::sensor::IMUCalibration; + using rm::hal::sensor::DepthMetadata; + using rm::hal::sensor::DistortionModel; + using rm::hal::sensor::StreamType; + + CameraIntrinsics intr; + CHECK(intr.fx == 0.f && intr.fy == 0.f); + CHECK(intr.cx == 0.f && intr.cy == 0.f); + CHECK(intr.width == 0 && intr.height == 0); + CHECK(intr.distortion_model == DistortionModel::BrownConrady); + CHECK(!intr.valid); + + CameraExtrinsics extr; + CHECK(!extr.valid); + for (float v : extr.rotation) CHECK(v == 0.f); + for (float v : extr.translation) CHECK(v == 0.f); + + IMUCalibration imu_cal; + CHECK(imu_cal.stream == StreamType::GYRO); + CHECK(!imu_cal.valid); + for (float v : imu_cal.scale_bias) CHECK(v == 0.f); + for (float v : imu_cal.noise_variances) CHECK(v == 0.f); + for (float v : imu_cal.bias_variances) CHECK(v == 0.f); + + DepthMetadata dm; + CHECK(dm.depth_scale == 0.001f); + CHECK(dm.depth_min_meters == 0.1f); + CHECK(dm.depth_max_meters == 10.0f); + CHECK(!dm.valid); +} + +// ── CameraConfig defaults ────────────────────────────────────────────────────── +static void test_camera_config_defaults() +{ + using rm::hal::sensor::CameraConfig; + using rm::hal::sensor::AlignMode; + using rm::hal::sensor::FrameAggregateMode; + using rm::hal::sensor::SyncMode; + using rm::hal::sensor::PixelEncoding; + + CameraConfig cfg; + CHECK(cfg.width == 1280); + CHECK(cfg.height == 720); + CHECK(cfg.fps == 30); + CHECK(cfg.color_encoding == PixelEncoding::BGR8); + CHECK(cfg.enable_color); + CHECK(cfg.depth_width == 640); + CHECK(cfg.depth_height == 480); + CHECK(cfg.depth_fps == 30); + CHECK(cfg.enable_depth); + CHECK(!cfg.enable_ir); + CHECK(cfg.ring_buffer_depth == 4); + CHECK(CameraConfig::MIN_RING_BUFFER_DEPTH == 2); + CHECK(cfg.align_mode == AlignMode::None); + CHECK(cfg.frame_aggregate_mode == FrameAggregateMode::Any); + CHECK(cfg.sync_mode == SyncMode::FreeRun); + CHECK(!cfg.enable_gmsl_trigger); + CHECK(cfg.gmsl_trigger_fps_hz == 30.0f); +} + +// ── SyncConfig defaults ──────────────────────────────────────────────────────── +static void test_sync_config_defaults() +{ + using rm::hal::sensor::SyncConfig; + using rm::hal::sensor::SyncMode; + + SyncConfig cfg; + CHECK(cfg.mode == SyncMode::FreeRun); + CHECK(cfg.depth_delay_us == 0); + CHECK(cfg.color_delay_us == 0); + CHECK(cfg.trigger2image_delay_us == 0); + CHECK(cfg.trigger_out_delay_us == 0); + CHECK(!cfg.trigger_out_enabled); + CHECK(cfg.frames_per_trigger == 1); +} + +// ── ImageFrame / FrameSet defaults ──────────────────────────────────────────── +static void test_image_frame_defaults() +{ + using rm::hal::sensor::ImageFrame; + using rm::hal::sensor::FrameSet; + using rm::hal::sensor::PixelEncoding; + + ImageFrame f; + CHECK(f.encoding == PixelEncoding::BGR8); + CHECK(f.width == 0); + CHECK(f.height == 0); + CHECK(f.stride == 0); + CHECK(f.data.empty()); + CHECK(f.frame_number == 0u); + CHECK(f.actual_exposure_us == 0.f); + CHECK(f.actual_gain == 0.f); + CHECK(!f.auto_exposure_enabled); + + FrameSet fs; + CHECK(fs.color == nullptr); + CHECK(fs.depth == nullptr); + CHECK(fs.ir_left == nullptr); + CHECK(fs.ir_right == nullptr); +} + +// ── Lidar2DConfig defaults ───────────────────────────────────────────────────── +static void test_lidar2d_defaults() +{ + using rm::hal::sensor::Lidar2DConfig; + using rm::hal::sensor::LidarScanMode; + using rm::hal::sensor::LaserScanData; + + Lidar2DConfig cfg; + CHECK(cfg.port == 6543); + CHECK(cfg.range_min == 0.1); + CHECK(cfg.range_max == 30.0); + CHECK(cfg.scan_frequency_hz == 10); + CHECK(cfg.scan_mode == LidarScanMode::Standard); + CHECK(cfg.angle_resolution_deg == 1.0f); + CHECK(cfg.rpm == 600); + CHECK(cfg.raw_bytes); + CHECK(!cfg.with_checksum); + CHECK(!cfg.with_intensity); + CHECK(cfg.output_360); + CHECK(cfg.recv_buf_size == 1024 * 1024); + CHECK(cfg.udp_timeout_ms == 5000); + CHECK(!cfg.enable_intensity_filter); + CHECK(cfg.min_intensity == 0.f); + CHECK(cfg.mask == 0); + CHECK(cfg.error_circle == 3); + CHECK(!cfg.with_deshadow); + + // LaserScanData defaults + LaserScanData scan; + CHECK(scan.angle_min == 0.0); + CHECK(scan.angle_max == 0.0); + CHECK(scan.angle_increment == 0.0); + CHECK(scan.scan_time == 0.0); + CHECK(scan.range_min == 0.0); + CHECK(scan.range_max == 0.0); + CHECK(scan.ranges.empty()); + CHECK(scan.intensities.empty()); +} + +// ── ImuConfig / ImuData defaults ────────────────────────────────────────────── +static void test_imu_defaults() +{ + using rm::hal::sensor::ImuConfig; + using rm::hal::sensor::ImuData; + using rm::hal::sensor::AccelRange; + using rm::hal::sensor::GyroRange; + using rm::hal::sensor::ImuFusionMode; + + ImuConfig cfg; + CHECK(cfg.baudrate == 460800); + CHECK(cfg.output_rate_hz == 200); + CHECK(cfg.accel_range == AccelRange::G8); + CHECK(cfg.gyro_range == GyroRange::DPS2000); + CHECK(cfg.fusion_mode == ImuFusionMode::Interpolation); + CHECK(cfg.enable_accel_correction); + CHECK(cfg.enable_gyro_correction); + CHECK(!cfg.enable_mag_correction); + + ImuData d; + CHECK(d.accel_x == 0.0 && d.accel_y == 0.0 && d.accel_z == 0.0); + CHECK(d.gyro_x == 0.0 && d.gyro_y == 0.0 && d.gyro_z == 0.0); + // Quaternion identity: w=1, x=y=z=0 + CHECK(d.quat_w == 1.0); + CHECK(d.quat_x == 0.0 && d.quat_y == 0.0 && d.quat_z == 0.0); + CHECK(!d.has_orientation); + CHECK(!d.has_linear_accel); + CHECK(d.euler_roll == 0.0); + CHECK(d.euler_pitch == 0.0); + CHECK(d.euler_yaw == 0.0); +} + +// ── ErrorInfo defaults ───────────────────────────────────────────────────────── +static void test_error_info_defaults() +{ + rm::hal::ErrorInfo ei; + CHECK(ei.code == rm::hal::ErrorCode::OK); + CHECK(ei.message.empty()); + CHECK(ei.sdk_error_detail.empty()); +} + +// ── DeviceInfo / ConnectionType ──────────────────────────────────────────────── +static void test_device_info_defaults() +{ + using rm::hal::DeviceInfo; + using rm::hal::ConnectionType; + + DeviceInfo d; + CHECK(d.type.empty()); + CHECK(d.serial_number.empty()); + CHECK(d.name.empty()); + CHECK(d.connection == ConnectionType::Unknown); + CHECK(d.port.empty()); + CHECK(d.firmware_version.empty()); +} + +// ── PointXYZI / PointCloudXYZI defaults ────────────────────────────────────── +static void test_pointcloud_defaults() +{ + using rm::hal::sensor::PointXYZI; + using rm::hal::sensor::PointCloudXYZI; + + PointXYZI p; + CHECK(p.x == 0.f); + CHECK(p.y == 0.f); + CHECK(p.z == 0.f); + CHECK(p.intensity == 0.f); + CHECK(p.time_offset_s == 0.f); + CHECK(p.ring == 0u); + + PointCloudXYZI cloud; + CHECK(cloud.valid_count == 0); + CHECK(cloud.scan_duration_s == 0.0); + CHECK(cloud.sequence == 0u); + CHECK(cloud.points.empty()); +} + +// ── AudioConfig defaults ─────────────────────────────────────────────────────── +static void test_audio_config_defaults() +{ + rm::hal::sensor::AudioConfig cfg; + CHECK(cfg.sample_rate == 16000u); + CHECK(cfg.channels == 4u); + CHECK(cfg.enable_doa); +} + // ── main ─────────────────────────────────────────────────────────────────────── int main() { @@ -350,6 +693,20 @@ int main() RUN(audio_frame_total_samples); RUN(stream_index_operators); RUN(stream_index_hash); + // ── new tests ── + RUN(sensor_timestamp_ordered_set); + RUN(hal_factory_register_create); + RUN(hal_factory_enumerate); + RUN(calibration_defaults); + RUN(camera_config_defaults); + RUN(sync_config_defaults); + RUN(image_frame_defaults); + RUN(lidar2d_defaults); + RUN(imu_defaults); + RUN(error_info_defaults); + RUN(device_info_defaults); + RUN(pointcloud_defaults); + RUN(audio_config_defaults); std::puts("=== All tests passed ==="); return 0; }