如何适配新的雷达
概述 / Overview
本页是 激光雷达插件适配 的总入口。MDCS 把雷达 / 相机视为一类 传感器插件,通过 Medulla2 的 `IOObject` 体系热插拔。开发新雷达适配的目标是写一个 .NET DLL,包含名为 `MainIOObject` 的类,继承自下列基类之一:
This is the entry page for adapting a new lidar to MDCS. Medulla2 treats lidars (and cameras) as sensor plugins: a single .NET DLL whose entry class is named `MainIOObject` and inherits from one of the bases below.
| 传感器 / Sensor | 基类 / Base class | 文件 / File | 详细教程 / Detailed guide |
|---|---|---|---|
| 2D 激光雷达 / 2D lidar | `Lidar2DIOObject` | `D:\src\M2\OfficialPlugins\LidarController\Lidar2DIOObject.cs` | 2D激光雷达适配 |
| 3D 激光雷达 / 3D lidar | `Lidar3DIOObject` | `D:\src\M2\OfficialPlugins\LidarController\Lidar3DIOObject.cs` | 3D激光雷达适配 |
| 相机(2D/3D)/ Camera | `IOObject`(约定 `MainIOObject` 类名)/ `IOObject` (convention: class name `MainIOObject`) | `D:\src\M2\MedullaCore\Types\IO.cs:47` | 3D相机适配 |
适配工作量速查 / Adaptation effort cheat-sheet
| 难度 / Difficulty | 雷达类型 / Lidar type | 典型工作 / Typical work | 工时 / Effort |
|---|---|---|---|
| ⭐ Easy | 厂商提供 .NET SDK 的 2D / 3D 雷达 / 2D/3D with vendor .NET SDK | 包一层适配,把 SDK 回调写到 cachedLidar / Wrap SDK callbacks into `cachedLidar` | 0.5–1 d |
| ⭐⭐ Medium | 协议公开但需自己解析报文(TCP / UDP)/ Open protocol, parse frames | 帧解析 + 字节序 + 点云转换 / Frame parsing + endianness + point conversion | 1–3 d |
| ⭐⭐⭐ Hard | 协议私有 / 半私有,需逆向 / 现场抓包 / Closed protocol, reverse-engineer | + 协议分析 + 异常恢复 / Plus protocol analysis & failure recovery | 3–10 d |
适配前先做的事 / Before you start
1. 拿到雷达的硬件说明书与通信手册(角分辨率、扫描频率、协议、数据格式)。
Get the hardware datasheet and protocol manual (angular resolution, scan rate, protocol, frame layout).
2. 用厂商工具确认雷达 数据流通 — 设备通电、连上电脑、能看到点云。
Use the vendor tool to verify the device powers up and streams.
3. 决定使用的通讯方式(TCP / UDP / 串口 / USB CAN)。
Pick the transport (TCP / UDP / serial / USB CAN).
4. 用 MDCS-plugin-helper 创建工程骨架:
Scaffold the project with MDCS-plugin-helper:
cd MDCS-plugin-helper
python generate.py
# 选择 lidar sensor plugin / Select "lidar sensor plugin"
# 输入工程名,例如 / Project name e.g.: WLR716Lidar
关键概念 / Key concepts
数据模型 / Data model
雷达插件每扫完一帧,把点云填入基类的 `cachedLidar` 字段(`LidarPoint2D[]` 或 `LidarPoint3D[]`),然后调用 `output()`。`output()` 把当前帧序列化到 `DObject`(共享内存);Detour 在另一进程订阅这个 DObject 拿数据。
A lidar plugin fills its frame into `cachedLidar` (`LidarPoint2D[]` for 2D, `LidarPoint3D[]` for 3D) once per full scan, then calls `output()`. `output()` serialises to a named `DObject` (shared memory); Detour subscribes from another process and reads from there.
2D 点 / 2D point:
public class LidarPoint2D
{
public float th; // 角度 / angle (rad or degrees per plugin)
public float d; // 距离 / distance (mm)
public float intensity; // 归一化反射率 / normalised reflectivity
}
3D 点 / 3D point:
public class LidarPoint3D
{
public float d; // 距离 / distance
public float azimuth; // 方位角 / azimuth
public float altitude; // 俯仰角 / altitude
public float intensity;
public float progression; // 用于多回波合并 / multi-echo
}
生命周期 / Lifecycle
io load plugins/WLR716Lidar.dll → Reflection 找到 MainIOObject 并实例化
→ 插件 Start(...) 启动通信线程
→ 线程内循环: 读包 → 解析 → fill cachedLidar → output()
→ Medulla / Detour 在 DObject 上等到帧后继续工作
启动命令(`startup.iocmd`): The Medulla startup script (`startup.iocmd`) wires it up:
lidar = io load plugins/WLR716Lidar.dll
lidar Start 192.168.0.2 2110
关键字段 / Key fields
| 字段 / Field | 类型 | 含义 / Meaning |
|---|---|---|
| `cachedLidar` | `LidarPoint2D[]` / `3D[]` | 最新一整帧点云 / latest full-scan cloud |
| `frame` | `long` | 帧计数器,单调递增 / monotonically increasing frame counter |
| `scanC` | `int` | 雷达自上电起的帧号 / device's own frame count since power-on |
| `interval` | `double` | 上一帧到当前帧的耗时(ms)/ elapsed ms since previous frame |
| `tick` | `long` | 输出时刻的系统 tick / system tick at `output()` |
| `angleBias` | `float` | 角度偏置;用于硬件倒装或安装角误差 / angle offset (mount correction) |
| `mirror` | `bool` | 倒装标志,true 时 `th := -th` / mirror flag — negates `th` |
| `ReflexRange` | `float` | 反射率归一化分母 / reflectivity normaliser |
| `angleStart/End` | `float` | 输出帧的角度过滤区间 / output FOV filter |
| `minDist/maxDist` | `float` | 距离过滤区间 / distance filter |
| `pointsN` | `int` | 当前帧点数(output 前更新)/ point count |
插件发现 / Plugin discovery
Medulla 加载 DLL 时通过反射查找 类名为 `MainIOObject` 的类型,并要求其有无参构造函数。无属性标记。 At load time Medulla looks for the type named `MainIOObject` via reflection; it must have a parameterless constructor. There is no `[Plugin]` attribute.
最小骨架 / Minimal skeleton
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;
using LidarController;
namespace WLR716Lidar
{
public class MainIOObject : Lidar2DIOObject
{
private string _ip;
private int _port = 2110;
public MainIOObject()
{
// 在出厂正装情况下,雷达正前方对应的扫描起 / 止角度(看产品手册)
// For factory-default mounting, the scan start/end relative to lidar forward.
ScanStartAngle = -135;
ScanEndAngle = 135;
ScanAngleSgn = 1; // 逆时针 / counter-clockwise
}
public void Start(string ip, int port = 2110)
{
_ip = ip; _port = port;
new Thread(() =>
{
while (true)
{
try { Loop(); }
catch (Exception ex)
{
Console.WriteLine($"{ex.Message}");
Thread.Sleep(1000);
}
}
}).Start();
}
private void Loop()
{
using var tcp = new TcpClient(_ip, _port);
using var ns = tcp.GetStream();
var thetaList = new List<float>();
var distList = new List<int>();
var intensList = new List<float>();
var ticStart = DateTime.Now;
while (true)
{
// ... read frame header + payload (vendor-specific) ...
// when one full scan accumulated:
var cloud = new List<LidarPoint2D>(thetaList.Count);
for (int i = 0; i < thetaList.Count; i++)
cloud.Add(new LidarPoint2D {
th = (mirror ? -thetaList[i] : thetaList[i]) + angleBias,
d = distList[i],
intensity = intensList[i] / ReflexRange
});
cachedLidar = cloud.ToArray();
frame += 1;
scanC += 1;
interval = (DateTime.Now - ticStart).TotalMilliseconds;
ticStart = DateTime.Now;
tick = DateTime.Now.Ticks;
output();
thetaList.Clear(); distList.Clear(); intensList.Clear();
}
}
}
}
详细的协议解析、安装校准与 3D 雷达细节见对应的子页面: For per-protocol parsing, install-time calibration and 3D-specific details, see the dedicated pages:
校准与对齐 / Calibration & alignment
雷达适配完成后必做: After the plugin works, always:
- 外参标定 / Extrinsic calibration: 把雷达坐标系对齐到车体坐标系。教程见 标定激光雷达外参。
- 水平度复核 / Horizontality check: 用水平仪 ± IMU 反馈确认雷达扫描平面与地面平行(高位叉车场景尤其重要)。
- 时间戳一致性 / Timestamp coherence: 多传感器 SLAM 场景中,`tick` 与系统 NTP 必须一致;> 50 ms 偏差会引起 SLAM 漂移。
- 强度归一化 / Intensity normalisation: 通过 `ReflexRange` 调整,让反光板的强度明显高于普通墙面(在 Detour 可视化中表现为偏红)。
Detour 端怎么收 / Detour-side consumption
Detour 在 `D:\src\Detour\DetourCore\CartDefinition\Lidar.cs:302-311` 通过 `DObject(name)` 订阅雷达数据,等待 `Wait()` 后反序列化 `LidarOutput`,再喂给 SLAM。所以插件作者完全不需要直接调用 Detour API — 把帧塞进 `cachedLidar` + 调 `output()` 即可。
Detour subscribes via `DObject(name)` and deserialises `LidarOutput` once `Wait()` returns. Plugin authors don't call Detour APIs directly — `cachedLidar` + `output()` is the whole contract.
外部里程计 / IMU 数据是另一条通路:通过 `TightCoupler.PostExternalFeed(...)`(见 `D:\src\Detour\DetourCore\Algorithms\TightCoupler.ExternalCoupler.cs`)。雷达插件本身不走这条。
External odometry / IMU enters Detour via a separate path (`TightCoupler.PostExternalFeed`). That is not the lidar plugin's job.
失败模式与对策 / Common failures
- 雷达连不上 / Can't connect: 检查 IP / 防火墙,看 `Start()` 抛出的异常被外层 catch 后 sleep 重连。
- 解析帧长不对 / Wrong frame length: 字节序错误,确认大端 / 小端;用 vendor 工具抓 1 帧对比。
- 点云 0° 不在正前方 / 0° not aligned to forward: 用 `angleBias` 或 `ScanStartAngle/EndAngle` 修正。
- 反射率值不合理 / Reflectivity off: 调整 `ReflexRange`;不同雷达把强度 / 反光率 / 回波 三个量混用,要看手册区分。
- 帧率不稳定 / Jittery frame rate: 多线程同步问题。把读包改为完整 `while(want_len) Read`(同 `TestHe3051\Class1.cs`)。
工程示例 / Working example
完整可运行示例:`D:\src\cookbook\MDCS-plugin-helper\TestHe3051\Class1.cs`(145 行,TCP 流,He3051 雷达)。
Complete working example: `D:\src\cookbook\MDCS-plugin-helper\TestHe3051\Class1.cs` (145 lines, TCP, He3051 lidar).