Detour
DetourLite独立插件开发
1. 前言
Detour仓库里的 `Standalone` 目录给出了一个可直接复用的扩展方式:把扩展代码单独编译成 `*Plugin.dll`,部署到 DetourLite 的 `plugins` 目录,在不改动 `DetourCore` / `DetourDance` 主工程的前提下扩展算法核。
这种 Standalone Plugin 适合以下场景:
- 增加自定义 HTTP API。
- 对接 ROS2、原生 C/C++ 动态库、UDP/TCP 传感器。
- 新增自定义 2D / 3D 传感器组件类型。
- 在启动时执行一次性初始化逻辑。
仓库中的参考工程:
- `Standalone/DemoPlugin`
- `Standalone/StandaloneSensors`
- `Standalone/DllImportSensor`
2. 加载机制
DetourLite 的插件加载逻辑在 `DetourLite/DetourLiteMain.cs` 中,规则很直接:
- 启动时扫描当前目录下 `plugins` 文件夹以及所有子目录。
- 所有文件名匹配 `*Plugin.dll` 的程序集都会被 `Assembly.LoadFrom(...)` 加载。
- 如果程序集里导出了一个名为 `Plugin` 的类,并且该类里存在 `static void Main()`,则该方法会在 `DetourLib.Init()` 之后被调用。
因此有两个层次的能力:
- 只要 DLL 名称以 `Plugin.dll` 结尾,程序集就会被加载。
- 如果还提供 `Plugin.Main()`,就可以执行启动初始化逻辑。
这意味着:
- 纯组件型插件 可以只提供导出的组件类,不一定非要写 `Plugin.Main()`。
- 启动型插件 则应提供 `Plugin.Main()`,在里面注册 HTTP 路由、启动线程、初始化第三方库等。
3. 组件是如何注册进 Detour 的
DetourCore 使用 `DetourCore/Misc/JSONLoader.cs` 从当前 AppDomain 中所有已加载程序集的导出类型里查找组件类型。
所以自定义组件要满足下面几个条件:
- 类型必须是 `public`。
- 类型必须继承 `LayoutDefinition.Component`,通常继承 `Lidar.Lidar2D` 或 `DetourDance.Lidar3D`。
- 类型必须有无参构造函数。
- 类型所在程序集必须在 Detour 读取 `detour.json` 之前已经被加载。
组件类型通常用属性声明:
[LayoutDefinition.ComponentType(Name = "ROS2d lidar")]
public class ROS2Lidar2D : Lidar.Lidar2D
{
}
公开字段可配合以下属性暴露到参数界面和状态输出:
- `FieldMember`
- `StatusMember`
- `MethodMember`
4. 你可以做的两类插件
4.1 启动型插件
这类插件只关心 DetourLite 启动后执行逻辑,不一定要新增配置里的组件类型。最常见用途是挂额外 API。
`Standalone/DemoPlugin/loader.cs` 就是这种模式。它在 `Plugin.Main()` 里注册了:
- `/eraseCloud`
- `/get2dlm`
- `/mycall`
- `/getPCD`
最小示例:
using DetourCore;
namespace MyStandalonePlugin
{
public class Plugin
{
public static void Main()
{
PicoHttpServer.AddGetHandler("/helloPlugin", () =>
{
return "hello from standalone plugin";
});
}
}
}
4.2 组件型插件
这类插件把新传感器或新部件编译进独立 DLL,让 DetourLite 在读取 `detour.json` 时识别出来。
仓库里的对应示例:
- `Standalone/DemoPlugin/ros2lidar2d.cs`
- `Standalone/StandaloneSensors/Mid360Lidar.cs`
- `Standalone/StandaloneSensors/LeishenC16Lidar.cs`
- `Standalone/DllImportSensor/CppLidar.cs`
5. 2D 传感器插件写法
2D 传感器通常继承 `DetourCore.CartDefinition.Lidar.Lidar2D`,重点是重写两个方法:
- `InitReadLidar()`
- `ReadLidar()`
最小骨架:
using System.Threading;
using DetourCore.CartDefinition;
[LayoutDefinition.ComponentType(Name = "My UDP Lidar")]
public class MyUdpLidar : Lidar.Lidar2D
{
private readonly object sync = new();
private Lidar.LidarFrame latest = new();
private int scanC = 0;
public override void InitReadLidar()
{
new Thread(() =>
{
while (true)
{
latest = new Lidar.LidarFrame()
{
counter = ++scanC,
raw = new[]
{
new Lidar.Lidar2D.RawLidar
{
th = 0,
d = 1000,
intensity = 1
}
}
};
lock (sync)
Monitor.PulseAll(sync);
}
}).Start();
}
public override Lidar.LidarFrame ReadLidar()
{
lock (sync)
Monitor.Wait(sync);
return latest;
}
}
2D 输出约定:
- `th`:角度,单位是 度。
- `d`:距离,单位是 毫米。
- `intensity`:反射强度,`float`。
- `counter`:单调递增帧号。
Detour 的 2D 预处理、去拖尾、车体滤波、反光板提取、激光里程计等逻辑都在基类中完成,所以插件作者只需要稳定地产生原始帧。
6. 3D 传感器插件写法
3D 传感器通常继承 `DetourDance.Lidar3D`,同样重写:
- `InitReadLidar()`
- `ReadLidar()`
3D 返回值是 `LidarOutput3D`,关键字段如下:
- `azimuth`:水平角,单位度。
- `altitude`:俯仰角,单位度。
- `d`:距离,单位毫米。
- `intensity`:反射强度。
- `progression`:该点在当前扫描周期里的进度,推荐归一化到 `0..1`。
- `tick`:单调递增扫描序号。
如果你的数据源来自原生库,可以参考 `Standalone/DllImportSensor/CppLidar.cs`;如果来自 ROS2,可以参考 `Standalone/DemoPlugin/ros2lidar2d.cs`;如果来自 UDP 包流,可以参考 `Standalone/StandaloneSensors/*`。
7. 开发环境与引用 DLL
Detour 独立插件不是通过 NuGet 分发 SDK,而是直接引用发布到 MDCS 站点的参考 DLL。
运行包下载入口:
插件开发常用引用:
可选辅助引用:
为方便拉取这些 DLL,提供一个下载脚本:
典型用法:
Invoke-WebRequest `
-Uri "https://mdcs.lessokaji.com/res/get-detour-plugin-sdk.ps1" `
-OutFile ".\get-detour-plugin-sdk.ps1"
powershell -ExecutionPolicy Bypass `
-File .\get-detour-plugin-sdk.ps1 `
-Destination .\tools
执行后会得到:
- `tools\RefDetourCore.dll`
- `tools\RefDetourDance.dll`
- `tools\deps\RefFundamentalLib.dll`
- `tools\deps\LessokajiWeaverUtilities.dll`
8. csproj 参考写法
`Standalone/DemoPlugin/DemoPlugin.csproj` 给出了一个可直接工作的最小模式。
一个简化版示例如下:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="FundamentalLib">
<HintPath>..\tools\deps\RefFundamentalLib.dll</HintPath>
</Reference>
<Reference Include="DetourCore">
<HintPath>..\tools\RefDetourCore.dll</HintPath>
</Reference>
<Reference Include="DetourDance">
<HintPath>..\tools\RefDetourDance.dll</HintPath>
</Reference>
<Reference Include="LessokajiWeaverUtilities">
<HintPath>..\tools\deps\LessokajiWeaverUtilities.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
如果希望本地构建后直接落到运行目录,可以像 `Standalone/DemoPlugin` 那样把 `OutputPath` 设到 DetourLite 的 `plugins` 目录,这对调试最方便。
9. 构建与部署
建议流程:
- 在独立工程中编译插件 DLL。
- 确认文件名以 `Plugin.dll` 结尾,例如 `StandaloneSensorsPlugin.dll`。
- 把 DLL 复制到 DetourLite 运行目录下的 `plugins` 子目录。
- 如果插件依赖额外原生库,也一起放到 DetourLite 可搜索到的位置。
- 启动 DetourLite,检查控制台是否打印:
- 找到并加载了 `*Plugin.dll`
- 找到了 `Plugin.Main()`
- 或至少程序集被成功加载
本仓库当前已验证可编译的示例:
- `Standalone/DemoPlugin`
- `Standalone/StandaloneSensors`
- `Standalone/DllImportSensor`
10. 当前限制
这一点非常重要:
- DetourLite 当前会自动扫描 `plugins\*Plugin.dll`。
- Windows 版 Detour.exe 当前仓库里 没有 对等的自动插件加载器。
这会带来两个直接影响:
- 如果插件只是给 DetourLite 增加 HTTP API,这没有问题。
- 如果插件新增了自定义组件类型,而你又想在 Windows `Detour.exe` 里直接通过 UI 新建、编辑、加载这种组件,那么客户端进程也必须先把同一个程序集加载进来,否则 UI 的类型列表和 JSON 反序列化都看不到该类型。
换句话说,当前代码最稳妥的使用方式 是:
- 纯扩展 API:只部署到 DetourLite。
- 自定义组件:优先用于 Lite 端独立运行场景;如果还要配合 Windows UI 使用,则需要让客户端也加载同一个插件程序集。
11. 推荐开发方法
结合仓库里的 `Standalone` 示例,推荐按下面的顺序开发:
- 先做一个只包含 `Plugin.Main()` 的最小插件,确认 DLL 能被 DetourLite 发现并执行。
- 再加一个最小 HTTP 路由,确认运行时初始化逻辑没问题。
- 如果要接传感器,再新建一个继承 `Lidar2D` 或 `Lidar3D` 的类型,先返回最小假数据。
- 假数据跑通后,再接入 ROS2 / UDP / 原生 DLL 等真实数据源。
- 最后再补 `FieldMember` / `StatusMember`,把参数和状态整理到可维护的程度。
这样排查最简单,能快速区分是:
- 加载失败,
- 初始化失败,
- 组件识别失败,
- 还是数据源本身有问题。
12. 结语
Standalone Plugin 的核心价值是:把项目级扩展放在独立 DLL 里维护,而不是不断改 Detour 主工程。对于项目交付、客户定制、特定传感器桥接、快速试验都更合适。
如果只是想扩 API,写启动型插件即可;如果要接自定义雷达,就按 `Lidar2D` / `Lidar3D` 的约定返回原始帧,让 Detour 继续负责后面的预处理和 SLAM 计算。