插件开发清单
概述 / Overview
本页是 MDCS 插件开发中 该做与不该做(Do's & Don'ts)的速查表。从生产经验中总结,每一条都对应一个真实踩过的坑。
This page is the MDCS plugin-development Do's & Don'ts. Each rule reflects a real production pitfall.
Do's
命名与契约 / Naming & contract
- Medulla 插件入口类名必须叫 `MainIOObject` —— 严格大小写。
Medulla plugin entry class must be exactly `MainIOObject`.
- 每个插件目录加 README + CHANGELOG,写清适配的硬件型号 + 版本 + 联系人。
- AssemblyInformationalVersion 含 助记词 如 `1.4.0+kindling`,便于现场区分两个数字相同的二进制。
生命周期 / Lifecycle
- 构造函数廉价无副作用 —— 硬件连接放在 `Init()`。
Constructor must be cheap and side-effect-free; hardware contact in `Init()`.
- `Init()` 内捕获异常,设 `eStop = true` + Hedingben toast —— 不要让 `Init()` 抛出,否则 LadderLogic 部分启动部分不启动,难诊断。
Catch exceptions in `Init()`; set `eStop = true` rather than throwing.
- 每次部署前用 `io load` 在 干净控制台测试一次加载 —— 提前发现 Costura 缺失依赖。
LadderLogic
- 不阻塞 `Operation()` 超 scanInterval —— 起 worker 线程或用 `TriggerOnce` / `WaitSignal` 拆分。
Don't block `Operation()` for longer than `scanInterval`. Spawn a worker or split with helpers.
- 共享字段加锁 —— 多个 LadderLogic 并发访问同一字段时用 `lock` 或 `Interlocked`。
- 发布期间监控 Hedingben —— `resume = true` 默认会静默重启 buggy 循环 ;新插件上线时人在场看 toast。
IO 表 / IO fields
- 上位字段 `*Cmd` 后缀,下位 `*Est` 后缀 —— 项目约定,避免同名冲突。
- `[AsInitParam]` 字段加默认值 —— 即使没人设也能初始化。
- 敏感字段加 `[IOObjectWatch]` —— 电量低 / 温度高 自动告警。
Movement / Clumsy
- Movement 实现可重入 —— 不要 static 字段保存状态。
- 在 `Get()` 第一帧读 pose,不要在构造时读 —— 构造与执行间车可能已经移动。
- Movement 长度 ≤ 50 行 —— 超过就拆成多个 Movement 组合。
Detour / 定位 / TightCoupler
- 诚实报协方差 —— 谎报会让 TightCoupler 让该源 统治融合 → 全局发散。
- 外部反馈 `counter` 单调递增 —— 用来检测丢帧 / 乱序。
- `integrated = true` 配 `startingTick` —— 重置(IMU 重启)时给新 baseline。
工程 / Project
- 每个 csproj 用 `..\tools\` HintPath —— 仓库一致约定。
- FodyWeavers.xml 含 Costura + LessokajiWeaver。
- Release 模式发布 —— Debug 会保留更多 PDB / 符号;体积大。
- 测试 .NET 4.8 ↔ .NET 6/8 兼容 —— 部分老 driver 还在 .NET Framework。
DObject
- 名 ≤ 32 字符 —— 超长会被静默截断。
- 读者用 `Wait()` 阻塞 —— 比轮询省 CPU。
- 每次 Post 含 frame counter —— 读者能检测丢帧。
文档 / Documentation
- README 写清 硬件型号 + 协议版本。
- CHANGELOG 含具体行为变化 —— 不是 "fix bug"。
- startup.iocmd 示例放在 INSTALL.txt —— 客户照抄。
Don'ts
命名
- ❌ 不要把入口类改名(如 `MyLidar : Lidar2DIOObject`) —— 加载器找不到。
- ❌ 不要给 IOObject 加 `[Plugin]` 属性 —— MDCS 不用 manifest,加了也无效。
加载
- ❌ 不要热替换 DLL —— Costura 缓存导致未定义行为。停 → 替换 → 启动。
- ❌ 不要在 `static` 构造函数里碰硬件 —— 加载时立刻触发。
- ❌ 不要假设 `Init()` 立刻被调 —— 它要 startup.iocmd 里的 `<inst> Init` 显式触发。
IO 表
- ❌ `[AsUpperIO]` 不要标在 `readonly` 字段上 —— 编译过,运行时 `MissingMethodException`。
- ❌ 不要从外部直接写 `MedullaCartUpperIO<tag>` DObject —— 总走 `SetUpperIO`,否则失同步。
- ❌ 不要把高速字段都 `[IOObjectMonitor]` 加 `VerboseLog` —— 日志爆炸。
LadderLogic
- ❌ 不要在 `Operation()` 里 `Thread.Sleep` —— 阻塞整个 loop 线程。
- ❌ 不要假设 LadderLogic 是实时 —— Windows 上 1–15 ms 抖动;真实时需要 PLC。
- ❌ 不要在多 LadderLogic 之间共享 可变字段而不加锁。
Movement
- ❌ 不要在 Movement 里 `Thread.Sleep` —— 用 `yield return` 多次。
- ❌ 不要在 `Get()` 抛异常但不写 `eStop = true` —— 车继续按上一帧 `vCmd` 走。
- ❌ 不要从 UI 线程调 `DriveTask.WaitDriveTask` —— 它阻塞。
Queue
- ❌ 不要假设 Queue 的不同行真的并行 —— 当前实现是串行(行 i 等行 i-1)。详见 Queue 机制 的"超标量实际行为"段。
- ❌ 不要在 action 内 忽略异常 —— 让它传出,让上层决定恢复。
Detour
- ❌ 不要在雷达插件里调 `TightCoupler.PostExternalFeed` —— 雷达走 DObject `output()` 路径;TightCoupler 是 非雷达来源的入口。
- ❌ 不要谎报协方差 —— 永远不要 "我超准请相信我"。
- ❌ 不要在 SLAM 输出帧间 内部重新加载地图 —— 用 Detour 的 reload API。
SimpleComposer / Fleet
- ❌ 不要在 `BusinessLogic.Plan` 里阻塞超过 100 ms —— UI 假死。
- ❌ `[CarType]` 标 class,不要标实例字段 —— 反射查找按 Type。
- ❌ 不要在 SimpleComposer 进程内调阻塞 HTTP —— 用 async/await。
Costura / 构建
- ❌ 不要给 核心 DLL 加 Costura.Fody —— `MedullaCore.dll` / `ClumsyCore.dll` 等保持 loose DLL,让插件共载。
- ❌ 不要直接引用 impl DLL —— 引用 `Ref<Name>.dll`;否则带入 Costura 内嵌的整套依赖。
DObject
- ❌ 不要在 Linux 上假设 `io_shared` MMF —— Linux 上是文件后备。
- ❌ 不要 cache 多个 `new DObject("name")` 实例 —— 一个 name 一个,跨进程共享。
性能反模式 / Performance anti-patterns
- ❌ 每帧 `new` 大数组 —— 用 buffer 池或预分配。
- ❌ 每帧 `Console.WriteLine` —— 日志走 DLog,主题化、可关闭。
- ❌ 每帧序列化大对象到 DObject —— 用紧凑二进制,避免 JSON。
- ❌ Movement 内每帧 `LINQ` 链 —— `foreach` + 索引循环。
安全 / Safety
- e-stop 必须 独立于 Movement 逻辑 —— 硬件 e-stop 线 + 软件 watchdog 双层。
- 激光雷达 < 300 mm 范围内任何点 → 触发硬 stop —— 不能等 Movement 层减速。
- 功能安全设备(SIL-2 / PL-d)独立于 MDCS —— MDCS 是控制层,不是安全层。