<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-Hans-CN">
	<id>https://wiki2.lessokaji.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Artheru</id>
	<title>MDCS wiki2 - 用户贡献 [zh-cn]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki2.lessokaji.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Artheru"/>
	<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E7%89%B9%E6%AE%8A:%E7%94%A8%E6%88%B7%E8%B4%A1%E7%8C%AE/Artheru"/>
	<updated>2026-05-16T14:55:57Z</updated>
	<subtitle>用户贡献</subtitle>
	<generator>MediaWiki 1.40.0</generator>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E6%A0%B8%E5%BF%83%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97&amp;diff=1051</id>
		<title>核心开发指南</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E6%A0%B8%E5%BF%83%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97&amp;diff=1051"/>
		<updated>2026-05-16T14:00:53Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
本页是 MDCS '''核心平台开发'''的总入口，面向 ''修改 MDCS 本身''的开发者（添加新的 SLAM 后端、扩展 LadderLogic、修改 DPS 调度算法等）。完整离线副本见 `D:\src\cookbook\HOW_TO_DEV_CORE.md`。&lt;br /&gt;
&lt;br /&gt;
This is the hub for '''MDCS core platform development''' — for developers ''modifying MDCS itself'' (adding a new SLAM backend, extending LadderLogic, modifying DPS scheduling, etc.). Offline mirror: `D:\src\cookbook\HOW_TO_DEV_CORE.md`.&lt;br /&gt;
&lt;br /&gt;
== 1. 仓库布局 / Repo layout ==&lt;br /&gt;
7 个并列仓库，按依赖自底向上构建：&lt;br /&gt;
7 sibling repositories, built bottom-up by dependency:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 仓库 / Repo !! 产物 / Output !! 作用 / Role&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\Fundamentals\` || `FundamentalLib.dll`, `FundamentalLib2.dll`, `LessokajiProtect.dll` || 通用类型 + IPC + 日志 + 许可&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\LessokajiWeaver\` || `LessokajiWeaver.Fody.dll` || 编译期 IL 注入&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\CycleGUI\` || `CycleGUI.dll`, `libVRender.dll`（C++）|| 懒协程 UI 框架&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\M2\` || `MedullaCore.dll`, `OfficialPlugins/*.dll` || 硬件抽象内核 + 一方插件&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\Detour\` || `Detour.exe`, `DetourLite.exe`, `DetourCore.dll` || 定位系统&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\Clumsy\` || `ClumsyCore.dll`, `ClumsyDance.dll` || 车载自动驾驶&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\Simple\` || `SimpleCore.dll`, `SimpleComposer.exe` || 车队管理&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
详细的内核子页：&lt;br /&gt;
Per-subsystem deep-dive pages:&lt;br /&gt;
&lt;br /&gt;
* [[Special:MyLanguage/MDCS仓库布局与构建链|MDCS仓库布局与构建链]]&lt;br /&gt;
* [[Special:MyLanguage/DObject共享内存协议|DObject共享内存协议]] — Fundamentals 内核&lt;br /&gt;
* [[Special:MyLanguage/CycleGUI|CycleGUI]] — UI 框架&lt;br /&gt;
* [[Special:MyLanguage/Medulla软件架构|Medulla软件架构]] — 加载器 + 脚本引擎&lt;br /&gt;
* [[Special:MyLanguage/LadderLogic框架|LadderLogic框架]] — 周期循环&lt;br /&gt;
* [[Special:MyLanguage/Detour软件架构|Detour软件架构]] — 定位流水线&lt;br /&gt;
* [[Special:MyLanguage/DriveTask调度循环|DriveTask调度循环]] — Movement 宿主&lt;br /&gt;
* [[Special:MyLanguage/SimpleAgvInterface Queue机制|SimpleAgvInterface Queue机制]] — 任务流水线&lt;br /&gt;
* [[Special:MyLanguage/Simple软件架构|Simple软件架构]] — 调度内核&lt;br /&gt;
* [[Special:MyLanguage/调度内核运行原理|调度内核运行原理]] — DPS + 包络 + 可达性&lt;br /&gt;
* [[Special:MyLanguage/DPS调度算法详解|DPS调度算法详解]] — 死锁规避算法&lt;br /&gt;
* [[Special:MyLanguage/LessokajiWeaver编译后处理工具|LessokajiWeaver编译后处理工具]] — Fody 织入&lt;br /&gt;
* [[Special:MyLanguage/MDCS跨切面约定|MDCS跨切面约定]] — 命名 / 线程 / 错误处理 / 版本&lt;br /&gt;
* [[Special:MyLanguage/MDCS平台发布流程|MDCS平台发布流程]] — Release 流程&lt;br /&gt;
&lt;br /&gt;
== 2. 构建工具链 / Build chain ==&lt;br /&gt;
=== 顺序 / Order ===&lt;br /&gt;
1. LessokajiWeaver — 构建时依赖 / build-time dep&lt;br /&gt;
2. Fundamentals — 用 Weaver / uses Weaver&lt;br /&gt;
3. CycleGUI — 用 Fundamentals + 需要 `libVRender` C++ 先构建&lt;br /&gt;
4. M2 / MedullaCore — 用 Fundamentals + CycleGUI&lt;br /&gt;
5. M2 / OfficialPlugins/* — 用 MedullaCore&lt;br /&gt;
6. Detour / DetourCore&lt;br /&gt;
7. Clumsy / ClumsyCore，然后 ClumsyDance&lt;br /&gt;
8. Simple / SimpleCore，然后 SimpleComposer&lt;br /&gt;
&lt;br /&gt;
=== 引用程序集分发 / Reference-assembly distribution ===&lt;br /&gt;
每个核心 DLL 都同时产生一个 ''引用程序集''（`Ref&amp;lt;Name&amp;gt;.dll`），通过 [refasmer](https://github.com/JetBrains/Refasmer) 生成。插件 csproj 引用 `Ref&amp;lt;Name&amp;gt;.dll`，不引用 impl，所以：&lt;br /&gt;
Each core DLL also emits a '''reference-only assembly''' (`Ref&amp;lt;Name&amp;gt;.dll`) via [refasmer]. Plugins reference the `Ref` assembly, not the impl. Therefore:&lt;br /&gt;
&lt;br /&gt;
* 插件构建不会拉入 Costura 织入的传递依赖。&lt;br /&gt;
* 公共 API 由 `Ref` assembly 固定 —— `internal` 成员对插件不可见。&lt;br /&gt;
&lt;br /&gt;
【注意】 改动 `MedullaCore` 公共面后必须重新构建 impl + ref；插件用旧 `Ref` 构建后会静默缺失新成员。&amp;lt;br&amp;gt;&lt;br /&gt;
After any change to a `MedullaCore` public surface, rebuild both impl and ref assemblies; plugins built against the old ref will silently miss new members.&lt;br /&gt;
&lt;br /&gt;
=== Costura.Fody 胖 DLL ===&lt;br /&gt;
插件输出是单 `.dll`（所有依赖嵌入为 base64 资源）。核心 DLL 自身 ''不要''加 Costura.Fody，因为多个插件都要共载它们。&lt;br /&gt;
&lt;br /&gt;
Plugin outputs are fat DLLs (all deps embedded). Core DLLs themselves '''should not''' use Costura.Fody since multiple plugins co-load them.&lt;br /&gt;
&lt;br /&gt;
=== LessokajiWeaver IL 注入 ===&lt;br /&gt;
* `[AsInitParam] / [AsUpperIO] / [AsLowerIO]` → 字段重写为同步到 DObject 的 property&lt;br /&gt;
* `[ReplaceWithCompileInfo]` → 编译时戳 + git 哈希&lt;br /&gt;
* `[Translatable] / [T(zh=, en=)]` → getter 按 `CultureInfo.CurrentUICulture` 分派&lt;br /&gt;
&lt;br /&gt;
详见 [[Special:MyLanguage/LessokajiWeaver编译后处理工具|LessokajiWeaver编译后处理工具]] 与 [[Special:MyLanguage/如何使用LessokajiWeaver的多语言功能|如何使用LessokajiWeaver的多语言功能]]。&lt;br /&gt;
&lt;br /&gt;
== 3. 跨切面约定 / Cross-cutting conventions ==&lt;br /&gt;
=== 命名 / Naming ===&lt;br /&gt;
* 插件入口类: `MainIOObject`（精确匹配）。&lt;br /&gt;
* 引用程序集: `Ref&amp;lt;Name&amp;gt;.dll`。&lt;br /&gt;
* DObject 槽名: `&amp;lt;Producer&amp;gt;&amp;lt;Tag&amp;gt;`（如 `MedullaCartLowerIO&amp;lt;tag&amp;gt;`）。&lt;br /&gt;
&lt;br /&gt;
=== 线程 / Threading ===&lt;br /&gt;
* `LadderLogic` 每个 `[UseLadderLogic]` 类自己的线程。&lt;br /&gt;
* `DriveTask` 每个 Movement 自己的线程。&lt;br /&gt;
* DObject `Wait()` 阻塞；要并行多个就用 worker 线程。&lt;br /&gt;
* CycleGUI `Draw*` 调用从任意线程都安全。&lt;br /&gt;
&lt;br /&gt;
=== 错误处理三层 / 3-layer error model ===&lt;br /&gt;
# 插件级 `[UseLadderLogic(resume=true)]` —— 捕获并重启循环（默认）。&lt;br /&gt;
# 进程级 `Hedingben.ToastText` —— UI 弹窗。&lt;br /&gt;
# 看门狗级（Wawa）—— 进程崩了由 Wawa 重启。&lt;br /&gt;
&lt;br /&gt;
不要同时做三层；选合适的级别。&amp;lt;br&amp;gt;&lt;br /&gt;
Don't do all three — pick the right level.&lt;br /&gt;
&lt;br /&gt;
=== 版本助记词 / Version mnemonics ===&lt;br /&gt;
每次修改编译产物，AssemblyInformationalVersion 加一个单词（如 `1.4.0+kindling`）。便于在调试时区分两个数字相同的二进制。&lt;br /&gt;
&lt;br /&gt;
Each modification adds a single mnemonic word (e.g. `1.4.0+kindling`) to AssemblyInformationalVersion. Lets you disambiguate two binaries with the same number in the field.&lt;br /&gt;
&lt;br /&gt;
== 4. 测试 / Tests ==&lt;br /&gt;
* 单元测试稀疏；机器人代码本质上靠 ''集成测试''。&lt;br /&gt;
* `D:\src\Fundamentals\Test\` 覆盖 DObject + DLog（最关键）。&lt;br /&gt;
* 多数&amp;quot;测试&amp;quot;通过 `[MovementTest]` / `[IOObjectUtility]` 在 UI 上点按钮跑。&lt;br /&gt;
* 修改 `DObject` 后必跑 Fundamentals 单元测试 —— IPC bug 是灾难性的。&lt;br /&gt;
* 修改任一 `*Core` 后跑模拟车 SimpleComposer 集成测试。&lt;br /&gt;
&lt;br /&gt;
== 5. 发布 / Release ==&lt;br /&gt;
见 [[Special:MyLanguage/MDCS平台发布流程|MDCS平台发布流程]]。&lt;br /&gt;
&lt;br /&gt;
== 6. 关联文档 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/操作指南|操作指南]] — 运维者视角&lt;br /&gt;
* [[Special:MyLanguage/插件开发指南|插件开发指南]] — 插件作者视角&lt;br /&gt;
* [[Special:MyLanguage/车体抽象原理|车体抽象原理]] — 三层模型&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E6%93%8D%E4%BD%9C%E6%8C%87%E5%8D%97&amp;diff=1050</id>
		<title>操作指南</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E6%93%8D%E4%BD%9C%E6%8C%87%E5%8D%97&amp;diff=1050"/>
		<updated>2026-05-16T14:00:52Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
本页是 MDCS '''运维与使用''' 的总入口，面向集成商 / 现场工程师 / 操作员。它把现有的使用手册按 ''从部署到日常运维''的顺序串联起来。完整的离线副本见 `D:\src\cookbook\HOW_TO_USE.md`。&lt;br /&gt;
&lt;br /&gt;
This is the hub for MDCS '''operations and usage''', for integrators / commissioning engineers / operators. It stitches the existing usage manuals into a deploy-to-daily-ops sequence. The offline mirror lives at `D:\src\cookbook\HOW_TO_USE.md`.&lt;br /&gt;
&lt;br /&gt;
== 1. 系统组件 / System anatomy ==&lt;br /&gt;
MDCS 在生产现场是 4 个进程协同工作：&lt;br /&gt;
MDCS is 4 cooperating processes on a production site:&lt;br /&gt;
&lt;br /&gt;
* '''Medulla''' — 硬件抽象层 + 插件加载器 / hardware-abstraction layer + plugin loader (`D:\src\M2`)&lt;br /&gt;
* '''Detour''' — 定位系统 / positioning system (`D:\src\Detour`)&lt;br /&gt;
* '''Clumsy''' — 车载自动驾驶 / on-vehicle autopilot (`D:\src\Clumsy`)&lt;br /&gt;
* '''SimpleComposer''' — 车队 UI + 调度 / fleet UI + scheduler (`D:\src\Simple\SimpleComposer`)&lt;br /&gt;
&lt;br /&gt;
进程间通过共享内存（[[Special:MyLanguage/DObject共享内存协议|DObject]]）通讯。详细分层见 [[Special:MyLanguage/车体抽象原理|车体抽象原理]]。&lt;br /&gt;
&lt;br /&gt;
The processes communicate via shared memory ([[Special:MyLanguage/DObject共享内存协议|DObject]]). Layered details: [[Special:MyLanguage/车体抽象原理|vehicle abstraction]].&lt;br /&gt;
&lt;br /&gt;
== 2. 部署流程 / Deployment ==&lt;br /&gt;
按顺序：&lt;br /&gt;
In order:&lt;br /&gt;
&lt;br /&gt;
# 硬件清单与选型 / Hardware checklist &amp;amp; selection — [[Special:MyLanguage/激光雷达选型测试报告|激光雷达选型测试报告]], `wiki/MDCS-Walkthrough`&lt;br /&gt;
# Detour 部署 / Install Detour — [[Special:MyLanguage/下载并部署Detour|下载并部署Detour]], [[Special:MyLanguage/安装Detour|安装Detour]]&lt;br /&gt;
# 完整 MDCS 部署 / Full MDCS deployment — [[Special:MyLanguage/使用手册 - MDCS软件部署方法介绍|MDCS软件部署方法介绍]], [[Special:MyLanguage/使用手册 - Linux MDCS部署配置和操作说明|Linux MDCS部署配置和操作说明]]&lt;br /&gt;
# 许可激活 / Licensing — 经 `auth.lessokaji.com` 申请机器绑定 license / via `auth.lessokaji.com` for machine-bound license&lt;br /&gt;
# 网络拓扑 / Network topology — 默认端口 Medulla 8081 / Detour WebUI / Clumsy 30080 / SimpleComposer Nancy 9050+&lt;br /&gt;
&lt;br /&gt;
== 3. 站点初始化 / Site bring-up ==&lt;br /&gt;
# 车体几何 layout JSON 配置 — `LayoutDefinition.CartLayout` at `D:\src\Detour\DetourCore\CartDefinition\LayoutDefinition.cs:79`&lt;br /&gt;
# 标定（顺序敏感）/ Calibration (order matters):&lt;br /&gt;
#* 轮径 + 轮距 / Wheel diameter + track — [[Special:MyLanguage/MDCS引擎适配机器人入门教学|MDCS引擎适配机器人入门教学]]&lt;br /&gt;
#* 激光雷达水平度 / Lidar horizontality — [[Special:MyLanguage/标定与校准|标定与校准]]&lt;br /&gt;
#* 激光雷达外参 / Lidar extrinsics — [[Special:MyLanguage/标定激光雷达外参|标定激光雷达外参]]&lt;br /&gt;
#* 双雷达外参 / Dual-lidar extrinsics — [[Special:MyLanguage/使用手册 - 双雷达标定手册|使用手册 - 双雷达标定手册]]&lt;br /&gt;
# SLAM 建图 + 编辑 / SLAM mapping + edit — [[Special:MyLanguage/激光SLAM建图|激光SLAM建图]], [[Special:MyLanguage/激光地图编辑指南|激光地图编辑指南]]&lt;br /&gt;
# SimpleComposer 场景搭建 / Scene authoring — [[Special:MyLanguage/开发手册 - SimpleComposer界面开发 - CAD工具|CAD工具]]&lt;br /&gt;
# 试运行 / Test drive — 手动 + 自动 / manual + automated&lt;br /&gt;
&lt;br /&gt;
== 4. 任务下发 / Mission dispatch ==&lt;br /&gt;
3 个入口：&lt;br /&gt;
3 surfaces:&lt;br /&gt;
&lt;br /&gt;
* SimpleComposer UI (&amp;quot;Send car to site&amp;quot;, &amp;quot;Run mission template&amp;quot;)&lt;br /&gt;
* Nancy HTTP API (`POST /api/dispatch`)&lt;br /&gt;
* WMS 集成 / WMS integration — 见 [[Special:MyLanguage/如何基于SimpleCore核心库进行调度系统开发|如何基于SimpleCore核心库进行调度系统开发]]&lt;br /&gt;
&lt;br /&gt;
任务编译流程 / Mission compilation: 业务 → `SegmentPlan` → TopazScript → 车载 ClumsyCar / 自评估车。详见 [[Special:MyLanguage/AGV任务运行逻辑|AGV任务运行逻辑]]。&lt;br /&gt;
&lt;br /&gt;
== 5. 标定速查 / Calibration cookbook ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 步骤 / Step !! 文档 / Doc !! 允许误差 / Tolerance&lt;br /&gt;
|-&lt;br /&gt;
| 轮径 + 轮距 || [[Special:MyLanguage/MDCS引擎适配机器人入门教学|引擎适配教学]] || ≤ 2% drift / 5 m&lt;br /&gt;
|-&lt;br /&gt;
| 激光雷达水平 || [[Special:MyLanguage/标定与校准|标定与校准]] || ≤ 0.3°&lt;br /&gt;
|-&lt;br /&gt;
| 激光雷达外参 || [[Special:MyLanguage/标定激光雷达外参|标定激光雷达外参]] || ≤ 5 mm / 0.2°&lt;br /&gt;
|-&lt;br /&gt;
| SLAM 地图 || [[Special:MyLanguage/激光SLAM建图|激光SLAM建图]] || ≤ ±50 mm / ±2°&lt;br /&gt;
|-&lt;br /&gt;
| 二维码（纯模式）|| [[Special:MyLanguage/纯二维码导航|纯二维码导航]] || ≤ ±5 mm / ±0.2°&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 6. 监控与诊断 / Monitoring &amp;amp; diagnostics ==&lt;br /&gt;
* SimpleComposer 主画布：车辆状态颜色（绿/蓝/黄/红）+ 锁占用 + Trace 面板。&lt;br /&gt;
* DLog：结构化日志 `logs/&amp;lt;YYYY-MM-DD&amp;gt;.log`，主题 `$lidar`, `$slam`, `$dobject`, `$watchdog` 等。&lt;br /&gt;
* Hedingben (Wawa)：toast 通知 / toast notifications — [[Special:MyLanguage/看门狗Wawa使用说明|看门狗Wawa使用说明]]&lt;br /&gt;
* 录制与回放 / Recording &amp;amp; replay — [[Special:MyLanguage/使用手册 - 数据录制与回放手册|数据录制与回放手册]]&lt;br /&gt;
&lt;br /&gt;
== 7. 常见运维流程 / Common procedures ==&lt;br /&gt;
* 添加车辆到现网 / Add a vehicle — 完整流程见 HOW_TO_USE.md §6.1&lt;br /&gt;
* 不停机更新插件 / Hot-swap plugin — 车辆 &amp;quot;暂停服务&amp;quot; → 推送 DLL → 重启 Medulla → &amp;quot;重新服务&amp;quot; / take out of service → push DLL → restart Medulla → return&lt;br /&gt;
* 地图增量更新 / Map incremental update — 推荐人工审批模式 / manual-approval mode recommended&lt;br /&gt;
* 锁恢复 / Lock recovery — Trace 面板定位卡死 + 手动释放（管理员）/ Trace panel + manual release&lt;br /&gt;
&lt;br /&gt;
详见 HOW_TO_USE.md §6 与 [[Special:MyLanguage/使用手册 - 车队管控|使用手册 - 车队管控]]。&lt;br /&gt;
&lt;br /&gt;
== 8. 灾难恢复 / Disaster recovery ==&lt;br /&gt;
* SimpleComposer 场景 JSON 文件 / scene JSON — 每晚备份&lt;br /&gt;
* SLAM 地图（`*.2dlm` / `*.gtex` / `*.memslam` / `tagmap.json`）— 每次编辑后备份&lt;br /&gt;
* 单车 IPC 替换流程 / Per-vehicle IPC restore — 详见 HOW_TO_USE.md §8.4&lt;br /&gt;
* 重新激活 license — 通过 `auth.lessokaji.com`&lt;br /&gt;
&lt;br /&gt;
== 9. 关联文档 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/插件开发指南|插件开发指南]] — 插件作者视角&lt;br /&gt;
* [[Special:MyLanguage/核心开发指南|核心开发指南]] — 平台开发者视角&lt;br /&gt;
* [[Special:MyLanguage/MDCS-Walkthrough|MDCS-Walkthrough]] — 整体概述&lt;br /&gt;
* [[Special:MyLanguage/车体抽象原理|车体抽象原理]] — 三层模型&lt;br /&gt;
&lt;br /&gt;
[[Category:使用手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E6%8F%92%E4%BB%B6%E6%B5%8B%E8%AF%95%E4%B8%8E%E5%8F%91%E5%B8%83&amp;diff=1049</id>
		<title>插件测试与发布</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E6%8F%92%E4%BB%B6%E6%B5%8B%E8%AF%95%E4%B8%8E%E5%8F%91%E5%B8%83&amp;diff=1049"/>
		<updated>2026-05-16T14:00:50Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
本页讲 MDCS 插件的 ''测试 + 打包 + 发布''流程 —— 从 bench-test 单个插件，到把插件做成可分发的版本。&lt;br /&gt;
&lt;br /&gt;
This page covers MDCS plugin ''testing + packaging + release'' — from bench-testing a single plugin to packaging a deliverable version.&lt;br /&gt;
&lt;br /&gt;
== 1. Bench-test Medulla 插件 ==&lt;br /&gt;
=== 控制台测试 / Console testing ===&lt;br /&gt;
启动 Medulla 控制台后，可以直接 [[Special:MyLanguage/startup.iocmd脚本语法|startup.iocmd]] 命令：&lt;br /&gt;
After starting the Medulla console, use script commands:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
io load plugins/yourname.dll&lt;br /&gt;
your = io init &amp;lt;TypeName&amp;gt; &amp;lt;args&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# 读字段 / read field&lt;br /&gt;
your.&amp;lt;field&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# 写字段 / write field&lt;br /&gt;
your.&amp;lt;field&amp;gt; = 100&lt;br /&gt;
&lt;br /&gt;
# 调方法 / invoke a [IOObjectUtility]&lt;br /&gt;
your &amp;lt;UtilityMethod&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 雷达插件可视化 / Lidar visualisation ===&lt;br /&gt;
Medulla 的 `LidarTypes/MainIOObject` 暴露一个 `[IOObjectUtility]` 用于显示 2D 点云。加载完雷达后点这个按钮 ：&lt;br /&gt;
* 确认 `frame` 单调递增。&lt;br /&gt;
* 点云形状符合预期（前方、左右、距离合理）。&lt;br /&gt;
* 反光强度归一化后（`ReflexRange`）反光板明显偏红。&lt;br /&gt;
&lt;br /&gt;
Lidar plugin: confirm `frame` increments monotonically, geometry matches expectation, reflectors visibly red after `ReflexRange` normalisation.&lt;br /&gt;
&lt;br /&gt;
=== Cart 插件验证 / Cart plugin validation ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 检查 / Check !! 方法 / Method&lt;br /&gt;
|-&lt;br /&gt;
| `Init()` 不抛 || 看 `Hedingben` 是否有红色 toast&lt;br /&gt;
|-&lt;br /&gt;
| LadderLogic 启动 || `[IOObjectMonitor]` 看一个 ''iteration 计数器''&lt;br /&gt;
|-&lt;br /&gt;
| `[AsUpperIO]` 设置生效 || `your.vCmd = 100`，观察硬件电机&lt;br /&gt;
|-&lt;br /&gt;
| `[AsLowerIO]` 读取正常 || `your.batteryPercent` 应给真实值&lt;br /&gt;
|-&lt;br /&gt;
| `[UseLadderLogic(resume=true)]` 验证 || 故意制造异常，看是否自动重启循环 + Hedingben toast&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 2. Bench-test Movement ==&lt;br /&gt;
`[MovementTest]` 属性在 Clumsy 测试 UI 自动注册一个按钮。要求 ：&lt;br /&gt;
* 车在静止 / 仿真环境中&lt;br /&gt;
* 没有 ''任何''其它 Movement 在跑&lt;br /&gt;
&lt;br /&gt;
`[MovementTest]` registers a button automatically. Requires the vehicle to be still / simulated, with no other Movement running.&lt;br /&gt;
&lt;br /&gt;
按下按钮 → Movement 开始；UI 实时显示驱动指令；异常时显示 stack trace。&lt;br /&gt;
Pressing the button starts the Movement; UI shows live drive commands; exception → stack trace surfaced.&lt;br /&gt;
&lt;br /&gt;
== 3. Bench-test SimpleComposer 插件 ==&lt;br /&gt;
=== 仿真车 / Virtual vehicle ===&lt;br /&gt;
SimpleComposer 内置 ''仿真车''类型（不需要真硬件） ：&lt;br /&gt;
* 添加 `DummyCar`，配置你的 `[CarType]` 关联。&lt;br /&gt;
* 发派任务从 UI 派；运动学按理想模型计算。&lt;br /&gt;
&lt;br /&gt;
A built-in virtual car type lets you test without real hardware. Configure a `DummyCar` bound to your `[CarType]`; missions dispatched from the UI; kinematics calculated ideally.&lt;br /&gt;
&lt;br /&gt;
=== Trace 面板 / Trace panel ===&lt;br /&gt;
观察 ：&lt;br /&gt;
* 每辆车的 `Queue` 行执行时间线&lt;br /&gt;
* 锁的获取 / 释放序列&lt;br /&gt;
* 任务 status 状态转换（`Programming → Forecasted → Queued → Running → Finished`）&lt;br /&gt;
* 异常路径&lt;br /&gt;
&lt;br /&gt;
Inspect: per-car Queue row timeline, lock acquire/release sequence, status transitions, exception paths.&lt;br /&gt;
&lt;br /&gt;
=== HTTP API 测试 / API testing ===&lt;br /&gt;
SimpleComposer 的 Nancy API 可以用 curl / Postman 测试 ：&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://localhost:9050/api/dispatch \&lt;br /&gt;
  -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
  -d '{&amp;quot;car&amp;quot;: &amp;quot;TestCar&amp;quot;, &amp;quot;target&amp;quot;: &amp;quot;siteA&amp;quot;, &amp;quot;mission_type&amp;quot;: &amp;quot;Go&amp;quot;}'&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 4. 录制与回放 / Recording &amp;amp; replay ==&lt;br /&gt;
DObject 自动 ''录制''（每槽可独立开关 `dobj.Recording = true`）写 `.dorec` 文件到 `medulla/recordings/`。&lt;br /&gt;
&lt;br /&gt;
DObject auto-recording (per-slot `Recording = true` flag) writes `.dorec` files into `medulla/recordings/`.&lt;br /&gt;
&lt;br /&gt;
回放工具用于：&lt;br /&gt;
* 重现现场 bug（同一序列输入 → 相同输出）&lt;br /&gt;
* 离线对比 SLAM 算法（同一传感器流跑多个版本）&lt;br /&gt;
* 培训（不用真车）&lt;br /&gt;
&lt;br /&gt;
Replay flows are documented at [[Special:MyLanguage/使用手册 - 数据录制与回放手册|使用手册 - 数据录制与回放手册]]。&lt;br /&gt;
&lt;br /&gt;
== 5. 版本与助记词 / Version + mnemonic ==&lt;br /&gt;
按项目惯例 ：&lt;br /&gt;
Project convention:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;xml&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;AssemblyInformationalVersion&amp;gt;1.4.0+kindling&amp;lt;/AssemblyInformationalVersion&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Major.Minor.Patch + ''单词''助记词。每次改动换助记词。&lt;br /&gt;
Bump a fresh single-word mnemonic on each modification.&lt;br /&gt;
&lt;br /&gt;
或用 `[ReplaceWithCompileInfo]` —— LessokajiWeaver 注入时间戳 + git hash 到 static string 字段（[[Special:MyLanguage/LessokajiWeaver编译后处理工具|详情]]）。&lt;br /&gt;
Alternative: `[ReplaceWithCompileInfo]` injects compile timestamp + git hash automatically.&lt;br /&gt;
&lt;br /&gt;
== 6. 打包 / Packaging ==&lt;br /&gt;
=== Release 构建 / Release build ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
dotnet publish -c Release -f net6.0&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
输出在 `bin/Release/net6.0/publish/`。Costura.Fody 已经把依赖打到单 DLL，所以只需要 ``&amp;lt;plugin&amp;gt;.dll`。&lt;br /&gt;
Output at `bin/Release/net6.0/publish/`. Costura.Fody has embedded deps; only one DLL needed.&lt;br /&gt;
&lt;br /&gt;
=== 测试加载 / Test load ===&lt;br /&gt;
'''关键''' ：在一个 ''干净''的 Medulla 控制台（没有其它插件）加载新 DLL：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
io load plugins/yourname.dll&lt;br /&gt;
your = io init ...&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如果你 ''忘了 Costura''，其它插件可能 ''碰巧''加载了你需要的 DLL，但生产环境干净启动时会出错。这个测试能立刻发现。&lt;br /&gt;
&lt;br /&gt;
If you forgot Costura, other plugins may incidentally provide the dep, but a clean production install will fail. This test surfaces it instantly.&lt;br /&gt;
&lt;br /&gt;
=== Release artefact / Release zip ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
your-plugin-1.4.0+kindling.zip&lt;br /&gt;
├── your-plugin.dll&lt;br /&gt;
├── README.md            # 简短说明&lt;br /&gt;
├── CHANGELOG.md         # 本版本变更&lt;br /&gt;
├── INSTALL.txt          # 部署 startup.iocmd 片段&lt;br /&gt;
└── (optional) docs/     # 详细使用文档&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 7. 发布 / Release ==&lt;br /&gt;
* '''内部分发''' / Internal: 上传到 `https://dl.lessokaji.com`（按客户分目录）。&lt;br /&gt;
* '''授权门''' / Auth gate: 通过 `auth.lessokaji.com` 控制谁能下载。&lt;br /&gt;
* '''Wiki 发布说明''' / Wiki release notes: 更新对应 wiki 页（如 `Detour版本发布记录`）。&lt;br /&gt;
* '''DEV_LOG''': 在仓库 `DEV_LOG.md` 加一条记录，含 ''助记词''与变更摘要。&lt;br /&gt;
&lt;br /&gt;
== 8. 兼容性矩阵 / Compatibility matrix ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 你的插件目标 / Your plugin's target !! 兼容 / Compatible with&lt;br /&gt;
|-&lt;br /&gt;
| RefMedullaCore 1.x || Medulla 1.x runtime&lt;br /&gt;
|-&lt;br /&gt;
| RefMedullaCore 2.x || Medulla 2.x runtime（即当前 M2，默认）&lt;br /&gt;
|-&lt;br /&gt;
| RefClumsyCore 1.x || Clumsy 1.x&lt;br /&gt;
|-&lt;br /&gt;
| RefClumsyCore 2.x || Clumsy 2.x（当前）&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
在 csproj 里 pin 一个主版本，在 CHANGELOG 里写清次版本要求。&lt;br /&gt;
Pin a major version in csproj; document minor-version requirements in CHANGELOG.&lt;br /&gt;
&lt;br /&gt;
== 9. 持续集成 / CI ==&lt;br /&gt;
推荐流水线 ：&lt;br /&gt;
Recommended pipeline:&lt;br /&gt;
&lt;br /&gt;
# 拉源 → 单元测试（如果有）→ 构建 Debug 与 Release。&lt;br /&gt;
# Lint csproj：检查 ''都引用了 `Ref&amp;lt;Name&amp;gt;.dll`'' 而不是 impl DLL，避免误打包。&lt;br /&gt;
# 自动版本号注入（git tag → AssemblyInformationalVersion）。&lt;br /&gt;
# Release zip 自动生成 + 上传到内部仓库。&lt;br /&gt;
&lt;br /&gt;
== 10. 常见错误清单 / Pre-release checklist ==&lt;br /&gt;
* [ ] 测试加载 (干净环境)&lt;br /&gt;
* [ ] `[AsUpperIO]` / `[AsLowerIO]` 字段不是 readonly&lt;br /&gt;
* [ ] `Init()` 不抛异常&lt;br /&gt;
* [ ] LadderLogic `resume = true` 时也手动捕异常 + 写 Lower IO `eStop = true`&lt;br /&gt;
* [ ] Costura.Fody 在 FodyWeavers.xml&lt;br /&gt;
* [ ] AssemblyInformationalVersion 含助记词&lt;br /&gt;
* [ ] CHANGELOG.md 写了本版变更&lt;br /&gt;
* [ ] startup.iocmd 示例片段在 INSTALL.txt&lt;br /&gt;
* [ ] 在仿真车里跑过完整任务&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/插件契约与打包约定|插件契约与打包约定]]&lt;br /&gt;
* [[Special:MyLanguage/插件开发清单|插件开发清单]]&lt;br /&gt;
* [[Special:MyLanguage/startup.iocmd脚本语法|startup.iocmd脚本语法]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 数据录制与回放手册|数据录制与回放手册]]&lt;br /&gt;
* [[Special:MyLanguage/LessokajiWeaver编译后处理工具|LessokajiWeaver编译后处理工具]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91%E6%B8%85%E5%8D%95&amp;diff=1048</id>
		<title>插件开发清单</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91%E6%B8%85%E5%8D%95&amp;diff=1048"/>
		<updated>2026-05-16T14:00:50Z</updated>

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

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
本页是 MDCS '''插件开发'''的总入口。MDCS 把所有可扩展能力归为 5 个 ''插件家族''，每个家族对应一类 ''扩展点 + 基类 + 打包约定''。完整离线副本见 `D:\src\cookbook\HOW_TO_CUSTOMIZE.md`。&lt;br /&gt;
&lt;br /&gt;
This is the hub for MDCS '''plugin development'''. MDCS organises every extension point into 5 ''plugin families'', each with its own base class + packaging convention. Offline mirror: `D:\src\cookbook\HOW_TO_CUSTOMIZE.md`.&lt;br /&gt;
&lt;br /&gt;
== 1. 5 类插件 / The five families ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 家族 / Family !! 宿主进程 / Host process !! 基类 / Base !! 入门页 / Tutorial&lt;br /&gt;
|-&lt;br /&gt;
| 雷达插件 / Lidar plugin || Medulla || `Lidar2DIOObject` / `Lidar3DIOObject` || [[Special:MyLanguage/如何适配新的雷达|如何适配新的雷达]] / [[Special:MyLanguage/2D激光雷达适配|2D激光雷达适配]]&lt;br /&gt;
|-&lt;br /&gt;
| 相机插件 / Camera plugin || Medulla || `IOObject`（约定 `MainIOObject` 类名）|| [[Special:MyLanguage/3D相机适配|3D相机适配]]&lt;br /&gt;
|-&lt;br /&gt;
| 底盘适配 / Cart hardware || Medulla || `CartDefinition` || [[Special:MyLanguage/MDCS引擎适配机器人入门教学|MDCS引擎适配机器人入门教学]]&lt;br /&gt;
|-&lt;br /&gt;
| Movement / 运动适配 || Clumsy || `MovementDefinition` || [[Special:MyLanguage/巡线行走|巡线行走]] / [[Special:MyLanguage/MovementDefinition详解|MovementDefinition详解]]&lt;br /&gt;
|-&lt;br /&gt;
| 车队插件 / Fleet plugin || SimpleComposer || `ClumsyCar` / `Car` / `BusinessLogic` + `[CarType]` || [[Special:MyLanguage/如何基于SimpleCore核心库进行调度系统开发|如何基于SimpleCore核心库进行调度系统开发]]&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
底层共通约定见以下子页：&lt;br /&gt;
Shared conventions live on these sub-pages:&lt;br /&gt;
&lt;br /&gt;
* [[Special:MyLanguage/插件契约与打包约定|插件契约与打包约定]] — `MainIOObject` 命名 + Costura + LessokajiWeaver + 部署路径&lt;br /&gt;
* [[Special:MyLanguage/IOObject属性参考|IOObject属性参考]] — `[IOObjectMonitor] / [IOObjectUtility] / [IOObjectWebUtility] / [IOObjectWatch]`&lt;br /&gt;
* [[Special:MyLanguage/CartDefinition属性参考|CartDefinition属性参考]] — `[AsInitParam] / [AsUpperIO] / [AsLowerIO] / [UseLadderLogic]`&lt;br /&gt;
* [[Special:MyLanguage/LadderLogic框架|LadderLogic框架]] — 周期循环宿主&lt;br /&gt;
* [[Special:MyLanguage/startup.iocmd脚本语法|startup.iocmd脚本语法]] — 启动脚本&lt;br /&gt;
* [[Special:MyLanguage/DObject共享内存协议|DObject共享内存协议]] — IPC&lt;br /&gt;
* [[Special:MyLanguage/MovementDefinition详解|MovementDefinition详解]] — Movement 契约&lt;br /&gt;
* [[Special:MyLanguage/SimpleAgvInterface Queue机制|SimpleAgvInterface Queue机制]] — Queue 行为&lt;br /&gt;
* [[Special:MyLanguage/TightCoupler外部反馈API|TightCoupler外部反馈API]] — 外部位姿源接入&lt;br /&gt;
* [[Special:MyLanguage/插件测试与发布|插件测试与发布]] — 测试 + 打包 + 发布&lt;br /&gt;
* [[Special:MyLanguage/插件开发清单|插件开发清单]] — Do's &amp;amp; Don'ts&lt;br /&gt;
&lt;br /&gt;
== 2. 工程脚手架 / Scaffolding ==&lt;br /&gt;
脚手架工具：`D:\src\cookbook\MDCS-plugin-helper\generate.py`（交互式 Python 脚本）。&lt;br /&gt;
Scaffolder: `D:\src\cookbook\MDCS-plugin-helper\generate.py` (interactive).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cd D:\src\cookbook\MDCS-plugin-helper&lt;br /&gt;
python generate.py&lt;br /&gt;
# Choose 1 — Create a project&lt;br /&gt;
# Pick the plugin family&lt;br /&gt;
# Enter project name&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
它会下载 ''参考 DLL''（`Ref&amp;lt;Name&amp;gt;.dll` 系列）到 `ref/`，并下载对应 boilerplate `.cs` 文件。&lt;br /&gt;
It downloads the reference DLLs (`Ref&amp;lt;Name&amp;gt;.dll`) into `ref/` and a boilerplate `.cs` file.&lt;br /&gt;
&lt;br /&gt;
== 3. 车型适配案例 / Vehicle case studies ==&lt;br /&gt;
按车型分类的端到端案例：&lt;br /&gt;
End-to-end case studies by vehicle type:&lt;br /&gt;
&lt;br /&gt;
* [[Special:MyLanguage/叉车适配案例|叉车适配案例]] — 凌鸟 LB14&lt;br /&gt;
* [[Special:MyLanguage/潜伏顶升车(KIVA类小车)适配案例|潜伏顶升车(KIVA类小车)适配案例]] — 浙江迈睿 KIVA&lt;br /&gt;
* [[Special:MyLanguage/全向车适配案例|全向车适配案例]] — 麦克纳姆 + 四舵&lt;br /&gt;
* [[Special:MyLanguage/牵引车适配案例|牵引车适配案例]] — 单 / 双阿克曼&lt;br /&gt;
&lt;br /&gt;
== 4. 特殊场景 / Specialty applications ==&lt;br /&gt;
* [[Special:MyLanguage/自动识别工位并取放货|自动识别工位并取放货]]&lt;br /&gt;
* [[Special:MyLanguage/双车/多车联动|双车 / 多车联动]]&lt;br /&gt;
* [[Special:MyLanguage/联动天眼系统进行装卸车|联动天眼系统进行装卸车]]&lt;br /&gt;
* [[Special:MyLanguage/设备跟随联动|设备跟随联动]]&lt;br /&gt;
* [[Special:MyLanguage/识别料框并堆垛拆垛|识别料框并堆垛拆垛]]&lt;br /&gt;
* [[Special:MyLanguage/汽车面差检测|汽车面差检测]]&lt;br /&gt;
* [[Special:MyLanguage/清洁机器人|清洁机器人]]&lt;br /&gt;
* [[Special:MyLanguage/复合卷料机械手叉车|复合卷料机械手叉车]]&lt;br /&gt;
&lt;br /&gt;
== 5. 关联文档 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/操作指南|操作指南]] — 运维者视角&lt;br /&gt;
* [[Special:MyLanguage/核心开发指南|核心开发指南]] — 平台开发者视角&lt;br /&gt;
* [[Special:MyLanguage/车体抽象原理|车体抽象原理]] — 三层模型&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E6%8F%92%E4%BB%B6%E5%A5%91%E7%BA%A6%E4%B8%8E%E6%89%93%E5%8C%85%E7%BA%A6%E5%AE%9A&amp;diff=1046</id>
		<title>插件契约与打包约定</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E6%8F%92%E4%BB%B6%E5%A5%91%E7%BA%A6%E4%B8%8E%E6%89%93%E5%8C%85%E7%BA%A6%E5%AE%9A&amp;diff=1046"/>
		<updated>2026-05-16T14:00:47Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
本页讲所有 MDCS 插件 ''共通'' 的约定 —— 不论是雷达 / 相机 / 底盘 / Movement / 还是 SimpleComposer 插件，这些规则都适用。具体的 ''基类签名''见各家族页面。&lt;br /&gt;
&lt;br /&gt;
This page covers the conventions ''shared by every MDCS plugin'' — lidar, camera, cart, Movement, fleet — regardless of family. The family-specific ''base-class contracts'' live on their own pages.&lt;br /&gt;
&lt;br /&gt;
== 1. 命名约定 / Naming convention ==&lt;br /&gt;
=== MainIOObject 命名 ===&lt;br /&gt;
'''Medulla 端插件''' (lidar / camera / cart) 的入口类 ''必须叫 `MainIOObject`'' —— 严格区分大小写。&lt;br /&gt;
The Medulla-side plugin entry class ''must be named exactly `MainIOObject`'' — case-sensitive.&lt;br /&gt;
&lt;br /&gt;
Medulla 加载器 (`D:\src\M2\MedullaCore\MedullaIO.cs:299`) 反射查找：&lt;br /&gt;
The loader does reflection lookup:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
var type = asm.GetTypes().FirstOrDefault(t =&amp;gt; t.Name == &amp;quot;MainIOObject&amp;quot;);&lt;br /&gt;
return Activator.CreateInstance(type);  // parameterless ctor required&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
没有 `[Plugin]` 属性，没有 manifest。仅靠类名 + 无参构造函数。&lt;br /&gt;
No `[Plugin]` attribute, no manifest — just the class name and a parameterless ctor.&lt;br /&gt;
&lt;br /&gt;
【注意】 改类名或删默认构造函数 → 加载静默失败 → 几分钟后由 `Hedingben` toast 间接报错。先用 `io load &amp;lt;yourplugin&amp;gt;.dll` 在 Medulla console 单独测试加载。&amp;lt;br&amp;gt;&lt;br /&gt;
Renaming or removing the parameterless ctor → silent load failure surfacing later in `Hedingben`. Test `io load` in isolation first.&lt;br /&gt;
&lt;br /&gt;
=== Clumsy / Simple 插件类名 ===&lt;br /&gt;
Clumsy `MovementDefinition` 子类 + Simple `Car` / `ClumsyCar` / `BusinessLogic` 子类不要求特定类名，但用 ''属性'' 标识：&lt;br /&gt;
Clumsy / Simple plugins don't need a specific name; they use attributes to identify themselves:&lt;br /&gt;
&lt;br /&gt;
* `[CarType]` 标 Simple 端 Car 子类&lt;br /&gt;
* `[MovementTest]` 标 Clumsy Movement 测试入口&lt;br /&gt;
* `[CoderMethod]` 标 SimpleCore TopazScript 编码器方法&lt;br /&gt;
&lt;br /&gt;
=== 文件夹与 DLL 命名 / Folder &amp;amp; DLL naming ===&lt;br /&gt;
* 一方插件 / First-party: `D:\src\M2\OfficialPlugins\&amp;lt;VendorModel&amp;gt;\`&lt;br /&gt;
* 三方插件 / Third-party: 任意；通常 `&amp;lt;vendor-name&amp;gt;\&amp;lt;model&amp;gt;\`&lt;br /&gt;
* DLL 名 = `&amp;lt;VendorModel&amp;gt;.dll`，部署到 `medulla/plugins/&amp;lt;vendor&amp;gt;/` 或 `simplecomposer/plugins/`&lt;br /&gt;
&lt;br /&gt;
== 2. 项目文件 / .csproj ==&lt;br /&gt;
=== 目标框架 / Target framework ===&lt;br /&gt;
* '''.NET 6.0''' or '''.NET 8.0''' — 推荐 / preferred&lt;br /&gt;
* '''.NET Framework 4.8''' — 部分旧雷达驱动用 / some legacy lidar drivers&lt;br /&gt;
&lt;br /&gt;
=== 引用 / References ===&lt;br /&gt;
所有插件 csproj 都引用 ''参考程序集''（`Ref&amp;lt;Name&amp;gt;.dll`），不引用 impl DLL：&lt;br /&gt;
All plugin csproj reference '''reference-only assemblies''' (`Ref&amp;lt;Name&amp;gt;.dll`), not the implementation DLLs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;xml&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;ItemGroup&amp;gt;&lt;br /&gt;
  &amp;lt;PackageReference Include=&amp;quot;Fody&amp;quot; Version=&amp;quot;6.x&amp;quot; PrivateAssets=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
  &amp;lt;PackageReference Include=&amp;quot;Costura.Fody&amp;quot; Version=&amp;quot;5.x&amp;quot; /&amp;gt;&lt;br /&gt;
  &amp;lt;Reference Include=&amp;quot;RefFundamentalLib&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;HintPath&amp;gt;..\tools\RefFundamentalLib.dll&amp;lt;/HintPath&amp;gt;&lt;br /&gt;
  &amp;lt;/Reference&amp;gt;&lt;br /&gt;
  &amp;lt;Reference Include=&amp;quot;RefMedullaCore&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;HintPath&amp;gt;..\tools\RefMedullaCore.dll&amp;lt;/HintPath&amp;gt;&lt;br /&gt;
  &amp;lt;/Reference&amp;gt;&lt;br /&gt;
  &amp;lt;Reference Include=&amp;quot;RefLidarController&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;HintPath&amp;gt;..\tools\RefLidarController.dll&amp;lt;/HintPath&amp;gt;&lt;br /&gt;
  &amp;lt;/Reference&amp;gt;&lt;br /&gt;
&amp;lt;/ItemGroup&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
`..\tools\` 模式 ''全仓库一致'' —— 不要改成 NuGet。&lt;br /&gt;
The `..\tools\` HintPath pattern is repo-wide; don't try to NuGet-ify it.&lt;br /&gt;
&lt;br /&gt;
=== Costura.Fody 胖 DLL ===&lt;br /&gt;
插件输出是单 `.dll`，所有依赖（包括 LessokajiWeaver 工具库）嵌入为 base64 资源。客户只需要 1 个文件部署。&lt;br /&gt;
Plugin output is a single `.dll` with all deps embedded as base64 resources. Customer deployment = 1 file.&lt;br /&gt;
&lt;br /&gt;
`FodyWeavers.xml` 在工程根：&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;xml&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;Weavers&amp;gt;&lt;br /&gt;
  &amp;lt;Costura/&amp;gt;&lt;br /&gt;
  &amp;lt;LessokajiWeaver/&amp;gt;&lt;br /&gt;
&amp;lt;/Weavers&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
【注意】 ''Costura 缓存''可能让你看不到 FundamentalLib 升级 —— 重新构建插件后再部署，否则胖 DLL 里还是旧版 FundamentalLib。&amp;lt;br&amp;gt;&lt;br /&gt;
Costura caches dependencies in the fat DLL. After upgrading FundamentalLib, '''rebuild plugins''' before deploying.&lt;br /&gt;
&lt;br /&gt;
== 3. 版本与助记词 / Version + mnemonic ==&lt;br /&gt;
每次修改插件，AssemblyInformationalVersion 加 ''单词助记词''：&lt;br /&gt;
Every plugin modification adds a single mnemonic word to AssemblyInformationalVersion:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;xml&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;AssemblyInformationalVersion&amp;gt;1.4.0+kindling&amp;lt;/AssemblyInformationalVersion&amp;gt;&lt;br /&gt;
&amp;lt;!-- 下次改动 / next change --&amp;gt;&lt;br /&gt;
&amp;lt;AssemblyInformationalVersion&amp;gt;1.4.1+lantern&amp;lt;/AssemblyInformationalVersion&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
目的：现场调试时 ''肉眼区分''两个数字看起来一样的二进制。&lt;br /&gt;
Purpose: visually disambiguate two binaries with the same number in the field.&lt;br /&gt;
&lt;br /&gt;
或者用 `[ReplaceWithCompileInfo]` —— LessokajiWeaver 在编译期注入时间戳 + git 哈希到 static string 字段。&lt;br /&gt;
Alternative: `[ReplaceWithCompileInfo]` — LessokajiWeaver injects timestamp + git hash at compile time.&lt;br /&gt;
&lt;br /&gt;
== 4. 部署 / Deployment ==&lt;br /&gt;
=== Medulla 端 / Medulla-side ===&lt;br /&gt;
DLL 放到 `medulla/plugins/&amp;lt;vendor&amp;gt;/&amp;lt;plugin&amp;gt;.dll`，`startup.iocmd` 加：&lt;br /&gt;
Drop the DLL into `medulla/plugins/&amp;lt;vendor&amp;gt;/`, add to `startup.iocmd`:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
foo = io load plugins/&amp;lt;vendor&amp;gt;/&amp;lt;plugin&amp;gt;.dll&lt;br /&gt;
foo Init                    # if your plugin has an Init method&lt;br /&gt;
foo Start &amp;lt;args&amp;gt;            # invoke whatever it needs&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
startup.iocmd 语法见 [[Special:MyLanguage/startup.iocmd脚本语法|startup.iocmd脚本语法]]。&lt;br /&gt;
&lt;br /&gt;
=== SimpleComposer 端 / SimpleComposer-side ===&lt;br /&gt;
DLL 放到 `simplecomposer/plugins/&amp;lt;plugin&amp;gt;.dll`。SimpleComposer 启动时（`D:\src\Simple\SimpleComposer\Program.cs:107-162`）自动扫所有 DLL，按 `[CarType]` / `HeuristicsContainer` / `CustomOperationsBeforeLoading.Set` / `BusinessLogic` 类型分类注册。&lt;br /&gt;
&lt;br /&gt;
Drop into `simplecomposer/plugins/`. SimpleComposer auto-scans on boot.&lt;br /&gt;
&lt;br /&gt;
== 5. 加载生命周期 / Load lifecycle ==&lt;br /&gt;
&lt;br /&gt;
  Medulla:&lt;br /&gt;
    io load X.dll → reflection scan → instantiate MainIOObject&lt;br /&gt;
       ↓ (your ctor runs; no hardware contact)&lt;br /&gt;
    cart Init → connect hardware → spawn LadderLogics&lt;br /&gt;
       ↓&lt;br /&gt;
    Operation(0), Operation(1), ...&lt;br /&gt;
&lt;br /&gt;
  SimpleComposer:&lt;br /&gt;
    Program.cs startup&lt;br /&gt;
       → load every plugins/*.dll&lt;br /&gt;
       → scan for [CarType] / HeuristicsContainer / BusinessLogic / CustomOps&lt;br /&gt;
       → register&lt;br /&gt;
       → MyAsmHelper.Init() invoked&lt;br /&gt;
&lt;br /&gt;
【注意】 Medulla 加载器 ''不调'' `Init()`。你需要 startup.iocmd 显式调用 `cart Init`。&amp;lt;br&amp;gt;&lt;br /&gt;
The Medulla loader does NOT call `Init()`. You must invoke `cart Init` from startup.iocmd.&lt;br /&gt;
&lt;br /&gt;
== 6. 一切重启 vs 热替换 / Restart vs hot swap ==&lt;br /&gt;
* '''Medulla 插件''': '''必须'''停 Medulla → 替换 DLL → 启动。热替换会导致 Costura 缓存冲突 / undefined behaviour。&lt;br /&gt;
* '''SimpleComposer 插件''': 同上 —— 关 SimpleComposer → 替换 → 启动。&lt;br /&gt;
* '''Clumsy MovementDefinition''': 同 Clumsy 进程重启。&lt;br /&gt;
&lt;br /&gt;
Hot swap is not supported. Always stop-replace-start.&lt;br /&gt;
&lt;br /&gt;
== 7. 跨平台 / Cross-platform ==&lt;br /&gt;
* Medulla / Detour / Clumsy / SimpleComposer 都可在 Linux 运行（.NET 6/8）。&lt;br /&gt;
* DObject 在 Linux 用文件后备（见 [[Special:MyLanguage/DObject共享内存协议|DObject 共享内存协议]]）。&lt;br /&gt;
* libVRender (CycleGUI 渲染) 已有 Linux 构建，但 Windows 是主战场。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/startup.iocmd脚本语法|startup.iocmd脚本语法]]&lt;br /&gt;
* [[Special:MyLanguage/IOObject属性参考|IOObject属性参考]]&lt;br /&gt;
* [[Special:MyLanguage/CartDefinition属性参考|CartDefinition属性参考]]&lt;br /&gt;
* [[Special:MyLanguage/LadderLogic框架|LadderLogic框架]]&lt;br /&gt;
* [[Special:MyLanguage/插件测试与发布|插件测试与发布]]&lt;br /&gt;
* [[Special:MyLanguage/Medulla软件架构|Medulla软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/MDCS-plugin-helper|MDCS-plugin-helper]] — 脚手架工具&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=TightCoupler%E5%A4%96%E9%83%A8%E5%8F%8D%E9%A6%88API&amp;diff=1045</id>
		<title>TightCoupler外部反馈API</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=TightCoupler%E5%A4%96%E9%83%A8%E5%8F%8D%E9%A6%88API&amp;diff=1045"/>
		<updated>2026-05-16T14:00:26Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
`TightCoupler` 是 Detour 的 ''多源位姿融合器''。非 SLAM 来源的位姿（轮编里程计 / IMU / RTK / UWB / 第三方视觉等）通过 `PostExternalFeed` API 推入 TightCoupler，与激光 / 地纹 / 二维码等 SLAM 后端一起做紧耦合优化，输出统一的 6-DoF 位姿。&lt;br /&gt;
&lt;br /&gt;
`TightCoupler` is Detour's '''multi-source pose fuser'''. Non-SLAM poses (wheel odometry / IMU / RTK / UWB / third-party vision) enter through the `PostExternalFeed` API. They join the SLAM backends (laser / ground-texture / QR) in a tightly-coupled optimisation that emits the unified 6-DoF pose.&lt;br /&gt;
&lt;br /&gt;
实现：`D:\src\Detour\DetourCore\Algorithms\TightCoupler.ExternalCoupler.cs:14`。&lt;br /&gt;
Implementation: `D:\src\Detour\DetourCore\Algorithms\TightCoupler.ExternalCoupler.cs:14`.&lt;br /&gt;
&lt;br /&gt;
整体设计见 [[Special:MyLanguage/多定位源的自动综合|多定位源的自动综合]]。&lt;br /&gt;
For the design rationale see [[Special:MyLanguage/多定位源的自动综合|multi-source positioning fusion]].&lt;br /&gt;
&lt;br /&gt;
== API 签名 / API signature ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public static void PostExternalFeed(ExternalFeed obj, string name);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次调用提交 ''一帧''的外部位姿观测。`name` 是 ''来源标识''（如 `&amp;quot;WheelImu&amp;quot;` / `&amp;quot;RTK&amp;quot;`），用来在融合器内部跟踪不同来源的协方差、检查超时、重映射。&lt;br /&gt;
&lt;br /&gt;
Each call submits ''one frame'' of an external pose observation. `name` identifies the source (e.g. `&amp;quot;WheelImu&amp;quot;`, `&amp;quot;RTK&amp;quot;`) so the fuser can track per-source covariance, detect silence, and remap as needed.&lt;br /&gt;
&lt;br /&gt;
== `ExternalFeed` 结构体 / Struct ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public struct ExternalFeed&lt;br /&gt;
{&lt;br /&gt;
    public string name;&lt;br /&gt;
    public long   counter;&lt;br /&gt;
    public bool   hasTranslation;&lt;br /&gt;
    public bool   hasRotation;&lt;br /&gt;
    public bool   is3D;&lt;br /&gt;
    public bool   is2D;&lt;br /&gt;
    public bool   integrated;&lt;br /&gt;
    public bool   toRemap;&lt;br /&gt;
    public long   startingTick;&lt;br /&gt;
    public float  x, y, z, th, pitch, roll;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
字段语义 / Field semantics:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 字段 / Field !! 含义 / Meaning&lt;br /&gt;
|-&lt;br /&gt;
| `name` || 来源名（同上）；融合器按此聚类&lt;br /&gt;
|-&lt;br /&gt;
| `counter` || 单调递增；用来检测丢帧 / out-of-order&lt;br /&gt;
|-&lt;br /&gt;
| `hasTranslation` || 该观测包含位置（x, y, [z]）/ includes translation&lt;br /&gt;
|-&lt;br /&gt;
| `hasRotation` || 该观测包含朝向（th, pitch, roll）/ includes rotation&lt;br /&gt;
|-&lt;br /&gt;
| `is2D` / `is3D` || 二选一；表示是平面运动还是 3D 6-DoF&lt;br /&gt;
|-&lt;br /&gt;
| `integrated` || true = 累积量（相对位姿）；false = 单帧绝对量 / cumulative vs single-frame&lt;br /&gt;
|-&lt;br /&gt;
| `toRemap` || true = 绝对参考系（如 RTK），融合时需要全局重映射 / absolute frame, needs global remap&lt;br /&gt;
|-&lt;br /&gt;
| `startingTick` || 累积量的基准 tick；用来检测重置（如 IMU 重启）&lt;br /&gt;
|-&lt;br /&gt;
| `x, y, z, th, pitch, roll` || 实际位姿（mm / rad）&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 典型来源配置 / Canonical source configurations ==&lt;br /&gt;
`TightCoupler.ExternalCoupler.cs:49-54` 中的注释列出了典型用法：&lt;br /&gt;
The source comment lists canonical configurations:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 来源 / Source !! 标志组合 / Flags&lt;br /&gt;
|-&lt;br /&gt;
| 轮编 + IMU / Wheel + IMU || `hasTranslation | hasRotation | is2D | integrated | startingTick`&lt;br /&gt;
|-&lt;br /&gt;
| 仅 IMU / IMU only || `hasRotation | (is2D 或 is3D) | integrated`&lt;br /&gt;
|-&lt;br /&gt;
| RTK GNSS || `hasTranslation | toRemap`（绝对，需重映射）&lt;br /&gt;
|-&lt;br /&gt;
| UWB / 基站 / Beacon || `hasTranslation | is2D`（绝对二维位置）&lt;br /&gt;
|-&lt;br /&gt;
| 第三方视觉 / 3rd-party vision || 视具体输出而定&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 提交模板 / Submission template ==&lt;br /&gt;
轮编 + IMU 紧耦合 ：&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using DetourCore.Algorithms;&lt;br /&gt;
&lt;br /&gt;
TightCoupler.PostExternalFeed(new ExternalFeed&lt;br /&gt;
{&lt;br /&gt;
    name           = &amp;quot;wheel_imu&amp;quot;,&lt;br /&gt;
    counter        = tick++,&lt;br /&gt;
    hasTranslation = true,&lt;br /&gt;
    hasRotation    = true,&lt;br /&gt;
    is2D           = true,&lt;br /&gt;
    integrated     = true,&lt;br /&gt;
    startingTick   = baselineTick,&lt;br /&gt;
    x  = odomX,&lt;br /&gt;
    y  = odomY,&lt;br /&gt;
    th = odomTh&lt;br /&gt;
}, name: &amp;quot;WheelImu&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
RTK ：&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
TightCoupler.PostExternalFeed(new ExternalFeed&lt;br /&gt;
{&lt;br /&gt;
    name           = &amp;quot;rtk&amp;quot;,&lt;br /&gt;
    counter        = gpsTick++,&lt;br /&gt;
    hasTranslation = true,&lt;br /&gt;
    is2D           = true,&lt;br /&gt;
    integrated     = false,&lt;br /&gt;
    toRemap        = true,&lt;br /&gt;
    x = rtkEastingMm,&lt;br /&gt;
    y = rtkNorthingMm&lt;br /&gt;
}, name: &amp;quot;RTK&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 协方差与权重 / Covariance &amp;amp; weighting ==&lt;br /&gt;
TightCoupler 内部按 ''协方差矩阵的逆''加权 —— 不需要插件作者设权重；插件 ''必须诚实''地报协方差。错报协方差是融合崩坏的 ''首要''原因 ：一个谎报&amp;quot;我超准&amp;quot;的源会 ''统治''融合结果。&lt;br /&gt;
&lt;br /&gt;
TightCoupler weights by the inverse of the covariance matrix — no manual weight tuning needed; sources must ''honestly'' report covariance. Mis-reporting is the #1 cause of fusion blowups.&lt;br /&gt;
&lt;br /&gt;
来源 ：&lt;br /&gt;
* SLAM 后端：Hessian 求逆&lt;br /&gt;
* 二维码 PnP：重投影残差&lt;br /&gt;
* IMU / 里程计：硬件规格 + 时间间隔&lt;br /&gt;
* RTK：GNSS DOP&lt;br /&gt;
&lt;br /&gt;
== 异常路径 / Failure paths ==&lt;br /&gt;
* '''源静默 / Silence''': 超过 ''timeout''（融合器配置） → 该源从下一帧位姿图中删除，其它源不受影响。&lt;br /&gt;
* '''跳变 / Wild jump''': TightCoupler 内置 ''卡方检验''；&amp;gt; 5σ 的观测直接丢弃。&lt;br /&gt;
* '''融合发散 / Divergence''': 残差持续上升 → 触发 ''软重启''（保留主导源，重置其它源的位姿先验）。&lt;br /&gt;
&lt;br /&gt;
== 调试 / Debug ==&lt;br /&gt;
Detour UI 的&amp;quot;融合面板&amp;quot; 显示 ：&lt;br /&gt;
* 每个源的最新观测时刻 + 协方差&lt;br /&gt;
* 当前主导源&lt;br /&gt;
* 残差直方图&lt;br /&gt;
&lt;br /&gt;
打开 ''单源调试模式''可强制只用一个源（用于诊断单源故障）。&lt;br /&gt;
There's a &amp;quot;single-source debug mode&amp;quot; forcing the fusion to use only one source.&lt;br /&gt;
&lt;br /&gt;
== 与雷达 Lidar 插件的区别 / vs lidar plugins ==&lt;br /&gt;
雷达数据 ''不走''此 API：&lt;br /&gt;
* 雷达插件 `output()` 把帧推到 ''DObject''；&lt;br /&gt;
* Detour 端 `Lidar2D.ReadLidar()` 订阅该 DObject，把 SLAM 观测加到内部位姿图。&lt;br /&gt;
&lt;br /&gt;
只有 ''非传感器''的位姿来源走 `PostExternalFeed`。&lt;br /&gt;
&lt;br /&gt;
Lidar data does NOT go through this API. Lidar plugins `output()` to DObject; Detour subscribes via `Lidar2D.ReadLidar()`. Only ''non-sensor'' pose sources go through `PostExternalFeed`.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/多定位源的自动综合|多定位源的自动综合]]&lt;br /&gt;
* [[Special:MyLanguage/Detour软件架构|Detour软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/Detour激光SLAM算法详解|Detour激光SLAM算法详解]]&lt;br /&gt;
* [[Special:MyLanguage/输入GPS外部定位|输入GPS外部定位]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 同时使用激光、地纹、二维码、轮编里程计和IMU进行鲁棒定位|同时使用激光、地纹、二维码、轮编里程计和IMU进行鲁棒定位]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;br /&gt;
[[Category:定位导航相关手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=Startup.iocmd%E8%84%9A%E6%9C%AC%E8%AF%AD%E6%B3%95&amp;diff=1044</id>
		<title>Startup.iocmd脚本语法</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=Startup.iocmd%E8%84%9A%E6%9C%AC%E8%AF%AD%E6%B3%95&amp;diff=1044"/>
		<updated>2026-05-16T14:00:25Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
`startup.iocmd` 是 Medulla 启动时执行的 ''迷你脚本''。语法极简：每行一条命令，支持对象加载、字段读写、方法调用。它的目的是让现场调试人员能在不写 C# 的情况下加载和初始化插件。&lt;br /&gt;
&lt;br /&gt;
`startup.iocmd` is the tiny script Medulla executes at boot. The grammar is minimal — one command per line, with object loading, field read/write, and method invocation. Its purpose is to let on-site engineers load and initialise plugins without writing C#.&lt;br /&gt;
&lt;br /&gt;
实现：`D:\src\M2\MedullaCore\Core\MedullaScriptEngine.cs:22-130`。&lt;br /&gt;
Implementation: `D:\src\M2\MedullaCore\Core\MedullaScriptEngine.cs:22-130`.&lt;br /&gt;
&lt;br /&gt;
启动入口：`Startup.runScript(filename)` 在 CWD 找 `startup.iocmd`，逐行执行。&lt;br /&gt;
Boot entry: `Startup.runScript(filename)` reads `startup.iocmd` from CWD line by line (`Startup.cs:374-402`).&lt;br /&gt;
&lt;br /&gt;
== 命令语法 / Command grammar ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 形式 / Form !! 语法 / Syntax !! 示例 / Example !! 语义 / Semantics&lt;br /&gt;
|-&lt;br /&gt;
| 加载 + 赋值 / Load + assign || `name = io load &amp;lt;dll&amp;gt;` || `lidar = io load plugins/Lidar.dll` || 加载 DLL，反射找 `MainIOObject`，实例化，注册到 `Objects[name]`&lt;br /&gt;
|-&lt;br /&gt;
| 工厂方法 / Factory method || `name = obj method arg1 arg2 ...` || `lidar = io init WLR716Lidar 192.168.0.2 2110` || 调用 obj.method 并把返回赋值给 name&lt;br /&gt;
|-&lt;br /&gt;
| 方法调用 / Method call || `name method arg1 arg2 ...` || `cart Init` || 调用 name.method（无返回）&lt;br /&gt;
|-&lt;br /&gt;
| 字段读 / Field read || `name.field` || `cart.batteryPercent` || 反射读字段或 property，控制台输出&lt;br /&gt;
|-&lt;br /&gt;
| 字段写 / Field write || `name.field = value` || `cart.TimeoutThreshold = 1000` || 反射写；`TypeDescriptor.ConvertFromString` 解析 value&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
解析规则：按空格分词；引号内当一个 token。&lt;br /&gt;
Parsing: split on whitespace; quoted strings count as one token.&lt;br /&gt;
&lt;br /&gt;
== 支持的参数类型 / Supported parameter types ==&lt;br /&gt;
* 整型 / Integers: `short`, `ushort`, `int`, `uint`, `long`, `ulong`&lt;br /&gt;
* 浮点 / Floats: `float`, `double`&lt;br /&gt;
* `bool`：`true` / `false`&lt;br /&gt;
* `string`：可加引号 `&amp;quot;with spaces&amp;quot;` 或不加&lt;br /&gt;
* `byte[]`：十六进制串 `0x12AB`&lt;br /&gt;
* `IOObject` 引用：按 `Objects[name]` 解析（即用其它对象的名字）&lt;br /&gt;
&lt;br /&gt;
Hex literal: `0x12AB` for integer types.&lt;br /&gt;
&lt;br /&gt;
Overload resolution: by name + arity, or variadic `object[]` (`MedullaScriptEngine.cs:138-144`).&lt;br /&gt;
&lt;br /&gt;
== 完整示例 / Full example ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
# Lidar plugin&lt;br /&gt;
lidar = io load plugins/LidarController.dll&lt;br /&gt;
lidar = lidar init WLR716Lidar 192.168.0.2 2110&lt;br /&gt;
lidar Start&lt;br /&gt;
lidar.TimeoutThreshold = 1000&lt;br /&gt;
&lt;br /&gt;
# Cart adapter plugin&lt;br /&gt;
cart = io load plugins/MyCart.dll&lt;br /&gt;
cart Init&lt;br /&gt;
cart.brakeOn = false&lt;br /&gt;
&lt;br /&gt;
# Camera&lt;br /&gt;
cam = io load plugins/Camera.dll&lt;br /&gt;
cam = cam init 192.168.0.50 8081&lt;br /&gt;
cam Start&lt;br /&gt;
&lt;br /&gt;
# Diagnostics: open the LidarController's web utility for the loaded lidar&lt;br /&gt;
# (it'll appear at https://&amp;lt;host&amp;gt;:8081/lidar/ShowFrame)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Web endpoint 自动注册 / Auto web-endpoint registration ==&lt;br /&gt;
赋值语句 `name = io load ...` 不仅注册对象，还会把对象上 `[IOObjectWebUtility]` 标注的方法自动挂到 HTTP 路径 `/&amp;lt;name&amp;gt;/&amp;lt;methodname&amp;gt;`。&lt;br /&gt;
The assignment `name = io load ...` also auto-registers any `[IOObjectWebUtility]` methods at `/&amp;lt;name&amp;gt;/&amp;lt;methodname&amp;gt;`.&lt;br /&gt;
&lt;br /&gt;
启动后可访问 `https://&amp;lt;host&amp;gt;:&amp;lt;MedullaIO.CPort&amp;gt;/&amp;lt;name&amp;gt;/&amp;lt;method&amp;gt;` 调用插件方法。&lt;br /&gt;
After boot, hit `https://&amp;lt;host&amp;gt;:&amp;lt;MedullaIO.CPort&amp;gt;/&amp;lt;name&amp;gt;/&amp;lt;method&amp;gt;` to invoke.&lt;br /&gt;
&lt;br /&gt;
== 注释与空行 / Comments &amp;amp; blank lines ==&lt;br /&gt;
* 空行 / blank lines: 忽略&lt;br /&gt;
* `# 开头` / lines starting with `#`：注释&lt;br /&gt;
* 没有 ''多行命令''；每条命令必须写在一行。&lt;br /&gt;
&lt;br /&gt;
== 错误处理 / Error handling ==&lt;br /&gt;
脚本里某行失败 → ''继续''执行下一行；错误打到 DLog + Hedingben toast。所以 `startup.iocmd` 是 &amp;quot;尽力而为&amp;quot;模型 —— 你应该用 web 端点或控制台后续确认状态，不要假设全部成功。&lt;br /&gt;
&lt;br /&gt;
Failed lines log to DLog + Hedingben toast and execution continues. `startup.iocmd` is &amp;quot;best-effort&amp;quot; — confirm state via web endpoint or console afterwards.&lt;br /&gt;
&lt;br /&gt;
== 扩展点 / Extension hooks ==&lt;br /&gt;
* 添加新的内建函数：扩展 `MedullaScriptEngine.evaluate` (line 80+)。&lt;br /&gt;
* 加新的参数类型：扩展 `MedullaScriptEngine.cs:69` 的 type coercion switch。&lt;br /&gt;
* 保持语法最简 —— 没有括号、没有嵌套调用。'''单行可读''' 是它的设计原则。&lt;br /&gt;
&lt;br /&gt;
* New built-in functions: extend `MedullaScriptEngine.evaluate` (line 80+).&lt;br /&gt;
* New parameter types: extend the coercion switch at line 69.&lt;br /&gt;
* Keep the grammar minimal — no parens, no nested calls. '''Single-line readability''' is the design.&lt;br /&gt;
&lt;br /&gt;
== 注意 / Caveats ==&lt;br /&gt;
* `startup.iocmd` 在 ''CWD'' 加载，不在 Medulla 安装目录 —— 启动脚本要把工作目录设到正确位置。&amp;lt;br&amp;gt;&lt;br /&gt;
  Loaded from CWD, not Medulla's install dir — your launcher must set the working directory.&lt;br /&gt;
* 没有 `if / for / while` —— 控制流需要写 C# 插件。&lt;br /&gt;
* `name` 标识符不能含 `.` 或 `=` 或空格。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/插件契约与打包约定|插件契约与打包约定]]&lt;br /&gt;
* [[Special:MyLanguage/IOObject属性参考|IOObject属性参考]]&lt;br /&gt;
* [[Special:MyLanguage/Medulla软件架构|Medulla软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/Medulla|Medulla]] / [[Special:MyLanguage/Medulla-API|Medulla-API]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=SimpleAgvInterface_Queue%E6%9C%BA%E5%88%B6&amp;diff=1043</id>
		<title>SimpleAgvInterface Queue机制</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=SimpleAgvInterface_Queue%E6%9C%BA%E5%88%B6&amp;diff=1043"/>
		<updated>2026-05-16T14:00:23Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
`SimpleAgvInterface.Queue(params Func&amp;lt;Task&amp;gt;[] actions)` 是 Clumsy 车载侧的 ''任务流水线''机制。它把多个 ''异步 action''组织成可串可并的执行图，使任务作者能以声明式风格写出 &amp;quot;巡线 → 取货 → 升叉 → 倒车&amp;quot;这样的业务序列。&lt;br /&gt;
&lt;br /&gt;
`SimpleAgvInterface.Queue(params Func&amp;lt;Task&amp;gt;[] actions)` is the '''task pipeline''' on the Clumsy on-vehicle side. It organises multiple async actions into a serial/parallel execution graph so mission authors can write business sequences declaratively.&lt;br /&gt;
&lt;br /&gt;
实现：`D:\src\Clumsy\ClumsyCore\Interfaces\SimpleAgvInterface.cs:410`。&lt;br /&gt;
Implementation: `D:\src\Clumsy\ClumsyCore\Interfaces\SimpleAgvInterface.cs:410`.&lt;br /&gt;
&lt;br /&gt;
== 基本用法 / Basic usage ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public void Fetch(double sx, double sy, double dx, double dy)&lt;br /&gt;
{&lt;br /&gt;
    Queue(&lt;br /&gt;
      async () =&amp;gt;&lt;br /&gt;
      {&lt;br /&gt;
          DriveTask.WaitDriveTask(new SteeringLineFollowing&lt;br /&gt;
          { srcX = currentX, srcY = currentY, dstX = sx, dstY = sy, basespeed = 600 }.Follow());&lt;br /&gt;
      },&lt;br /&gt;
      async () =&amp;gt;&lt;br /&gt;
      {&lt;br /&gt;
          DriveTask.WaitDriveTask(new AutoFetchGood&lt;br /&gt;
          { aX = sx, aY = sy, bX = dx, bY = dy, initSpeed = 500, stopDist = 80 }.Get());&lt;br /&gt;
      },&lt;br /&gt;
      async () =&amp;gt;&lt;br /&gt;
      {&lt;br /&gt;
          SetUpperIO(&amp;quot;forkHeightTgt&amp;quot;, 50);&lt;br /&gt;
          await WaitForLowerIO(&amp;quot;forkAtTarget&amp;quot;, 4000);&lt;br /&gt;
      },&lt;br /&gt;
      async () =&amp;gt;&lt;br /&gt;
      {&lt;br /&gt;
          DriveTask.WaitDriveTask(new SteeringLineFollowingReverse&lt;br /&gt;
          { srcX = dx, srcY = dy, dstX = sx, dstY = sy, basespeed = 200 }.ReverseFollow());&lt;br /&gt;
      });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
按提交顺序，4 个 action 形成 4 ''行''（rows），框架依次执行。&lt;br /&gt;
The 4 actions form 4 ''rows''; the framework executes them.&lt;br /&gt;
&lt;br /&gt;
== 数据结构 / Data structure ==&lt;br /&gt;
内部维护两个队列（`SimpleAgvInterface.cs:308-352`）：&lt;br /&gt;
Internally there are two queues:&lt;br /&gt;
&lt;br /&gt;
* '''`staticTaskLs : List&amp;lt;List&amp;lt;Task&amp;gt;&amp;gt;`''' — 静态共享队列；每行是 `List&amp;lt;Task&amp;gt;`。&lt;br /&gt;
* '''`taskLs : List&amp;lt;List&amp;lt;Task&amp;gt;&amp;gt;`''' — 当前实例的队列；可与 static 共享或独立。&lt;br /&gt;
* '''`queueInfo : List&amp;lt;SaiInfo&amp;gt;`''' — 每行的状态字符串、提交线程 ID、异常上下文。&lt;br /&gt;
&lt;br /&gt;
每次 `Queue(actions)` 调用 ：&lt;br /&gt;
Each `Queue(actions)` call:&lt;br /&gt;
&lt;br /&gt;
# 把 `actions[i]` ContinueWith 到 `taskLs[i]` 的最后一个 Task 后面。&lt;br /&gt;
# `actions[i]` 内部 `await taskLs[i-1].Last()` —— 即等上一 ''行''最新提交的 action 完成。&lt;br /&gt;
# 注册到 `queueInfo[i]`。&lt;br /&gt;
&lt;br /&gt;
# Chain `actions[i]` after `taskLs[i].Last()` (the previous action in the same row).&lt;br /&gt;
# Inside `actions[i]`, `await taskLs[i-1].Last()` (the previous row's latest action).&lt;br /&gt;
# Register in `queueInfo[i]`.&lt;br /&gt;
&lt;br /&gt;
== &amp;quot;超标量&amp;quot;实际行为 / The &amp;quot;super-scalar&amp;quot; reality ==&lt;br /&gt;
【发现】 当前实现 ''没有''真正的跨行并发。第 i 行的 action ''必须''等第 i-1 行的 ''最新提交''完成，所以多行 Queue 调用实际上是 ''串行执行''。&amp;quot;超标量流水线&amp;quot;是设计目标，不是当前实现。&lt;br /&gt;
【发现】 The current implementation does NOT actually run rows in parallel. Row i awaits row i-1's most recent Task, so multi-row Queue calls execute '''sequentially'''. The &amp;quot;super-scalar pipeline&amp;quot; is design aspiration, not current behaviour.&lt;br /&gt;
&lt;br /&gt;
也就是说 ：&lt;br /&gt;
In practice:&lt;br /&gt;
&lt;br /&gt;
* '''单次 `Queue(a, b, c)`''' → a, b, c 串行（b 等 a 完成，c 等 b 完成）。&lt;br /&gt;
* '''连续 `Queue(a, b)`; `Queue(c, d)`''' → a → b → c → d 串行；c 等 b 完成（同行）；d 等 c 完成（同行）。&lt;br /&gt;
* '''并发要靠 action 内 `Task.WhenAll`''' —— 若你想 a + b 同时开始，自己在 a 内 `Task.WhenAll(taskA, taskB)`。&lt;br /&gt;
&lt;br /&gt;
* `Queue(a, b, c)` runs a → b → c sequentially.&lt;br /&gt;
* Repeated `Queue` calls also serialise via row chaining.&lt;br /&gt;
* For concurrency '''within''' an action, use `Task.WhenAll` explicitly.&lt;br /&gt;
&lt;br /&gt;
== `WaitAsync` / `Wait` ==&lt;br /&gt;
等待指定行完成：&lt;br /&gt;
Wait for a specific row to finish:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
await agv.WaitAsync(pipe: 2);  // wait for row 2's latest task&lt;br /&gt;
// or sync&lt;br /&gt;
agv.Wait(pipe: 0);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== `TryLock` / `Leave` ==&lt;br /&gt;
交管 ''锁''的两种执行路径，由 `useSimpleCallTraffic` 开关决定（`SimpleAgvInterface.cs:40`）：&lt;br /&gt;
&lt;br /&gt;
Traffic locks have two paths controlled by `useSimpleCallTraffic`:&lt;br /&gt;
&lt;br /&gt;
=== 同步内置 / In-process synchronous ===&lt;br /&gt;
当 `useSimpleCallTraffic = true`（同一进程跑调度内核 + 车载）：&lt;br /&gt;
* `TryLock(siteID, route_id)` 轮询 `PilotBase.latestLock == siteID`（`SimpleAgvInterface.cs:124`）&lt;br /&gt;
* `Leave(siteID)` 直接更新本地 `PilotBase.leaves`&lt;br /&gt;
&lt;br /&gt;
=== HTTP RPC ===&lt;br /&gt;
跨进程时（车载与调度内核不在同一进程）：&lt;br /&gt;
* `TryLock` → POST `http://{host}:{port}/trylock` body `{carid, siteid}`（line 170）&lt;br /&gt;
* `Leave` → GET `/leave?carid={id}&amp;amp;siteid={siteID}`（line 260）&lt;br /&gt;
&lt;br /&gt;
== 异常处理 / Error handling ==&lt;br /&gt;
* Action 内异常 → 写到 `queueInfo[i].exception`；该行后续 action 跳过；其它行继续。&lt;br /&gt;
* 调度可通过 `Flush()` 显式终结队列；之后 `Queue(...)` 抛 `_pipelineError`（line 112）。&lt;br /&gt;
* 任务级失败上报靠 ''任务脚本编译时插入的 try/catch'' + Hedingben toast。&lt;br /&gt;
&lt;br /&gt;
* Exceptions in actions → recorded in `queueInfo[i].exception`; subsequent actions in that row skipped; other rows continue.&lt;br /&gt;
* `Flush()` ends the queue; later `Queue(...)` throws `_pipelineError`.&lt;br /&gt;
&lt;br /&gt;
== 实战模式 / Patterns ==&lt;br /&gt;
=== 顺序业务动作 / Sequential business action ===&lt;br /&gt;
最常见。每个 action 是一段独立的 ''检查 → 行驶 → 等硬件''微操。&lt;br /&gt;
Most common. Each action is a self-contained &amp;quot;check → drive → wait hw&amp;quot; micro-op.&lt;br /&gt;
&lt;br /&gt;
=== 锁先于动作 / Lock-then-act ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
Queue(&lt;br /&gt;
  async () =&amp;gt; { if (!TryLock(targetSite, routeId)) return; /* signal failure */ },&lt;br /&gt;
  async () =&amp;gt; { /* the actual drive */ },&lt;br /&gt;
  async () =&amp;gt; Leave(prevSite)&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 设备 IO 等待 / Wait-for-IO ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
Queue(&lt;br /&gt;
  async () =&amp;gt; { SetUpperIO(&amp;quot;jackTarget&amp;quot;, 80); },&lt;br /&gt;
  async () =&amp;gt; { await WaitForLowerIO(&amp;quot;jackAtTarget&amp;quot;, timeoutMs: 4000); }&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 错误恢复 / Error recovery ===&lt;br /&gt;
Action 内部 try/catch；失败时把 cart 切到 &amp;quot;故障&amp;quot;状态并 toast：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
Queue(&lt;br /&gt;
  async () =&amp;gt;&lt;br /&gt;
  {&lt;br /&gt;
      try { await DoFragileThing(); }&lt;br /&gt;
      catch (Exception e)&lt;br /&gt;
      {&lt;br /&gt;
          Hedingben.ToastText($&amp;quot;Action failed: {e.Message}&amp;quot;, &amp;quot;AGV&amp;quot;);&lt;br /&gt;
          DriveStop();&lt;br /&gt;
          throw;&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 调度侧 AGVInterface / Fleet-side AGVInterface ==&lt;br /&gt;
SimpleCore 端的 `AGVInterface` (`D:\src\Simple\SimpleCore\BasicProps\AGVInterface.cs`) 提供 ''同款 Queue 机制''，用于自评估车 ：调度直接在自己进程里跑 Queue 来翻译 TopazScript。详见 [[Special:MyLanguage/如何基于SimpleCore核心库进行调度系统开发|如何基于SimpleCore核心库进行调度系统开发]]。&lt;br /&gt;
&lt;br /&gt;
The fleet-side `AGVInterface` provides the same Queue mechanism for self-evaluating cars (scheduler runs Queue locally to translate TopazScript).&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/MovementDefinition详解|MovementDefinition详解]]&lt;br /&gt;
* [[Special:MyLanguage/DriveTask调度循环|DriveTask调度循环]]&lt;br /&gt;
* [[Special:MyLanguage/车体抽象原理|车体抽象原理]]&lt;br /&gt;
* [[Special:MyLanguage/如何基于SimpleCore核心库进行调度系统开发|如何基于SimpleCore核心库进行调度系统开发]]&lt;br /&gt;
* [[Special:MyLanguage/Clumsy-API|Clumsy-API]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=MovementDefinition%E8%AF%A6%E8%A7%A3&amp;diff=1042</id>
		<title>MovementDefinition详解</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=MovementDefinition%E8%AF%A6%E8%A7%A3&amp;diff=1042"/>
		<updated>2026-05-16T14:00:21Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
`MovementDefinition` 是 Clumsy 中描述 ''一个运动行为''的抽象基类。每个 Movement 是一段 ''可迭代''的控制循环，由 `DriveTask` 宿主按 ''DriveTaskInterval''（默认 20 ms）周期推进。它是 Clumsy 自动驾驶的 ''基本运动语汇''。&lt;br /&gt;
&lt;br /&gt;
`MovementDefinition` is the Clumsy abstract base that describes a single motion behaviour. Each Movement is an iterable control loop, ticked by `DriveTask` at `DriveTaskInterval` (default 20 ms). It's Clumsy's '''basic motion vocabulary'''.&lt;br /&gt;
&lt;br /&gt;
定义：`D:\src\Clumsy\ClumsyCore\Pilot\MovementDefinition.cs:41-48`。&lt;br /&gt;
Definition: `D:\src\Clumsy\ClumsyCore\Pilot\MovementDefinition.cs:41-48`.&lt;br /&gt;
&lt;br /&gt;
== 契约 / Contract ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public abstract class MovementDefinition&lt;br /&gt;
{&lt;br /&gt;
    public abstract IEnumerable&amp;lt;bool&amp;gt; Get();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public abstract class MovementDefinition&amp;lt;T&amp;gt; : MovementDefinition&lt;br /&gt;
{&lt;br /&gt;
    public new abstract IEnumerable&amp;lt;T&amp;gt; Get();&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
`Get()` 返回一个 ''可迭代''；框架每次调 `MoveNext()` 推一格。&lt;br /&gt;
&lt;br /&gt;
`Get()` returns an enumerable; the framework calls `MoveNext()` once per tick.&lt;br /&gt;
&lt;br /&gt;
* '''每次 `yield return true`''' = &amp;quot;继续 tick&amp;quot; / &amp;quot;keep ticking&amp;quot;&lt;br /&gt;
* '''迭代器耗尽（不再 `yield`）''' = &amp;quot;Movement 完成&amp;quot; / &amp;quot;Movement done&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 生命周期 / Lifecycle ==&lt;br /&gt;
&lt;br /&gt;
  你的代码 / Your code:&lt;br /&gt;
    DriveTask.WaitDriveTask(new MyMovement{...}.Get());&lt;br /&gt;
       │&lt;br /&gt;
       ▼&lt;br /&gt;
  DriveTask 内部 / Inside DriveTask (D:\src\Clumsy\ClumsyCore\DriveTask.cs:104-144):&lt;br /&gt;
    new Thread + AboveNormal priority&lt;br /&gt;
       │&lt;br /&gt;
       ▼ 循环 / loop:&lt;br /&gt;
    enumerator.MoveNext()&lt;br /&gt;
    Thread.Sleep(Configuration.conf.DriveTaskInterval) // 20 ms&lt;br /&gt;
    ...&lt;br /&gt;
       │&lt;br /&gt;
       ▼ 完成 / done (MoveNext returns false):&lt;br /&gt;
    stopped_reason = 0&lt;br /&gt;
    Monitor.PulseAll(wait)&lt;br /&gt;
       │&lt;br /&gt;
       ▼ 异常 / on exception:&lt;br /&gt;
    stopped_reason = -1&lt;br /&gt;
    capture &amp;amp; rethrow on Wait()&lt;br /&gt;
&lt;br /&gt;
调用方 `WaitDriveTask` 在 `Monitor.Wait(wait)` 上阻塞直到完成。&lt;br /&gt;
Caller `WaitDriveTask` blocks on `Monitor.Wait(wait)` until completion.&lt;br /&gt;
&lt;br /&gt;
== 最小 Movement / Minimal Movement ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using ClumsyCore.Pilot;&lt;br /&gt;
using ClumsyCore.DrivePlans;&lt;br /&gt;
using System.Collections.Generic;&lt;br /&gt;
&lt;br /&gt;
public class MyStraight : MovementDefinition&lt;br /&gt;
{&lt;br /&gt;
    public float dx, dy;          // displacement&lt;br /&gt;
    public float basespeed = 600; // mm/s&lt;br /&gt;
&lt;br /&gt;
    public override IEnumerable&amp;lt;bool&amp;gt; Get()&lt;br /&gt;
    {&lt;br /&gt;
        var sx = currentX;        // pose at start&lt;br /&gt;
        var sy = currentY;&lt;br /&gt;
        var driver = new SteeringLineFollowing&lt;br /&gt;
        {&lt;br /&gt;
            srcX = sx, srcY = sy,&lt;br /&gt;
            dstX = sx + dx, dstY = sy + dy,&lt;br /&gt;
            basespeed = basespeed&lt;br /&gt;
        };&lt;br /&gt;
        foreach (var t in driver.Follow()) yield return t;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== [MovementTest] 属性 / Attribute ==&lt;br /&gt;
'''目标''' / Target: 类 / class&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 在 Clumsy 测试 UI 注册一个 ''测试按钮''；按下立即在车上跑这个 Movement（车应静止 / 在仿真）。&lt;br /&gt;
Registers a test button in the Clumsy UI; pressing it runs the Movement on a stationary (or simulated) vehicle.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[MovementTest(name = &amp;quot;测试我的直线 / Test my line&amp;quot;)]&lt;br /&gt;
public class MyStraight : MovementDefinition { ... }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 可用原语 / Primitive library ==&lt;br /&gt;
Movement 通常 ''组合''下层原语而不是从零写控制律。下面是仓库里现成的：&lt;br /&gt;
Movements usually compose lower-level primitives rather than implement control law from scratch.&lt;br /&gt;
&lt;br /&gt;
=== ClumsyCore/Legacy/（几何跟踪基础） ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Primitive !! 用途 / Use&lt;br /&gt;
|-&lt;br /&gt;
| `SteeringLineFollowing` || 阿克曼正向纯跟踪&lt;br /&gt;
|-&lt;br /&gt;
| `SteeringLineFollowingReverse` || 阿克曼反向跟踪&lt;br /&gt;
|-&lt;br /&gt;
| `OmniWheelTracking` || 全向轮速跟踪&lt;br /&gt;
|-&lt;br /&gt;
| `LeftRightWheelTracking` || 差速轮速跟踪&lt;br /&gt;
|-&lt;br /&gt;
| `OmniBezierTracking` / `LeftRightBezierTracking` || Bezier 曲线跟踪&lt;br /&gt;
|-&lt;br /&gt;
| `TrajectoryReplanningObstacleAvoidance` || 动态避障&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== ClumsyDance/Movements/（行为聚合） ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Movement !! 用途 / Use&lt;br /&gt;
|-&lt;br /&gt;
| `SlotFollowing` || 槽位跟踪（叉车叉孔 / KIVA 腿对）&lt;br /&gt;
|-&lt;br /&gt;
| `ReflexDock` || 反射式对接&lt;br /&gt;
|-&lt;br /&gt;
| `KivaFollowShelfOfManyLegs` || 多腿料架导航&lt;br /&gt;
|-&lt;br /&gt;
| `TopHatFollowing` || 礼帽形特征跟踪&lt;br /&gt;
|-&lt;br /&gt;
| `BezierMPC` || Bezier 参考的 MPC&lt;br /&gt;
|-&lt;br /&gt;
| `LRWheelOdometryCalculator` || 编码器积分助手&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== ClumsyDance/Detectors/（感知） ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Detector !! 用途&lt;br /&gt;
|-&lt;br /&gt;
| `LidarDetectTray` || 通用托盘检测（2 腿 / 3 腿可调）&lt;br /&gt;
|-&lt;br /&gt;
| `Lidar2dDetect2LegTray`, `Lidar2dDetect3LegTray`, `Lidar3dDetectTray` || 特化变体&lt;br /&gt;
|-&lt;br /&gt;
| `Cam3dDetectTray` || 3D 相机检测&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== MovementDefinition&amp;lt;T&amp;gt; 泛型变体 / Generic variant ==&lt;br /&gt;
当 Movement 需要 ''返回''一个对象（如已配置好的子 Movement）：&lt;br /&gt;
When a Movement needs to ''return'' a typed handle (e.g. a pre-configured sub-Movement):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class BuildFollower : MovementDefinition&amp;lt;SteeringLineFollowing&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    public float srcX, srcY, dstX, dstY;&lt;br /&gt;
    public override IEnumerable&amp;lt;SteeringLineFollowing&amp;gt; Get()&lt;br /&gt;
    {&lt;br /&gt;
        yield return new SteeringLineFollowing&lt;br /&gt;
        {&lt;br /&gt;
            srcX = srcX, srcY = srcY,&lt;br /&gt;
            dstX = dstX, dstY = dstY,&lt;br /&gt;
            basespeed = 800&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 异常 + 中止 / Exceptions &amp;amp; abort ==&lt;br /&gt;
* `Get()` 内抛异常 → `DriveTask.Wait()` 返回时 ''重新抛出''。&lt;br /&gt;
* `PilotBase.SecurityCheck()` 在没收到控制信号超时（默认 300 ms）时调 `DriveStop()`；当前 Movement 的 enumerator 不会再被推进，但 ''资源清理责任在 Movement 作者''（在 yield 前后包 try/finally）。&lt;br /&gt;
* 调用者按 `WaitDriveTask(...)` 同步等；要异步用 `DriveTask.WaitDriveTask` 包在另一线程或 `SimpleAgvInterface.Queue`。&lt;br /&gt;
&lt;br /&gt;
Exceptions from `Get()` re-throw when `WaitDriveTask` returns. The watchdog (`SecurityCheck`, default 300 ms timeout) calls `DriveStop()`; the enumerator stops being advanced but Movement-owned cleanup is on the author (wrap in try/finally).&lt;br /&gt;
&lt;br /&gt;
== 与 Pilot 的关系 / Relation to Pilot ==&lt;br /&gt;
* `PilotBase` 是 Movement 的 ''宿主进程上下文''（提供 pose、IDriveWriter 等）。&lt;br /&gt;
* `PilotDefinition&amp;lt;T&amp;gt;` 加 typed config。&lt;br /&gt;
* 95% 的车型定制不用碰 PilotBase —— 写 Movement 就够。&lt;br /&gt;
&lt;br /&gt;
`PilotBase` is Movement's ''host context'' (pose, drive writers); `PilotDefinition&amp;lt;T&amp;gt;` adds typed config. 95% of customisation only needs Movements.&lt;br /&gt;
&lt;br /&gt;
== 常见错误 / Gotchas ==&lt;br /&gt;
* '''在 `Get()` 用 `Thread.Sleep`''' → 阻塞 DriveTask 线程，定时器漂移。正确方式：`yield return` 多次。&amp;lt;br&amp;gt;&lt;br /&gt;
  Don't `Thread.Sleep` inside `Get()`; just `yield return` multiple times.&lt;br /&gt;
* '''Movement 跨实例共享状态''' → Movement 应是 ''可重入''的；不要用 static 字段保存进度。&amp;lt;br&amp;gt;&lt;br /&gt;
  Movements should be reentrant; don't use static fields for progress.&lt;br /&gt;
* '''忘了在不需要继续时 `yield break`''' → 看起来在原地踏步。&lt;br /&gt;
* '''构造时读 pose''' → 应在 `Get()` 第一帧读，避免构造与执行间车移动了。&amp;lt;br&amp;gt;&lt;br /&gt;
  Read pose inside `Get()`, not in the ctor — otherwise the car may have moved.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/巡线行走|巡线行走]]&lt;br /&gt;
* [[Special:MyLanguage/绕障行走|绕障行走]]&lt;br /&gt;
* [[Special:MyLanguage/DriveTask调度循环|DriveTask调度循环]]&lt;br /&gt;
* [[Special:MyLanguage/SimpleAgvInterface Queue机制|SimpleAgvInterface Queue机制]]&lt;br /&gt;
* [[Special:MyLanguage/车体抽象原理|车体抽象原理]]&lt;br /&gt;
* [[Special:MyLanguage/Clumsy-API|Clumsy-API]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;br /&gt;
[[Category:运动控制使用手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=MDCS%E8%B7%A8%E5%88%87%E9%9D%A2%E7%BA%A6%E5%AE%9A&amp;diff=1041</id>
		<title>MDCS跨切面约定</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=MDCS%E8%B7%A8%E5%88%87%E9%9D%A2%E7%BA%A6%E5%AE%9A&amp;diff=1041"/>
		<updated>2026-05-16T14:00:19Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
本页汇总 MDCS 平台 ''跨子系统''的约定 —— 命名、线程、错误处理、版本、文件布局。新加入团队或修改核心代码前先读这里。&lt;br /&gt;
&lt;br /&gt;
This page collects MDCS-wide ''cross-cutting conventions'' — naming, threading, error handling, versioning, file layout. Read this before joining the team or modifying core code.&lt;br /&gt;
&lt;br /&gt;
== 命名约定 / Naming conventions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 实体 / Entity !! 约定 / Convention !! 例 / Example&lt;br /&gt;
|-&lt;br /&gt;
| Medulla 插件入口类 / Medulla plugin entry || `MainIOObject`（严格大小写）|| `LidarController.MainIOObject`&lt;br /&gt;
|-&lt;br /&gt;
| 插件文件夹（一方）|| `D:\src\M2\OfficialPlugins\&amp;lt;VendorModel&amp;gt;\` || `OfficialPlugins\LidarController\`&lt;br /&gt;
|-&lt;br /&gt;
| 引用程序集 / Reference DLL || `Ref&amp;lt;Name&amp;gt;.dll` || `RefMedullaCore.dll`&lt;br /&gt;
|-&lt;br /&gt;
| Simple 车型类 / Simple car class || 用 `[CarType]` 属性，类名任意 || `[CarType] public class LB14 : ClumsyCar`&lt;br /&gt;
|-&lt;br /&gt;
| DObject 槽名 / DObject slot name || `&amp;lt;Producer&amp;gt;&amp;lt;Tag&amp;gt;` || `MedullaCartLowerIO&amp;lt;tag&amp;gt;`&lt;br /&gt;
|-&lt;br /&gt;
| Wiki 页 / Wiki page || 现有约定 ：`开发手册 - ...`, `使用手册 - ...`, `XXX适配案例`, `XXX技术概述` || `开发手册 - SimpleComposer界面开发 - CAD工具`&lt;br /&gt;
|-&lt;br /&gt;
| 上下位 IO 字段 / IO field || 上位 `*Cmd`，下位 `*Est`（或更具体的 `*OK`, `*Pct` 等）|| `vCmd`, `vEst`, `batteryPercent`&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 线程约定 / Threading conventions ==&lt;br /&gt;
* '''LadderLogic''' 每个 `[UseLadderLogic]` 类自己的线程；threads 共享 CartDefinition 字段，自行同步。&lt;br /&gt;
* '''DriveTask''' 每个 Movement 自己的线程（`ThreadPriority.AboveNormal`）。&lt;br /&gt;
* '''DObject `Wait()`''' 阻塞当前线程；要并行多源就开多线程。&lt;br /&gt;
* '''CycleGUI Draw 调用''' 从任意线程都安全（内部 marshall 到 UI 线程）。&lt;br /&gt;
* '''SimpleComposer Nancy 处理器''' 在 Nancy 线程池跑；不要长阻塞。&lt;br /&gt;
* '''Tasks''' 多用 `async`/`await`；尽量避免 `Task.Run`（除非 explicit 后台计算）。&lt;br /&gt;
&lt;br /&gt;
* LadderLogic, DriveTask each have own thread.&lt;br /&gt;
* DObject `Wait()` is blocking; parallelise across sources via worker threads.&lt;br /&gt;
* CycleGUI Draw is thread-safe.&lt;br /&gt;
* Nancy handlers run on Nancy's thread pool; no long blocks.&lt;br /&gt;
* Use `async/await`; avoid `Task.Run` unless explicit background work.&lt;br /&gt;
&lt;br /&gt;
== 错误处理三层 / 3-layer error model ==&lt;br /&gt;
=== Layer 1 — 插件级 / Plugin-level ===&lt;br /&gt;
`[UseLadderLogic(resume = true)]`（默认）捕获 `Operation()` 异常并在 `resumeRetryDelay` 后重试。日常 ''可恢复''错误用这一层。&lt;br /&gt;
&lt;br /&gt;
The default `resume = true` catches LadderLogic exceptions + retries after `resumeRetryDelay`. Use for routine recoverable errors.&lt;br /&gt;
&lt;br /&gt;
=== Layer 2 — 进程级 / Process-level ===&lt;br /&gt;
`Hedingben.ToastText(message, source)` 把异常呈现给操作工。逻辑能继续但人需要知道时用。&lt;br /&gt;
&lt;br /&gt;
`Hedingben.ToastText` surfaces errors as UI toasts. Use when logic can proceed but the human should know.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
catch (Exception e)&lt;br /&gt;
{&lt;br /&gt;
    Hedingben.ToastText($&amp;quot;Failed: {e.Message}&amp;quot;, $&amp;quot;{Name}:ERR&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Layer 3 — 看门狗级 / Watchdog-level ===&lt;br /&gt;
Wawa（[[Special:MyLanguage/看门狗Wawa使用说明|看门狗Wawa使用说明]]）是进程监控；进程崩了会重启。对应&amp;quot;世界着火&amp;quot;级别的错误 —— 让它 `throw`，让 watchdog 接手。&lt;br /&gt;
&lt;br /&gt;
Wawa is the process watchdog. For &amp;quot;the world is on fire&amp;quot; — `throw` and let the watchdog restart.&lt;br /&gt;
&lt;br /&gt;
=== 选层 / Picking the right layer ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 情况 / Situation !! 用哪层 / Use&lt;br /&gt;
|-&lt;br /&gt;
| PLC 通讯偶尔超时 || Layer 1（resume + 重试）&lt;br /&gt;
|-&lt;br /&gt;
| 雷达连接断开 || Layer 1（重连循环）+ Layer 2（toast）&lt;br /&gt;
|-&lt;br /&gt;
| 关键传感器永久失效 || Layer 2（toast）+ 把 `eStop = true`&lt;br /&gt;
|-&lt;br /&gt;
| 内存溢出 / panic || Layer 3（throw → watchdog 重启）&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
不要 ''同时''做三层 —— 选最合适的。&lt;br /&gt;
Don't do all three; pick the right one.&lt;br /&gt;
&lt;br /&gt;
== 版本与助记词 / Versioning ==&lt;br /&gt;
每次修改编译产物 ：&lt;br /&gt;
Every modification of a compiled artefact:&lt;br /&gt;
&lt;br /&gt;
* `AssemblyInformationalVersion` 加 ''单词助记词'' ：`1.4.0+kindling`&lt;br /&gt;
* 下次改 ：`1.4.1+lantern`&lt;br /&gt;
* 助记词 ''单一''单词；不要 hyphen / 空格&lt;br /&gt;
&lt;br /&gt;
或用 `[ReplaceWithCompileInfo]` —— LessokajiWeaver 自动注入时间戳 + git hash。&lt;br /&gt;
Or `[ReplaceWithCompileInfo]` — auto-inject timestamp + git hash.&lt;br /&gt;
&lt;br /&gt;
目的 ：现场调试时区分两个数字看起来一样的二进制。&lt;br /&gt;
Purpose: visually disambiguate field binaries with same number.&lt;br /&gt;
&lt;br /&gt;
== 文件 / 路径 约定 / Path conventions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 用途 / Use !! 约定路径 / Convention&lt;br /&gt;
|-&lt;br /&gt;
| 临时工作文件 / 草稿 || `D:\src\ai-deck\&amp;lt;project&amp;gt;\` （非 repo 根；可丢弃）&lt;br /&gt;
|-&lt;br /&gt;
| 项目级开发日志 || `D:\src\&amp;lt;repo&amp;gt;\DEV_LOG.md`（仓库根；append-only）&lt;br /&gt;
|-&lt;br /&gt;
| 插件 csproj 引用 / HintPath || `..\tools\Ref&amp;lt;Name&amp;gt;.dll`&lt;br /&gt;
|-&lt;br /&gt;
| 部署时 plugin 位置 || `medulla/plugins/&amp;lt;vendor&amp;gt;/&amp;lt;plugin&amp;gt;.dll`，`simplecomposer/plugins/`&lt;br /&gt;
|-&lt;br /&gt;
| startup 脚本 || `startup.iocmd`（Medulla CWD）&lt;br /&gt;
|-&lt;br /&gt;
| SLAM 地图文件 || `*.2dlm` / `*.gtex` / `*.gtm` / `*.memslam` / `tagmap.json`&lt;br /&gt;
|-&lt;br /&gt;
| DObject 录制 || `.dorec`&lt;br /&gt;
|-&lt;br /&gt;
| 日志 / Logs || `logs/&amp;lt;YYYY-MM-DD&amp;gt;.log`（DLog）+ `hedingben.log`&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== DEV_LOG.md ==&lt;br /&gt;
仓库根 `DEV_LOG.md` 是 ''append-only''的开发日志 ：&lt;br /&gt;
The `DEV_LOG.md` at each repo root is the '''append-only''' dev log:&lt;br /&gt;
&lt;br /&gt;
* 每条新条目追加到末尾，绝不修改旧条目。&lt;br /&gt;
* 重要发现用前缀 ：`【发现】 / 【注意】 / 【灵感】`&lt;br /&gt;
* 编译产物的版本助记词也记在这里。&lt;br /&gt;
&lt;br /&gt;
DEV_LOG 的目的是让 ''下一个读者''（也许 6 个月后的自己）能复原当时的判断。&lt;br /&gt;
DEV_LOG's goal: the next reader (maybe you in 6 months) can reconstruct the decisions.&lt;br /&gt;
&lt;br /&gt;
== 注释风格 / Comment style ==&lt;br /&gt;
* '''头部注释稀疏''' —— 代码应自解释。&lt;br /&gt;
* '''中文注释常见'''于领域特定处（运动学、标定、传感）。&lt;br /&gt;
* '''关键代码用 ''发现''标记''' —— 文件里也用 `// 【发现】 ...` 提醒后续读者。&lt;br /&gt;
&lt;br /&gt;
* Header comments sparse — code should be self-evident.&lt;br /&gt;
* Chinese comments are normal in domain-specific spots.&lt;br /&gt;
* `// 【发现】 ...` flags for important inline notes.&lt;br /&gt;
&lt;br /&gt;
== 测试纪律 / Testing discipline ==&lt;br /&gt;
* 单元测试稀疏；机器人代码本质上靠 ''集成''测试。&lt;br /&gt;
* `D:\src\Fundamentals\Test\` 覆盖 DObject + DLog（最关键基础）。&lt;br /&gt;
* 多数&amp;quot;测试&amp;quot;通过 `[MovementTest]` / `[IOObjectUtility]` UI 按钮跑。&lt;br /&gt;
* '''改 DObject 后必跑 Fundamentals 单元测试''' —— IPC bug 灾难性。&lt;br /&gt;
* '''改 *Core 后跑仿真车 SimpleComposer 集成测试'''。&lt;br /&gt;
&lt;br /&gt;
== 文档 / Documentation ==&lt;br /&gt;
* '''Wiki 是事实标准'''；代码注释 / DEV_LOG 是 ''临时记''。&lt;br /&gt;
* 公共 API 改动必同时 update wiki。&lt;br /&gt;
* 私下试验在 `ai-deck/`，不要污染 wiki。&lt;br /&gt;
&lt;br /&gt;
== Git 约定 / Git ==&lt;br /&gt;
* Branch 名 ：`feature/&amp;lt;short-name&amp;gt;`, `fix/&amp;lt;issue&amp;gt;`, `release/&amp;lt;version&amp;gt;`&lt;br /&gt;
* Commit 消息含助记词与一句话变更摘要&lt;br /&gt;
* PR / MR 描述含 ：变更动机 + 关键决策 + 测试方法&lt;br /&gt;
&lt;br /&gt;
== License / 许可 ==&lt;br /&gt;
* 客户机器绑定 license 由 `auth.lessokaji.com` 签发；`LessokajiProtect.dll` 在 Fundamentals。&lt;br /&gt;
* 开发机用 ''dev license''（无限期但带水印）。&lt;br /&gt;
* 不要在源码里 hardcode license / serial。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/MDCS仓库布局与构建链|MDCS仓库布局与构建链]]&lt;br /&gt;
* [[Special:MyLanguage/插件契约与打包约定|插件契约与打包约定]]&lt;br /&gt;
* [[Special:MyLanguage/插件开发清单|插件开发清单]]&lt;br /&gt;
* [[Special:MyLanguage/核心开发指南|核心开发指南]]&lt;br /&gt;
* [[Special:MyLanguage/看门狗Wawa使用说明|看门狗Wawa使用说明]]&lt;br /&gt;
* [[Special:MyLanguage/通用约定|通用约定]] — 已有的&amp;quot;通用约定&amp;quot;页（业务侧）&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;br /&gt;
[[Category:开发手册]]&lt;br /&gt;
[[Category:通识和入门教学]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=MDCS%E5%B9%B3%E5%8F%B0%E5%8F%91%E5%B8%83%E6%B5%81%E7%A8%8B&amp;diff=1040</id>
		<title>MDCS平台发布流程</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=MDCS%E5%B9%B3%E5%8F%B0%E5%8F%91%E5%B8%83%E6%B5%81%E7%A8%8B&amp;diff=1040"/>
		<updated>2026-05-16T14:00:19Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
本页是 MDCS 平台 ''内核 + 一方插件 + 客户端''的发布流程。客户端单插件发布见 [[Special:MyLanguage/插件测试与发布|插件测试与发布]]。&lt;br /&gt;
&lt;br /&gt;
This page covers MDCS '''kernel + first-party plugin + client''' release flow. Single-plugin release: see [[Special:MyLanguage/插件测试与发布|insert plugin testing &amp;amp; release]].&lt;br /&gt;
&lt;br /&gt;
== 0. 准备 / Pre-flight ==&lt;br /&gt;
* 所有目标变更已合并到主分支&lt;br /&gt;
* 单元测试通过（特别是 `D:\src\Fundamentals\Test\`）&lt;br /&gt;
* 仿真车集成测试通过&lt;br /&gt;
* DEV_LOG.md 已记录本期所有 ''发现 / 注意 / 灵感''&lt;br /&gt;
* 已确认变更不破坏向后兼容（或明确标注 break）&lt;br /&gt;
&lt;br /&gt;
== 1. 版本号 / Version bumping ==&lt;br /&gt;
按 ''SemVer + 助记词'' ：&lt;br /&gt;
SemVer + mnemonic:&lt;br /&gt;
&lt;br /&gt;
* '''Major''' (`X.0.0+...`)：破坏性 API 变更 / breaking API change&lt;br /&gt;
* '''Minor''' (`x.X.0+...`)：新功能 / new features&lt;br /&gt;
* '''Patch''' (`x.x.X+...`)：bug 修复 / bug fix&lt;br /&gt;
* '''Mnemonic''' (`+lantern`)：每次构建唯一单词，便于现场区分&lt;br /&gt;
&lt;br /&gt;
更新位置 ：&lt;br /&gt;
Update locations:&lt;br /&gt;
&lt;br /&gt;
# 各仓库根 `Directory.Build.props` 中的 `&amp;lt;Version&amp;gt;`&lt;br /&gt;
# 各 csproj 中的 `&amp;lt;AssemblyInformationalVersion&amp;gt;`（含助记词）&lt;br /&gt;
# `CHANGELOG.md`（如果有）&lt;br /&gt;
# DEV_LOG.md 加一条发布条目&lt;br /&gt;
&lt;br /&gt;
== 2. 构建顺序 / Build order ==&lt;br /&gt;
按依赖自底向上 ：&lt;br /&gt;
Bottom-up:&lt;br /&gt;
&lt;br /&gt;
# `LessokajiWeaver` — `dotnet build -c Release`&lt;br /&gt;
# `Fundamentals` — 同&lt;br /&gt;
# `CycleGUI` —— C++ libVRender 先构建 + 复制 .dll 到 Windows / Linux 输出&lt;br /&gt;
# `M2` —— MedullaCore 先，OfficialPlugins 后&lt;br /&gt;
# `Detour` —— DetourCore，然后 Detour.exe（.NET 4.8）+ DetourLite.exe（.NET 8）&lt;br /&gt;
# `Clumsy` —— ClumsyCore，然后 ClumsyDance&lt;br /&gt;
# `Simple` —— SimpleCore，然后 SimpleComposer&lt;br /&gt;
&lt;br /&gt;
每个仓库构建后 ：&lt;br /&gt;
After each:&lt;br /&gt;
&lt;br /&gt;
* 验证 `Ref&amp;lt;Name&amp;gt;.dll` 已重新生成（如果公共 API 变了）&lt;br /&gt;
* 把新 Ref 复制到下游仓库的 `tools/` 目录&lt;br /&gt;
* 提交 Ref 到下游仓库的 git&lt;br /&gt;
&lt;br /&gt;
== 3. 烟雾测试 / Smoke tests ==&lt;br /&gt;
=== 单元 / Unit ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
dotnet test D:\src\Fundamentals\Test\&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
特别盯 DObject 测试 —— IPC bug 灾难性。&lt;br /&gt;
Watch DObject tests especially — IPC bugs are catastrophic.&lt;br /&gt;
&lt;br /&gt;
=== 集成 / Integration ===&lt;br /&gt;
仿真车 SimpleComposer ：&lt;br /&gt;
SimpleComposer with virtual car:&lt;br /&gt;
&lt;br /&gt;
# 启动 Medulla + 仿真 `Lidar + Cart` 插件&lt;br /&gt;
# 启动 DetourLite&lt;br /&gt;
# 启动 SimpleComposer，加载场景&lt;br /&gt;
# 派 5–10 个混合任务&lt;br /&gt;
# 观察 Hedingben 是否有红色 toast&lt;br /&gt;
# 检查任务完成率 100%&lt;br /&gt;
&lt;br /&gt;
=== 现场冒烟 / Site smoke ===&lt;br /&gt;
（可选 / 大改动时必做）在 ''一辆真车''上跑核心场景 ：&lt;br /&gt;
(For major changes) run the core scenarios on one real vehicle:&lt;br /&gt;
&lt;br /&gt;
* 巡线行走&lt;br /&gt;
* 自动识别工位并取放货&lt;br /&gt;
* 充电流程&lt;br /&gt;
* 异常停 + 恢复&lt;br /&gt;
&lt;br /&gt;
== 4. 一方插件重建 / Rebuild first-party plugins ==&lt;br /&gt;
所有 `D:\src\M2\OfficialPlugins\*` 都引用核心 Ref。任何核心 API 变更都要求 ：&lt;br /&gt;
All first-party plugins reference core Refs. Any core API change requires:&lt;br /&gt;
&lt;br /&gt;
# 拉取最新核心仓库&lt;br /&gt;
# `dotnet build -c Release` 各插件&lt;br /&gt;
# 在干净 Medulla 控制台测试加载（确保 Costura 内嵌没问题）&lt;br /&gt;
# 把生成的 `&amp;lt;plugin&amp;gt;.dll` 复制到发布 staging&lt;br /&gt;
&lt;br /&gt;
== 5. 打包 / Bundle ==&lt;br /&gt;
=== Medulla bundle ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
medulla-&amp;lt;version&amp;gt;.zip&lt;br /&gt;
├── MedullaConsole.exe&lt;br /&gt;
├── MedullaCore.dll&lt;br /&gt;
├── plugins/&lt;br /&gt;
│   ├── LidarController/LidarController.dll&lt;br /&gt;
│   ├── CartActivator/CartActivator.dll&lt;br /&gt;
│   ├── Camera/Camera.dll&lt;br /&gt;
│   └── ...&lt;br /&gt;
├── tools/&lt;br /&gt;
│   └── (运行时工具)&lt;br /&gt;
├── README.md&lt;br /&gt;
├── CHANGELOG.md&lt;br /&gt;
└── startup.iocmd.sample&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Detour bundle ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
detour-&amp;lt;version&amp;gt;.zip&lt;br /&gt;
├── Detour.exe              (or DetourLite.exe)&lt;br /&gt;
├── DetourCore.dll&lt;br /&gt;
├── CycleGUI.dll&lt;br /&gt;
├── libVRender.dll&lt;br /&gt;
├── README.md&lt;br /&gt;
└── CHANGELOG.md&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== SimpleComposer bundle ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
simplecomposer-&amp;lt;version&amp;gt;.zip&lt;br /&gt;
├── SimpleComposer.exe&lt;br /&gt;
├── SimpleCore.dll&lt;br /&gt;
├── plugins/  (空，客户填一方/三方车型 plugin)&lt;br /&gt;
├── README.md&lt;br /&gt;
└── CHANGELOG.md&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 6. 发布到 dl.lessokaji.com ==&lt;br /&gt;
* 上传 3 个 bundle 到 `dl.lessokaji.com`（内部 SFTP / S3）&lt;br /&gt;
* 路径 ：`releases/&amp;lt;component&amp;gt;/&amp;lt;version&amp;gt;/`&lt;br /&gt;
* 在 `auth.lessokaji.com` 注册新版本（用于授权门控）&lt;br /&gt;
&lt;br /&gt;
== 7. 文档更新 / Documentation update ==&lt;br /&gt;
* Wiki 发布说明 ：`wiki/Detour版本发布记录`，Medulla / Simple 各自的发布说明页&lt;br /&gt;
* 主页 `首页` 更新版本徽章（如有）&lt;br /&gt;
* HOW_TO_USE.md / HOW_TO_CUSTOMIZE.md / HOW_TO_DEV_CORE.md（在 `D:\src\cookbook\`）&lt;br /&gt;
&lt;br /&gt;
== 8. 通知 / Notify ==&lt;br /&gt;
* 内部团队 ：邮件 + Slack / 微信 群通知&lt;br /&gt;
* 客户 ：按其升级合约的节奏；major 变更要预约升级窗口&lt;br /&gt;
* CHANGELOG 高亮 ''行为变更''（不只是 &amp;quot;fix bug&amp;quot;）&lt;br /&gt;
&lt;br /&gt;
== 9. 回滚预案 / Rollback plan ==&lt;br /&gt;
每个 bundle 至少保留 ''前一版本''可下载。客户回滚 ：&lt;br /&gt;
Keep at least the prior version available. Client rollback:&lt;br /&gt;
&lt;br /&gt;
# 停服务&lt;br /&gt;
# 替换为旧 bundle&lt;br /&gt;
# 启动；验证关键场景&lt;br /&gt;
&lt;br /&gt;
''数据''兼容 ：&lt;br /&gt;
Data compatibility:&lt;br /&gt;
* DObject 协议（FundamentalLib 版本哈希）必须兼容；否则需重启全套&lt;br /&gt;
* SLAM 地图文件 ：通常向后兼容&lt;br /&gt;
* SimpleComposer 场景 JSON：通常向后兼容&lt;br /&gt;
* Plugin 接口（Ref assembly）：major 变更不兼容；minor 兼容&lt;br /&gt;
&lt;br /&gt;
== 10. 发布后 / Post-release ==&lt;br /&gt;
* 在 ''新版本'' Medulla 控制台跑一遍 `io load` + `cart Init` + 核心场景，做最后冒烟。&lt;br /&gt;
* 收集第一周客户反馈到 ''发布后看板''。&lt;br /&gt;
* DEV_LOG.md 加 &amp;quot;release N+0day notes&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
== 11. 紧急补丁 / Emergency patch ==&lt;br /&gt;
* 直接在 `release/&amp;lt;X.Y&amp;gt;` 分支上 fix&lt;br /&gt;
* Patch 版本号 + 助记词&lt;br /&gt;
* 通过 ''快速通道''跳过部分 smoke（在 hot incident 时）&lt;br /&gt;
* 但 ''必须''跑 DObject 单元测试&lt;br /&gt;
&lt;br /&gt;
== 12. 大版本规划 / Major version planning ==&lt;br /&gt;
* Pre-release ''alpha / beta''先给 ''友好客户''&lt;br /&gt;
* Beta 周期 ≥ 2 周&lt;br /&gt;
* Public release 必须有 ''迁移文档''（旧 → 新 API）&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/MDCS仓库布局与构建链|MDCS仓库布局与构建链]]&lt;br /&gt;
* [[Special:MyLanguage/MDCS跨切面约定|MDCS跨切面约定]]&lt;br /&gt;
* [[Special:MyLanguage/插件测试与发布|插件测试与发布]]&lt;br /&gt;
* [[Special:MyLanguage/Detour版本发布记录|Detour版本发布记录]]&lt;br /&gt;
* [[Special:MyLanguage/核心开发指南|核心开发指南]]&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=MDCS%E4%BB%93%E5%BA%93%E5%B8%83%E5%B1%80%E4%B8%8E%E6%9E%84%E5%BB%BA%E9%93%BE&amp;diff=1039</id>
		<title>MDCS仓库布局与构建链</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=MDCS%E4%BB%93%E5%BA%93%E5%B8%83%E5%B1%80%E4%B8%8E%E6%9E%84%E5%BB%BA%E9%93%BE&amp;diff=1039"/>
		<updated>2026-05-16T14:00:17Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
MDCS 由 7 个并列仓库组成；按依赖关系自底向上构建。本页详述各仓库职责、产物、构建顺序、引用程序集机制。&lt;br /&gt;
&lt;br /&gt;
MDCS consists of 7 sibling repositories built bottom-up by dependency. This page details responsibilities, outputs, build order, and the reference-assembly mechanism.&lt;br /&gt;
&lt;br /&gt;
== 仓库一览 / Repositories ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 仓库 / Repo !! 解决方案 / Solution !! 产物 / Output !! 角色 / Role&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\Fundamentals\` || Fundamentals.sln || FundamentalLib.dll, FundamentalLib2.dll, LessokajiProtect.dll, LessokajiLicencer.dll || 通用类型 + IPC + 日志 + 许可&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\LessokajiWeaver\` || LessokajiWeaver.sln || LessokajiWeaver.Fody.dll, LessokajiWeaverUtilities.dll || 编译期 IL 注入（Fody）&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\CycleGUI\` || CycleGUI.sln || CycleGUI.dll, libVRender.dll（C++/Vulkan）, webVRender（WASM）|| 懒协程 UI 框架&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\M2\` || M2.sln || MedullaCore.dll, OfficialPlugins/*.dll || 硬件抽象内核 + 一方插件（雷达 / 相机 / Cart）&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\Detour\` || Detour.sln || Detour.exe（.NET 4.8 GUI），DetourLite.exe（.NET 8 headless），DetourCore.dll || 定位系统&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\Clumsy\` || Clumsy.sln || ClumsyCore.dll, ClumsyDance.dll || 车载自动驾驶&lt;br /&gt;
|-&lt;br /&gt;
| `D:\src\Simple\` || Simple.sln || SimpleCore.dll, SimpleComposer.exe（Nancy HTTP）|| 车队管理&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 依赖图 / Dependency graph ==&lt;br /&gt;
&lt;br /&gt;
                 Fundamentals ◄────────── (everything)&lt;br /&gt;
                       ▲&lt;br /&gt;
                       │&lt;br /&gt;
                    CycleGUI ◄─ Detour ─── (M2, Clumsy, Simple use indirectly)&lt;br /&gt;
                       ▲           ▲&lt;br /&gt;
                       │           │&lt;br /&gt;
                       M2 ◄────── Clumsy ◄── Simple&lt;br /&gt;
                                     ▲&lt;br /&gt;
                                     └─── LessokajiWeaver (build-time only)&lt;br /&gt;
&lt;br /&gt;
== 构建顺序 / Build order ==&lt;br /&gt;
自底向上 / Bottom-up:&lt;br /&gt;
&lt;br /&gt;
# '''LessokajiWeaver''' — 构建时依赖（Fody 织入）&lt;br /&gt;
# '''Fundamentals''' — 用 Weaver&lt;br /&gt;
# '''CycleGUI''' — 用 Fundamentals；需 libVRender C++ 先构建&lt;br /&gt;
# '''M2 / MedullaCore''' — 用 Fundamentals + CycleGUI&lt;br /&gt;
# '''M2 / OfficialPlugins/\*''' — 用 MedullaCore&lt;br /&gt;
# '''Detour / DetourCore''' — 用 Fundamentals + CycleGUI&lt;br /&gt;
# '''Clumsy / ClumsyCore''' → '''ClumsyDance'''&lt;br /&gt;
# '''Simple / SimpleCore''' → '''SimpleComposer'''&lt;br /&gt;
&lt;br /&gt;
每个仓库独立 .sln；可单独 `dotnet build` / 在 IDE 打开。&lt;br /&gt;
Each repo has its own .sln; independently buildable.&lt;br /&gt;
&lt;br /&gt;
== 引用程序集机制 / Reference-assembly mechanism ==&lt;br /&gt;
=== 概念 / Concept ===&lt;br /&gt;
每个核心 DLL 同时产生一个 ''仅引用''的程序集 `Ref&amp;lt;Name&amp;gt;.dll`，通过 [[https://github.com/JetBrains/Refasmer refasmer]] 工具生成。&lt;br /&gt;
&lt;br /&gt;
Each core DLL also emits a '''reference-only assembly''' (`Ref&amp;lt;Name&amp;gt;.dll`) via refasmer.&lt;br /&gt;
&lt;br /&gt;
例如 `MedullaCore.dll` → `RefMedullaCore.dll`：后者只含 public/protected API 签名，没有方法体。&lt;br /&gt;
E.g. `MedullaCore.dll` → `RefMedullaCore.dll`: signature-only.&lt;br /&gt;
&lt;br /&gt;
=== 为什么 / Why ===&lt;br /&gt;
插件 csproj ''引用'' `Ref&amp;lt;Name&amp;gt;.dll`，不引用 impl DLL ：&lt;br /&gt;
Plugin csproj references the `Ref` assembly:&lt;br /&gt;
&lt;br /&gt;
* 插件构建不会拉入 Costura 织入的传递依赖（FodyWeavers.xml + LessokajiWeaver 等）。&lt;br /&gt;
* 公共 API 由 Ref assembly 固定 —— `internal` 成员对插件不可见，强制良好的封装。&lt;br /&gt;
* 插件 DLL 体积小、加载快。&lt;br /&gt;
&lt;br /&gt;
* No Costura transitives pulled into plugin builds.&lt;br /&gt;
* Public API frozen by the Ref assembly; `internal` hidden from plugins — enforced encapsulation.&lt;br /&gt;
* Smaller plugin DLLs, faster load.&lt;br /&gt;
&lt;br /&gt;
=== 生成 / Generation ===&lt;br /&gt;
每个核心 csproj 有 ''AfterBuild'' 目标调 refasmer，输出到 `&amp;lt;repo&amp;gt;/tools/Ref&amp;lt;Name&amp;gt;.dll`：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;xml&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;Target Name=&amp;quot;GenerateRefAssembly&amp;quot; AfterTargets=&amp;quot;Build&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;Exec Command=&amp;quot;refasmer $(TargetPath) -o $(SolutionDir)tools\Ref$(AssemblyName).dll&amp;quot; /&amp;gt;&lt;br /&gt;
&amp;lt;/Target&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
详见 `D:\src\M2\OfficialPlugins\CartActivator\CartActivator.csproj:47-54`。&lt;br /&gt;
&lt;br /&gt;
=== 更新流程 / Update flow ===&lt;br /&gt;
改动 `MedullaCore` 公共面 → 重新 build → 同时 ''rebuild Ref assembly'' → 把 ''两个 DLL''（impl + ref）都提交到仓库。&lt;br /&gt;
&lt;br /&gt;
Change `MedullaCore` public surface → build → rebuild Ref → commit '''both'''.&lt;br /&gt;
&lt;br /&gt;
【注意】 插件 csproj 用旧 `Ref` 构建 ''不会出错''，但运行时调新成员会 `MissingMethodException`。&amp;lt;br&amp;gt;&lt;br /&gt;
A plugin built against the old Ref doesn't fail to compile but throws `MissingMethodException` at runtime when calling the new member.&lt;br /&gt;
&lt;br /&gt;
== `tools/` 目录约定 / `tools/` directory convention ==&lt;br /&gt;
每个仓库根有 ''`tools/`'' 子目录，存放 ''其它仓库的 Ref assembly'' + 二进制工具（refasmer.exe 等）。插件 csproj 通过相对 HintPath 引用 ：&lt;br /&gt;
&lt;br /&gt;
Each repo root has a `tools/` subdirectory containing reference assemblies from other repos + binary tools (refasmer.exe). Plugin csproj uses relative HintPaths:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;xml&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;Reference Include=&amp;quot;RefFundamentalLib&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;HintPath&amp;gt;..\tools\RefFundamentalLib.dll&amp;lt;/HintPath&amp;gt;&lt;br /&gt;
&amp;lt;/Reference&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
【发现】 `..\tools\` 是 ''仓库一致''约定 —— 每个 MDCS 插件 csproj 都这么写。不要改成 NuGet 包；现有工具链不支持。&lt;br /&gt;
The `..\tools\` HintPath pattern is repo-wide. Don't try to NuGet-ify it.&lt;br /&gt;
&lt;br /&gt;
== Costura.Fody 胖 DLL 打包 / Fat DLL packaging ==&lt;br /&gt;
插件 / 客户端 exe 用 ''Costura.Fody''把所有依赖打到一个 DLL 的 base64 资源里。最终客户只需要 1 个文件。&lt;br /&gt;
Plugins / client exes use Costura.Fody to embed all deps as base64 resources in one DLL. Customer deploys one file.&lt;br /&gt;
&lt;br /&gt;
注意 ：&lt;br /&gt;
* '''核心 DLL（MedullaCore, ClumsyCore, ...）不用 Costura'''。它们要被多个插件 ''共载''，loose DLL 必要。&lt;br /&gt;
* '''插件 / 顶层 exe 用 Costura'''。简化部署。&lt;br /&gt;
&lt;br /&gt;
Note: '''Core DLLs (MedullaCore, ClumsyCore, ...) must not use Costura''' — they need to be shared between plugins. '''Plugins / top-level exes use Costura''' for deployment simplicity.&lt;br /&gt;
&lt;br /&gt;
== LessokajiWeaver 编译期注入 / Compile-time IL injection ==&lt;br /&gt;
Fody 加上 `LessokajiWeaver.Fody.dll` 在编译后做 IL 重写。当前生效的规则 ：&lt;br /&gt;
Fody + `LessokajiWeaver.Fody.dll` rewrites IL post-compile. Active rules:&lt;br /&gt;
&lt;br /&gt;
* `[AsInitParam] / [AsUpperIO] / [AsLowerIO]` 字段 → 自动同步 DObject 的 property&lt;br /&gt;
* `[ReplaceWithCompileInfo]` 字段 → 编译时戳 + git 哈希&lt;br /&gt;
* `[Translatable] / [T(zh=, en=)]` 字段 → getter 按 `CultureInfo.CurrentUICulture` 分派&lt;br /&gt;
&lt;br /&gt;
详见 [[Special:MyLanguage/LessokajiWeaver编译后处理工具|LessokajiWeaver编译后处理工具]] 与 [[Special:MyLanguage/如何使用LessokajiWeaver的多语言功能|如何使用LessokajiWeaver的多语言功能]]。&lt;br /&gt;
&lt;br /&gt;
== 跨平台 / Cross-platform ==&lt;br /&gt;
* 多数仓库 .NET 6/8，部分 .NET Framework 4.8（旧 lidar driver / Detour.exe）。&lt;br /&gt;
* Linux 支持 ：Medulla, Detour (Lite), Clumsy, SimpleComposer 都可跑。&lt;br /&gt;
* CycleGUI 的 libVRender 在 Linux 上有 build；Windows 是主战场。&lt;br /&gt;
&lt;br /&gt;
== 仓库间集成测试 / Cross-repo integration ==&lt;br /&gt;
没有单一 mega-solution。集成测试靠：&lt;br /&gt;
There's no monolithic solution. Integration tests rely on:&lt;br /&gt;
&lt;br /&gt;
# 改动核心后重 build 所有受影响插件。&lt;br /&gt;
# 在仿真车 SimpleComposer 里跑完整任务序列。&lt;br /&gt;
# `D:\src\Fundamentals\Test\` 单元测试（DObject + DLog 等关键基础）。&lt;br /&gt;
&lt;br /&gt;
== 仓库镜像 / Repo mirror ==&lt;br /&gt;
* 内部 git：`git.lessokaji.com`（私有）。&lt;br /&gt;
* 下载站：`dl.lessokaji.com`（编译产物）。&lt;br /&gt;
* 认证 / 许可：`auth.lessokaji.com`。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/核心开发指南|核心开发指南]]&lt;br /&gt;
* [[Special:MyLanguage/MDCS跨切面约定|MDCS跨切面约定]]&lt;br /&gt;
* [[Special:MyLanguage/MDCS平台发布流程|MDCS平台发布流程]]&lt;br /&gt;
* [[Special:MyLanguage/LessokajiWeaver编译后处理工具|LessokajiWeaver编译后处理工具]]&lt;br /&gt;
* [[Special:MyLanguage/Medulla软件架构|Medulla软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/Detour软件架构|Detour软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/Simple软件架构|Simple软件架构]]&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=LadderLogic%E6%A1%86%E6%9E%B6&amp;diff=1038</id>
		<title>LadderLogic框架</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=LadderLogic%E6%A1%86%E6%9E%B6&amp;diff=1038"/>
		<updated>2026-05-16T14:00:15Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
'''LadderLogic''' 是 Medulla 提供的 ''周期循环宿主''。CartDefinition 插件用嵌套类 `: LadderLogic&amp;lt;MyCart&amp;gt;` + `[UseLadderLogic(IntervalMs = N)]` 声明一个定时执行的控制循环；框架按 N 毫秒间隔调用 `Operation(int iteration)`。多个循环可在同一 Cart 上共存，各自在独立线程。&lt;br /&gt;
&lt;br /&gt;
'''LadderLogic''' is Medulla's ''cyclic-loop host''. A CartDefinition plugin declares a timed loop via a nested class `: LadderLogic&amp;lt;MyCart&amp;gt;` plus `[UseLadderLogic(IntervalMs = N)]`; the framework calls `Operation(int iteration)` every N ms. Multiple loops can coexist on one cart, each on its own thread.&lt;br /&gt;
&lt;br /&gt;
实现：`D:\src\M2\OfficialPlugins\CartActivator\LadderLogic.cs:7-272`，属性定义 `D:\src\M2\OfficialPlugins\CartActivator\Attrs.cs:26-36`。&lt;br /&gt;
Implementation: `D:\src\M2\OfficialPlugins\CartActivator\LadderLogic.cs:7-272`; attribute at `Attrs.cs:26-36`.&lt;br /&gt;
&lt;br /&gt;
== 最小骨架 / Minimal skeleton ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class MyCart : CartDefinition&lt;br /&gt;
{&lt;br /&gt;
    [AsUpperIO] public float vCmd;&lt;br /&gt;
    [AsLowerIO] public float vEst;&lt;br /&gt;
&lt;br /&gt;
    public override void Init()&lt;br /&gt;
    {&lt;br /&gt;
        // connect to hardware here&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    [UseLadderLogic(IntervalMs = 50)]&lt;br /&gt;
    public class Loop : LadderLogic&amp;lt;MyCart&amp;gt;&lt;br /&gt;
    {&lt;br /&gt;
        public override void Operation(int iteration)&lt;br /&gt;
        {&lt;br /&gt;
            // iteration == 0 on first tick after Init/restart&lt;br /&gt;
            self.WriteToHw(self.vCmd);&lt;br /&gt;
            self.vEst = self.ReadFromHw();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 属性参数 / Attribute parameters ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 字段 / Field !! 默认 / Default !! 含义 / Meaning&lt;br /&gt;
|-&lt;br /&gt;
| `logic: Type` || (自动)|| LadderLogic 子类（嵌套类自动绑定）&lt;br /&gt;
|-&lt;br /&gt;
| `scanInterval: int` (or `IntervalMs`)|| 50 || tick 周期（ms）&lt;br /&gt;
|-&lt;br /&gt;
| `realTime: bool` || false || 保留，暂未使用 / reserved&lt;br /&gt;
|-&lt;br /&gt;
| `resume: bool` || true || 异常后自动重启循环 / auto-restart on exception&lt;br /&gt;
|-&lt;br /&gt;
| `resumeRetryDelay: int` || 1000 || 重启前等多久（ms）&lt;br /&gt;
|-&lt;br /&gt;
| `timeoutInterval: double` || 10000 || 保留 / reserved&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 生命周期 / Lifecycle ==&lt;br /&gt;
&lt;br /&gt;
  CartDefinition.Init()                  ← 你的初始化&lt;br /&gt;
       │&lt;br /&gt;
       ▼&lt;br /&gt;
  CartDefinition.createLadders()         ← 反射扫 [UseLadderLogic]&lt;br /&gt;
       │&lt;br /&gt;
       ▼ 对每个属性 / for each attribute&lt;br /&gt;
  实例化 LadderLogic&amp;lt;T&amp;gt;, 注入 self = this&lt;br /&gt;
       │&lt;br /&gt;
       ▼&lt;br /&gt;
  创建 MultimediaTimer + 工作线程&lt;br /&gt;
       │&lt;br /&gt;
       ▼ 周期触发 / every scanInterval ms&lt;br /&gt;
  Operation(iteration: 0)   ← 第一次 / first tick&lt;br /&gt;
  Operation(iteration: 1)&lt;br /&gt;
  Operation(iteration: 2)&lt;br /&gt;
  ...&lt;br /&gt;
       │&lt;br /&gt;
       ▼ 异常时 / on exception&lt;br /&gt;
  Hedingben.ToastText(...)&lt;br /&gt;
  如果 resume=true 等 resumeRetryDelay 后重启 / resume after delay&lt;br /&gt;
&lt;br /&gt;
== Tick 精度 / Tick precision ==&lt;br /&gt;
基于 MultimediaTimer（OS 决定，Windows 约 1–15 ms 分辨率）。&lt;br /&gt;
Based on MultimediaTimer; OS-dependent (~1–15 ms on Windows).&lt;br /&gt;
&lt;br /&gt;
'''超时行为''' / Overrun: 若 `Operation()` 耗时 &amp;gt; scanInterval，下一 tick 排队执行（不丢 tick）。长期超时会累积延迟。&lt;br /&gt;
If `Operation()` exceeds scanInterval, the next tick queues behind (no tick skipping). Sustained overrun accrues latency.&lt;br /&gt;
&lt;br /&gt;
【注意】 不要在 `Operation()` 里阻塞 / sleep / 等 IO 超过 scanInterval。&amp;lt;br&amp;gt;&lt;br /&gt;
Don't block / sleep / wait IO longer than scanInterval inside `Operation()`.&lt;br /&gt;
&lt;br /&gt;
== 多 LadderLogic 共存 / Multiple loops ==&lt;br /&gt;
一个 CartDefinition 可以有多个 `[UseLadderLogic]` 属性，每个一对独立的线程 + Timer：&lt;br /&gt;
&lt;br /&gt;
A CartDefinition can declare multiple loops, each on its own thread:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[UseLadderLogic(IntervalMs = 50)]&lt;br /&gt;
public class CtrlLoop : LadderLogic&amp;lt;MyCart&amp;gt; { ... }   // motion control 50 Hz&lt;br /&gt;
&lt;br /&gt;
[UseLadderLogic(IntervalMs = 200)]&lt;br /&gt;
public class BatteryLoop : LadderLogic&amp;lt;MyCart&amp;gt; { ... }  // battery / telemetry 5 Hz&lt;br /&gt;
&lt;br /&gt;
[UseLadderLogic(IntervalMs = 1000)]&lt;br /&gt;
public class DiagLoop : LadderLogic&amp;lt;MyCart&amp;gt; { ... }     // health 1 Hz&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
线程之间共享 cart 字段；如果同一字段同时被多个 loop 读写，自己处理同步（lock / Interlocked）。&lt;br /&gt;
Threads share cart fields; if multiple loops touch the same field, synchronise (lock / Interlocked).&lt;br /&gt;
&lt;br /&gt;
== 内置工具方法 / Built-in helpers ==&lt;br /&gt;
`LadderLogic&amp;lt;T&amp;gt;` 基类提供以下状态机助手（`LadderLogic.cs:50-272`）：&lt;br /&gt;
The `LadderLogic&amp;lt;T&amp;gt;` base class offers these state-machine helpers (`LadderLogic.cs:50-272`):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 方法 / Method !! 用途 / Use&lt;br /&gt;
|-&lt;br /&gt;
| `TriggerOnce(bool active, int millis, Action a)` || 信号持续 `millis` ms 后触发 `a()` 一次 / fire once after signal held `millis` ms&lt;br /&gt;
|-&lt;br /&gt;
| `WaitSignal(bool sig, int timeout)` || 等信号上升或超时 / wait for signal rising or timeout&lt;br /&gt;
|-&lt;br /&gt;
| `TriggerIfChanged&amp;lt;T&amp;gt;(Func&amp;lt;T&amp;gt; g, Action&amp;lt;T&amp;gt; h)` || 值变化触发 / fire when value changes&lt;br /&gt;
|-&lt;br /&gt;
| `FlipFlop&amp;lt;T&amp;gt;(ref T field, int millis, T[] vals)` || 周期切换值 / cycle values periodically (e.g. blinking LED)&lt;br /&gt;
|-&lt;br /&gt;
| `SwitcherTrigger(bool, Action, bool)` || 边沿触发一次 / edge-trigger one-shot&lt;br /&gt;
|-&lt;br /&gt;
| `ModSignal(Func&amp;lt;bool&amp;gt;, int delay, int span)` || 时间窗内信号检测 / time-window detection&lt;br /&gt;
|-&lt;br /&gt;
| `IsochronousFork(Action)` || &amp;quot;同步分叉&amp;quot;，一次只跑一个 / one-at-a-time async fork&lt;br /&gt;
|-&lt;br /&gt;
| `PriorityActions` || 高优先级动作执行器 / priority action runner&lt;br /&gt;
|-&lt;br /&gt;
| `DaemonVal(float send, float read, float r, int t, Action&amp;lt;bool&amp;gt;)` || 多变量一致性看门狗 / multi-var consistency watcher&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
用这些替代裸 `if (Math.Abs(now - lastEvent) &amp;gt; T) ...` 的散乱时间状态机。&lt;br /&gt;
&lt;br /&gt;
Use these helpers to replace ad-hoc `if (Math.Abs(now - lastEvent) &amp;gt; T) ...` time state-machines scattered through the loop.&lt;br /&gt;
&lt;br /&gt;
== 错误传播 / Error propagation ==&lt;br /&gt;
* `Operation()` 抛异常 → 框架捕获 → log 到 DLog + Hedingben toast `&amp;quot;Ladder broken: &amp;lt;msg&amp;gt;&amp;quot;` 在 UI 显示。&lt;br /&gt;
* 若 `resume = true`：等 `resumeRetryDelay` 后重新调用 `Operation(iteration: 0)`（iteration 重置）。&lt;br /&gt;
* 若 `resume = false`：循环退出，cart 进入&amp;quot;半工作&amp;quot;状态 —— 其它 loop 还在跑。&lt;br /&gt;
&lt;br /&gt;
【发现】 `resume = true` 是默认值。一个 buggy 插件不会让整个进程崩，但会 ''无限重试'' —— 务必在开发期密切看 Hedingben。&amp;lt;br&amp;gt;&lt;br /&gt;
`resume = true` is the default. A buggy plugin won't crash the process but will silently retry forever — watch Hedingben during development.&lt;br /&gt;
&lt;br /&gt;
== iteration 参数 / iteration parameter ==&lt;br /&gt;
* `iteration == 0`：首次 tick 或异常重启后的首次 tick；框架自动清空内部跟踪状态（如 `TriggerOnce` 的计时器）。&lt;br /&gt;
* `iteration &amp;gt; 0`：递增；可用来做 &amp;quot;每 N tick 做一次&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
== 调试 / Debugging ==&lt;br /&gt;
* `Hedingben` toast 是最直接的报错通道 —— 总打开。&lt;br /&gt;
* DLog 有详细 stack trace 用 `$ladder` 主题。&lt;br /&gt;
* 用 `[IOObjectMonitor]` 暴露 `iteration` 或自定义计数器到 Medulla dashboard 看跳速。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/CartDefinition属性参考|CartDefinition属性参考]]&lt;br /&gt;
* [[Special:MyLanguage/MDCS引擎适配机器人入门教学|MDCS引擎适配机器人入门教学]]&lt;br /&gt;
* [[Special:MyLanguage/Medulla软件架构|Medulla软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/看门狗Wawa使用说明|看门狗Wawa使用说明]]&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=IOObject%E5%B1%9E%E6%80%A7%E5%8F%82%E8%80%83&amp;diff=1037</id>
		<title>IOObject属性参考</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=IOObject%E5%B1%9E%E6%80%A7%E5%8F%82%E8%80%83&amp;diff=1037"/>
		<updated>2026-05-16T14:00:13Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
本页是 Medulla `IOObject` 基类与其上 ''全部装饰属性''的速查表。属性定义在 `D:\src\M2\MedullaCore\Types\IO.cs:9-70`。&lt;br /&gt;
&lt;br /&gt;
This page is the cheat sheet for the Medulla `IOObject` base class and ''all its decorating attributes''. Attribute definitions at `D:\src\M2\MedullaCore\Types\IO.cs:9-70`.&lt;br /&gt;
&lt;br /&gt;
== IOObject 基类 / IOObject base ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 成员 / Member !! 类型 / Type !! 含义 / Meaning&lt;br /&gt;
|-&lt;br /&gt;
| `Name` || `string` || 实例名（由 startup.iocmd 加载时设置）/ instance name set by the script engine on load&lt;br /&gt;
|-&lt;br /&gt;
| `TimeoutThreshold` || `float = 500` || 预期更新间隔（ms）；框架不强制，插件自己用 / expected update interval, plugin-enforced&lt;br /&gt;
|-&lt;br /&gt;
| `Methods` || `string[]` || 反射回填的公共方法名 / reflection-backed; for UI auto-binding&lt;br /&gt;
|-&lt;br /&gt;
| `OpenUI(Terminal)` || virtual || 打开 UI 面板的钩子 / UI panel creation hook&lt;br /&gt;
|-&lt;br /&gt;
| `CloseUI()` || virtual || 关闭 UI / UI teardown&lt;br /&gt;
|-&lt;br /&gt;
| `SwitchTerminal(Terminal)` || virtual || 把 UI 迁移到新 Terminal / migrate UI to a new Terminal&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
`MainIOObject` 类名是 Medulla 反射查找的硬约束（`MedullaIO.cs:299`）。&lt;br /&gt;
The class name `MainIOObject` is a hard-coded reflection lookup in `MedullaIO.cs:299`.&lt;br /&gt;
&lt;br /&gt;
== 装饰属性 / Decorating attributes ==&lt;br /&gt;
=== [IOObjectMonitor] ===&lt;br /&gt;
'''目标''' / Target: 字段 / field&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 字段值导出到 CycleGUI 的属性栅格；如果 `VerboseLog = true`，每次变化记到 DLog。&lt;br /&gt;
Exports the field to CycleGUI's property grid; logs to DLog if `VerboseLog = true`.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[IOObjectMonitor]&lt;br /&gt;
public float currentVelocity;&lt;br /&gt;
&lt;br /&gt;
[IOObjectMonitor(desc = &amp;quot;电池剩余百分比 / Battery percentage&amp;quot;)]&lt;br /&gt;
public int batteryPercent;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== [IOObjectUtility] ===&lt;br /&gt;
'''目标''' / Target: 方法（仅 ''零参数''）/ method (zero-arg only)&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 在 UI 的 &amp;quot;Actions&amp;quot; 组中显示为按钮。&lt;br /&gt;
Renders as a button in the UI's &amp;quot;Actions&amp;quot; group.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[IOObjectUtility]&lt;br /&gt;
public void StartScan() { /* trigger device start */ }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== [IOObjectWebUtility] ===&lt;br /&gt;
'''目标''' / Target: 方法 / method&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 自动注册 HTTP 端点 `/&amp;lt;IOName&amp;gt;/&amp;lt;MethodName&amp;gt;`。&lt;br /&gt;
Auto-registers an HTTP endpoint `/&amp;lt;IOName&amp;gt;/&amp;lt;MethodName&amp;gt;`.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[IOObjectWebUtility(type = WebUtilityType.API)]&lt;br /&gt;
public byte[] GetCurrentFrame() { /* return JSON or binary */ }&lt;br /&gt;
&lt;br /&gt;
[IOObjectWebUtility(type = WebUtilityType.WebPage)]&lt;br /&gt;
public byte[] StatusPage() { /* return HTML */ }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
子类型 / Subtypes (`type` enum):&lt;br /&gt;
* `WebPage` — 返回 `byte[]`，作为完整页面 / returns `byte[]` as a full page&lt;br /&gt;
* `API` — JSON API&lt;br /&gt;
* `Utility` — 普通工具方法&lt;br /&gt;
* `DataChunk` — 大块二进制数据（流式 / chunked）&lt;br /&gt;
&lt;br /&gt;
=== [IOObjectWatch] ===&lt;br /&gt;
'''目标''' / Target: 字段 / field&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 看门狗。当 JS 表达式 `jsWatcher` 在字段上为 true → 触发告警模板 `jsAlertTemplate`。&lt;br /&gt;
Watchdog. When the JS predicate `jsWatcher` on the field evaluates true → fires the alert template `jsAlertTemplate`.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[IOObjectWatch(&lt;br /&gt;
    watchVal = &amp;quot;value&amp;quot;,&lt;br /&gt;
    jsWatcher = &amp;quot;value &amp;lt; 10&amp;quot;,&lt;br /&gt;
    jsAlertTemplate = &amp;quot;Battery critically low: ${value}%&amp;quot;,&lt;br /&gt;
    jsAlertColorTemplate = &amp;quot;red&amp;quot;,&lt;br /&gt;
    holdIfAlert = true)]&lt;br /&gt;
public int batteryPercent;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== [IOObjectSerialize] ===&lt;br /&gt;
'''目标''' / Target: 类 / class&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 自定义序列化器。当前 build 未使用 / unused in current build.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[IOObjectSerialize(Serializer = typeof(MyCustomSerializer))]&lt;br /&gt;
public class MyIO : IOObject { ... }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 与 CartDefinition 属性的区别 / vs CartDefinition attributes ==&lt;br /&gt;
注意 `[AsUpperIO] / [AsLowerIO] / [AsInitParam] / [UseLadderLogic]` 不是 `IOObject` 的属性 —— 它们属于 `CartDefinition`（位于 `CartActivator` 插件）。详见 [[Special:MyLanguage/CartDefinition属性参考|CartDefinition属性参考]]。&lt;br /&gt;
&lt;br /&gt;
The `[AsUpperIO]` / `[AsLowerIO]` / `[AsInitParam]` / `[UseLadderLogic]` attributes are NOT on `IOObject` — they live in the `CartActivator` plugin's `CartDefinition` family. See [[Special:MyLanguage/CartDefinition属性参考|CartDefinition属性参考]].&lt;br /&gt;
&lt;br /&gt;
== Web 端点路由 / Web endpoint routing ==&lt;br /&gt;
Medulla 启动 HTTP server（`Startup.cs:204-256`）后：&lt;br /&gt;
After Medulla starts its HTTP server:&lt;br /&gt;
&lt;br /&gt;
* `GET /getIOs` — 列出所有加载的 IOObject + 它们的字段 + 方法 + 端点&lt;br /&gt;
* `GET/POST /&amp;lt;IOName&amp;gt;/&amp;lt;MethodName&amp;gt;` — 调用 `[IOObjectWebUtility]` 方法&lt;br /&gt;
* `GET /&amp;lt;IOName&amp;gt;/&amp;lt;FieldName&amp;gt;` — 读 `[IOObjectMonitor]` 字段（如果配置开放）&lt;br /&gt;
&lt;br /&gt;
端口由 `MedullaIO.CPort` 控制（默认 8081 / `MedullaIO.cs:34`）。&lt;br /&gt;
&lt;br /&gt;
Port controlled by `MedullaIO.CPort` (default 8081).&lt;br /&gt;
&lt;br /&gt;
== 调试技巧 / Debug tips ==&lt;br /&gt;
* '''字段不出现在 dashboard''' → 检查 `[IOObjectMonitor]` 是否标在 ''public 字段''上。&lt;br /&gt;
* '''按钮不出现''' → `[IOObjectUtility]` 标在 ''public 零参数方法''上吗？&lt;br /&gt;
* '''Web 端点 404''' → IO 名是否正确？看 `/getIOs` 列表确认。&lt;br /&gt;
* '''告警不触发''' → `jsWatcher` 表达式语法错误是常见原因；用 `console.log(value)` 调试。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/CartDefinition属性参考|CartDefinition属性参考]]&lt;br /&gt;
* [[Special:MyLanguage/插件契约与打包约定|插件契约与打包约定]]&lt;br /&gt;
* [[Special:MyLanguage/Medulla软件架构|Medulla软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/Medulla|Medulla]] / [[Special:MyLanguage/Medulla-API|Medulla-API]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=DriveTask%E8%B0%83%E5%BA%A6%E5%BE%AA%E7%8E%AF&amp;diff=1036</id>
		<title>DriveTask调度循环</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=DriveTask%E8%B0%83%E5%BA%A6%E5%BE%AA%E7%8E%AF&amp;diff=1036"/>
		<updated>2026-05-16T14:00:12Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
`DriveTask` 是 Clumsy 内部把 [[Special:MyLanguage/MovementDefinition详解|`MovementDefinition`]] 的 ''迭代器''变成 ''周期控制循环''的工具。每次调用 `DriveTask.WaitDriveTask(IEnumerable&amp;lt;bool&amp;gt;)` ：&lt;br /&gt;
1. 起一条 ''新线程''（`ThreadPriority.AboveNormal`）。&lt;br /&gt;
2. 每隔 `Configuration.conf.DriveTaskInterval`（默认 20 ms）调一次 `enumerator.MoveNext()`。&lt;br /&gt;
3. 阻塞调用方直到迭代器结束（正常完成或异常）。&lt;br /&gt;
&lt;br /&gt;
`DriveTask` is Clumsy's mechanism for turning a [[Special:MyLanguage/MovementDefinition详解|MovementDefinition]]'s iterator into a '''timed control loop'''. Each `DriveTask.WaitDriveTask(IEnumerable&amp;lt;bool&amp;gt;)` call:&lt;br /&gt;
1. Spawns a new thread at `ThreadPriority.AboveNormal`.&lt;br /&gt;
2. Calls `enumerator.MoveNext()` every `Configuration.conf.DriveTaskInterval` (default 20 ms).&lt;br /&gt;
3. Blocks the caller until the iterator ends (success or exception).&lt;br /&gt;
&lt;br /&gt;
实现：`D:\src\Clumsy\ClumsyCore\DriveTask.cs:34-205`。&lt;br /&gt;
Implementation: `D:\src\Clumsy\ClumsyCore\DriveTask.cs:34-205`.&lt;br /&gt;
&lt;br /&gt;
== 标准用法 / Standard usage ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
DriveTask.WaitDriveTask(new SteeringLineFollowing&lt;br /&gt;
{&lt;br /&gt;
    srcX = sx, srcY = sy,&lt;br /&gt;
    dstX = dx, dstY = dy,&lt;br /&gt;
    basespeed = 600&lt;br /&gt;
}.Follow());&lt;br /&gt;
// Returns when the Movement finishes (normal or thrown)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 循环细节 / Loop details ==&lt;br /&gt;
&lt;br /&gt;
  WaitDriveTask(IEnumerable&amp;lt;bool&amp;gt;) 入口:&lt;br /&gt;
       │&lt;br /&gt;
       ▼&lt;br /&gt;
  new DriveTask:&lt;br /&gt;
    enumerator = iterable.GetEnumerator();&lt;br /&gt;
    Thread(prio=AboveNormal) starts:&lt;br /&gt;
       │&lt;br /&gt;
       ▼ 循环 / loop:&lt;br /&gt;
    if (stopToken) break;&lt;br /&gt;
    bool keepGoing = enumerator.MoveNext();&lt;br /&gt;
    if (!keepGoing) { stopped_reason = 0; break; }&lt;br /&gt;
    Thread.Sleep(DriveTaskInterval);   // 20 ms 默认&lt;br /&gt;
       │&lt;br /&gt;
       ▼ 退出 / exit (any cause):&lt;br /&gt;
    Monitor.PulseAll(wait)&lt;br /&gt;
       │&lt;br /&gt;
       ▼&lt;br /&gt;
  WaitDriveTask blocks on Monitor.Wait(wait)&lt;br /&gt;
       └─→ 醒来后检查 exception，重新抛 / wakes, rethrows captured exception if any&lt;br /&gt;
&lt;br /&gt;
== `DoUntil` 并行检查 / Parallel termination check ==&lt;br /&gt;
`DoUntil(IEnumerable&amp;lt;bool&amp;gt; condition)` 起一条 ''并行''任务，独立轮询 `condition`；若 `condition.MoveNext()` 返回 true，停止主 Movement 并设置 `stopped_reason = stop_condition_id`：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
var driveTask = DriveTask.WaitDriveTaskBegin(new SteeringLineFollowing { ... }.Follow());&lt;br /&gt;
driveTask.DoUntil(new ObstacleCloseCheck { distThres = 200 }.Get());&lt;br /&gt;
driveTask.Wait();  // blocks; returns when either main Movement done OR DoUntil fires&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
`stopped_reason` 区分了正常完成 (0)、`DoUntil` 触发（其条件 id）、异常 (-1)。&lt;br /&gt;
`stopped_reason` differentiates normal (0), `DoUntil` triggered (the cond ID), and exception (-1).&lt;br /&gt;
&lt;br /&gt;
== 与 SecurityCheck 的关系 / Relation to SecurityCheck ==&lt;br /&gt;
`PilotBase.SecurityCheck()`（`D:\src\Clumsy\ClumsyCore\Pilot\PilotDefinition.cs:64-75`）是 ''独立''的看门狗，每个 Movement 都不感知它。SecurityCheck 监测 ：&lt;br /&gt;
* 当前 DriveTask 是否在 `Configuration.conf.DriveTaskTimeout`（默认 300 ms）内有控制信号产生。&lt;br /&gt;
* 若没有 → 调 `DriveStop()` 强制车停 → 主 DriveTask 的 `stopToken = true` → 下一 tick 退出循环。&lt;br /&gt;
&lt;br /&gt;
【发现】 SecurityCheck 不杀 Movement 的 enumerator；它只是让 DriveTask 在下一 tick 退出循环。所以 ''Movement 作者需自己确保 `Get()` 在迭代器内可被中断''（不要 sleep 长时间）。&amp;lt;br&amp;gt;&lt;br /&gt;
SecurityCheck doesn't kill the enumerator; it just lets the loop bail next tick. Movement authors must keep `Get()` interruptible (no long sleeps).&lt;br /&gt;
&lt;br /&gt;
== 异常传播 / Exception propagation ==&lt;br /&gt;
* `MoveNext()` 抛异常 → 捕获到 `_exception` 字段 → `stopped_reason = -1` → 触发 `Monitor.PulseAll`。&lt;br /&gt;
* `WaitDriveTask` 调用方 ''醒来后''重新抛该异常。&lt;br /&gt;
* 上游 `SimpleAgvInterface.Queue` 的 action 内异常 → 记到 `queueInfo[i].exception`，跳过本行剩余 action。&lt;br /&gt;
&lt;br /&gt;
【注意】 不要把异常 ''吞''在 Movement 内。让它传出，让上层决定恢复策略。&amp;lt;br&amp;gt;&lt;br /&gt;
Don't swallow exceptions inside Movements; let them propagate so the upper layer decides.&lt;br /&gt;
&lt;br /&gt;
== Thread Priority 与延迟 / Thread priority &amp;amp; latency ==&lt;br /&gt;
* DriveTask 线程是 `AboveNormal`，比 LadderLogic 略高。&lt;br /&gt;
* tick 抖动主要来自 `Thread.Sleep` 精度（Windows ~1–15 ms 量级）。&lt;br /&gt;
* 高速 / 高精度场景（如 [[Special:MyLanguage/汽车面差检测|汽车面差检测]]）可能需要在 PLC 层做关键回路，DriveTask 仅做高层规划。&lt;br /&gt;
&lt;br /&gt;
== 在 SimpleAgvInterface 中的使用 / Usage in SimpleAgvInterface ==&lt;br /&gt;
`SimpleAgvInterface.Queue` 的每个 action 内通常一次 `DriveTask.WaitDriveTask(...)`。详见 [[Special:MyLanguage/SimpleAgvInterface Queue机制|SimpleAgvInterface Queue机制]]。&lt;br /&gt;
&lt;br /&gt;
Each action inside `SimpleAgvInterface.Queue` typically calls `DriveTask.WaitDriveTask` once.&lt;br /&gt;
&lt;br /&gt;
== 配置 / Configuration ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 字段 / Field !! 默认 / Default !! 文件 / File&lt;br /&gt;
|-&lt;br /&gt;
| `DriveTaskInterval` || 20 ms || `D:\src\Clumsy\ClumsyCore\Configuration.cs`&lt;br /&gt;
|-&lt;br /&gt;
| `DriveTaskTimeout` || 300 ms || same&lt;br /&gt;
|-&lt;br /&gt;
| `basicSpeed` || 项目相关 || same&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
从嵌入资源 `ClumsyCore.res.defaultconf.json` 加载，或用 `Configuration.FromFile(fn)` 覆盖。&lt;br /&gt;
Loaded from embedded `ClumsyCore.res.defaultconf.json` or overridden via `Configuration.FromFile`.&lt;br /&gt;
&lt;br /&gt;
== 常见错误 / Common mistakes ==&lt;br /&gt;
* '''忘了 `WaitDriveTask` 是阻塞的''' → 在 UI 线程调会冻 UI。包到 `SimpleAgvInterface.Queue` 的 async action 里。&lt;br /&gt;
* '''Movement Get() 不 yield''' → 立即耗尽，DriveTask 立即返回，看起来&amp;quot;完成了 0 ms&amp;quot;。Always `yield return` at least once per control cycle.&lt;br /&gt;
* '''DriveStop 中无 vCmd = 0''' → 即使 enumerator 停了，最后一帧的 vCmd 还在 DObject 里，车继续动。在 `DriveStop()` 里显式清零所有 IDriveWriter 状态。&lt;br /&gt;
&lt;br /&gt;
* `WaitDriveTask` is blocking; calling it on the UI thread freezes UI. Wrap in `SimpleAgvInterface.Queue`'s async action.&lt;br /&gt;
* If `Get()` never yields, DriveTask exits immediately.&lt;br /&gt;
* `DriveStop()` must explicitly zero all IDriveWriter outputs; otherwise the last frame's `vCmd` stays in DObject.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/MovementDefinition详解|MovementDefinition详解]]&lt;br /&gt;
* [[Special:MyLanguage/SimpleAgvInterface Queue机制|SimpleAgvInterface Queue机制]]&lt;br /&gt;
* [[Special:MyLanguage/巡线行走|巡线行走]]&lt;br /&gt;
* [[Special:MyLanguage/绕障行走|绕障行走]]&lt;br /&gt;
* [[Special:MyLanguage/车体抽象原理|车体抽象原理]]&lt;br /&gt;
* [[Special:MyLanguage/Clumsy-API|Clumsy-API]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;br /&gt;
[[Category:运动控制使用手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=DObject%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98%E5%8D%8F%E8%AE%AE&amp;diff=1035</id>
		<title>DObject共享内存协议</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=DObject%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98%E5%8D%8F%E8%AE%AE&amp;diff=1035"/>
		<updated>2026-05-16T14:00:10Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
'''DObject''' 是 MDCS 的跨进程 ''命名共享内存 pub/sub'' 通讯原语。Medulla 把传感器数据、上下位 IO、调度状态等都通过 DObject 发布；Detour / Clumsy / SimpleComposer / 监控工具按名字订阅。整个 MDCS 的进程间通讯只有这一条总线。&lt;br /&gt;
&lt;br /&gt;
'''DObject''' is MDCS's cross-process named-shared-memory pub/sub primitive. Medulla publishes sensor data, upper/lower IO, scheduler state via DObject; Detour / Clumsy / SimpleComposer / monitoring tools subscribe by name. It's the sole IPC bus in MDCS.&lt;br /&gt;
&lt;br /&gt;
实现：`D:\src\Fundamentals\FundamentalLib\DObject.cs:18-344`。&lt;br /&gt;
Implementation: `D:\src\Fundamentals\FundamentalLib\DObject.cs:18-344`.&lt;br /&gt;
&lt;br /&gt;
== 存储 / Storage ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 平台 / Platform !! 后端 / Backing&lt;br /&gt;
|-&lt;br /&gt;
| Windows || `MemoryMappedFile(&amp;quot;io_shared&amp;quot;, 256 MB)` + meta `MemoryMappedFile(&amp;quot;io_shared_meta&amp;quot;, 1024 B)`&lt;br /&gt;
|-&lt;br /&gt;
| Linux || `$TMPDIR/io_shared` 文件 / file-backed fallback&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
容量 256 MB 是预分配上限；具体 DObject 槽按需扩展但不能超过总容量。&lt;br /&gt;
&lt;br /&gt;
The 256 MB pool is preallocated; per-DObject slots grow on demand but the pool cap is hard.&lt;br /&gt;
&lt;br /&gt;
== 二进制布局 / Binary layout ==&lt;br /&gt;
&lt;br /&gt;
  [meta 1024 B]&lt;br /&gt;
    [magic: 4 B  0x90 0x97 0x92 0x96]&lt;br /&gt;
    [FundamentalLib version hash: 4 B]&lt;br /&gt;
    [process count: 1 B]&lt;br /&gt;
    [PID list: 32 B (8 × int32)]&lt;br /&gt;
    [reserved]&lt;br /&gt;
&lt;br /&gt;
  [payload area ≤ 256 MB]&lt;br /&gt;
    slot 0:&lt;br /&gt;
      [next_offset: int32]&lt;br /&gt;
      [name_len:    int32]&lt;br /&gt;
      [name:        ASCII (≤ 32 B)]&lt;br /&gt;
      [size:        int32]&lt;br /&gt;
      [reserved:    int32]&lt;br /&gt;
      [payload:     N B]&lt;br /&gt;
    slot 1: same&lt;br /&gt;
    ...&lt;br /&gt;
    EOF: next_offset = 0&lt;br /&gt;
&lt;br /&gt;
== API ==&lt;br /&gt;
=== 构造 / Constructor ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
var dobj = new DObject(string name, int defSize = 1024);&lt;br /&gt;
// name ≤ 32 chars; creates new slot or attaches to existing&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 写入 / Post ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
dobj.Post(byte[] payload, bool recording = false);&lt;br /&gt;
// 1. acquires per-DObject named Mutex (&amp;quot;io_mutex_&amp;lt;name&amp;gt;&amp;quot;)&lt;br /&gt;
// 2. Marshal.Copy(payload) into the slot&lt;br /&gt;
// 3. signals per-DObject EventWaitHandle (wakes all Wait())&lt;br /&gt;
// 4. if recording == true, also append to .dorec file&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 读取 / Read ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
var reader = dobj.Reader(int offset, int len);&lt;br /&gt;
// reader is a delegate; on each invoke it:&lt;br /&gt;
// 1. acquires Mutex&lt;br /&gt;
// 2. copies [offset, offset+len) into a fresh byte[]&lt;br /&gt;
// 3. releases Mutex, returns the bytes&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 等待 / Wait ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
bool gotNewFrame = dobj.Wait(int timeoutMs);&lt;br /&gt;
// blocks on the EventWaitHandle; returns true on signal, false on timeout&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 并发模型 / Concurrency ==&lt;br /&gt;
* '''每 DObject 一把命名 Mutex''' (`io_mutex_&amp;lt;name&amp;gt;`) 保护 Post/Read 互斥。&lt;br /&gt;
* '''每 DObject 一个 EventWaitHandle''' 用于 `Wait()` 唤醒。&lt;br /&gt;
* Mutex + EventWaitHandle 都是系统级原语（Windows: 命名 Mutex + AutoReset Event；Linux: FSWEventWaitHandle）。&lt;br /&gt;
* 读者使用 `Wait` 阻塞到下次写入；轮询读取也 OK 但浪费 CPU。&lt;br /&gt;
&lt;br /&gt;
* Per-DObject named Mutex protects Post/Read mutual exclusion.&lt;br /&gt;
* Per-DObject EventWaitHandle wakes `Wait()`.&lt;br /&gt;
* Both are kernel-level primitives — cross-process safe.&lt;br /&gt;
&lt;br /&gt;
== 版本校验 / Version safety ==&lt;br /&gt;
Meta 块的第二个 4 字节存 ''FundamentalLib 编译哈希''。读取时若 `X.conf.EnsureFlibSame = true` 且哈希不匹配 → 抛异常。这能立刻发现&amp;quot;同一系统里加载了两个不同版本的 FundamentalLib&amp;quot;的灾难性 bug。&lt;br /&gt;
&lt;br /&gt;
The meta block stores the compiling FundamentalLib's version hash. On read, if `X.conf.EnsureFlibSame = true` and the hash differs → throw. Catches the &amp;quot;two different FundamentalLib.dll versions loaded in one system&amp;quot; disaster bug.&lt;br /&gt;
&lt;br /&gt;
【发现】 这是 MDCS 调试中最有用的护栏 —— 重新构建 FundamentalLib 但忘了重建消费者时，立刻抛错。&lt;br /&gt;
The single most useful guardrail in MDCS debugging.&lt;br /&gt;
&lt;br /&gt;
== 录制 / Recording ==&lt;br /&gt;
`dobj.Recording = true` 时每次 `Post()` 同时把数据追加到磁盘 `.dorec` 流。&lt;br /&gt;
With `dobj.Recording = true` every `Post()` also appends to a disk `.dorec` stream.&lt;br /&gt;
&lt;br /&gt;
格式 / Format: `[name] [DateTime.ToBinary] [RND id] [length] [payload]`。&lt;br /&gt;
&lt;br /&gt;
清理 / Cleanup: 按 ''年龄''（`RecordingKeepSeconds`）和 ''总磁盘大小''（`RecordingDiskSizeLimitMB`）双重边界，自动滚动。&lt;br /&gt;
Auto-cleanup bounded by both age (`RecordingKeepSeconds`) and total disk size (`RecordingDiskSizeLimitMB`).&lt;br /&gt;
&lt;br /&gt;
回放工具见 [[Special:MyLanguage/使用手册 - 数据录制与回放手册|使用手册 - 数据录制与回放手册]]。&lt;br /&gt;
&lt;br /&gt;
== 常见 DObject 槽 / Common slot names ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 名字 / Name !! 发布者 / Producer !! 订阅者 / Subscriber !! 内容 / Payload&lt;br /&gt;
|-&lt;br /&gt;
| `Lidar2D`（或厂商后缀）|| Medulla 雷达插件 || Detour || 打包的 `LidarOutput`（点云 + tick）&lt;br /&gt;
|-&lt;br /&gt;
| `pose` || Detour || Clumsy, SimpleComposer || 6-DoF 位姿 + 协方差 + tick&lt;br /&gt;
|-&lt;br /&gt;
| `MedullaCartLowerIO&amp;lt;tag&amp;gt;` || Medulla CartActivator || SimpleComposer || `[AsLowerIO]` 字段 JSON&lt;br /&gt;
|-&lt;br /&gt;
| `MedullaCartUpperIO&amp;lt;tag&amp;gt;` || SimpleComposer / Clumsy || Medulla CartActivator || `[AsUpperIO]` 字段 JSON&lt;br /&gt;
|-&lt;br /&gt;
| 自定义 || 任意 || 任意 || 自定义二进制&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 使用模式 / Patterns ==&lt;br /&gt;
=== 写者侧 / Publisher pattern ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class MyLidar : Lidar2DIOObject&lt;br /&gt;
{&lt;br /&gt;
    void Loop()&lt;br /&gt;
    {&lt;br /&gt;
        while (true)&lt;br /&gt;
        {&lt;br /&gt;
            var cloud = ReadFromDevice();&lt;br /&gt;
            cachedLidar = cloud;&lt;br /&gt;
            frame += 1;&lt;br /&gt;
            tick = DateTime.Now.Ticks;&lt;br /&gt;
            output();  // 内部调用 dobj.Post(serializedCloud)&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 读者侧 / Subscriber pattern ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
var dobj = new DObject(&amp;quot;Lidar2D&amp;quot;);&lt;br /&gt;
var reader = dobj.Reader(0, 8 * 1024);  // expected payload size&lt;br /&gt;
while (true)&lt;br /&gt;
{&lt;br /&gt;
    if (dobj.Wait(5000))&lt;br /&gt;
    {&lt;br /&gt;
        var bytes = reader();&lt;br /&gt;
        var output = LidarOutput.Deserialize(bytes);&lt;br /&gt;
        ProcessFrame(output.points);&lt;br /&gt;
    }&lt;br /&gt;
    else&lt;br /&gt;
    {&lt;br /&gt;
        Console.WriteLine(&amp;quot;Lidar timeout!&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 注意事项 / Caveats ==&lt;br /&gt;
* '''DObject 名 ≤ 32 字符''' —— 超长会截断 / silently truncated.&lt;br /&gt;
* '''Post 是阻塞的''' —— 所有订阅者都未释放 Mutex 之前你写不进 / blocks while any subscriber holds the mutex.&lt;br /&gt;
* '''Reader 不重试''' —— 槽消失或重建时 reader 失效，需要重新 `new Reader`.&lt;br /&gt;
* '''跨主机不工作''' —— 同主机进程间专用 / Single-host only; not designed for cross-machine.&lt;br /&gt;
* '''256 MB 是硬限''' —— 大量大尺寸槽要监控用量 / monitor usage if many large slots.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/DObject|DObject (legacy page)]]&lt;br /&gt;
* [[Special:MyLanguage/Medulla软件架构|Medulla软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 数据录制与回放手册|数据录制与回放手册]]&lt;br /&gt;
* [[Special:MyLanguage/核心开发指南|核心开发指南]]&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=CartDefinition%E5%B1%9E%E6%80%A7%E5%8F%82%E8%80%83&amp;diff=1034</id>
		<title>CartDefinition属性参考</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=CartDefinition%E5%B1%9E%E6%80%A7%E5%8F%82%E8%80%83&amp;diff=1034"/>
		<updated>2026-05-16T14:00:06Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
本页是底盘适配插件（`: CartDefinition`）所用的 ''全部属性的速查表''。这些属性住在 `CartActivator` 插件，由 LessokajiWeaver 在编译期注入实际行为。&lt;br /&gt;
&lt;br /&gt;
This page is the cheat sheet for all attributes used in cart-adapter plugins (`: CartDefinition`). They live in the `CartActivator` plugin, with actual behaviour injected at compile time by LessokajiWeaver.&lt;br /&gt;
&lt;br /&gt;
定义：`D:\src\M2\OfficialPlugins\CartActivator\Attrs.cs`。&lt;br /&gt;
Definitions: `D:\src\M2\OfficialPlugins\CartActivator\Attrs.cs`.&lt;br /&gt;
&lt;br /&gt;
== 字段属性 / Field attributes ==&lt;br /&gt;
=== [AsInitParam] ===&lt;br /&gt;
'''目标''' / Target: 字段 / field&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 该字段成为 ''启动配置项''；可通过 startup.iocmd 或 JSON 配置传入；持久化到 LowerIO JSON 流。&lt;br /&gt;
The field becomes a ''startup config item''; settable from startup.iocmd or JSON config; persisted to the LowerIO JSON stream.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[AsInitParam] public string plcIp = &amp;quot;192.168.0.100&amp;quot;;&lt;br /&gt;
[AsInitParam] public int    plcPort = 502;&lt;br /&gt;
[AsInitParam] public float  wheelBase = 1320f;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
设置方式 / How to set:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
# startup.iocmd&lt;br /&gt;
cart.plcIp = &amp;quot;192.168.0.50&amp;quot;&lt;br /&gt;
cart.plcPort = 502&lt;br /&gt;
cart Init&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== [AsUpperIO] ===&lt;br /&gt;
'''目标''' / Target: 字段 / field&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 上位 IO —— ''命令向下流''。Weaver 把字段重写为 property，setter 自动同步到 DObject `MedullaCartUpperIO&amp;lt;tag&amp;gt;`。&lt;br /&gt;
Upper IO — '''commands flowing down'''. Weaver rewrites the field as a property whose setter syncs to DObject `MedullaCartUpperIO&amp;lt;tag&amp;gt;`.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[AsUpperIO] public float vCmd;          // forward velocity command&lt;br /&gt;
[AsUpperIO] public float steerCmd;      // steer angle command&lt;br /&gt;
[AsUpperIO] public bool  brakeOn;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
调度 / Clumsy 写这些字段；CartDefinition 的 LadderLogic 读后写硬件。&lt;br /&gt;
Scheduler / Clumsy writes; the CartDefinition's LadderLogic reads + writes hardware.&lt;br /&gt;
&lt;br /&gt;
=== [AsLowerIO] ===&lt;br /&gt;
'''目标''' / Target: 字段 / field&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 下位 IO —— ''状态向上流''。Weaver 把字段写入自动同步到 DObject `MedullaCartLowerIO&amp;lt;tag&amp;gt;`。&lt;br /&gt;
Lower IO — '''status flowing up'''. Field writes auto-sync to DObject `MedullaCartLowerIO&amp;lt;tag&amp;gt;`.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[AsLowerIO] public float vEst;&lt;br /&gt;
[AsLowerIO] public float steerEst;&lt;br /&gt;
[AsLowerIO] public int   batteryPercent;&lt;br /&gt;
[AsLowerIO] public bool  eStop;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
LadderLogic 读硬件后写这些字段；调度 / Clumsy 订阅 DObject 读。&lt;br /&gt;
LadderLogic writes after reading hardware; scheduler / Clumsy subscribes to the DObject.&lt;br /&gt;
&lt;br /&gt;
== 类属性 / Class attributes ==&lt;br /&gt;
=== [UseLadderLogic] ===&lt;br /&gt;
'''目标''' / Target: 嵌套类 `: LadderLogic&amp;lt;MyCart&amp;gt;` / nested class extending `LadderLogic&amp;lt;MyCart&amp;gt;`&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 启动一个独立线程 + Timer，按 `IntervalMs` 周期调用嵌套类的 `Operation()`。&lt;br /&gt;
Spawns an independent thread + Timer that calls the nested class's `Operation()` every `IntervalMs`.&lt;br /&gt;
&lt;br /&gt;
详见 [[Special:MyLanguage/LadderLogic框架|LadderLogic框架]]。&lt;br /&gt;
&lt;br /&gt;
参数 / Parameters:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 字段 / Field !! 默认 !! 含义&lt;br /&gt;
|-&lt;br /&gt;
| `IntervalMs` (alias `scanInterval`) || 50 || tick 周期 (ms)&lt;br /&gt;
|-&lt;br /&gt;
| `resume` || true || 异常自动重启&lt;br /&gt;
|-&lt;br /&gt;
| `resumeRetryDelay` || 1000 || 重启前等多久 (ms)&lt;br /&gt;
|-&lt;br /&gt;
| `realTime` || false || 保留&lt;br /&gt;
|-&lt;br /&gt;
| `timeoutInterval` || 10000 || 保留&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
可以在同一 CartDefinition 上重复声明 —— 多个循环共存。&lt;br /&gt;
Can be declared multiple times on the same CartDefinition for multiple concurrent loops.&lt;br /&gt;
&lt;br /&gt;
=== [UseMedullaAPI] ===&lt;br /&gt;
'''目标''' / Target: 类 / class&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 启用 Medulla 高级 API（详细文档见 [[Special:MyLanguage/Medulla-API|Medulla-API]]）。&lt;br /&gt;
Enables Medulla higher-level APIs (see linked API page).&lt;br /&gt;
&lt;br /&gt;
=== [UseManualController] ===&lt;br /&gt;
'''目标''' / Target: 类 / class&lt;br /&gt;
&lt;br /&gt;
'''效果''' / Effect: 注册手动遥控器接管 —— 启用后操作工可用手柄覆盖自动控制。&lt;br /&gt;
Registers manual override — operator can take over autonomous control via a handheld.&lt;br /&gt;
&lt;br /&gt;
== 调用契约 / Calling contract ==&lt;br /&gt;
=== 初始化顺序 / Init order ===&lt;br /&gt;
&lt;br /&gt;
  io load &amp;lt;plugin&amp;gt;.dll&lt;br /&gt;
       → MedullaIO.load() finds MainIOObject → new MainIOObject() (no hardware contact)&lt;br /&gt;
&lt;br /&gt;
  cart Init&lt;br /&gt;
       → Init() runs → connect to PLC / CAN / serial&lt;br /&gt;
       → CartDefinition.createLadders() introspects [UseLadderLogic]&lt;br /&gt;
       → for each ladder: spawn timer + thread&lt;br /&gt;
       → first Operation(iteration=0)&lt;br /&gt;
       → ...&lt;br /&gt;
&lt;br /&gt;
【注意】 构造函数必须 ''廉价且无副作用''。硬件接触放在 `Init()` 里。&amp;lt;br&amp;gt;&lt;br /&gt;
The ctor must be cheap and side-effect-free. Hardware contact lives in `Init()`.&lt;br /&gt;
&lt;br /&gt;
=== DObject 同步时机 / DObject sync timing ===&lt;br /&gt;
* `[AsUpperIO]` 字段 setter 触发 DObject `Post`（命令向下）&lt;br /&gt;
* `[AsLowerIO]` 字段 setter 触发 DObject `Post`（状态向上）&lt;br /&gt;
* `[AsInitParam]` 一次性 JSON 注入；运行时改 ''不会''重新初始化硬件&lt;br /&gt;
&lt;br /&gt;
第三条意味着 ：运行时改 `[AsInitParam]` 字段不会自动重连 PLC；需要重新 `cart Init`。&amp;lt;br&amp;gt;&lt;br /&gt;
Changing an `[AsInitParam]` field at runtime won't reconnect the PLC; you must re-`cart Init`.&lt;br /&gt;
&lt;br /&gt;
== 还可以用 IOObject 系列属性 / IOObject attributes also apply ==&lt;br /&gt;
`CartDefinition` 继承 `IOObject`，所以 `[IOObjectMonitor] / [IOObjectUtility] / [IOObjectWebUtility] / [IOObjectWatch]` 都能用 —— 详见 [[Special:MyLanguage/IOObject属性参考|IOObject属性参考]]。&lt;br /&gt;
&lt;br /&gt;
`CartDefinition` inherits from `IOObject`, so `[IOObjectMonitor] / [IOObjectUtility] / [IOObjectWebUtility] / [IOObjectWatch]` apply. See the IOObject reference.&lt;br /&gt;
&lt;br /&gt;
== 常见错误 / Gotchas ==&lt;br /&gt;
* '''`[AsUpperIO]` 标在 `readonly` 字段上''' → 编译过，运行时 `MissingMethodException`（Weaver 注入 setter 与 readonly 冲突）。&amp;lt;br&amp;gt;&lt;br /&gt;
  `[AsUpperIO]` on a `readonly` field → compiles, runtime `MissingMethodException`. Make it mutable.&lt;br /&gt;
* '''忘了 `cart Init`''' → ladder 没启动，但插件加载成功，看起来&amp;quot;工作&amp;quot;但 Lower IO 不更新。&lt;br /&gt;
* '''Init 抛异常''' → 部分 ladder 起来部分没起来；难以排查。在 Init 里捕获异常，设 `eStop = true` 而不是抛。&amp;lt;br&amp;gt;&lt;br /&gt;
  Catch exceptions in `Init()`; set `eStop = true` instead of throwing.&lt;br /&gt;
* '''字段名冲突''' → 同名 `[AsUpperIO]` 和 `[AsLowerIO]` 用不同 setter，运行时迷惑。约定 ：上位字段加 `Cmd` 后缀，下位加 `Est` 后缀。&amp;lt;br&amp;gt;&lt;br /&gt;
  Same-named fields with different IO directions confuse the runtime. Convention: upper = `*Cmd`, lower = `*Est`.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/LadderLogic框架|LadderLogic框架]]&lt;br /&gt;
* [[Special:MyLanguage/IOObject属性参考|IOObject属性参考]]&lt;br /&gt;
* [[Special:MyLanguage/MDCS引擎适配机器人入门教学|MDCS引擎适配机器人入门教学]]&lt;br /&gt;
* [[Special:MyLanguage/叉车适配案例|叉车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/插件契约与打包约定|插件契约与打包约定]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%9C%B0%E7%BA%B9SLAM&amp;diff=1033</id>
		<title>地纹SLAM</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%9C%B0%E7%BA%B9SLAM&amp;diff=1033"/>
		<updated>2026-05-16T11:44:38Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Redirect&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;#REDIRECT [[地纹SLAM技术概述]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E7%BA%AF%E4%BA%8C%E7%BB%B4%E7%A0%81%E5%AF%BC%E8%88%AA&amp;diff=1032</id>
		<title>纯二维码导航</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E7%BA%AF%E4%BA%8C%E7%BB%B4%E7%A0%81%E5%AF%BC%E8%88%AA&amp;diff=1032"/>
		<updated>2026-05-16T11:43:15Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
&amp;quot;纯二维码导航&amp;quot;是 [[Special:MyLanguage/二维码识别导航|二维码识别导航]] 的极简变体：完全不使用激光 / 地纹 / 天花板 SLAM，仅靠地面二维码作为唯一定位源。它对仓库 / 工厂的改造代价最低，但限制也最多。&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Pure-QR navigation&amp;quot; is the minimal form of [[Special:MyLanguage/二维码识别导航|QR-based navigation]]: no laser SLAM, no ground-texture, no ceiling SLAM — only on-floor QR tags. It has the lowest deployment cost but the most restrictions.&lt;br /&gt;
&lt;br /&gt;
== 适用场景 / When to use ==&lt;br /&gt;
* 单线性产线 / 单环路（车少、路径少）&lt;br /&gt;
* 改造成本 ''硬约束''的项目（不能停产装激光、不能挂相机）&lt;br /&gt;
* 简单业务，定位精度要求 ≥ ±20 mm 即可&lt;br /&gt;
&lt;br /&gt;
== 与其它方案的对比 / vs others ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 项 / Item !! 纯二维码 / Pure-QR !! 二维码 + SLAM 混合 / Hybrid !! [[Special:MyLanguage/色带和磁条导航|色带 / 磁条]]&lt;br /&gt;
|-&lt;br /&gt;
| 部署成本 / Deploy cost || 最低 || 中 || 中&lt;br /&gt;
|-&lt;br /&gt;
| 路径灵活度 / Path freedom || 中（受二维码布点约束）|| 高 || 低（必须沿线）&lt;br /&gt;
|-&lt;br /&gt;
| 高速行驶 / High speed || 困难（视场必须见 ≥ 1 标签）|| 简单 || 简单&lt;br /&gt;
|-&lt;br /&gt;
| 重定位 / Re-loc || 即时（任一标签都行）|| 即时 || 困难&lt;br /&gt;
|-&lt;br /&gt;
| 维护 / Maintenance || 二维码贴布需保持完整 || 二维码 + SLAM 地图 || 色带 / 磁条&lt;br /&gt;
|-&lt;br /&gt;
| 失效 / Failure mode || 看不到标签 = 停车 || SLAM 兜底 || 离线 = 停车&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 系统要求 / System requirements ==&lt;br /&gt;
* '''二维码密度''': 每 1.0–1.5 m 一个标签（车速 ≤ 1 m/s）；要求 ''任意时刻视场至少有 1 个标签''。&lt;br /&gt;
* '''标签精度''': 边长 100–150 mm；位姿标定误差 ≤ ±5 mm。&lt;br /&gt;
* '''下视相机''': 全局快门，30–60 FPS，IR 环形灯。&lt;br /&gt;
* '''地面平整度''': 起伏 ≤ ±5 mm（俯仰会让相机视场偏离）。&lt;br /&gt;
* '''车体速度上限''': 1 m/s（高速会因相机视场过短而丢标签）。&lt;br /&gt;
&lt;br /&gt;
== 工作模式 / Operating modes ==&lt;br /&gt;
=== 站点对站点 / Site-to-site ===&lt;br /&gt;
每个 ''任务''指定 ''目标站点（一个二维码）''。AGV：&lt;br /&gt;
# 当前视场识别一个标签 X，反推车体位姿；&lt;br /&gt;
# 已知目标站点 Y 的二维码位置；&lt;br /&gt;
# 沿 X → Y 的预设路径走（每个相邻标签间是直线段）；&lt;br /&gt;
# 沿途连续识别中间标签做小修正；&lt;br /&gt;
# 抵达 Y 二维码时停车。&lt;br /&gt;
&lt;br /&gt;
Each task names a target site (a QR). AGV reads its current tag X, computes pose, plans path along predefined tag-graph to Y, follows tag-by-tag updates, stops at Y.&lt;br /&gt;
&lt;br /&gt;
=== 完全跟踪 / Continuous track ===&lt;br /&gt;
更高速场景：AGV 把多个标签 ''并行''识别，做平均位姿，连续控制车体姿态。仅当相机分辨率足够 + 标签间距足够小时可用。&lt;br /&gt;
&lt;br /&gt;
For higher speeds: detect multiple tags simultaneously and average. Requires high camera resolution and dense tag layout.&lt;br /&gt;
&lt;br /&gt;
== MDCS 集成 / MDCS integration ==&lt;br /&gt;
* '''Detour''': 只配 QR backend，关闭其它 SLAM。位姿协方差完全来自 QR PnP。&lt;br /&gt;
* '''Clumsy''': 巡线 Movement 一致；不同的是 ''没有 SLAM 兜底''，丢失任一标签 5 s 后必须停车。&lt;br /&gt;
* '''SimpleComposer''': 路径图直接对应 ''标签图''，每个标签 = 一个站点；段是相邻标签间的直线。&lt;br /&gt;
* '''调度''': DPS 交管仍然适用。&lt;br /&gt;
&lt;br /&gt;
== 故障与恢复 / Failure &amp;amp; recovery ==&lt;br /&gt;
* '''丢标签''' / Tag-loss: 任何 ''2 s 内''看不到标签 → 减速；''5 s''仍无标签 → 停车 + 报警，等人工把车搬到任一标签上。&lt;br /&gt;
* '''标签污损''' / Tag damage: 每周巡检；自动检测置信度突然下降的标签并报告。&lt;br /&gt;
* '''误识别''' / Misread: PnP 重投影残差 &amp;gt; 2 像素的检测视为不可信。&lt;br /&gt;
&lt;br /&gt;
* Tag-loss within 2 s → slow down; &amp;gt; 5 s → stop and alarm; operator must re-place on any tag.&lt;br /&gt;
* Weekly tag audit + automatic confidence-drop detection.&lt;br /&gt;
* PnP reprojection residual &amp;gt; 2 px is rejected.&lt;br /&gt;
&lt;br /&gt;
== 部署建议 / Deployment tips ==&lt;br /&gt;
* 在交叉口前后各 3 个标签 ''密集''布点。&lt;br /&gt;
* 取放工位 ''正上方'' 1 个标签 + 距离 200 mm 外再 1 个，确保对位精确。&lt;br /&gt;
* 避免在车道狭窄处只有 1 个标签 —— 必须 ≥ 2 个冗余。&lt;br /&gt;
* 使用 ''可清洁覆盖膜''保护标签；磨损先损坏覆盖膜，不损坏标签本体。&lt;br /&gt;
&lt;br /&gt;
== 已知边界 / Known limits ==&lt;br /&gt;
* 不能 ''高速跑长直线''：标签间距与车速成反比。&lt;br /&gt;
* 不能在 ''反光地面''（如抛光大理石）上用：相机看不清标签。&lt;br /&gt;
* 不能 ''跨楼层''：每层都要独立标签图。&lt;br /&gt;
* 现场如有大量振动 / 油污落到标签 → 频繁损耗，不建议。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/二维码识别导航|二维码识别导航]]&lt;br /&gt;
* [[Special:MyLanguage/色带和磁条导航|色带和磁条导航]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 在地纹导航中使用二维码快速找回定位|地纹导航中使用二维码快速找回定位]]&lt;br /&gt;
&lt;br /&gt;
[[Category:定位导航相关手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E6%B8%85%E6%B4%81%E6%9C%BA%E5%99%A8%E4%BA%BA&amp;diff=1031</id>
		<title>清洁机器人</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E6%B8%85%E6%B4%81%E6%9C%BA%E5%99%A8%E4%BA%BA&amp;diff=1031"/>
		<updated>2026-05-16T11:43:12Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
清洁机器人是 MDCS 在 ''商业清洁 / 工业清洁''场景的应用：把自动驾驶 + 清洁工具（拖布、刷子、吸水扒、消毒喷雾）集成在一台 AGV 上，自动覆盖大面积地面。MDCS 提供其底盘 / 定位 / 调度的基础设施；清洁工具的协议是 ''厂商私有''，通过 Medulla 适配。&lt;br /&gt;
&lt;br /&gt;
A cleaning robot is the MDCS application in ''commercial / industrial cleaning'': autonomous driving + cleaning tools (mop, brush, squeegee, disinfectant sprayer) on one AGV for large-area floor coverage. MDCS provides chassis / localisation / scheduling; cleaning-tool protocols are vendor-specific and adapted via Medulla.&lt;br /&gt;
&lt;br /&gt;
== 业务场景 / Scenarios ==&lt;br /&gt;
* 大型仓库 / 工厂车间地面拖洗&lt;br /&gt;
* 商超 / 机场 / 地铁站清洁&lt;br /&gt;
* 食品厂 GMP 区域消毒&lt;br /&gt;
* 医院床下消毒&lt;br /&gt;
* 矿区粉尘清扫&lt;br /&gt;
&lt;br /&gt;
== 与其它 AGV 的区别 / vs other AGV roles ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 项 / Item !! 清洁机器人 / Cleaning !! 仓储 AGV&lt;br /&gt;
|-&lt;br /&gt;
| 主要任务 / Primary task || ''面''覆盖 (full-coverage path) || ''点对点''&lt;br /&gt;
|-&lt;br /&gt;
| 路径 / Path || 弓形 / 螺旋 / 边缘 || A* / 流场&lt;br /&gt;
|-&lt;br /&gt;
| 速度 / Speed || 0.3–1 m/s || 1–2 m/s&lt;br /&gt;
|-&lt;br /&gt;
| 携带 / Payload || 水 / 化学品 || 货&lt;br /&gt;
|-&lt;br /&gt;
| 清洁完成度 / Completeness || 决定客户验收 || N/A&lt;br /&gt;
|-&lt;br /&gt;
| 失效成本 / Failure cost || 漏清污染 || 任务延误&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 全覆盖规划 / Full-coverage planning ==&lt;br /&gt;
清洁机器人的核心是 ''覆盖路径''而不是 ''最短路径''。MDCS 在 SimpleCore 中支持几种覆盖策略：&lt;br /&gt;
&lt;br /&gt;
The core problem is '''coverage planning''', not shortest-path:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 策略 / Strategy !! 适用 / Suitable !! 实现 / Implementation&lt;br /&gt;
|-&lt;br /&gt;
| Boustrophedon (弓形) || 矩形区 / rectangular zones || 分解为 cell + 每 cell 弓形&lt;br /&gt;
|-&lt;br /&gt;
| 边缘 + 内填 / Wall-follow + fill || 边界复杂 || 先沿边走一圈，再做内填&lt;br /&gt;
|-&lt;br /&gt;
| 螺旋 / Spiral || 圆形 / 自由场景 || 从中心向外螺旋&lt;br /&gt;
|-&lt;br /&gt;
| 自适应 / Adaptive || 动态污染 || 用相机识别脏污点，热点优先&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Medulla 适配清洁工具 / Adapting cleaning tools ==&lt;br /&gt;
清洁工具的典型 IO 表（以拖洗机为例）：&lt;br /&gt;
Typical IO map for a mop-and-scrub robot:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class CleaningBot : CartDefinition&lt;br /&gt;
{&lt;br /&gt;
    // 底盘 IO（同标准差速 / 全向车）&lt;br /&gt;
&lt;br /&gt;
    // 清洁工具 IO&lt;br /&gt;
    [AsUpperIO] public bool   mopOn;             // 拖布旋转&lt;br /&gt;
    [AsUpperIO] public bool   sprayOn;           // 喷水&lt;br /&gt;
    [AsUpperIO] public bool   squeegeeOn;        // 吸水扒&lt;br /&gt;
    [AsUpperIO] public bool   vacuumOn;          // 吸尘&lt;br /&gt;
    [AsUpperIO] public bool   disinfectantOn;    // 消毒液&lt;br /&gt;
    [AsUpperIO] public float  brushDownPressure; // 刷盘压力&lt;br /&gt;
&lt;br /&gt;
    [AsLowerIO] public float  cleanWaterL;&lt;br /&gt;
    [AsLowerIO] public float  dirtyWaterL;&lt;br /&gt;
    [AsLowerIO] public bool   filterOK;          // 过滤器堵塞？&lt;br /&gt;
    [AsLowerIO] public bool   detergentLow;&lt;br /&gt;
    [AsLowerIO] public int    brushHours;        // 刷盘运行小时&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 调度集成 / Scheduling integration ==&lt;br /&gt;
清洁机器人的任务通常是 ''区域型''而非 ''点型''。SimpleComposer 任务接口需要支持：&lt;br /&gt;
Cleaning jobs are ''area-typed'', not ''point-typed''. SimpleComposer needs:&lt;br /&gt;
&lt;br /&gt;
* '''ZoneMission''' — 指定要清洁的 ''多边形区域''&lt;br /&gt;
* '''CoverageStrategy''' — 选择上面表中的策略&lt;br /&gt;
* '''ScheduleConstraints''' — 时间窗（如商超开门前清完）&lt;br /&gt;
* '''RechargeAndRefill''' — 自动充电 + 加水 + 倒污水&lt;br /&gt;
&lt;br /&gt;
== 相关问题 / Related concerns ==&lt;br /&gt;
=== 与人共用空间 / Sharing space with humans ===&lt;br /&gt;
清洁机器人 ''常在有人区域工作''，激光雷达 + 视觉 ''必须''有人员识别：&lt;br /&gt;
* 检测到人员 1.5 m 内 → 减速&lt;br /&gt;
* 检测到人员 0.5 m 内 → 停车 + 让行&lt;br /&gt;
* 完成任务后倒车回到工作面继续&lt;br /&gt;
&lt;br /&gt;
Cleaning robots typically share space with humans. Lidar + vision MUST detect people: ≥ 1.5 m → slow; ≤ 0.5 m → stop and yield; resume after clear.&lt;br /&gt;
&lt;br /&gt;
=== 湿地面 / Wet floors ===&lt;br /&gt;
* 拖洗后地面湿，AGV 自身行驶时可能打滑；速度降至 0.3 m/s。&lt;br /&gt;
* 警告标识：在湿地面边缘自动放置 ''警示锥''（高端机型）。&lt;br /&gt;
&lt;br /&gt;
=== 与电梯协同 / Elevator coordination ===&lt;br /&gt;
跨楼层清洁需要 ''呼梯协议''（HTTP 调用电梯控制器）。属于 MDCS 之外的集成；SimpleComposer 提供 hook 点。&lt;br /&gt;
Multi-floor cleaning needs an elevator-calling protocol (HTTP to the lift controller). MDCS provides a hook; the actual protocol is integration-specific.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/全向车适配案例|全向车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/MDCS引擎适配机器人入门教学|MDCS引擎适配机器人入门教学]]&lt;br /&gt;
* [[Special:MyLanguage/可达性状态编程|可达性状态编程]]&lt;br /&gt;
* [[Special:MyLanguage/绕障行走|绕障行走]]&lt;br /&gt;
&lt;br /&gt;
[[Category:特殊技术方案]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E6%B5%81%E5%9C%BA%E8%A7%84%E5%88%92&amp;diff=1030</id>
		<title>流场规划</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E6%B5%81%E5%9C%BA%E8%A7%84%E5%88%92&amp;diff=1030"/>
		<updated>2026-05-16T11:43:11Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
流场规划（Flow-field planning）是 [[Special:MyLanguage/DPS调度算法详解|DPS]] 之外的另一种调度内核选项。它把仓库 / 工厂的地面预计算为一个 ''矢量场''（每格指向&amp;quot;应该走的方向&amp;quot;），车辆按场行驶。流场对 ''车辆数极多 / 任务动态高''的场景有显著优势 —— 不需要为每辆车单独跑 A*。&lt;br /&gt;
&lt;br /&gt;
Flow-field planning is an alternative scheduling-kernel option to [[Special:MyLanguage/DPS调度算法详解|DPS]]. The warehouse / factory floor is pre-computed as a '''vector field''' (each cell points the way to go); vehicles simply follow the field. Flow-field is advantageous for ''very large fleets / very dynamic tasks'' — no per-car A* required.&lt;br /&gt;
&lt;br /&gt;
== 适用场景 / When to use ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 项 / Item !! DPS !! 流场 / Flow-field&lt;br /&gt;
|-&lt;br /&gt;
| 车队规模 / Fleet size || ≤ ~30 || 30 – 数千 / 30 – thousands&lt;br /&gt;
|-&lt;br /&gt;
| 任务动态性 / Task dynamics || 中等 || 极高（每秒大量任务）&lt;br /&gt;
|-&lt;br /&gt;
| 死锁保证 / Deadlock-free || 是 / Yes (algorithmic) || 经验性，需调（empirical）&lt;br /&gt;
|-&lt;br /&gt;
| 通道宽度 / Lane width || 单车通过即可 || ≥ 2 车并行&lt;br /&gt;
|-&lt;br /&gt;
| 离线计算 / Offline cost || 低 / Low || 高（重映射地图需要重算流场）&lt;br /&gt;
|-&lt;br /&gt;
| 路径最优性 / Path optimality || 全局最优 || 接近最优（流场近似）&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
典型用例：电商物流仓（一日万级任务、数百 KIVA 车）；不适合：单线性产线（车数 &amp;lt; 10）、复杂场景多车型混跑（建议 DPS）。&lt;br /&gt;
&lt;br /&gt;
Typical use: e-commerce sortation warehouses (tens of thousands of tasks/day, hundreds of KIVAs). Not for: small linear production lines (DPS is simpler), heavily mixed-vehicle complex layouts.&lt;br /&gt;
&lt;br /&gt;
== 算法原理 / Algorithm overview ==&lt;br /&gt;
=== 离线 / Offline ===&lt;br /&gt;
# 把地面离散为格点（典型 100 × 100 mm）。&lt;br /&gt;
# 对每个 ''目标站点''  T，求解到 T 的距离场（Dijkstra / 平面波传播）。&lt;br /&gt;
# 每个格的 ''流向''= 距离场负梯度方向。&lt;br /&gt;
# 多种 ''场层''可叠加：单车流场、紧急通道流场、双向回路流场。&lt;br /&gt;
&lt;br /&gt;
=== 在线 / Online ===&lt;br /&gt;
# 车收到任务（目标站点 T）→ 选 ''T 对应的流场层''。&lt;br /&gt;
# 车每个 tick 查当前位置所在格的 ''流向 + 速度上限''，作为局部目标。&lt;br /&gt;
# 实时局部避让在 Clumsy [[Special:MyLanguage/绕障行走|绕障行走]] 中处理。&lt;br /&gt;
# 当场内多车密集时，启用 ''拥挤检测'': 车队主动减速或绕到旁路流场层。&lt;br /&gt;
&lt;br /&gt;
=== 死锁与无死锁 / Deadlock &amp;amp; no-deadlock ===&lt;br /&gt;
流场本身 ''不能算法上保证''无死锁。MDCS 通过两个补丁来规避：&lt;br /&gt;
* '''拥挤检测 + 调速''': 一个区域车辆密度超过阈值后，进入流量限制。&lt;br /&gt;
* '''DPS 兜底''': 在 ''关键路口 / 工位口''仍用 DPS 锁，保证局部无死锁。&lt;br /&gt;
&lt;br /&gt;
The flow field itself does '''not''' algorithmically prevent deadlock. MDCS uses two patches:&lt;br /&gt;
* Density-triggered congestion detection + rate limiting.&lt;br /&gt;
* DPS fallback on critical intersections and station mouths.&lt;br /&gt;
&lt;br /&gt;
== 数据结构 / Data structures ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class FlowFieldLayer&lt;br /&gt;
{&lt;br /&gt;
    public int   Id;&lt;br /&gt;
    public int   TargetSiteId;&lt;br /&gt;
    public byte[,] FlowDir;       // 每格存方向编码 0..7（8 个邻居方向） / 8-way direction encoding&lt;br /&gt;
    public ushort[,] SpeedCap;    // mm/s&lt;br /&gt;
    public float[,] Distance;     // 距离场（用于优先级 / 比较）&lt;br /&gt;
    public bool[,] Reserved;      // 为应急通道保留？&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
存储格式：二进制紧凑布局（每格 ≤ 8 字节）；100 m × 100 m 工地约 80 MB / 流场。&lt;br /&gt;
Storage: binary-packed (≤ 8 bytes / cell); ~80 MB / flow-field for a 100 × 100 m site.&lt;br /&gt;
&lt;br /&gt;
== 在 MDCS 中启用 / Enabling in MDCS ==&lt;br /&gt;
* SimpleComposer 配置 → 调度内核 → 切换为&amp;quot;流场&amp;quot;。&lt;br /&gt;
* 必须先用 [[Special:MyLanguage/开发手册 - SimpleComposer界面开发 - CAD工具|CAD 工具]]为每个目标站点跑离线流场生成。&lt;br /&gt;
* 启用后 DPS 仍在路口 / 工位口生效；车体行驶段交给流场。&lt;br /&gt;
&lt;br /&gt;
== 与 DPS 的混合 / Hybrid with DPS ==&lt;br /&gt;
推荐配置（大规模仓储）：&lt;br /&gt;
* '''行驶段''' = 流场（粗粒度引导）&lt;br /&gt;
* '''路口段''' = DPS（保证无死锁）&lt;br /&gt;
* '''工位口''' = DPS + 可达性状态（确保进出顺序）&lt;br /&gt;
&lt;br /&gt;
Recommended hybrid (large warehouses):&lt;br /&gt;
* '''Traverse segments''' = flow-field (coarse guidance).&lt;br /&gt;
* '''Intersections''' = DPS (algorithmic deadlock-free).&lt;br /&gt;
* '''Station mouths''' = DPS + reachability state (ordering).&lt;br /&gt;
&lt;br /&gt;
== 限制 / Caveats ==&lt;br /&gt;
* 地图变化（增删站点 / 路径）要 ''重新生成所有流场''；典型 5–30 分钟离线。&lt;br /&gt;
* 流场对 ''朝向''是无知的 —— 阿克曼车辆需用流场 + ''曲率惩罚''搜索局部可行路径。&lt;br /&gt;
* 多车型流场：每种车型一套（包络不同）。&lt;br /&gt;
&lt;br /&gt;
* Map changes force a full off-line regeneration of every flow-field (5–30 min).&lt;br /&gt;
* Flow-fields are heading-agnostic — Ackermann vehicles need a local pass with curvature penalties.&lt;br /&gt;
* Per vehicle-type: one flow-field set each (different envelopes).&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/DPS调度算法详解|DPS调度算法详解]]&lt;br /&gt;
* [[Special:MyLanguage/调度内核运行原理|调度内核运行原理]]&lt;br /&gt;
* [[Special:MyLanguage/基于网络流的业务规划器|基于网络流的业务规划器]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 寻路启发器功能|寻路启发器功能]]&lt;br /&gt;
* [[Special:MyLanguage/Simple软件架构|Simple软件架构]]&lt;br /&gt;
&lt;br /&gt;
[[Category:技术报告]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E6%B1%BD%E8%BD%A6%E9%9D%A2%E5%B7%AE%E6%A3%80%E6%B5%8B&amp;diff=1029</id>
		<title>汽车面差检测</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E6%B1%BD%E8%BD%A6%E9%9D%A2%E5%B7%AE%E6%A3%80%E6%B5%8B&amp;diff=1029"/>
		<updated>2026-05-16T11:43:11Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
&amp;quot;汽车面差检测&amp;quot;是 MDCS 在汽车主机厂的特色应用：在车身总装下线时，AGV 携带 3D 扫描设备绕车一圈，自动测量 ''相邻钣金件之间的间隙（gap）与高低差（flush）''，结果实时与设计公差比较，给出可视化的质量报告。&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Automotive panel-gap inspection&amp;quot; is an MDCS application in vehicle assembly: at end-of-line, an AGV with a 3D scanner circles the car body, automatically measuring ''gaps and flushness between adjacent panels'' and comparing against design tolerances in real time, producing a visual quality report.&lt;br /&gt;
&lt;br /&gt;
== 应用背景 / Why ==&lt;br /&gt;
* 传统人工卡尺测量 ''慢、漏检、主观'';&lt;br /&gt;
* 固定龙门 3D 扫描设备 ''成本高、占用厂房''；&lt;br /&gt;
* AGV 移动检测：低成本、高覆盖、可批量。&lt;br /&gt;
&lt;br /&gt;
* Manual feeler-gauge measurement is slow, inconsistent, subjective.&lt;br /&gt;
* Fixed gantry 3D scanners are expensive and floor-space heavy.&lt;br /&gt;
* An AGV-mounted scanner is cheap, comprehensive, batch-friendly.&lt;br /&gt;
&lt;br /&gt;
== 系统组成 / Components ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 部件 / Part !! 规格 / Spec&lt;br /&gt;
|-&lt;br /&gt;
| AGV || 全向或差速车，载重 ≥ 100 kg，运动平稳 / Omni or diff drive, payload ≥ 100 kg&lt;br /&gt;
|-&lt;br /&gt;
| 3D 扫描仪 / 3D scanner || 线激光或结构光，精度 ≤ 0.05 mm&lt;br /&gt;
|-&lt;br /&gt;
| 多自由度云台 / Multi-DOF gimbal || 让扫描器对准车身曲面 / orient scanner to panel surface&lt;br /&gt;
|-&lt;br /&gt;
| 车型识别 / Vehicle ID || 二维码 / RFID 读取来车的车型 + VIN&lt;br /&gt;
|-&lt;br /&gt;
| 标定 / Calibration || 扫描器外参 + 车型 CAD 对齐&lt;br /&gt;
|-&lt;br /&gt;
| 上位系统 / Back-end || 公差库 + 报告生成 + MES 对接&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 工作流程 / Workflow ==&lt;br /&gt;
# 车下线后停在指定工位，触发 AGV 启动检测任务。&lt;br /&gt;
# AGV 读取车型 + VIN，加载对应 CAD 与公差库。&lt;br /&gt;
# AGV 按预设路径绕车（前 / 侧 / 尾），每个测点暂停，云台精对位扫描器。&lt;br /&gt;
# 3D 扫描器拍摄局部点云，识别两块钣金件的特征边沿。&lt;br /&gt;
# 边沿之间的 ''最小距离'' = 间隙；切线高度差 = 高低差。&lt;br /&gt;
# 与公差比较，标记 OK / NG。&lt;br /&gt;
# 完成后生成 ''彩虹图''报告（绿 = OK，红 = NG，灰 = 无数据），并发到 MES。&lt;br /&gt;
&lt;br /&gt;
# Vehicle stops at the inspection station; AGV starts.&lt;br /&gt;
# AGV reads VIN, loads matching CAD + tolerances.&lt;br /&gt;
# AGV follows a per-model path: stop, orient, scan at each measurement point.&lt;br /&gt;
# Extract edge features of adjacent panels.&lt;br /&gt;
# Min distance between edges → gap; tangent height difference → flush.&lt;br /&gt;
# Compare to tolerance; tag OK / NG.&lt;br /&gt;
# Generate rainbow report (green / red / grey) and post to MES.&lt;br /&gt;
&lt;br /&gt;
== 关键算法 / Algorithms ==&lt;br /&gt;
* '''边沿提取''' / Edge extraction: 从扫描点云中通过 ''法线突变''检测钣金件边沿。&lt;br /&gt;
* '''特征匹配''' / Feature matching: 把当前扫描特征与 CAD 中的 ''nominal'' 特征关联。&lt;br /&gt;
* '''3D 配准''' / 3D registration: ICP 把扫描结果对齐到车身 CAD 坐标。&lt;br /&gt;
* '''间隙计算''' / Gap computation: 两条边沿点云的 ''最小距离''；高低差用法向投影。&lt;br /&gt;
* '''统计''' / Stats: 多次重复测同一点的 σ；σ 大于阈值的测点视为不可信，标记 ''待复测''。&lt;br /&gt;
&lt;br /&gt;
== MDCS 集成 / MDCS integration ==&lt;br /&gt;
* '''Detour 端''': AGV 全程需要 &amp;lt; ±10 mm 定位（在工位附近）；常用激光 SLAM + 反光板。&lt;br /&gt;
* '''Clumsy 端''': 自定义 `PanelInspect` Movement，控制云台 + 扫描器 + 在每个测点停 1 s。&lt;br /&gt;
* '''Medulla 端''': 扫描器作为 [[Special:MyLanguage/3D相机适配|3D 相机插件]]，提供 ''点云 + 触发同步''。&lt;br /&gt;
* '''SimpleComposer 端''': 业务插件 `BodyInspector`，定义车型 ↔ 测点 ↔ 公差的映射。&lt;br /&gt;
* '''上位 MES''': REST API 接收报告 + 触发返工流程。&lt;br /&gt;
&lt;br /&gt;
== 精度链 / Precision chain ==&lt;br /&gt;
最终测量精度受三方约束：&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 误差源 / Error source !! 典型量级 / Typical&lt;br /&gt;
|-&lt;br /&gt;
| Detour 定位 / Localisation || ±5 mm&lt;br /&gt;
|-&lt;br /&gt;
| 云台机械重复 / Gimbal repeatability || ±0.05 mm&lt;br /&gt;
|-&lt;br /&gt;
| 扫描器 ''自身''精度 / Scanner intrinsic || ±0.05 mm&lt;br /&gt;
|-&lt;br /&gt;
| 标定残差 / Calibration residual || ±0.1 mm&lt;br /&gt;
|-&lt;br /&gt;
| 车体晃动 / Vehicle micro-motion || ±0.1 mm&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
合成误差 ~0.3 mm，符合 OEM 通用 ±0.5 mm 间隙公差。&lt;br /&gt;
&lt;br /&gt;
Synthesised error ≈ 0.3 mm, satisfies typical OEM ±0.5 mm tolerance.&lt;br /&gt;
&lt;br /&gt;
== 限制 / Limitations ==&lt;br /&gt;
* '''反光车漆''' / Specular paint: 黑车与镜面漆面扫描效果差；建议下线前喷防反光临时涂层。&lt;br /&gt;
* '''新车型上线''' / New vehicle model intro: 必须先用 CMM 标定一份 ''golden'' 测点；流程不可省。&lt;br /&gt;
* '''扫描器与车体距离敏感''': 距离 &amp;lt; ±3 mm 内才能保精度；云台机械刚度要求高。&lt;br /&gt;
* '''节拍''' / Throughput: 完整一台车测 ~60–120 测点，AGV 节拍 ≈ 3–5 min；汽车工厂线节拍紧时需要多 AGV 并行。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/3D相机适配|3D相机适配]]&lt;br /&gt;
* [[Special:MyLanguage/3D激光雷达适配|3D激光雷达适配]]&lt;br /&gt;
* [[Special:MyLanguage/标定与校准|标定与校准]]&lt;br /&gt;
* [[Special:MyLanguage/全息座舱|全息座舱]] — 相关 3D 重建应用&lt;br /&gt;
&lt;br /&gt;
[[Category:特殊技术方案]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8CycleGUI%E5%BF%AB%E9%80%9F%E5%BC%80%E5%8F%91UI%E7%95%8C%E9%9D%A2&amp;diff=1028</id>
		<title>如何使用CycleGUI快速开发UI界面</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8CycleGUI%E5%BF%AB%E9%80%9F%E5%BC%80%E5%8F%91UI%E7%95%8C%E9%9D%A2&amp;diff=1028"/>
		<updated>2026-05-16T11:43:04Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
[[Special:MyLanguage/CycleGUI|CycleGUI]] 是 MDCS 自研的 ''按需协程''GUI 框架。本页给开发者一份 ''最小学习路径''：开一个画面、画一点云、加一个按钮、读一个输入。&lt;br /&gt;
&lt;br /&gt;
[[Special:MyLanguage/CycleGUI|CycleGUI]] is MDCS's lazy-coroutine GUI framework. This page is the minimum learning path: open a panel, draw a cloud, add a button, accept an input.&lt;br /&gt;
&lt;br /&gt;
== 1. 开一个画面 / Open a panel ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using CycleGUI;&lt;br /&gt;
&lt;br /&gt;
// 任何线程都可以 / from any thread&lt;br /&gt;
var painter = UI.GetPainter(&amp;quot;my-debug&amp;quot;);&lt;br /&gt;
painter.DrawText(&amp;quot;Hello from algorithm thread&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
`UI.GetPainter(name)` 拿到一个 ''命名画板''；后续所有 `painter.DrawXxx` 累积到同一画面。&lt;br /&gt;
`UI.GetPainter(name)` returns a ''named painter''; subsequent `DrawXxx` calls accumulate into the same view.&lt;br /&gt;
&lt;br /&gt;
== 2. 画点云 / Draw a point cloud ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
var painter = UI.GetPainter(&amp;quot;lidar-frame&amp;quot;);&lt;br /&gt;
foreach (var pt in cachedLidar)&lt;br /&gt;
{&lt;br /&gt;
    var c = pt.intensity &amp;gt; 0.8 ? Color.Red : Color.White;&lt;br /&gt;
    painter.DrawDot3D(c,&lt;br /&gt;
        new Vector3((float)(pt.d * Math.Cos(pt.th)),&lt;br /&gt;
                    (float)(pt.d * Math.Sin(pt.th)),&lt;br /&gt;
                    0));&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
CycleGUI 把所有 `Draw` 命令存到 ''当前画面''；在 GUI 主线程下一次重绘时一次性渲染。&lt;br /&gt;
CycleGUI buffers all `Draw` commands for the current frame and renders them when the UI thread next repaints.&lt;br /&gt;
&lt;br /&gt;
== 3. 加一个按钮 / Add a button ==&lt;br /&gt;
按钮 + 通用 UI 控件用 ''协程''风格 —— 把 UI 逻辑写成 `IEnumerable&amp;lt;Cycle&amp;gt;` 由框架托管：&lt;br /&gt;
Buttons and other widgets use coroutines:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
UI.CastCoroutine(MyControl());&lt;br /&gt;
&lt;br /&gt;
IEnumerable&amp;lt;Cycle&amp;gt; MyControl()&lt;br /&gt;
{&lt;br /&gt;
    while (true)&lt;br /&gt;
    {&lt;br /&gt;
        var panel = UI.GetPanel(&amp;quot;control&amp;quot;);&lt;br /&gt;
        if (panel.Button(&amp;quot;Start scan&amp;quot;))&lt;br /&gt;
        {&lt;br /&gt;
            StartScan();  // 调用算法&lt;br /&gt;
        }&lt;br /&gt;
        if (panel.Button(&amp;quot;Stop&amp;quot;))&lt;br /&gt;
        {&lt;br /&gt;
            StopScan();&lt;br /&gt;
        }&lt;br /&gt;
        yield return Cycle.Next;   // 等下一帧&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
`UI.CastCoroutine` 把一个 `IEnumerable&amp;lt;Cycle&amp;gt;` 投递给 GUI 主线程；它只在需要重绘时被推进。&lt;br /&gt;
`UI.CastCoroutine` posts a coroutine to the UI thread; it advances only on repaint.&lt;br /&gt;
&lt;br /&gt;
== 4. 读一个输入 / Read an input ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
IEnumerable&amp;lt;Cycle&amp;gt; ConfigPanel()&lt;br /&gt;
{&lt;br /&gt;
    int speed = 600;&lt;br /&gt;
    while (true)&lt;br /&gt;
    {&lt;br /&gt;
        var p = UI.GetPanel(&amp;quot;config&amp;quot;);&lt;br /&gt;
        speed = p.IntInput(&amp;quot;speed (mm/s)&amp;quot;, speed, min: 0, max: 2000);&lt;br /&gt;
        p.Text($&amp;quot;Current speed = {speed}&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
        if (p.Button(&amp;quot;Apply&amp;quot;))&lt;br /&gt;
        {&lt;br /&gt;
            Configuration.conf.basicSpeed = speed;&lt;br /&gt;
        }&lt;br /&gt;
        yield return Cycle.Next;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 5. 等用户输入 / Wait for user input ==&lt;br /&gt;
有时算法要等用户点确认才继续 —— 用 `yield return WaitForButton(...)`：&lt;br /&gt;
&lt;br /&gt;
Sometimes the algorithm has to wait for user confirmation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
IEnumerable&amp;lt;Cycle&amp;gt; ApprovalFlow()&lt;br /&gt;
{&lt;br /&gt;
    while (true)&lt;br /&gt;
    {&lt;br /&gt;
        var p = UI.GetPanel(&amp;quot;approve&amp;quot;);&lt;br /&gt;
        p.Text(&amp;quot;Pending: cart approach to station-17&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
        yield return p.WaitForButton(&amp;quot;Approve&amp;quot;);  // 阻塞协程直到点击&lt;br /&gt;
        ConfirmApproach();&lt;br /&gt;
&lt;br /&gt;
        yield return Cycle.Seconds(1);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 6. 跨线程 / 跨进程 / Cross-thread / cross-process ==&lt;br /&gt;
* CycleGUI 自动把任意线程的 `Draw` 调用 ''marshal''到 UI 主线程。&lt;br /&gt;
* 同一 CycleGUI 服务可被多个进程连接；每个进程独立维护自己的 painter，UI 端可同时显示。&lt;br /&gt;
&lt;br /&gt;
CycleGUI auto-marshals `Draw` calls onto the UI thread from any thread. Multiple processes can connect to one CycleGUI server simultaneously; each has its own painter, all visible together.&lt;br /&gt;
&lt;br /&gt;
== 7. 录制与回放 / Record &amp;amp; replay ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
UI.StartRecording(&amp;quot;session.cgr&amp;quot;);&lt;br /&gt;
// ... 跑算法 + 各种 Draw ...&lt;br /&gt;
UI.StopRecording();&lt;br /&gt;
&lt;br /&gt;
// 离线回放&lt;br /&gt;
UI.Replay(&amp;quot;session.cgr&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 最佳实践 / Best practices ==&lt;br /&gt;
* '''命名 painter / panel''' 反映 ''数据来源''（如 `lidar-front`、`slam-map`），不要根据 UI 位置命名。&lt;br /&gt;
* '''协程内不要 sleep'''；改用 `yield return Cycle.Seconds(N)` 让框架调度。&lt;br /&gt;
* '''重命名 painter 会丢历史''' —— 如果用户在多次会话间想保留视图开关状态，名称保持稳定。&lt;br /&gt;
* '''数据多时考虑节流''' —— 即便是协程按需调用，每帧画百万点云仍会卡；用空间下采样或频率限制。&lt;br /&gt;
&lt;br /&gt;
== 与 Detour / Clumsy / SimpleComposer 的集成 / Integration ==&lt;br /&gt;
* Detour: SLAM 中间状态默认在 `detour-slam` painter；可在 Detour UI 中开关。&lt;br /&gt;
* Clumsy: Movement 内部可调 `UI.GetPainter(&amp;quot;movement-debug&amp;quot;)` 加自定义可视化。&lt;br /&gt;
* SimpleComposer: CAD 画布本身就是一个 CycleGUI 实例，自定义工具通过协程加新的 layer。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/CycleGUI|CycleGUI]]&lt;br /&gt;
* [[Special:MyLanguage/开发手册 - SimpleComposer界面开发 - CAD工具|开发手册 - SimpleComposer界面开发 - CAD工具]]&lt;br /&gt;
* [[Special:MyLanguage/LessokajiWeaver编译后处理工具|LessokajiWeaver编译后处理工具]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%A4%A9%E8%8A%B1%E6%9D%BFSLAM&amp;diff=1027</id>
		<title>天花板SLAM</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%A4%A9%E8%8A%B1%E6%9D%BFSLAM&amp;diff=1027"/>
		<updated>2026-05-16T11:43:04Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
天花板 SLAM（Ceiling SLAM）使用 ''向上''的相机或 2D 激光，把天花板上的稳定特征（横梁、风口、灯具、消防喷淋、棋盘形吊顶板）作为视觉地标。在地面繁忙、特征频繁变动的仓储 / 生产场景下，天花板往往比地面更稳定，因此天花板 SLAM 是一种 ''与地面隔离''的高鲁棒定位手段。&lt;br /&gt;
&lt;br /&gt;
Ceiling SLAM uses an upward-facing camera or 2D laser. The ceiling — beams, vents, sprinklers, lights, tile grids — is usually stable when the floor is hectic; ceiling SLAM is a high-robustness localisation source ''decoupled from floor traffic''.&lt;br /&gt;
&lt;br /&gt;
== 适用场景 / Use cases ==&lt;br /&gt;
* '''生产线 + 高人流仓库''': 地面物动态，天花板稳定。&lt;br /&gt;
* '''室外搬运转入室内''': 室内激光 SLAM 漂，天花板辅助。&lt;br /&gt;
* '''特定吊顶纹路'''（如等距格栅）: 几何对位精度高。&lt;br /&gt;
&lt;br /&gt;
== 硬件 / Hardware ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 部件 / Part !! 典型规格 / Typical&lt;br /&gt;
|-&lt;br /&gt;
| 相机 / Camera || 向上工业相机 + 广角镜头 / upward industrial camera + wide-angle lens&lt;br /&gt;
|-&lt;br /&gt;
| 视场 / FOV || 2 × 2 m 在 5 m 净高时 / 2×2 m FOV at 5 m clear height&lt;br /&gt;
|-&lt;br /&gt;
| 帧率 / Frame rate || 30 – 60 FPS&lt;br /&gt;
|-&lt;br /&gt;
| 光照 / Lighting || 多数室内自然光足够；强反差或弱光要加 LED 补光&lt;br /&gt;
|-&lt;br /&gt;
| 替代：2D 激光向上 / Alt: 2D upward laser || 用于无相机方案 / camera-less alternative&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 算法 / Algorithm ==&lt;br /&gt;
=== 视觉变体 / Visual variant ===&lt;br /&gt;
1. 特征提取（ORB / KAZE）&lt;br /&gt;
2. 视差 + IMU 估姿（相机模型为针孔）&lt;br /&gt;
3. 关键帧 + Bundle Adjustment&lt;br /&gt;
4. 全局闭环（词袋）&lt;br /&gt;
&lt;br /&gt;
=== 几何变体（吊顶板）/ Geometric variant (drop-ceiling) ===&lt;br /&gt;
吊顶板的等距格栅是 ''周期性几何信号''：&lt;br /&gt;
* 检测格栅交点 → 局部坐标系下规则点阵&lt;br /&gt;
* 用相位匹配（FFT）求当前帧相对地图的位移&lt;br /&gt;
* 周期性歧义通过 ''临近格唯一标记''（如不同形状的喷淋头）解消&lt;br /&gt;
&lt;br /&gt;
The regular grid in drop-ceiling tiles is a periodic geometric signal. Detect grid intersections → regular point lattice → phase matching (FFT) for translation. Periodic ambiguity is resolved by unique markers (sprinklers, vent shapes).&lt;br /&gt;
&lt;br /&gt;
== 与激光 SLAM 的融合 / Fusion with laser SLAM ==&lt;br /&gt;
天花板 SLAM 输出位姿 + 协方差送入 Detour 的 [[Special:MyLanguage/Detour软件架构|TightCoupler]]：&lt;br /&gt;
* 地面激光主导时 → 天花板做约束。&lt;br /&gt;
* 地面失效（特征不足、动态过多）→ 天花板上升为主源。&lt;br /&gt;
* 二者完全独立的故障模式 → 总体鲁棒性提升。&lt;br /&gt;
&lt;br /&gt;
Ceiling pose + covariance feeds Detour's TightCoupler:&lt;br /&gt;
* Laser dominates by default; ceiling adds constraints.&lt;br /&gt;
* When laser degrades, ceiling promotes to primary.&lt;br /&gt;
* Independent failure modes → overall robustness gains.&lt;br /&gt;
&lt;br /&gt;
== 配置 / Configuration ==&lt;br /&gt;
* 在 Detour 配置中启用 `useCeilingSlam = true`。&lt;br /&gt;
* 在 [[Special:MyLanguage/开发手册 - SimpleComposer界面开发 - CAD工具|CAD]] 中标注 ''天花板可定位区域''（部分区域无天花板，如龙门吊正下方）。&lt;br /&gt;
* 录制天花板地图：与激光 SLAM 同时跑一次完整覆盖，离线后处理。&lt;br /&gt;
&lt;br /&gt;
== 常见问题 / Common issues ==&lt;br /&gt;
* '''高反光天花板''' / Reflective ceiling: 在 LED 灯下完全反射，特征丢；用偏振镜或拍其它角度。&lt;br /&gt;
* '''天花板临时遮挡''' / Temporary occlusion: 吊车 / 维护工人；触发临时切换到激光主源。&lt;br /&gt;
* '''高度差异区''' / Height changes: 经过低净高区域时相机参数（视场覆盖面积）变化；用多模型自适应或离散段地图。&lt;br /&gt;
&lt;br /&gt;
== 与天眼系统的区别 / vs Skyeye ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 项 / Item !! 天花板 SLAM !! [[Special:MyLanguage/天眼系统|天眼系统]]&lt;br /&gt;
|-&lt;br /&gt;
| 视角 / Viewpoint || AGV 向上看天花板 || 天花板向下看 AGV / 货物&lt;br /&gt;
|-&lt;br /&gt;
| 角色 / Role || 本车定位 || 全局监控 / 装卸车协作&lt;br /&gt;
|-&lt;br /&gt;
| 设备 / Equipment || 装在每辆 AGV 上 || 固定吊装在天花板&lt;br /&gt;
|-&lt;br /&gt;
| 用途 / Use || SLAM | || 装卸车、堆码识别&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/Detour软件架构|Detour软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/多定位源的自动综合|多定位源的自动综合]]&lt;br /&gt;
* [[Special:MyLanguage/具有高鲁棒性的激光SLAM算法|具有高鲁棒性的激光SLAM算法]]&lt;br /&gt;
* [[Special:MyLanguage/天眼系统|天眼系统]]&lt;br /&gt;
&lt;br /&gt;
[[Category:定位导航相关手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%A4%A9%E7%9C%BC%E7%B3%BB%E7%BB%9F&amp;diff=1026</id>
		<title>天眼系统</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%A4%A9%E7%9C%BC%E7%B3%BB%E7%BB%9F&amp;diff=1026"/>
		<updated>2026-05-16T11:43:03Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
天眼系统（Skyeye）是 MDCS 中的 ''高位 3D 视觉''组件：在装卸车口、密集仓储区等关键位置吊装俯拍 3D 相机，把全局货物 / 堆码 / 车辆状态实时回传给 SimpleComposer，让多 AGV 协同完成单车感知盲区的作业。&lt;br /&gt;
&lt;br /&gt;
Skyeye is MDCS's '''overhead 3D vision''' subsystem: ceiling-mounted 3D cameras at critical points (loading docks, dense storage) provide a global view of cargo / stacks / vehicles to SimpleComposer in real time, enabling multi-AGV cooperation where individual AGVs can't see.&lt;br /&gt;
&lt;br /&gt;
== 系统组件 / Components ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 部件 / Part !! 规格 / Spec&lt;br /&gt;
|-&lt;br /&gt;
| 高位 3D 相机 / Overhead 3D camera || ToF 或结构光，安装高度 ≥ 4 m / ToF or structured light, ≥ 4 m mount&lt;br /&gt;
|-&lt;br /&gt;
| 推理服务 / Inference service || 把 3D 点云转 ''格点占用 + 朝向''，发布到 SimpleComposer&lt;br /&gt;
|-&lt;br /&gt;
| 数据通道 / Channel || HTTP / WebSocket / DObject 共享内存&lt;br /&gt;
|-&lt;br /&gt;
| 标定 / Calibration || 相机外参对齐到仓库世界坐标系&lt;br /&gt;
|-&lt;br /&gt;
| SimpleComposer SkyeyeStrategy 插件 || 调度策略：[[Special:MyLanguage/联动天眼系统进行装卸车|联动天眼系统进行装卸车]]&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 典型应用 / Typical use cases ==&lt;br /&gt;
* '''半挂车装车 / 卸车''' — 见 [[Special:MyLanguage/联动天眼系统进行装卸车|联动天眼系统进行装卸车]]&lt;br /&gt;
* '''集装箱卸货'''&lt;br /&gt;
* '''密集堆垛区智能堆码''' — 见 [[Special:MyLanguage/识别料框并堆垛拆垛|识别料框并堆垛拆垛]]&lt;br /&gt;
* '''高架仓拣选辅助''' — 全局看哪一架还有空位&lt;br /&gt;
* '''异常监控''' — 货物倒塌、人员闯入、设备故障&lt;br /&gt;
&lt;br /&gt;
== 数据契约 / Data contract ==&lt;br /&gt;
天眼推理服务发布 ''帧''级数据：&lt;br /&gt;
The inference service publishes per-frame data:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;ts&amp;quot;: 1715843200,&lt;br /&gt;
  &amp;quot;areaId&amp;quot;: &amp;quot;DOCK_NORTH&amp;quot;,&lt;br /&gt;
  &amp;quot;frame&amp;quot;: 12345,&lt;br /&gt;
  &amp;quot;objects&amp;quot;: [&lt;br /&gt;
    { &amp;quot;id&amp;quot;: 17, &amp;quot;type&amp;quot;: &amp;quot;wood_pallet&amp;quot;,&lt;br /&gt;
      &amp;quot;x&amp;quot;: 850, &amp;quot;y&amp;quot;: 600, &amp;quot;z&amp;quot;: 230, &amp;quot;theta&amp;quot;: 0.04,&lt;br /&gt;
      &amp;quot;size&amp;quot;: {&amp;quot;w&amp;quot;: 1100, &amp;quot;l&amp;quot;: 1100, &amp;quot;h&amp;quot;: 150},&lt;br /&gt;
      &amp;quot;conf&amp;quot;: 0.91 },&lt;br /&gt;
    { &amp;quot;id&amp;quot;: 18, &amp;quot;type&amp;quot;: &amp;quot;kit_bin&amp;quot;,&lt;br /&gt;
      &amp;quot;x&amp;quot;: 1820, &amp;quot;y&amp;quot;: 600, &amp;quot;z&amp;quot;: 250, &amp;quot;theta&amp;quot;: 0.0, &amp;quot;conf&amp;quot;: 0.88 }&lt;br /&gt;
  ],&lt;br /&gt;
  &amp;quot;grid&amp;quot;: {&lt;br /&gt;
    &amp;quot;cellSize&amp;quot;: 200,&lt;br /&gt;
    &amp;quot;cells&amp;quot;: [ /* ... */ ]&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 与 AGV 自身感知的协同 / Cooperation with on-AGV sensing ==&lt;br /&gt;
天眼提供 ''全局'' + ''粗精度''（≈ ±50 mm），AGV 自身激光 / 3D 提供 ''局部'' + ''细精度''（≈ ±10 mm）。MDCS 把它们结合：&lt;br /&gt;
&lt;br /&gt;
Skyeye gives '''global + coarse''' (≈ ±50 mm); on-AGV sensing gives '''local + fine''' (≈ ±10 mm). They combine:&lt;br /&gt;
&lt;br /&gt;
* '''分工 / Division of labour''': Skyeye 告诉 AGV &amp;quot;去哪&amp;quot;；AGV 自己 &amp;quot;怎么去 + 精对位&amp;quot;。&lt;br /&gt;
* '''冲突仲裁 / Conflict resolution''': 二者分歧时，''以局部传感为准 + 让 Skyeye 重扫''。&lt;br /&gt;
* '''时间同步 / Time sync''': NTP / PTP，误差 &amp;lt; 100 ms。&lt;br /&gt;
&lt;br /&gt;
== 标定 / Calibration ==&lt;br /&gt;
天眼相机的外参 = ''相机到仓库世界坐标系''变换。标定流程：&lt;br /&gt;
Calibration of the camera-to-world transform:&lt;br /&gt;
&lt;br /&gt;
# 在相机视场内放 N ≥ 4 个 ''已知位置''的标定靶（地面贴 AprilTag）。&lt;br /&gt;
# 相机扫一帧 → 检测靶位置（相机坐标系）。&lt;br /&gt;
# 用标定靶世界坐标 + 相机坐标做最小二乘 → 外参。&lt;br /&gt;
# 误差 ≤ 20 mm 视为合格。&lt;br /&gt;
&lt;br /&gt;
# Place N ≥ 4 known-pose calibration targets in the FOV.&lt;br /&gt;
# Scan; detect their positions in the camera frame.&lt;br /&gt;
# Least-squares from world ↔ camera point pairs → extrinsics.&lt;br /&gt;
# Acceptable error ≤ 20 mm.&lt;br /&gt;
&lt;br /&gt;
详见 [[Special:MyLanguage/标定与校准|标定与校准]]。&lt;br /&gt;
&lt;br /&gt;
== 部署 / Deployment ==&lt;br /&gt;
* 推荐每个装卸口 ''≥ 1''台天眼，重要节点 2 台冗余。&lt;br /&gt;
* 安装位置：避开起吊 / 叉车作业的物理路径。&lt;br /&gt;
* 数据带宽：单 3D 相机 ~30 Mbps，多相机用千兆以太 / 私有 VLAN。&lt;br /&gt;
* 推理服务部署：本地 GPU 工控机（推荐）或边缘 / 云（按延迟需求）。&lt;br /&gt;
&lt;br /&gt;
== 失败模式 / Failure modes ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 失败 / Failure !! 处理 / Recovery&lt;br /&gt;
|-&lt;br /&gt;
| 相机断电 / Camera offline || SimpleComposer 标该区域 ''Skyeye-down''，暂停天眼依赖任务&lt;br /&gt;
|-&lt;br /&gt;
| 标定漂 / Drift || 自动重扫 ''参考靶''；偏差 &amp;gt; 50 mm 触发人工复测&lt;br /&gt;
|-&lt;br /&gt;
| 视觉遮挡 / Occlusion (overhead crane, banner) || 切到激光 SLAM + 局部 AGV 感知&lt;br /&gt;
|-&lt;br /&gt;
| 误识别 / False detection || 置信度阈值 + 多帧一致性 + 人工标注的负例反馈&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/联动天眼系统进行装卸车|联动天眼系统进行装卸车]]&lt;br /&gt;
* [[Special:MyLanguage/自动装卸车应用|自动装卸车应用]]&lt;br /&gt;
* [[Special:MyLanguage/3D相机适配|3D相机适配]]&lt;br /&gt;
* [[Special:MyLanguage/天花板SLAM|天花板SLAM]] — 注意视角相反&lt;br /&gt;
&lt;br /&gt;
[[Category:特殊技术方案]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%A4%9A%E5%AE%9A%E4%BD%8D%E6%BA%90%E7%9A%84%E8%87%AA%E5%8A%A8%E7%BB%BC%E5%90%88&amp;diff=1025</id>
		<title>多定位源的自动综合</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%A4%9A%E5%AE%9A%E4%BD%8D%E6%BA%90%E7%9A%84%E8%87%AA%E5%8A%A8%E7%BB%BC%E5%90%88&amp;diff=1025"/>
		<updated>2026-05-16T11:43:02Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
&amp;quot;多定位源的自动综合&amp;quot;是 Detour 把多种定位手段（激光 SLAM、地纹 SLAM、天花板 SLAM、二维码、IMU、轮编里程计、RTK、UWB）融合为单一 6-DoF 位姿的机制。核心是 TightCoupler —— 一个紧耦合的位姿图优化器，根据每个观测源的协方差自动加权。&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Auto multi-source positioning fusion&amp;quot; is Detour's mechanism for merging multiple localisation sources (laser SLAM, ground-texture SLAM, ceiling SLAM, QR, IMU, wheel odometry, RTK, UWB) into a single 6-DoF pose. The core is the TightCoupler — a tightly-coupled pose-graph optimiser that weights each source by its covariance.&lt;br /&gt;
&lt;br /&gt;
实现位置：`D:\src\Detour\DetourCore\Algorithms\TightCoupler.ExternalCoupler.cs`。&lt;br /&gt;
Implementation: `D:\src\Detour\DetourCore\Algorithms\TightCoupler.ExternalCoupler.cs`.&lt;br /&gt;
&lt;br /&gt;
== 为什么需要 / Why fuse ==&lt;br /&gt;
任何单一定位源都有 ''盲点''：&lt;br /&gt;
Every single source has '''blind spots''':&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 源 / Source !! 强 / Strength !! 弱 / Weakness&lt;br /&gt;
|-&lt;br /&gt;
| 激光 SLAM || 几何信息丰富的工业场景精度高 / High accuracy in geom-rich industrial sites || 长走廊 / 镜面 / 重对称场景退化&lt;br /&gt;
|-&lt;br /&gt;
| 地纹 SLAM || 平整地面 mm 量级 / Sub-cm on smooth floors || 湿水 / 油污地遮挡&lt;br /&gt;
|-&lt;br /&gt;
| 天花板 SLAM || 地面动态时仍稳 / Stable when floor is busy || 高反射 / 低净高场景&lt;br /&gt;
|-&lt;br /&gt;
| 二维码 || 已知二维码处亚厘米 / Sub-cm where tags present || 视场无二维码时无输出&lt;br /&gt;
|-&lt;br /&gt;
| IMU + 轮编里程计 || 高频、低延迟 / High-rate, low-latency || 累积漂移，无全局信息&lt;br /&gt;
|-&lt;br /&gt;
| RTK GNSS || 室外厘米级 / Outdoor cm-level || 室内不可用&lt;br /&gt;
|-&lt;br /&gt;
| UWB || 室内 ~10 cm / Indoor ~10 cm || 多径效应 + 基站布设成本&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
''多源融合''让强项互补、弱项互救：例如长走廊上激光退化、地纹接管；天眼区有遮挡时二维码补全。&lt;br /&gt;
Fusion lets strengths complement and weaknesses recover: e.g. laser drops in long corridors while ground-texture takes over; QR catches up where ceiling is occluded.&lt;br /&gt;
&lt;br /&gt;
== 紧耦合 / Tight coupling ==&lt;br /&gt;
&amp;quot;紧耦合&amp;quot;是与&amp;quot;松耦合&amp;quot;对比的术语：&lt;br /&gt;
&amp;quot;Tight&amp;quot; vs &amp;quot;loose&amp;quot; coupling:&lt;br /&gt;
&lt;br /&gt;
* '''松耦合 / Loose''': 每个源独立产生 ''位姿''，最后用 EKF 加权平均位姿。简单但损失原始观测信息。&lt;br /&gt;
* '''紧耦合 / Tight''': 每个源的 ''原始观测''（约束）进入同一个位姿图，统一优化。MDCS 使用紧耦合。&lt;br /&gt;
&lt;br /&gt;
Loose: each source emits a pose; EKF averages them. Tight: each source's raw observation enters one pose graph; everything optimised jointly. MDCS uses tight coupling.&lt;br /&gt;
&lt;br /&gt;
== 外部反馈接口 / External-feed API ==&lt;br /&gt;
非 Detour 内置的定位源（轮编里程计 / IMU / RTK / UWB / 第三方视觉）通过 `TightCoupler.PostExternalFeed()` 接入：&lt;br /&gt;
&lt;br /&gt;
Non-built-in sources (wheel odom / IMU / RTK / UWB / third-party vision) enter Detour through `TightCoupler.PostExternalFeed()`:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
TightCoupler.PostExternalFeed(new ExternalFeed&lt;br /&gt;
{&lt;br /&gt;
    name           = &amp;quot;wheel_imu_2&amp;quot;,&lt;br /&gt;
    counter        = tick++,&lt;br /&gt;
    hasTranslation = true,&lt;br /&gt;
    hasRotation    = true,&lt;br /&gt;
    is2D           = true,        // 2D 平面运动 / 2D planar&lt;br /&gt;
    integrated     = true,        // 这是个 ''累积'' 量 / cumulative&lt;br /&gt;
    toRemap        = false,       // 不是绝对参考系 / not absolute frame&lt;br /&gt;
    startingTick   = baselineTick,// 累积起算的基准 tick / baseline&lt;br /&gt;
    x = odomX, y = odomY, th = odomTh&lt;br /&gt;
}, name: &amp;quot;WheelImu&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
主要字段：&lt;br /&gt;
* `hasTranslation / hasRotation` — 提供的自由度&lt;br /&gt;
* `is2D / is3D` — 平面 vs 3D&lt;br /&gt;
* `integrated` — 累积量（必须配合 `startingTick`）还是单帧量&lt;br /&gt;
* `toRemap` — true 表示绝对参考系（如 RTK），融合时需要做全局重映射&lt;br /&gt;
&lt;br /&gt;
== 自动加权 / Automatic weighting ==&lt;br /&gt;
TightCoupler 不需要手动设置每个源的权重；权重 = 协方差矩阵的逆。每个源必须 ''诚实''汇报自己的协方差：&lt;br /&gt;
&lt;br /&gt;
TightCoupler doesn't require hand-tuned per-source weights. Weight = covariance inverse. Each source must report its covariance honestly:&lt;br /&gt;
&lt;br /&gt;
* 激光 SLAM：Hessian 求逆给出&lt;br /&gt;
* 二维码：从 PnP 重投影残差估计&lt;br /&gt;
* IMU / 里程计：从硬件规格 + 时间间隔估计&lt;br /&gt;
* RTK：从 GNSS 接收机 reported DOP&lt;br /&gt;
&lt;br /&gt;
错报协方差是 ''融合崩坏''的最常见原因 —— 一个谎报&amp;quot;我超准&amp;quot;的源会 ''统治''融合结果。&lt;br /&gt;
Mis-reporting covariance is the single most common cause of fusion blow-ups — a source claiming to be much more accurate than reality will dominate.&lt;br /&gt;
&lt;br /&gt;
== 故障处理 / Failure handling ==&lt;br /&gt;
* '''源静默''' / Source silence: 超时 → 该源从下一帧的位姿图中 ''删除''，不影响其它源。&lt;br /&gt;
* '''源剧烈跳变''' / Wild source jump: TightCoupler 内置 ''卡方检验''，跳变 &amp;gt; 5σ 的观测直接丢弃。&lt;br /&gt;
* '''融合发散''' / Fusion divergence: 残差持续上升 → 触发 ''软重启''（保留主导源，重置其它源的位姿先验）。&lt;br /&gt;
&lt;br /&gt;
== 调试 / Debugging ==&lt;br /&gt;
Detour UI 的&amp;quot;融合面板&amp;quot;显示：&lt;br /&gt;
The Detour UI's &amp;quot;fusion panel&amp;quot; shows:&lt;br /&gt;
&lt;br /&gt;
* 每个源的最新观测时刻 + 协方差&lt;br /&gt;
* 当前主导源&lt;br /&gt;
* 残差直方图&lt;br /&gt;
* 协方差膨胀 / 收缩历史&lt;br /&gt;
&lt;br /&gt;
打开 ''单源调试模式''可以 ''强制只用一个源''跑定位（用于诊断单源故障）。&lt;br /&gt;
&lt;br /&gt;
A &amp;quot;single-source debug mode&amp;quot; forces the fusion to use only one source — handy for diagnosing a misbehaving one.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/Detour软件架构|Detour软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/Detour激光SLAM算法详解|Detour激光SLAM算法详解]]&lt;br /&gt;
* [[Special:MyLanguage/Detour地面纹理SLAM算法详解|Detour地面纹理SLAM算法详解]]&lt;br /&gt;
* [[Special:MyLanguage/二维码识别导航|二维码识别导航]]&lt;br /&gt;
* [[Special:MyLanguage/天花板SLAM|天花板SLAM]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 同时使用激光、地纹、二维码、轮编里程计和IMU进行鲁棒定位|使用手册 - 同时使用激光、地纹、二维码、轮编里程计和IMU进行鲁棒定位]]&lt;br /&gt;
* [[Special:MyLanguage/输入GPS外部定位|输入GPS外部定位]]&lt;br /&gt;
&lt;br /&gt;
[[Category:定位导航相关手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%A4%8D%E5%90%88%E5%8D%B7%E6%96%99%E6%9C%BA%E6%A2%B0%E6%89%8B%E5%8F%89%E8%BD%A6&amp;diff=1024</id>
		<title>复合卷料机械手叉车</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%A4%8D%E5%90%88%E5%8D%B7%E6%96%99%E6%9C%BA%E6%A2%B0%E6%89%8B%E5%8F%89%E8%BD%A6&amp;diff=1024"/>
		<updated>2026-05-16T11:43:01Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
&amp;quot;复合卷料机械手叉车&amp;quot;是 MDCS 中的复合机器人形态：把 ''叉车''+''机械手''+''卷料专用夹具''组合在一台 AGV 上，实现卷料（钢卷、纸卷、塑料卷、布卷等）的自动取放与上下料。它是 ''叉车适配 + 卷料夹具适配 + 机械手协同''的综合案例。&lt;br /&gt;
&lt;br /&gt;
The &amp;quot;composite coil-handling robotic forklift&amp;quot; combines '''forklift''' + '''robotic arm''' + '''coil-specific gripper''' on one AGV to autonomously handle coils (steel, paper, plastic, fabric, …). It's the composite of [[Special:MyLanguage/叉车适配案例|forklift adaptation]] + coil-gripper adaptation + manipulator coordination.&lt;br /&gt;
&lt;br /&gt;
== 业务场景 / Business scenarios ==&lt;br /&gt;
* '''钢卷仓库''' / Steel-coil yard: 单卷重 5–25 t，堆叠 3–5 层&lt;br /&gt;
* '''造纸 / 卷烟生产线''' / Paper / cigarette lines: 中等卷重，节拍紧&lt;br /&gt;
* '''汽车冲压线''' / Auto stamping: 卷料上料 + 废料下料&lt;br /&gt;
* '''纺织''' / Textile: 布卷搬运 + 上下料&lt;br /&gt;
&lt;br /&gt;
== 硬件组成 / Hardware ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 部件 / Part !! 规格 / Spec&lt;br /&gt;
|-&lt;br /&gt;
| 底盘 / Chassis || 重载叉车 / 牵引车 / heavy-load forklift or tractor&lt;br /&gt;
|-&lt;br /&gt;
| 机械手 / Arm || 6-DoF 工业臂或 SCARA / 6-DoF or SCARA&lt;br /&gt;
|-&lt;br /&gt;
| 卷料夹具 / Coil gripper || V 形托架 + 夹紧机构 / V-cradle + clamp; 或 ''C 形钩'' / C-hook&lt;br /&gt;
|-&lt;br /&gt;
| 视觉 / Vision || 3D 相机扫卷端面识别中心 + 直径 / 3D camera for coil-end ID&lt;br /&gt;
|-&lt;br /&gt;
| 激光雷达 / Lidar || 前向 + 侧向（多激光融合）&lt;br /&gt;
|-&lt;br /&gt;
| 负载传感 / Load sensor || 称重模块；溢出阈值即报警&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
机械手的关注点：足够长以触到地面卷 + 提到货架；足够强以拿动单卷重量；末端夹具与卷料 V 形配合好。&lt;br /&gt;
Key arm requirements: long enough to reach a floor coil and lift to rack, strong enough for single-coil weight, end-effector matches the V-cradle profile.&lt;br /&gt;
&lt;br /&gt;
== 1. Medulla 适配 / Medulla side ==&lt;br /&gt;
比标准叉车多了机械手 + 卷料夹具的 IO：&lt;br /&gt;
On top of a standard forklift, add IO for the arm + the coil-specific gripper:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class CoilFork : CartDefinition&lt;br /&gt;
{&lt;br /&gt;
    // 底盘 IO（同 LB14 forklift）/ Chassis IO same as a regular forklift...&lt;br /&gt;
&lt;br /&gt;
    // 机械手 IO / Arm IO&lt;br /&gt;
    [AsUpperIO] public float[] armJointTgt = new float[6];&lt;br /&gt;
    [AsUpperIO] public bool   gripperClose;&lt;br /&gt;
    [AsLowerIO] public float[] armJointEst = new float[6];&lt;br /&gt;
    [AsLowerIO] public bool   gripperClosed;&lt;br /&gt;
    [AsLowerIO] public float  coilWeight;&lt;br /&gt;
&lt;br /&gt;
    // 卷料夹具 IO / Coil gripper IO&lt;br /&gt;
    [AsUpperIO] public bool   vCradleLift;          // V 托架升起&lt;br /&gt;
    [AsUpperIO] public bool   clampOn;              // 夹紧&lt;br /&gt;
    [AsLowerIO] public bool   clampForceOK;         // 夹紧力到位&lt;br /&gt;
    [AsLowerIO] public bool   coilDetected;         // 卷在槽内&lt;br /&gt;
&lt;br /&gt;
    [AsInitParam] public string armConfig = &amp;quot;armconfigs/UR10.json&amp;quot;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 2. Clumsy 自动驾驶适配 / Clumsy side ==&lt;br /&gt;
卷料场景多了几个特定 Movement：&lt;br /&gt;
A few coil-specific Movements:&lt;br /&gt;
&lt;br /&gt;
* `CoilApproach` — 用 3D 相机识别卷端面，AGV 居中对位&lt;br /&gt;
* `CoilPickWithArm` — 机械手放 V 托架 → 卷滚入托架 → 升起 → 夹紧&lt;br /&gt;
* `CoilPlaceOnRack` — 把卷放到 V 形支架 (rack saddle)&lt;br /&gt;
* `CoilDestackFromGround` — 地面堆叠的卷分层取下&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public void PickCoilFromGround(double sx, double sy, int srcId)&lt;br /&gt;
{&lt;br /&gt;
    Queue(&lt;br /&gt;
      async () =&amp;gt; DriveTask.WaitDriveTask(new SteeringLineFollowing&lt;br /&gt;
              { srcX = currentX, srcY = currentY,&lt;br /&gt;
                dstX = sx, dstY = sy, basespeed = 400 }.Follow()),&lt;br /&gt;
      async () =&amp;gt;&lt;br /&gt;
      {&lt;br /&gt;
          // 3D 相机识别卷&lt;br /&gt;
          DriveTask.WaitDriveTask(new CoilApproach&lt;br /&gt;
              { stopDist = 600, coilDiamMax = 1500 }.Get());&lt;br /&gt;
      },&lt;br /&gt;
      async () =&amp;gt;&lt;br /&gt;
      {&lt;br /&gt;
          // 机械手伸出 + V 托架放到卷下&lt;br /&gt;
          await MoveArmTo(grabPose);&lt;br /&gt;
          SetUpperIO(&amp;quot;vCradleLift&amp;quot;, false);&lt;br /&gt;
          await Task.Delay(500);&lt;br /&gt;
          DriveTask.WaitDriveTask(new ForwardTracker&lt;br /&gt;
              { dstAhead = 800, basespeed = 100 }.Get()); // 微推让卷滚入托架&lt;br /&gt;
      },&lt;br /&gt;
      async () =&amp;gt;&lt;br /&gt;
      {&lt;br /&gt;
          SetUpperIO(&amp;quot;vCradleLift&amp;quot;, true);&lt;br /&gt;
          await WaitForLowerIO(&amp;quot;coilDetected&amp;quot;, 4000);&lt;br /&gt;
          SetUpperIO(&amp;quot;clampOn&amp;quot;, true);&lt;br /&gt;
          await WaitForLowerIO(&amp;quot;clampForceOK&amp;quot;, 3000);&lt;br /&gt;
      });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 3. SimpleComposer 端 / SimpleComposer side ==&lt;br /&gt;
* '''带卷包络''' / Loaded envelope: 卷直径影响 ''车体外缘''，必须声明对应大包络。&lt;br /&gt;
* '''可达性约束''' / Reachability: 持卷在低净高区段降速 + 禁止某些急转。&lt;br /&gt;
* '''机械手姿态约束''' / Arm pose: 行驶时机械手必须收回到 ''行驶姿态''；离开取放工位前不允许行驶。&lt;br /&gt;
&lt;br /&gt;
* The carrying envelope expands with coil diameter; declare an alternate envelope.&lt;br /&gt;
* Reachability prohibits sharp turns + low-clearance corridors when carrying.&lt;br /&gt;
* Arm pose must be 'stow' before driving; refuse motion otherwise.&lt;br /&gt;
&lt;br /&gt;
== 4. 安全 / Safety ==&lt;br /&gt;
* '''卷脱落自动停车''' / Coil-drop auto-stop: `coilDetected` 在搬运中突然为 false → e-stop。&lt;br /&gt;
* '''机械手碰撞监测''' / Arm collision: 关节力矩 &amp;gt; 阈值 → e-stop。&lt;br /&gt;
* '''负载超限''' / Overload: 单卷 &amp;gt; 设计载重 → 拒绝取。&lt;br /&gt;
* '''人员靠近''' / Personnel proximity: 工作半径内 1.5 m 任何动态物体 → e-stop。&lt;br /&gt;
&lt;br /&gt;
== 5. 节拍 / Throughput ==&lt;br /&gt;
单卷搬运参考节拍：&lt;br /&gt;
Per-coil cycle:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 阶段 / Stage !! 时间 / Time&lt;br /&gt;
|-&lt;br /&gt;
| 巡线到取卷工位 / Travel to source || 30–60 s&lt;br /&gt;
|-&lt;br /&gt;
| 3D 视觉识别 + 对位 / 3D recognition || 5–10 s&lt;br /&gt;
|-&lt;br /&gt;
| 机械手取卷 / Arm pickup || 15–25 s&lt;br /&gt;
|-&lt;br /&gt;
| 巡线到目标工位 / Travel to dest. || 30–60 s&lt;br /&gt;
|-&lt;br /&gt;
| 机械手放卷 / Arm place || 15–25 s&lt;br /&gt;
|-&lt;br /&gt;
| 离开 / Withdraw || 10–15 s&lt;br /&gt;
|-&lt;br /&gt;
| 单次总节拍 / Per-coil total || 1.5–3 min&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/叉车适配案例|叉车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/全向车适配案例|全向车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/使用叉车自动取托盘功能|使用叉车自动取托盘功能]]&lt;br /&gt;
* [[Special:MyLanguage/3D相机适配|3D相机适配]]&lt;br /&gt;
* [[Special:MyLanguage/识别料框并堆垛拆垛|识别料框并堆垛拆垛]]&lt;br /&gt;
&lt;br /&gt;
[[Category:特殊技术方案]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%9F%BA%E4%BA%8E%E7%BD%91%E7%BB%9C%E6%B5%81%E7%9A%84%E4%B8%9A%E5%8A%A1%E8%A7%84%E5%88%92%E5%99%A8&amp;diff=1023</id>
		<title>基于网络流的业务规划器</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%9F%BA%E4%BA%8E%E7%BD%91%E7%BB%9C%E6%B5%81%E7%9A%84%E4%B8%9A%E5%8A%A1%E8%A7%84%E5%88%92%E5%99%A8&amp;diff=1023"/>
		<updated>2026-05-16T11:43:01Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
&amp;quot;业务规划器&amp;quot;是调度链路最上层的决策器：把 ''业务任务列表''（订单、生产工单）映射成 ''AGV 任务集合''。基于网络流的实现把这一映射建模为 ''最大流 / 最小费用流''问题，让规划在大规模、多约束、多目标的场景下仍然高效收敛。&lt;br /&gt;
&lt;br /&gt;
The '''business planner''' is the topmost decision layer in the scheduling chain: it maps ''business jobs'' (orders, production work-orders) onto ''AGV tasks''. The network-flow implementation casts this as a ''max-flow / min-cost flow'' problem so planning stays tractable at large scale with many constraints and objectives.&lt;br /&gt;
&lt;br /&gt;
== 适用 / When applicable ==&lt;br /&gt;
* 货 ↔ AGV ↔ 工位 三方匹配（混型货物多）/ Goods ↔ AGV ↔ work-station three-way matching with mixed cargo types.&lt;br /&gt;
* 业务有明确成本指标（节拍、能耗、磨损分布）/ Business has clear cost metrics (cycle time, energy, wear distribution).&lt;br /&gt;
* 任务量 ≥ 100 / 小时 / Task volume ≥ 100 / hour.&lt;br /&gt;
&lt;br /&gt;
简单单线场景不需要网络流 —— 用 ''FIFO + 简单贪心''即可。&lt;br /&gt;
&lt;br /&gt;
For small linear deployments, FIFO + greedy is fine.&lt;br /&gt;
&lt;br /&gt;
== 网络流模型 / Network-flow model ==&lt;br /&gt;
最经典模型（最小费用最大流）：&lt;br /&gt;
&lt;br /&gt;
  S (源 / source)&lt;br /&gt;
   │&lt;br /&gt;
   ▼ cap=1 cost=0 (每个 AGV 一条供给边)&lt;br /&gt;
  ┌───────────────────────────────────┐&lt;br /&gt;
  │  Layer 1: AGV agent nodes          │&lt;br /&gt;
  └───────────────────────────────────┘&lt;br /&gt;
           │ cap=∞ cost=c_ij&lt;br /&gt;
           ▼ (c_ij = AGV_i 执行 task_j 的成本)&lt;br /&gt;
  ┌───────────────────────────────────┐&lt;br /&gt;
  │  Layer 2: Task nodes (each task = 1 node) │&lt;br /&gt;
  └───────────────────────────────────┘&lt;br /&gt;
           │ cap=1 cost=0&lt;br /&gt;
           ▼&lt;br /&gt;
  T (汇 / sink)&lt;br /&gt;
&lt;br /&gt;
求解 min-cost flow → 哪 AGV 做哪个任务的指派。&lt;br /&gt;
&lt;br /&gt;
Solve min-cost flow → which AGV does which task.&lt;br /&gt;
&lt;br /&gt;
成本 c_ij 通常包含：&lt;br /&gt;
The cost c_ij typically includes:&lt;br /&gt;
&lt;br /&gt;
* 当前位置到取货点的距离（行驶时间）/ distance from current position to pickup&lt;br /&gt;
* AGV 当前剩余电量与任务能耗的匹配度 / battery vs energy required&lt;br /&gt;
* 当前车型与货物匹配度（如：托盘叉车不能搬料筐）/ vehicle-cargo compatibility&lt;br /&gt;
* 公平性惩罚（让 ''每辆车工作量'' 均衡）/ workload-fairness penalty&lt;br /&gt;
* 时间窗（任务必须 N 秒内取走）/ time-window pressure&lt;br /&gt;
&lt;br /&gt;
== 求解器 / Solvers ==&lt;br /&gt;
SimpleCore 支持几种规模 / 方案：&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 规模 / Scale !! 算法 / Algorithm !! 复杂度 / Complexity&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt; 200 任务 || SSP (Successive Shortest Paths) || O(F · (n + m) log n)&lt;br /&gt;
|-&lt;br /&gt;
| 200 – 2000 任务 || Cost-scaling / Network Simplex || O(n³)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;gt; 2000 任务 || LP 松弛 + 圆整 / LP relaxation + rounding || 近似 / Approximate&lt;br /&gt;
|-&lt;br /&gt;
| 任意 || 增量重规划 / Incremental replanning || 上次解 + δ&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 增量重规划 / Incremental replanning ==&lt;br /&gt;
业务规划不能 ''每秒重头跑'' —— 上一次的解大部分可用。MDCS 采用 ''增量更新''：&lt;br /&gt;
* 新任务进入 → 加节点 + 增广路径。&lt;br /&gt;
* AGV 完成任务 → 移除节点 + 重路由其它任务。&lt;br /&gt;
* AGV 故障 → 标该供给边 cap=0，对其原任务重新分配。&lt;br /&gt;
&lt;br /&gt;
The business planner doesn't replan from scratch each second — most of the previous solution stays valid. MDCS uses incremental updates: add tasks via augmenting paths; remove completed tasks; on AGV failure, zero its supply edge and reassign its tasks.&lt;br /&gt;
&lt;br /&gt;
== 与调度内核的关系 / Relation to the scheduler kernel ==&lt;br /&gt;
业务规划器 ''上接''业务系统（WMS / ERP），''下接''调度内核（DPS / 流场）：&lt;br /&gt;
&lt;br /&gt;
  WMS / ERP&lt;br /&gt;
      │  订单 / 工单&lt;br /&gt;
      ▼&lt;br /&gt;
  [业务规划器 / Business planner]   ← 本页 / this page&lt;br /&gt;
      │  任务对 AGV 的指派&lt;br /&gt;
      ▼&lt;br /&gt;
  [调度内核 / Scheduler kernel]&lt;br /&gt;
   - DPS（[[Special:MyLanguage/DPS调度算法详解|DPS调度算法详解]]）&lt;br /&gt;
   - 流场（[[Special:MyLanguage/流场规划|流场规划]]）&lt;br /&gt;
      │  每辆车的具体路径&lt;br /&gt;
      ▼&lt;br /&gt;
  [Car.actualSendScript] → 单车执行&lt;br /&gt;
&lt;br /&gt;
业务规划只关心 ''谁做什么''，不关心具体路径与交管。&lt;br /&gt;
&lt;br /&gt;
The business planner only decides ''who does what''. Paths and traffic control are the scheduler kernel's job.&lt;br /&gt;
&lt;br /&gt;
== 调试与可视化 / Debug &amp;amp; visualisation ==&lt;br /&gt;
* 在 SimpleComposer 中开 ''Planner Trace''：每次规划的所有边 + cost + 求得流值都可视化。&lt;br /&gt;
* 增量重规划失败时，规划器会 ''回退到全量重跑''并打印 ''diff''。&lt;br /&gt;
* 性能采样：每 N 次规划记 ''解时间 + 任务数 + 改变的边数''。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/调度内核运行原理|调度内核运行原理]]&lt;br /&gt;
* [[Special:MyLanguage/DPS调度算法详解|DPS调度算法详解]]&lt;br /&gt;
* [[Special:MyLanguage/流场规划|流场规划]]&lt;br /&gt;
* [[Special:MyLanguage/Simple软件架构|Simple软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 车队管控|使用手册 - 车队管控]]&lt;br /&gt;
&lt;br /&gt;
[[Category:技术报告]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%85%B7%E6%9C%89%E9%AB%98%E9%B2%81%E6%A3%92%E6%80%A7%E7%9A%84%E6%BF%80%E5%85%89SLAM%E7%AE%97%E6%B3%95&amp;diff=1022</id>
		<title>具有高鲁棒性的激光SLAM算法</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%85%B7%E6%9C%89%E9%AB%98%E9%B2%81%E6%A3%92%E6%80%A7%E7%9A%84%E6%BF%80%E5%85%89SLAM%E7%AE%97%E6%B3%95&amp;diff=1022"/>
		<updated>2026-05-16T11:42:58Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
MDCS 中&amp;quot;激光 SLAM 鲁棒性&amp;quot;指的是在 ''特征贫乏''、''动态多''、''环境光干扰''、''传感器噪声''等不友好条件下仍能稳定输出位姿的能力。本页列出 Detour 激光 SLAM 用到的关键鲁棒性技巧（与 [[Special:MyLanguage/Detour激光SLAM算法详解|算法细节]] 是不同维度的视角）。&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Robustness&amp;quot; in MDCS laser SLAM means stable pose output even when the environment is featureless, dynamic-heavy, glare-prone, or sensor-noisy. This page lists the key techniques Detour uses (an orthogonal view to the [[Special:MyLanguage/Detour激光SLAM算法详解|algorithmic details]]).&lt;br /&gt;
&lt;br /&gt;
== 关键技巧 / Key techniques ==&lt;br /&gt;
=== 1. 多分辨率扫描匹配 / Multi-resolution scan matching ===&lt;br /&gt;
从粗到细搜索位姿：先在 10 cm 分辨率上找候选 → 再 2 cm → 再 5 mm。等效于 ''分支限界''，避免直接在原始分辨率求全局最优时的指数代价。&lt;br /&gt;
&lt;br /&gt;
Coarse-to-fine pose search (10 cm → 2 cm → 5 mm). Branch-and-bound equivalent; avoids the exponential cost of direct fine-grained search.&lt;br /&gt;
&lt;br /&gt;
=== 2. 紧耦合 IMU 预积分 / Tight-coupled IMU pre-integration ===&lt;br /&gt;
把 IMU 数据从激光帧之间积分得到 ''相对位姿先验''，作为扫描匹配的初值。在动态环境（车辆运动 + 旋转剧烈）下，避免位姿跳变。&lt;br /&gt;
&lt;br /&gt;
Integrate IMU between laser frames to give the scan matcher a relative-pose prior. Prevents pose jumps in dynamic-heavy / high-yaw conditions.&lt;br /&gt;
&lt;br /&gt;
=== 3. 动态障碍过滤 / Dynamic obstacle filtering ===&lt;br /&gt;
* '''占用一致性''': 维护&amp;quot;长期占用率&amp;quot;地图，与当前帧异常突现的点视为动态点丢弃。&lt;br /&gt;
* '''可见性约束''': 与上一帧比较 —— 一个点如果在上一帧应被现帧雷达看到却没有，视为动态。&lt;br /&gt;
* '''速度约束''': 同一区域连续帧出现的点云速度估计 &amp;gt; 步行速度 → 滤除。&lt;br /&gt;
&lt;br /&gt;
* Occupancy consistency: maintain a long-term occupancy map; reject points appearing where the map says 'free'.&lt;br /&gt;
* Visibility check: a point present in the previous frame but absent now (or vice versa, in expected region) is dynamic.&lt;br /&gt;
* Velocity gate: estimated cloud velocity &amp;gt; walking speed → reject.&lt;br /&gt;
&lt;br /&gt;
=== 4. 反光板辅助 / Reflector assistance ===&lt;br /&gt;
在长直走廊 / 镜面 / 重度对称的场景下，铺 ''高反光板''作为人工地标，几何匹配亚厘米精度。详见 [[Special:MyLanguage/使用手册 - 激光SLAM＋反光板建图手册|激光 SLAM + 反光板建图手册]]。&lt;br /&gt;
&lt;br /&gt;
In long corridors / mirrored / heavily symmetric scenes, deploy high-reflectivity panels as artificial landmarks for sub-cm geometric matching.&lt;br /&gt;
&lt;br /&gt;
=== 5. 自适应协方差 / Adaptive covariance ===&lt;br /&gt;
扫描匹配的输出位姿同时附带 ''协方差矩阵''（来自 Hessian 求逆）。当协方差大 → TightCoupler 自动降权，让 IMU / 里程计主导；协方差小 → 激光主导。&lt;br /&gt;
&lt;br /&gt;
The scan matcher outputs not only a pose but its covariance (from Hessian inverse). When uncertainty is high, TightCoupler down-weights laser and lets IMU / odom dominate; when low, laser leads.&lt;br /&gt;
&lt;br /&gt;
=== 6. 多源融合 / Multi-source fusion ===&lt;br /&gt;
激光不是唯一定位源 —— 与 [[Special:MyLanguage/地纹SLAM技术概述|地纹]]、[[Special:MyLanguage/天花板SLAM|天花板]]、[[Special:MyLanguage/二维码识别导航|二维码]]、IMU、RTK、UWB 紧耦合（[[Special:MyLanguage/多定位源的自动综合|多定位源的自动综合]]）。&lt;br /&gt;
&lt;br /&gt;
=== 7. 局部 / 全局重定位 / Local / global re-localisation ===&lt;br /&gt;
&amp;quot;丢线&amp;quot;分两级：&lt;br /&gt;
* '''局部丢失''' (协方差 &amp;gt; 阈值 持续 N 帧) → 小搜索半径重定位&lt;br /&gt;
* '''全局丢失''' (局部重定位失败 &amp;gt; T 秒) → 全图分支限界搜索（[[Special:MyLanguage/激光SLAM建图|建图]] 提供的初始地图）&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Lost&amp;quot; comes in two grades: local (covariance &amp;gt; threshold for N frames) → small search radius; global (local recovery fails &amp;gt; T seconds) → full-map branch-and-bound.&lt;br /&gt;
&lt;br /&gt;
=== 8. 长期地图自更新 / Long-term map self-update ===&lt;br /&gt;
环境会变（货架挪位、临时区域开通）。Detour 定期把&amp;quot;明显持久的占用变化&amp;quot;合并到地图：可选 ''自动''（默认关）或 ''手动审批''（推荐）。&lt;br /&gt;
&lt;br /&gt;
Environments drift. Detour can fold &amp;quot;obviously persistent occupancy changes&amp;quot; back into the map — manual approval (default) or auto-merge (opt-in).&lt;br /&gt;
&lt;br /&gt;
=== 9. 弱场景预测 / Weak-scene prediction ===&lt;br /&gt;
建图后离线分析地图：每个格点 ''几何信息量''有多大（垂直墙体的距离、特征覆盖角度）。低信息量区域在 UI 上显示为黄色，提示开发者 ''加反光板''或 ''减少在此区段做精动作''。&lt;br /&gt;
&lt;br /&gt;
Off-line analysis after mapping: per-cell ''geometric information score'' (perpendicular wall distance, feature angular coverage). Low-score regions show yellow in the UI — hint to add reflectors or avoid precise work there.&lt;br /&gt;
&lt;br /&gt;
== 不属于 SLAM 的鲁棒性 / Things outside SLAM proper ==&lt;br /&gt;
有些鲁棒性是 ''上层组件''的责任，不应混入 SLAM：&lt;br /&gt;
&lt;br /&gt;
* '''传感器故障切换''' / Sensor failover: Medulla 监测雷达 watchdog；超时切到备份雷达。&lt;br /&gt;
* '''突遇人员''': Clumsy 局部 e-stop（[[Special:MyLanguage/绕障行走|绕障行走]]）。&lt;br /&gt;
* '''任务级回退''': SimpleCore 收到位姿 ''totally lost''事件 → 全局停车，操作工介入。&lt;br /&gt;
&lt;br /&gt;
* Sensor failover: Medulla's watchdog flips to a backup lidar.&lt;br /&gt;
* Sudden people: Clumsy's local e-stop.&lt;br /&gt;
* Mission-level fallback: SimpleCore halts the fleet when localisation is utterly lost; operator intervention.&lt;br /&gt;
&lt;br /&gt;
== 调试与验证 / Validation ==&lt;br /&gt;
* '''SLAM 健康指标''': Detour UI 显示协方差、闭环数 / s、动态过滤丢点率、活跃 SLAM 后端。&lt;br /&gt;
* '''回放''': 用 [[Special:MyLanguage/使用手册 - 数据录制与回放手册|数据录制与回放]] 在离线复现疑难帧。&lt;br /&gt;
* '''Stress test''': 在仓库引入有意的扰动（多人通过、临时叉车），观察 SLAM 输出的位姿连续性。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/Detour激光SLAM算法详解|Detour激光SLAM算法详解]]&lt;br /&gt;
* [[Special:MyLanguage/激光SLAM的能力边界|激光SLAM的能力边界]]&lt;br /&gt;
* [[Special:MyLanguage/激光SLAM建图|激光SLAM建图]]&lt;br /&gt;
* [[Special:MyLanguage/多定位源的自动综合|多定位源的自动综合]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 分析定位问题|使用手册 - 分析定位问题]]&lt;br /&gt;
&lt;br /&gt;
[[Category:技术报告]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E4%BA%8C%E7%BB%B4%E7%A0%81%E8%AF%86%E5%88%AB%E5%AF%BC%E8%88%AA&amp;diff=1021</id>
		<title>二维码识别导航</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E4%BA%8C%E7%BB%B4%E7%A0%81%E8%AF%86%E5%88%AB%E5%AF%BC%E8%88%AA&amp;diff=1021"/>
		<updated>2026-05-16T11:42:55Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
二维码导航（QR-based navigation）把 ''二维码''作为地面 / 立面的人工地标，车上相机识别二维码 ID + 几何位姿，反推车体绝对位姿。它是 MDCS 中 ''最简单、最低成本''的定位方案，但对地面平整度与维护有较高要求。&lt;br /&gt;
&lt;br /&gt;
QR-code navigation uses QR tags on the floor or walls as artificial landmarks. An on-board camera reads tag ID + geometric pose, inverting to get the vehicle's absolute pose. It's the simplest, lowest-cost localisation option in MDCS, but demands smooth floors and ongoing tag maintenance.&lt;br /&gt;
&lt;br /&gt;
== 与其它方案的对比 / vs alternatives ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 项 / Item !! 二维码 / QR !! [[Special:MyLanguage/色带和磁条导航|色带 / 磁条]] !! 激光 SLAM&lt;br /&gt;
|-&lt;br /&gt;
| 路径自由度 / Path freedom || 极高（不需要连续轨道）|| 强制沿铺设线 || 自由&lt;br /&gt;
|-&lt;br /&gt;
| 部署成本 / Deploy cost || 低（贴二维码）|| 中（贴色带 / 磁条）|| 高（建图 + 调试）&lt;br /&gt;
|-&lt;br /&gt;
| 维护 / Maintenance || 高（二维码易磨损）|| 中 || 低&lt;br /&gt;
|-&lt;br /&gt;
| 定位精度 / Pose precision || ±10 mm / 0.5° (单二维码) || ±5 mm 横向 || ±5–20 mm&lt;br /&gt;
|-&lt;br /&gt;
| 地面平整度要求 / Flatness || 高（防相机俯仰误差）|| 中 || 低&lt;br /&gt;
|-&lt;br /&gt;
| 复用激光的能力 / Reuse with SLAM || 无 || 无 || N/A&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 硬件 / Hardware ==&lt;br /&gt;
* '''相机''': 下视工业相机 + LED 环形灯（防自然光干扰）&lt;br /&gt;
* '''二维码''': 5–15 cm 边长；推荐用 ArUco（识别速度 + 编码效率）或 AprilTag（鲁棒性最高）&lt;br /&gt;
* '''贴布''': 镭雕金属牌或防水贴纸 + 透明保护层&lt;br /&gt;
* '''分辨率''': 相机像素 / 二维码尺寸需保证 ≥ 20 像素 / 二维码方格边长&lt;br /&gt;
&lt;br /&gt;
== 算法 / Algorithm ==&lt;br /&gt;
=== 检测与解码 / Detection &amp;amp; decode ===&lt;br /&gt;
1. 灰度化 + 自适应阈值 → 边缘检测&lt;br /&gt;
2. 多边形拟合，筛选有 ''4 角 + 内方框''的候选&lt;br /&gt;
3. 透视变换矫正&lt;br /&gt;
4. 解码 ID（ArUco / AprilTag）&lt;br /&gt;
&lt;br /&gt;
=== 位姿解算 / Pose estimation ===&lt;br /&gt;
已知二维码的 ''边长'' L 和 ''4 个角点''像素坐标 → PnP 求解相机相对二维码位姿。再用 ''二维码 → 世界''变换（预先标定的二维码绝对位姿）反推车体在世界的位姿。&lt;br /&gt;
&lt;br /&gt;
Given the tag's edge length L and its four corner pixel coordinates, PnP solves the camera-relative pose. Multiplying by the tag-to-world transform (calibrated up-front) gives the vehicle's world pose.&lt;br /&gt;
&lt;br /&gt;
=== 多二维码融合 / Multi-tag fusion ===&lt;br /&gt;
单帧中若同时见到 N ≥ 2 个二维码，做 ''最小二乘''融合，精度提升 √N。&lt;br /&gt;
&lt;br /&gt;
If multiple tags are visible in one frame, fuse them by least squares; precision scales as √N.&lt;br /&gt;
&lt;br /&gt;
== 部署 / Deployment ==&lt;br /&gt;
'''布点规则 / Tag placement rules''':&lt;br /&gt;
* 关键停靠位（取放料工位、充电位）正下方贴&lt;br /&gt;
* 长直走廊每 1.5–3 m 一个&lt;br /&gt;
* 分叉前后各贴一对&lt;br /&gt;
* 高速段（&amp;gt; 1 m/s）二维码间距应 &amp;lt; 1 m&lt;br /&gt;
&lt;br /&gt;
'''标定 / Calibration''':&lt;br /&gt;
* 每个二维码贴完后，用 ''标尺 + 总站''测其绝对位姿，写入 Detour 配置。&lt;br /&gt;
* 标定误差直接进入定位结果，必须 ≤ 5 mm / 0.2°。&lt;br /&gt;
* 详见 [[Special:MyLanguage/纯二维码导航|纯二维码导航]] 与 [[Special:MyLanguage/使用手册 - 在地纹导航中使用二维码快速找回定位|地纹导航中使用二维码快速找回定位]]。&lt;br /&gt;
&lt;br /&gt;
'''维护 / Maintenance''':&lt;br /&gt;
* 每周检查关键二维码完整性。&lt;br /&gt;
* 磨损 / 部分遮挡 → 立即更换。&lt;br /&gt;
* 二维码贴布建议用 ''可剥离强力胶''加可清洁透明膜保护。&lt;br /&gt;
&lt;br /&gt;
== MDCS 集成 / MDCS integration ==&lt;br /&gt;
二维码定位作为 Detour 的一个 ''SLAM 后端''：&lt;br /&gt;
* `useQRSlam = true` 在 Detour 配置中启用&lt;br /&gt;
* 二维码地图存为 JSON（每条目：tagId + (x, y, z, th)）&lt;br /&gt;
* TightCoupler 把 QR 位姿与激光 / IMU 融合&lt;br /&gt;
&lt;br /&gt;
QR localisation enters Detour as a SLAM backend. Enable `useQRSlam = true`; provide a JSON tag map (tagId + 4-DoF world pose). TightCoupler fuses QR with laser / IMU.&lt;br /&gt;
&lt;br /&gt;
== 与&amp;quot;纯二维码导航&amp;quot;的区别 / vs pure-QR navigation ==&lt;br /&gt;
* '''混合模式'''（本页）: QR 作为 Detour 的一个观测源，与 SLAM 共存。&lt;br /&gt;
* '''纯二维码''': 只用二维码定位，无 SLAM；车体每帧必须看到 ≥ 1 个二维码。见 [[Special:MyLanguage/纯二维码导航|纯二维码导航]]。&lt;br /&gt;
&lt;br /&gt;
The hybrid mode (this page) lets QR coexist with SLAM. ''Pure''-QR mode (linked page) uses no SLAM and requires the camera to see ≥ 1 tag every frame.&lt;br /&gt;
&lt;br /&gt;
== 常见问题 / Common issues ==&lt;br /&gt;
* '''二维码反光''' / Glare: 调相机角度 + LED 偏振。&lt;br /&gt;
* '''叉车叉齿挡住''' / Forks blocking view: 相机不要装在叉齿下方。&lt;br /&gt;
* '''高速运动模糊''' / Motion blur: 相机用 ''全局快门''与短曝光（&amp;lt; 1 ms）。&lt;br /&gt;
* '''标定漂''' / Pose drift due to calibration: 每月做一次随机抽检（5 个二维码）。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/纯二维码导航|纯二维码导航]]&lt;br /&gt;
* [[Special:MyLanguage/色带和磁条导航|色带和磁条导航]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 在地纹导航中使用二维码快速找回定位|在地纹导航中使用二维码快速找回定位]]&lt;br /&gt;
* [[Special:MyLanguage/多定位源的自动综合|多定位源的自动综合]]&lt;br /&gt;
* [[Special:MyLanguage/Detour软件架构|Detour软件架构]]&lt;br /&gt;
&lt;br /&gt;
[[Category:定位导航相关手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=Simple%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84&amp;diff=1020</id>
		<title>Simple软件架构</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=Simple%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84&amp;diff=1020"/>
		<updated>2026-05-16T11:42:54Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
Simple 是 MDCS 的车队层，分为两部分：&lt;br /&gt;
'''SimpleCore'''（核心算法库 —— 交管、寻路、包络、可达性、调度内核），&lt;br /&gt;
'''SimpleComposer'''（基于 SimpleCore 的 UI 壳 + 插件宿主）。&lt;br /&gt;
&lt;br /&gt;
Simple is the fleet layer of MDCS, split in two parts:&lt;br /&gt;
'''SimpleCore''' (the algorithm library — traffic control, pathfinding, envelopes, reachability, scheduling kernel) and&lt;br /&gt;
'''SimpleComposer''' (a UI shell + plugin host built on top of SimpleCore).&lt;br /&gt;
&lt;br /&gt;
== SimpleCore ==&lt;br /&gt;
完全独立的 .NET 库（`D:\src\Simple\SimpleCore\`），可被任何应用引用。它定义了：&lt;br /&gt;
&lt;br /&gt;
A standalone .NET library at `D:\src\Simple\SimpleCore\`, embeddable in any application. It defines:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 模块 / Module !! 用途 / Use&lt;br /&gt;
|-&lt;br /&gt;
| `AbstractCar` (`PropType\AbstractCar.cs:346`) || 调度可见的车的最小契约 / minimal contract for a scheduled vehicle&lt;br /&gt;
|-&lt;br /&gt;
| `AGVInterface` (`BasicProps\AGVInterface.cs`) || 任务脚本（TopazScript）调用的车 API / car API called by mission scripts&lt;br /&gt;
|-&lt;br /&gt;
| `DPSPlanner` || [[Special:MyLanguage/DPS调度算法详解|DPS]] 多车死锁规划 / DPS multi-car deadlock-free planner&lt;br /&gt;
|-&lt;br /&gt;
| `Envelope` || 车体包络 + 站点占用判定 / envelope + site-occupancy logic&lt;br /&gt;
|-&lt;br /&gt;
| `Reachability` || 可达性状态机 / reachability state machine — see [[Special:MyLanguage/可达性状态编程|可达性状态编程]]&lt;br /&gt;
|-&lt;br /&gt;
| `FlowFieldPlanner` || [[Special:MyLanguage/流场规划|流场规划]] / flow-field planner&lt;br /&gt;
|-&lt;br /&gt;
| `NetworkFlowBizPlanner` || [[Special:MyLanguage/基于网络流的业务规划器|基于网络流的业务规划器]] / network-flow business planner&lt;br /&gt;
|-&lt;br /&gt;
| `World` || 地图（站点 + 路径段 + 区域）/ map (sites + path segments + zones)&lt;br /&gt;
|-&lt;br /&gt;
| `TopazEngine` || 任务脚本解释器 / mission-script interpreter&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
SimpleCore 不依赖 Windows、UI、SimpleComposer、Clumsy 或 Medulla —— 可在 Linux 工控机上单独跑。&lt;br /&gt;
SimpleCore depends on none of Windows, UI, SimpleComposer, Clumsy or Medulla — it runs standalone on Linux IPCs.&lt;br /&gt;
&lt;br /&gt;
== SimpleComposer ==&lt;br /&gt;
基于 SimpleCore 的 ''标准车队管理 UI''，位于 `D:\src\Simple\SimpleComposer\`。它提供：&lt;br /&gt;
&lt;br /&gt;
The standard fleet-management UI built on SimpleCore. It provides:&lt;br /&gt;
&lt;br /&gt;
* '''CAD 工具''' / CAD tool — 见 [[Special:MyLanguage/开发手册 - SimpleComposer界面开发 - CAD工具|开发手册 - CAD工具]]&lt;br /&gt;
* '''车型插件接口''' / car-type plugin interface — `Car` 与 `ClumsyCar` 子类化点&lt;br /&gt;
* '''任务脚本编辑器''' / mission-script editor&lt;br /&gt;
* '''实时监控面板''' / live monitoring panel — 包络、锁、车辆状态&lt;br /&gt;
* '''仿真''' / simulation — 整套调度算法可在不接真车的情况下跑&lt;br /&gt;
&lt;br /&gt;
`SimpleComposer.RCS.Car` (`D:\src\Simple\SimpleComposer\RCS\Car.cs:33`) 是车的 ''Composer 端基类''，封装 SimpleCore 的 `AbstractCar` 加上 UI 字段、地图坐标转换、可视化属性。`ClumsyCar` (`D:\src\Simple\SimpleComposer\RCS\ClumsyCar.cs:34`) 是它的子类，专为跑 MDCS 车载的车设计；自评估车（PLC / 磁条 / 二维码）则直接继承 `Car` 并实现 nested `AGV : AGVInterface`。&lt;br /&gt;
&lt;br /&gt;
`SimpleComposer.RCS.Car` is the Composer-side base — `AbstractCar` + UI fields + coordinate transforms + render props. `ClumsyCar` extends it for vehicles running MDCS on-board. Self-evaluating cars subclass `Car` directly with a nested `AGV : AGVInterface`.&lt;br /&gt;
&lt;br /&gt;
== 数据流 / Data flow ==&lt;br /&gt;
&lt;br /&gt;
  业务系统 (WMS / ERP) → SimpleComposer 任务接收&lt;br /&gt;
                              │&lt;br /&gt;
                              ▼&lt;br /&gt;
              SimpleComposer 任务编排 (CarProgram)&lt;br /&gt;
                              │&lt;br /&gt;
                              ▼&lt;br /&gt;
              SegmentPlan.Compile → TopazScript&lt;br /&gt;
                              │&lt;br /&gt;
                              ▼&lt;br /&gt;
              Car.actualSendScript(script)&lt;br /&gt;
              ├─ ClumsyCar: HTTP POST 到车载 → TopazEngine on-board&lt;br /&gt;
              └─ 自评估车: 本地 SelfEvaluating(agv, script) → 厂商命令&lt;br /&gt;
                              │&lt;br /&gt;
                              ▼ (周期心跳)&lt;br /&gt;
              Car.keepAlive() ← 车体上报 (x, y, th, siteId, ...)&lt;br /&gt;
&lt;br /&gt;
详见 [[Special:MyLanguage/AGV任务运行逻辑|AGV任务运行逻辑]] 与 [[Special:MyLanguage/调度内核运行原理|调度内核运行原理]]。&lt;br /&gt;
&lt;br /&gt;
== 算法层次 / Algorithm layering ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 层 / Layer !! 关切 / Concern !! 内核 / Engine&lt;br /&gt;
|-&lt;br /&gt;
| 业务规划 / Business || 哪台车做哪个任务 || `NetworkFlowBizPlanner`&lt;br /&gt;
|-&lt;br /&gt;
| 路径规划 / Pathfinding || 当前任务沿哪条路径 || A* / 流场（[[Special:MyLanguage/流场规划|流场规划]]）+ 启发器&lt;br /&gt;
|-&lt;br /&gt;
| 交管 / Traffic control || 多车并存如何避碰 || DPS（[[Special:MyLanguage/DPS调度算法详解|DPS算法]]）&lt;br /&gt;
|-&lt;br /&gt;
| 可达性 / Reachability || 当前状态下哪些路径有效 || 状态机（[[Special:MyLanguage/可达性状态编程|可达性状态编程]]）&lt;br /&gt;
|-&lt;br /&gt;
| 包络 / Envelope || 车体当前占用哪些站点 || 几何 + Reachability 联动&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 插件类型 / Plugin types ==&lt;br /&gt;
SimpleComposer 支持三类插件：&lt;br /&gt;
SimpleComposer supports three plugin classes:&lt;br /&gt;
&lt;br /&gt;
# '''车型插件''' / Car-type — 派生 `ClumsyCar` 或 `Car`，加 `[CarType]` 属性。新车上线主用。&lt;br /&gt;
# '''业务插件''' / Business — 派生 `BusinessLogic`，定义自定义任务类型、字段、动作。&lt;br /&gt;
# '''CAD 工具插件''' / CAD-tool — 派生 `CADToolPlugin`，给 CAD 画布加自定义可视化与编辑工具。&lt;br /&gt;
&lt;br /&gt;
详见 [[Special:MyLanguage/使用手册 - Simple:从使用到开发|使用手册 - Simple:从使用到开发]] 和 [[Special:MyLanguage/如何基于SimpleCore核心库进行调度系统开发|如何基于SimpleCore核心库进行调度系统开发]]。&lt;br /&gt;
&lt;br /&gt;
== 关键决策 / Key design choices ==&lt;br /&gt;
* '''SimpleCore 与 SimpleComposer 解耦''' —— SimpleCore 是纯算法库；SimpleComposer 可被替换。&lt;br /&gt;
* '''TopazScript 中间表达''' —— 任务脚本而不是 RPC，让 ClumsyCar 与自评估车共用同一编译产物。&lt;br /&gt;
* '''DPS 预先规划交管''' —— 牺牲灵活性换 ''可证明的无死锁''；动态场景多时切流场。&lt;br /&gt;
* '''可达性是状态机而不是布尔''' —— 同一物理车在 ''带载 / 不带载 / 高位 / 电量低''等状态下走不同路径。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/AGV任务运行逻辑|AGV任务运行逻辑]]&lt;br /&gt;
* [[Special:MyLanguage/调度内核运行原理|调度内核运行原理]]&lt;br /&gt;
* [[Special:MyLanguage/DPS调度算法详解|DPS调度算法详解]]&lt;br /&gt;
* [[Special:MyLanguage/流场规划|流场规划]]&lt;br /&gt;
* [[Special:MyLanguage/基于网络流的业务规划器|基于网络流的业务规划器]]&lt;br /&gt;
* [[Special:MyLanguage/Simple-API|Simple-API]]&lt;br /&gt;
* [[Special:MyLanguage/如何基于SimpleCore核心库进行调度系统开发|如何基于SimpleCore核心库进行调度系统开发]]&lt;br /&gt;
* [[Special:MyLanguage/开发手册 - SimpleComposer界面开发 - CAD工具|开发手册 - SimpleComposer界面开发 - CAD工具]]&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=Medulla%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84&amp;diff=1019</id>
		<title>Medulla软件架构</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=Medulla%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84&amp;diff=1019"/>
		<updated>2026-05-16T11:42:53Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
Medulla 是 MDCS 的硬件层：它把异构硬件（电机控制器、PLC、传感器、相机、雷达）抽象成 ''IOObject'' 与 ''字段''，并提供跨进程共享内存（DObject）让上层算法订阅数据，无关进程或语言。&lt;br /&gt;
&lt;br /&gt;
Medulla is the hardware-abstraction layer of MDCS: it wraps heterogeneous hardware (motor controllers, PLCs, sensors, cameras, lidars) into '''IOObject''' instances exposing typed fields, and exposes them across processes via the '''DObject''' shared-memory bus.&lt;br /&gt;
&lt;br /&gt;
== 关键概念 / Key concepts ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 概念 / Concept !! 含义 / Meaning&lt;br /&gt;
|-&lt;br /&gt;
| `IOObject` || 一切硬件抽象的基类 / base class for everything: lidars, cameras, carts. File: `D:\src\M2\MedullaCore\Types\IO.cs:47`&lt;br /&gt;
|-&lt;br /&gt;
| Sub-classes || `Lidar2DIOObject`, `Lidar3DIOObject`, `CartDefinition`, camera `MainIOObject`&lt;br /&gt;
|-&lt;br /&gt;
| `[AsInitParam]` || 启动配置字段 / start-time config field (JSON-serialised)&lt;br /&gt;
|-&lt;br /&gt;
| `[AsUpperIO]` / `[AsLowerIO]` || 上 / 下位 IO 字段 / command-down / status-up field&lt;br /&gt;
|-&lt;br /&gt;
| `[UseLadderLogic]` || 周期性控制循环（默认 50 ms）/ cyclic control loop (default 50 ms)&lt;br /&gt;
|-&lt;br /&gt;
| `[IOObjectMonitor]` || 注册字段到监控面板 / dashboard registration&lt;br /&gt;
|-&lt;br /&gt;
| `[IOObjectUtility]` / `WebUtility` || 注册方法为 UI 按钮 / web API / register as UI button / web endpoint&lt;br /&gt;
|-&lt;br /&gt;
| `DObject` || 命名共享内存（跨进程）/ named shared memory (cross-process). 详见 [[Special:MyLanguage/DObject|DObject]]&lt;br /&gt;
|-&lt;br /&gt;
| `LadderLogic&amp;lt;T&amp;gt;` || 周期性回调宿主 / cyclic-callback host&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 体系结构 / Architecture ==&lt;br /&gt;
&lt;br /&gt;
  Plugin DLL (CartDef, Lidar2DIOObject, ...)&lt;br /&gt;
        │  reflection-loaded by name (no [Plugin] attr)&lt;br /&gt;
        ▼&lt;br /&gt;
  MedullaCore: IOObject lifecycle, threading, DObject pub/sub&lt;br /&gt;
        │&lt;br /&gt;
        ▼&lt;br /&gt;
  DObject (shared memory, cross-process)&lt;br /&gt;
        ▲       ▲&lt;br /&gt;
        │       │&lt;br /&gt;
  Detour, Clumsy, SimpleComposer — subscribers&lt;br /&gt;
&lt;br /&gt;
Medulla 既是 ''加载器'' 又是 ''运行时''：插件 DLL 通过 `io load plugins/X.dll` 启动命令载入；反射查找入口类（`MainIOObject` 或 `CartDefinition` 派生），实例化，调用 `Init()`，启动配置的 LadderLogic 周期线程。&lt;br /&gt;
&lt;br /&gt;
Medulla is both loader and runtime: plugin DLLs are loaded via the `io load plugins/X.dll` startup command; reflection finds the entry type (named `MainIOObject` or derived from `CartDefinition`), instantiates it, calls `Init()`, and spawns the configured LadderLogic threads.&lt;br /&gt;
&lt;br /&gt;
== 插件加载示例 / Plugin loading example ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
# startup.iocmd&lt;br /&gt;
lidar = io load plugins/MyLidar.dll&lt;br /&gt;
lidar Start 192.168.0.2 2110&lt;br /&gt;
&lt;br /&gt;
cart  = io load plugins/MyCart.dll&lt;br /&gt;
cart  Init&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
`io load` 把 DLL 反射扫描后实例化入口类；其余命令通过赋值（左侧）保存引用，并把命令转发给该实例。&lt;br /&gt;
&lt;br /&gt;
`io load` reflection-scans the DLL, instantiates the entry type, and the variable assignment keeps a reference. Subsequent commands forward to the instance.&lt;br /&gt;
&lt;br /&gt;
== 上下位 IO 数据流 / Upper / lower IO data flow ==&lt;br /&gt;
见 [[Special:MyLanguage/车体抽象原理|车体抽象原理]]。简而言之：&lt;br /&gt;
* '''上位''' IO（`[AsUpperIO]`）= 调度 / Clumsy 写入命令；Medulla 转发到硬件。&lt;br /&gt;
* '''下位''' IO（`[AsLowerIO]`）= Medulla 读硬件状态；调度 / Clumsy 订阅。&lt;br /&gt;
* '''初始化参数''' IO（`[AsInitParam]`）= 启动时一次性 JSON 注入。&lt;br /&gt;
* '''监控'''（`[IOObjectMonitor]`）= 显示在 Medulla 仪表盘 + 暴露给 Web API。&lt;br /&gt;
&lt;br /&gt;
== LadderLogic 周期循环 / Ladder cycles ==&lt;br /&gt;
LadderLogic 是 Medulla 提供的 ''周期性控制循环''宿主。一个 `CartDefinition` 可以有多个 LadderLogic 嵌套类，每个 `[UseLadderLogic(IntervalMs = N)]`：&lt;br /&gt;
&lt;br /&gt;
A `CartDefinition` may declare multiple nested LadderLogic classes, each at a configured tick rate:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[UseLadderLogic(IntervalMs = 50)]&lt;br /&gt;
public class ControlLoop : LadderLogic&amp;lt;MyCart&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    public override void Run(MyCart self)&lt;br /&gt;
    {&lt;br /&gt;
        // 写 UpperIO → 读硬件 → 更新 LowerIO&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
[UseLadderLogic(IntervalMs = 200)]&lt;br /&gt;
public class BatteryMonitor : LadderLogic&amp;lt;MyCart&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    public override void Run(MyCart self)&lt;br /&gt;
    {&lt;br /&gt;
        // 较慢的循环，例如电池电压采样&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== DObject 共享内存 / DObject IPC ==&lt;br /&gt;
DObject 是 Medulla 提供的 ''命名共享内存 pub/sub''：发布者写一个二进制 blob，订阅者按名字订阅，等到下一帧再读。Detour 用 DObject 读雷达数据；SimpleComposer 用 DObject 读车辆状态。详见 [[Special:MyLanguage/DObject|DObject]]。&lt;br /&gt;
&lt;br /&gt;
DObject is the named binary pub/sub channel. The publisher writes a blob, subscribers wait on the name and consume. Used by Detour for lidar, SimpleComposer for vehicle state. See the dedicated page.&lt;br /&gt;
&lt;br /&gt;
== 与 Medulla 1（旧版）的区别 / vs legacy Medulla ==&lt;br /&gt;
* `D:\src\M2\` 是 Medulla2（当前在用），`D:\src\Medulla\` 是 ''legacy''。&lt;br /&gt;
* Medulla2 引入了：DObject 替代旧 IPC、属性化 IO 表（`[AsUpperIO]` 等）、`LessokajiWeaver` 编译后处理生成代理类。&lt;br /&gt;
* 已 ''不要''使用 legacy Medulla 编写新插件。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/Medulla|Medulla]] — 主页 / canonical entry&lt;br /&gt;
* [[Special:MyLanguage/Medulla-API|Medulla-API]]&lt;br /&gt;
* [[Special:MyLanguage/DObject|DObject]]&lt;br /&gt;
* [[Special:MyLanguage/车体抽象原理|车体抽象原理]]&lt;br /&gt;
* [[Special:MyLanguage/如何适配新的雷达|如何适配新的雷达]]&lt;br /&gt;
* [[Special:MyLanguage/LessokajiWeaver编译后处理工具|LessokajiWeaver编译后处理工具]]&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=LessokajiWeaver%E7%BC%96%E8%AF%91%E5%90%8E%E5%A4%84%E7%90%86%E5%B7%A5%E5%85%B7&amp;diff=1018</id>
		<title>LessokajiWeaver编译后处理工具</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=LessokajiWeaver%E7%BC%96%E8%AF%91%E5%90%8E%E5%A4%84%E7%90%86%E5%B7%A5%E5%85%B7&amp;diff=1018"/>
		<updated>2026-05-16T11:42:52Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
LessokajiWeaver 是 MDCS 用到的 ''编译后处理''工具链（基于 Fody）。它在 C# 程序集编译完成后做 IL 注入，自动生成 ''字段代理类''、''属性序列化代码''、''反射加速''等基础设施代码，让插件作者只需写少量声明性属性，剩下的&amp;quot;管道&amp;quot;由 Weaver 自动织入。&lt;br /&gt;
&lt;br /&gt;
LessokajiWeaver is MDCS's post-compile IL-weaving toolchain (Fody-based). It runs after C# assembly compilation and injects IL to generate ''field-proxy classes'', ''property serialisation'', ''reflection accelerators'', etc. Plugin authors write a few declarative attributes; the plumbing is woven in automatically.&lt;br /&gt;
&lt;br /&gt;
== 主要场景 / Main scenarios ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 场景 / Scenario !! Weaver 做的 / What Weaver does&lt;br /&gt;
|-&lt;br /&gt;
| Medulla `[AsUpperIO]` / `[AsLowerIO]` 字段 || 生成代理 setter / getter，自动同步到 DObject&lt;br /&gt;
|-&lt;br /&gt;
| Medulla `[AsInitParam]` || 生成 JSON 序列化代码，绑定到启动配置加载&lt;br /&gt;
|-&lt;br /&gt;
| Medulla `[IOObjectMonitor]` || 注册到监控面板 + Web API&lt;br /&gt;
|-&lt;br /&gt;
| CycleGUI `[Cyclic]` 协程方法 || 自动包装为 IEnumerable&amp;lt;Cycle&amp;gt; 安全宿主&lt;br /&gt;
|-&lt;br /&gt;
| Clumsy `[MovementTest]` || 注册到测试 UI 面板&lt;br /&gt;
|-&lt;br /&gt;
| 多语言（i18n）字符串字段 || 生成查表逻辑（见 [[Special:MyLanguage/自动I18N（多语言功能）|自动 I18N]]）&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 工作流程 / Pipeline ==&lt;br /&gt;
&lt;br /&gt;
  开发者 .cs → csc 编译 → .dll&lt;br /&gt;
                              │&lt;br /&gt;
                              ▼&lt;br /&gt;
                LessokajiWeaver.Fody (post-build)&lt;br /&gt;
                              │&lt;br /&gt;
                              ▼&lt;br /&gt;
                         织入后的 .dll&lt;br /&gt;
                              │&lt;br /&gt;
                              ▼&lt;br /&gt;
                   Medulla / Clumsy 运行时加载&lt;br /&gt;
&lt;br /&gt;
工程引用方式（已由 [[Special:MyLanguage/MDCS-plugin-helper|MDCS-plugin-helper]] 模板自动配置）：&lt;br /&gt;
&lt;br /&gt;
In a plugin's .csproj (auto-configured by the plugin-helper template):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;xml&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;ItemGroup&amp;gt;&lt;br /&gt;
  &amp;lt;PackageReference Include=&amp;quot;Fody&amp;quot; Version=&amp;quot;6.x&amp;quot; /&amp;gt;&lt;br /&gt;
  &amp;lt;Reference Include=&amp;quot;LessokajiWeaver.Fody&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;HintPath&amp;gt;..\tools\LessokajiWeaver.Fody.dll&amp;lt;/HintPath&amp;gt;&lt;br /&gt;
  &amp;lt;/Reference&amp;gt;&lt;br /&gt;
  &amp;lt;Reference Include=&amp;quot;LessokajiWeaverUtilities&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;HintPath&amp;gt;..\tools\LessokajiWeaverUtilities.dll&amp;lt;/HintPath&amp;gt;&lt;br /&gt;
  &amp;lt;/Reference&amp;gt;&lt;br /&gt;
&amp;lt;/ItemGroup&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
`FodyWeavers.xml` 在工程根目录：&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;xml&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;Weavers&amp;gt;&lt;br /&gt;
  &amp;lt;LessokajiWeaver/&amp;gt;&lt;br /&gt;
&amp;lt;/Weavers&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 为什么用 Weaver / Why weave ==&lt;br /&gt;
不用 Weaver 也能用反射做同样的事，但每次访问字段都付反射开销（~100x getter）。Weaver 在编译期把反射调用 ''变成普通 IL''，运行时零开销：&lt;br /&gt;
&lt;br /&gt;
Without Weaver you'd use reflection at runtime, paying ~100× cost per access. Weaver moves the reflection to compile-time, generating direct IL with zero runtime overhead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// 开发者写：&lt;br /&gt;
[AsUpperIO]&lt;br /&gt;
public float vCmd;&lt;br /&gt;
&lt;br /&gt;
// 编译后 Weaver 注入相当于：&lt;br /&gt;
private float _vCmd;&lt;br /&gt;
public float vCmd&lt;br /&gt;
{&lt;br /&gt;
    get =&amp;gt; _vCmd;&lt;br /&gt;
    set&lt;br /&gt;
    {&lt;br /&gt;
        _vCmd = value;&lt;br /&gt;
        IOSync.PostToDObject(this, &amp;quot;vCmd&amp;quot;, value);  // 自动 IPC&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 调试 / Debugging ==&lt;br /&gt;
* 织入失败：检查 `bin\&amp;lt;config&amp;gt;\` 下是否有 `FodyWeavers.xml` 编译报错；通常是属性用法错误（如把 `[AsUpperIO]` 加在 readonly 字段上）。&lt;br /&gt;
* 织入产物可视化：用 ILSpy / dnSpy 打开织入后的 dll，查看 setter / getter 注入逻辑。&lt;br /&gt;
* 性能 / Performance: Weaver 不引入额外运行时依赖；增加的体积通常 &amp;lt; 5%。&lt;br /&gt;
&lt;br /&gt;
== 兼容性 / Compatibility ==&lt;br /&gt;
* .NET Framework 4.6.2+ / .NET 6+&lt;br /&gt;
* 必须用 ''Release 模式''发布插件（Weaver 在 Debug 也运行，但 Debug 会保留更多调试符号导致体积偏大）。&lt;br /&gt;
* 与 ''AOT 编译''不兼容（IL 注入依赖 JIT）。&lt;br /&gt;
&lt;br /&gt;
== 与 LessokajiWeaverUtilities 的关系 / Utilities ==&lt;br /&gt;
`LessokajiWeaverUtilities.dll` 是 Weaver 注入代码运行时依赖的小库（attribute 定义 + 工具方法）。插件 csproj 必须引用，但开发者一般不直接调用。&lt;br /&gt;
&lt;br /&gt;
`LessokajiWeaverUtilities.dll` is the runtime support library (attribute definitions + helpers). Plugin csproj must reference it but developers rarely call into it directly.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/Medulla|Medulla]] / [[Special:MyLanguage/Medulla软件架构|Medulla软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/自动I18N（多语言功能）|自动I18N（多语言功能）]]&lt;br /&gt;
* [[Special:MyLanguage/如何使用LessokajiWeaver的多语言功能|如何使用LessokajiWeaver的多语言功能]]&lt;br /&gt;
* [[Special:MyLanguage/MDCS引擎适配机器人入门教学|MDCS引擎适配机器人入门教学]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=Detour%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84&amp;diff=1017</id>
		<title>Detour软件架构</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=Detour%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84&amp;diff=1017"/>
		<updated>2026-05-16T11:42:51Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
Detour 是 MDCS 的定位系统：它整合 ''激光 SLAM''、''地纹 SLAM''、''天花板 SLAM''、''二维码导航'' 等多种定位手段，输出统一的 6-DoF 位姿给上层（Clumsy 自动驾驶 / SimpleComposer 调度）。Detour 不接受外部插件 —— 它通过 [[Special:MyLanguage/DObject|DObject]] 共享内存订阅 Medulla 发布的传感器数据，并通过 ''外部反馈接口''接收里程计 / IMU / RTK / UWB。&lt;br /&gt;
&lt;br /&gt;
Detour is the MDCS positioning subsystem. It fuses laser SLAM, ground-texture SLAM, ceiling SLAM, QR navigation, and other localisation sources into a unified 6-DoF pose for the upper layers (Clumsy autopilot / SimpleComposer fleet). Detour does not accept plugins — it consumes Medulla sensor publications via DObject, and external poses (odometry / IMU / RTK / UWB) via the external-feed API.&lt;br /&gt;
&lt;br /&gt;
== 子模块 / Subsystems ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 模块 / Module !! 文件 / File !! 用途 / Use&lt;br /&gt;
|-&lt;br /&gt;
| Lidar consumer || `D:\src\Detour\DetourCore\CartDefinition\Lidar.cs:302-311` || 从 Medulla DObject 订阅雷达帧&lt;br /&gt;
|-&lt;br /&gt;
| Tight coupler || `D:\src\Detour\DetourCore\Algorithms\TightCoupler.ExternalCoupler.cs` || 接收外部位姿、做紧耦合融合&lt;br /&gt;
|-&lt;br /&gt;
| Laser SLAM || `D:\src\Detour\DetourCore\Algorithms\LaserSLAM*` || 见 [[Special:MyLanguage/Detour激光SLAM算法详解|Detour 激光 SLAM 算法详解]]&lt;br /&gt;
|-&lt;br /&gt;
| Ground-texture SLAM || `D:\src\Detour\DetourCore\Algorithms\GroundTexture*` || 见 [[Special:MyLanguage/Detour地面纹理SLAM算法详解|地面纹理 SLAM 算法详解]]&lt;br /&gt;
|-&lt;br /&gt;
| QR / fiducial || `D:\src\Detour\DetourCore\Algorithms\QR*` || 见 [[Special:MyLanguage/二维码识别导航|二维码识别导航]]&lt;br /&gt;
|-&lt;br /&gt;
| Multi-source fuser || `TightCoupler` || 见 [[Special:MyLanguage/多定位源的自动综合|多定位源的自动综合]]&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 数据流 / Data flow ==&lt;br /&gt;
&lt;br /&gt;
  Medulla 传感器插件&lt;br /&gt;
       │ output() → DObject(name)&lt;br /&gt;
       ▼&lt;br /&gt;
  Detour 订阅:&lt;br /&gt;
   - LidarConsumer.ReadLidar()&lt;br /&gt;
   - CameraConsumer (ground-texture / ceiling / QR)&lt;br /&gt;
       │&lt;br /&gt;
       ▼&lt;br /&gt;
  各 SLAM 后端 (laser / texture / ceiling / QR)&lt;br /&gt;
       │ 每个产生一组 ''观测约束''&lt;br /&gt;
       ▼&lt;br /&gt;
  TightCoupler (位姿图优化)&lt;br /&gt;
       │ + 外部反馈 (PostExternalFeed)&lt;br /&gt;
       ▼&lt;br /&gt;
  统一位姿 (x, y, z, th, pitch, roll, tick)&lt;br /&gt;
       │ 发布到 DObject &amp;quot;pose&amp;quot;&lt;br /&gt;
       ▼&lt;br /&gt;
  Clumsy / SimpleComposer 订阅&lt;br /&gt;
&lt;br /&gt;
== 外部反馈 / External feeds ==&lt;br /&gt;
非传感器位姿（轮编里程计 / IMU / RTK / UWB / 二次定位源）通过 ''外部反馈''进入 Detour：&lt;br /&gt;
&lt;br /&gt;
Non-sensor poses (wheel odometry, IMU, RTK, UWB, secondary sources) enter Detour through the ''external-feed'' API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
TightCoupler.PostExternalFeed(new ExternalFeed&lt;br /&gt;
{&lt;br /&gt;
    name = &amp;quot;wheel_imu&amp;quot;,&lt;br /&gt;
    counter = tick++,&lt;br /&gt;
    hasTranslation = true,&lt;br /&gt;
    hasRotation = true,&lt;br /&gt;
    is2D = true,&lt;br /&gt;
    integrated = true,           // 积分量 / cumulative&lt;br /&gt;
    x = odom_x, y = odom_y, th = odom_th&lt;br /&gt;
}, &amp;quot;WheelOdom&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
字段含义见 `TightCoupler.ExternalCoupler.cs`：`hasTranslation/hasRotation`（包含的自由度）、`integrated`（积分量 vs 单帧量）、`toRemap`（绝对参考系下，需要全局重映射）、`startingTick`（首帧基准）。&lt;br /&gt;
&lt;br /&gt;
Field semantics in `TightCoupler.ExternalCoupler.cs`: `hasTranslation/hasRotation` (DOFs), `integrated` (cumulative vs single-frame), `toRemap` (absolute, needs global remap), `startingTick` (reference baseline).&lt;br /&gt;
&lt;br /&gt;
== 多 SLAM 共存策略 / Multi-SLAM coexistence ==&lt;br /&gt;
* '''并行运行''': 默认每种 SLAM 后端都开，各产生自己的观测约束。&lt;br /&gt;
* '''置信加权''': TightCoupler 根据每个后端的协方差矩阵做加权融合。&lt;br /&gt;
* '''场景切换''': 某个后端连续 ''N 帧''置信度 &amp;lt; 阈值 → 临时禁用其约束。&lt;br /&gt;
* '''硬切换''': 用户可在 UI 中强制选择&amp;quot;单一定位源&amp;quot;模式（调试用）。&lt;br /&gt;
&lt;br /&gt;
* All SLAM backends run in parallel by default.&lt;br /&gt;
* TightCoupler weights by per-backend covariance.&lt;br /&gt;
* A backend that's degraded for N frames is temporarily disabled.&lt;br /&gt;
* A user can force single-source mode (debug).&lt;br /&gt;
&lt;br /&gt;
== 定位输出 / Positioning output ==&lt;br /&gt;
Detour 把当前最优位姿写到 ''pose'' 命名的 DObject：&lt;br /&gt;
&lt;br /&gt;
The current best pose is published to a DObject named `pose`:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public struct DetourPose&lt;br /&gt;
{&lt;br /&gt;
    public double x, y, z;       // mm&lt;br /&gt;
    public double th;            // 偏航 / yaw rad&lt;br /&gt;
    public double pitch, roll;   // rad&lt;br /&gt;
    public long   tick;          // 时间戳 / system tick&lt;br /&gt;
    public double cov_xy;        // 平面协方差 / planar covariance&lt;br /&gt;
    public double cov_th;        // 偏航协方差 / yaw covariance&lt;br /&gt;
    public string activeBackend; // 当前主用 SLAM 后端&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 与 Medulla 的关系 / Relation to Medulla ==&lt;br /&gt;
Detour ''不是'' Medulla 的插件；它是独立的 .NET 进程，与 Medulla 同主机但不同进程，二者只通过 DObject 共享内存通讯。这种设计使得 Detour 可以独立部署 / 升级 / 切换 SLAM 算法而不影响 Medulla。&lt;br /&gt;
&lt;br /&gt;
Detour is '''not''' a Medulla plugin. It's a separate .NET process on the same host, communicating with Medulla only via DObject shared memory. This decoupling lets Detour deploy / upgrade / swap SLAM algorithms independently.&lt;br /&gt;
&lt;br /&gt;
== DetourStandalone 与 DetourLite / Standalone &amp;amp; Lite variants ==&lt;br /&gt;
* '''DetourStandalone''': 完整 Detour，含全部 SLAM + UI。详见 [[Special:MyLanguage/DetourStandalonePluginGuide|DetourStandalonePluginGuide]]。&lt;br /&gt;
* '''DetourLite''': 精简版，只含必要功能 + 检查表自检。见 [[Special:MyLanguage/DetourLite检查表|DetourLite 检查表]]。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/Detour|Detour]]&lt;br /&gt;
* [[Special:MyLanguage/Detour简介|Detour简介]]&lt;br /&gt;
* [[Special:MyLanguage/Detour-API|Detour-API]]&lt;br /&gt;
* [[Special:MyLanguage/Detour激光SLAM算法详解|Detour激光SLAM算法详解]]&lt;br /&gt;
* [[Special:MyLanguage/Detour地面纹理SLAM算法详解|Detour地面纹理SLAM算法详解]]&lt;br /&gt;
* [[Special:MyLanguage/多定位源的自动综合|多定位源的自动综合]]&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=Detour%E6%BF%80%E5%85%89SLAM%E7%AE%97%E6%B3%95%E8%AF%A6%E8%A7%A3&amp;diff=1016</id>
		<title>Detour激光SLAM算法详解</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=Detour%E6%BF%80%E5%85%89SLAM%E7%AE%97%E6%B3%95%E8%AF%A6%E8%A7%A3&amp;diff=1016"/>
		<updated>2026-05-16T11:42:50Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
Detour 的激光 SLAM 是 MDCS 中最常用的主定位源：在仓库 / 工厂场景下，2D 激光雷达 ≥ 270° 视场扫到墙 / 立柱 / 货架轮廓，配合 IMU + 轮编里程计做紧耦合，构建 ''占用栅格 + 位姿图'' 地图，运行时做 ''扫描匹配 + 全局重定位''。&lt;br /&gt;
&lt;br /&gt;
Detour's laser SLAM is the most-used primary localisation source in MDCS. In warehouse / factory scenes a ≥ 270° FOV 2D lidar scans walls / pillars / rack outlines; with tight-coupled IMU + wheel odometry it builds an ''occupancy grid + pose graph'' and at runtime runs ''scan matching + global re-localisation''.&lt;br /&gt;
&lt;br /&gt;
详见 [[Special:MyLanguage/具有高鲁棒性的激光SLAM算法|具有高鲁棒性的激光 SLAM 算法]] 中关于鲁棒性技巧的展开。&lt;br /&gt;
See [[Special:MyLanguage/具有高鲁棒性的激光SLAM算法|robust-laser-SLAM]] for the robustness toolbox.&lt;br /&gt;
&lt;br /&gt;
== 算法栈 / Algorithm stack ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 阶段 / Stage !! 算法 / Algorithm !! 输入 / Input !! 输出 / Output&lt;br /&gt;
|-&lt;br /&gt;
| 建图 / Mapping || GraphSLAM + 多分辨率 ICP 闭环 || 录制的雷达 + IMU + 里程计序列 || 占用栅格 + 位姿图 (.map)&lt;br /&gt;
|-&lt;br /&gt;
| 重定位 / Re-localisation || 多分辨率分支限界 || 当前一帧雷达 + 粗位姿先验 || 6-DoF 位姿（含协方差）&lt;br /&gt;
|-&lt;br /&gt;
| 跟踪 / Tracking || 高斯-牛顿扫描匹配 || 当前帧 + 上一帧位姿 || 增量位姿&lt;br /&gt;
|-&lt;br /&gt;
| 反光板模式 / Reflector mode || 反光板高强度过滤 + 几何匹配 || 当前帧 ''反光板点''的子集 + 反光板地图 || 高精度位姿（&amp;lt; 5 mm / 0.1°）&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 构图流程 / Mapping pipeline ==&lt;br /&gt;
# '''数据录制 / Record''': 在场地中匀速、覆盖全部走廊地推 AGV / 推车，录制激光 + IMU + 里程计同步流。&lt;br /&gt;
# '''粗轨迹 / Coarse trajectory''': 用扫描匹配 + 里程计积分得粗轨迹。&lt;br /&gt;
# '''闭环检测 / Loop closure''': 多分辨率分支限界寻找回环候选；通过几何一致性筛选。&lt;br /&gt;
# '''位姿图优化 / Pose-graph optimisation''': g2o / Ceres 优化关键帧位姿。&lt;br /&gt;
# '''栅格化 / Rasterise''': 将所有关键帧的点云投影到全局栅格，生成可视化地图。&lt;br /&gt;
# '''编辑 / Edit''': 用 [[Special:MyLanguage/激光地图编辑指南|激光地图编辑指南]] 中的工具人工修正动态障碍、空洞等。&lt;br /&gt;
&lt;br /&gt;
# Record laser + IMU + odom synchronously at moderate speed covering all corridors.&lt;br /&gt;
# Get a coarse trajectory via scan matching + odometry integration.&lt;br /&gt;
# Loop-closure search with multi-res branch-and-bound.&lt;br /&gt;
# Pose-graph optimisation (g2o / Ceres).&lt;br /&gt;
# Rasterise to a global occupancy grid.&lt;br /&gt;
# Manually edit to remove dynamic / artifacts.&lt;br /&gt;
&lt;br /&gt;
== 运行时跟踪 / Runtime tracking ==&lt;br /&gt;
* 每帧雷达进来 → 用上一帧的位姿做先验 → 扫描匹配（高斯-牛顿）求增量位姿。&lt;br /&gt;
* 协方差由 Hessian 求逆给出；置信度低时 TightCoupler 自动降权。&lt;br /&gt;
* 当 IMU 里程计 + 激光观测出现 &amp;gt; 阈值偏差 → 触发&amp;quot;局部丢失&amp;quot;，进入 ''局部重定位''（小搜索半径）。&lt;br /&gt;
* 局部重定位失败超过 N 秒 → 全局重定位（用 [[Special:MyLanguage/激光SLAM建图|地图]] 全局搜索）。&lt;br /&gt;
&lt;br /&gt;
== 反光板模式 / Reflector mode ==&lt;br /&gt;
对极简洁、特征贫乏的场景（如纯白墙仓库），可以铺设 ''高反光板''作为人工地标。Detour 自动从激光帧中按 ''强度阈值''过滤出反光板点，做几何配对得到亚厘米级位姿。&lt;br /&gt;
&lt;br /&gt;
For featureless environments (white-wall warehouses) deploy high-reflectivity panels as artificial landmarks. Detour filters them by intensity and matches geometrically for sub-cm pose.&lt;br /&gt;
&lt;br /&gt;
* 反光板布局要求 ≥ 3 个 ''非共线'' 反光板可见。&lt;br /&gt;
* 强度阈值在 Detour 配置中调（雷达适配时 `ReflexRange` 已归一化）。&lt;br /&gt;
* 详见 [[Special:MyLanguage/使用手册 - 激光SLAM＋反光板建图手册|激光 SLAM + 反光板建图手册]]。&lt;br /&gt;
&lt;br /&gt;
== 关键参数 / Key parameters ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 参数 / Param !! 含义 / Meaning !! 默认 / Default&lt;br /&gt;
|-&lt;br /&gt;
| `scanMatchMaxIter` || 扫描匹配最大迭代次数 || 30&lt;br /&gt;
|-&lt;br /&gt;
| `scanMatchInitRange` || 初始搜索半径 (m) || 0.5&lt;br /&gt;
|-&lt;br /&gt;
| `loopClosureScore` || 闭环最低分数 || 0.7&lt;br /&gt;
|-&lt;br /&gt;
| `relocBranchDepth` || 重定位分支限界深度 || 6&lt;br /&gt;
|-&lt;br /&gt;
| `relocCovThresh` || 重定位接受的最大协方差 || 0.01 m² / 0.001 rad²&lt;br /&gt;
|-&lt;br /&gt;
| `reflexMinIntensity` || 反光板最低强度 || 0.7 (归一化后)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 已知能力边界 / Known limits ==&lt;br /&gt;
* 长直走廊（特征沿一个方向变化弱）易沿 Y 漂移；用反光板或 [[Special:MyLanguage/地纹SLAM技术概述|地纹]] 互补。&lt;br /&gt;
* 强动态环境（大量行人 / 叉车）需要 ''动态过滤''或 SLAM 限速（建图时 ≤ 0.5 m/s）。&lt;br /&gt;
* 户外阳光直射会使部分点云缺失；用激光遮光罩或避免高峰时段。&lt;br /&gt;
* 极端反光（镜面、塑料蒙皮）导致虚假点；建图前现场预查。&lt;br /&gt;
&lt;br /&gt;
详见 [[Special:MyLanguage/激光SLAM的能力边界|激光 SLAM 的能力边界]]。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/激光SLAM建图|激光SLAM建图]]&lt;br /&gt;
* [[Special:MyLanguage/激光SLAM的能力边界|激光SLAM的能力边界]]&lt;br /&gt;
* [[Special:MyLanguage/激光地图编辑指南|激光地图编辑指南]]&lt;br /&gt;
* [[Special:MyLanguage/具有高鲁棒性的激光SLAM算法|具有高鲁棒性的激光SLAM算法]]&lt;br /&gt;
* [[Special:MyLanguage/Detour软件架构|Detour软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/多定位源的自动综合|多定位源的自动综合]]&lt;br /&gt;
&lt;br /&gt;
[[Category:技术报告]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=Detour%E5%9C%B0%E9%9D%A2%E7%BA%B9%E7%90%86SLAM%E7%AE%97%E6%B3%95%E8%AF%A6%E8%A7%A3&amp;diff=1015</id>
		<title>Detour地面纹理SLAM算法详解</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=Detour%E5%9C%B0%E9%9D%A2%E7%BA%B9%E7%90%86SLAM%E7%AE%97%E6%B3%95%E8%AF%A6%E8%A7%A3&amp;diff=1015"/>
		<updated>2026-05-16T11:42:50Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
地面纹理 SLAM（Ground-Texture SLAM）使用向下的高分辨率工业相机，把地面的微观纹理（划痕、混凝土颗粒、油渍、地砖缝）作为 ''全场景同质''的视觉地标，做位姿估计与建图。在激光 SLAM 表现差的场景（特征贫乏、动态多、地面平整）下效果显著。&lt;br /&gt;
&lt;br /&gt;
Ground-texture SLAM uses a downward-facing high-res industrial camera and treats the floor's micro-features (scratches, concrete grit, oil stains, tile joints) as ''everywhere-homogeneous'' visual landmarks. It excels where laser SLAM is weak: featureless walls, heavy dynamic obstacles, but smooth uniform floors.&lt;br /&gt;
&lt;br /&gt;
入门见 [[Special:MyLanguage/地纹SLAM技术概述|地纹 SLAM 技术概述]]；本页讲算法细节。&lt;br /&gt;
For an intro see [[Special:MyLanguage/地纹SLAM技术概述|the ground-texture SLAM overview]]; this page covers the algorithmic core.&lt;br /&gt;
&lt;br /&gt;
== 算法原理 / Algorithm overview ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 模块 / Module !! 说明 / Description&lt;br /&gt;
|-&lt;br /&gt;
| 特征提取 / Feature extraction || ORB / FAST + BRIEF（每帧 ~500 个点）/ ORB / FAST + BRIEF (~500 features per frame)&lt;br /&gt;
|-&lt;br /&gt;
| 帧间匹配 / Frame matching || Hamming 距离 KNN + 比率测试 / KNN matching with ratio test&lt;br /&gt;
|-&lt;br /&gt;
| 位姿估计 / Pose estimation || PnP（2D-to-2D 因深度近似常数）/ 2D-2D PnP since depth ≈ const&lt;br /&gt;
|-&lt;br /&gt;
| 关键帧筛选 / Keyframe selection || 平移 ≥ 30 mm 或转角 ≥ 5° / displacement ≥ 30 mm or yaw ≥ 5°&lt;br /&gt;
|-&lt;br /&gt;
| 局部建图 / Local mapping || 关键帧池 + Bundle Adjustment / keyframe pool + BA&lt;br /&gt;
|-&lt;br /&gt;
| 全局闭环 / Global loop closure || 词袋（BoVW）/ Bag of Visual Words&lt;br /&gt;
|-&lt;br /&gt;
| 位姿图优化 / Pose-graph || g2o / Ceres&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 硬件配置 / Hardware setup ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 部件 / Part !! 典型规格 / Typical&lt;br /&gt;
|-&lt;br /&gt;
| 相机 / Camera || 工业 USB3 全局快门，120 FPS，1 MP&lt;br /&gt;
|-&lt;br /&gt;
| 安装高度 / Mount height || 80–150 mm 距地 / ground-clearance&lt;br /&gt;
|-&lt;br /&gt;
| 视场 / FOV || 200 × 150 mm 实测覆盖区&lt;br /&gt;
|-&lt;br /&gt;
| 照明 / Lighting || 自带 LED 环形灯，确保不被外光影响&lt;br /&gt;
|-&lt;br /&gt;
| 防护 / Protection || 抗油 / 抗尘罩，定期清洁&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 与激光 SLAM 的差异 / vs laser SLAM ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 项 / Item !! 激光 SLAM !! 地纹 SLAM&lt;br /&gt;
|-&lt;br /&gt;
| 主感知 / Primary sensing || 2D 距离扫描 / 2D ranges || 向下相机图像 / downward image&lt;br /&gt;
|-&lt;br /&gt;
| 全局位姿误差 / Global error || cm 量级（受墙体不规则影响）|| mm 量级（局部很准）&lt;br /&gt;
|-&lt;br /&gt;
| 全局漂移 / Global drift || 受闭环密度影响 || 重度依赖关键帧密度&lt;br /&gt;
|-&lt;br /&gt;
| 弱场景 / Weak case || 长走廊 / 反光板缺 || 极平整无纹理地面（如新漆面）&lt;br /&gt;
|-&lt;br /&gt;
| 重定位 / Re-loc || 全局粗定位足够 || 需要预先扫描全场地图&lt;br /&gt;
|-&lt;br /&gt;
| 计算需求 / Compute || CPU 中等 || CPU + GPU（GPU 加速特征匹配）；见 [[Special:MyLanguage/使用手册 - 如何在核显工控机上运行地纹导航|核显工控机]]&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 工作流程 / Workflow ==&lt;br /&gt;
&lt;br /&gt;
# 用专用扫描车在工地上 ''慢速、完全覆盖''采集地纹（推荐 ≤ 0.5 m/s）。&lt;br /&gt;
# 后处理生成 ''地纹地图''（特征点数据库 + 关键帧池）；典型大小 1 GB / 1000 m²。&lt;br /&gt;
# 上线时车上跑实时定位：每帧提特征 → 与本地关键帧匹配 → 估姿 → 周期性查全局 BoVW 做闭环。&lt;br /&gt;
# Detour TightCoupler 把地纹位姿与激光 / IMU 融合。&lt;br /&gt;
&lt;br /&gt;
# Slowly traverse the entire site (≤ 0.5 m/s) to capture textures.&lt;br /&gt;
# Off-line build the ''texture map'' (feature DB + keyframe pool); typically ~1 GB per 1000 m².&lt;br /&gt;
# At runtime: extract features per frame → match to local keyframes → estimate pose → periodically query global BoVW for loop-closure.&lt;br /&gt;
# Detour's TightCoupler fuses ground-texture pose with laser + IMU.&lt;br /&gt;
&lt;br /&gt;
== 关键参数 / Key parameters ==&lt;br /&gt;
* `featurePerFrame`: 500 (上下限 200–1000)&lt;br /&gt;
* `keyframeSpacing`: 30 mm&lt;br /&gt;
* `bowVocabSize`: 5000 个视觉单词&lt;br /&gt;
* `localBaWindow`: 10 关键帧&lt;br /&gt;
* `loopClosureMinScore`: 0.6&lt;br /&gt;
&lt;br /&gt;
== 适用与不适用 / Applicability ==&lt;br /&gt;
'''适合 / Use when''':&lt;br /&gt;
* 地面有微观纹理（混凝土、地砖、橡胶颗粒）&lt;br /&gt;
* 工作面平整、车体俯仰/翻滚小&lt;br /&gt;
* 激光 SLAM 表现差（长走廊、镜面墙）&lt;br /&gt;
&lt;br /&gt;
'''不适合 / Avoid when''':&lt;br /&gt;
* 地面持续湿水、油污（特征被遮）&lt;br /&gt;
* 地面定期翻新（特征漂变快）&lt;br /&gt;
* 上下楼梯 / 大坡度&lt;br /&gt;
&lt;br /&gt;
== 快速找回定位 / Re-localisation in ground texture ==&lt;br /&gt;
地纹场景下&amp;quot;丢失定位&amp;quot;恢复较难，因为没有全局粗位姿。MDCS 推荐 ''二维码辅助''方案：在关键位置贴二维码作为 ''重定位锚点''。具体见 [[Special:MyLanguage/使用手册 - 在地纹导航中使用二维码快速找回定位|使用手册 - 在地纹导航中使用二维码快速找回定位]]。&lt;br /&gt;
&lt;br /&gt;
In ground-texture mode, recovery is hard. The recommended pattern is QR-assisted re-localisation: paste QR tags at strategic points as anchors. See the dedicated guide.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/地纹SLAM技术概述|地纹SLAM技术概述]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 如何在核显工控机上运行地纹导航|使用手册 - 如何在核显工控机上运行地纹导航]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 在地纹导航中使用二维码快速找回定位|地纹导航中使用二维码快速找回定位]]&lt;br /&gt;
* [[Special:MyLanguage/Detour软件架构|Detour软件架构]]&lt;br /&gt;
* [[Special:MyLanguage/多定位源的自动综合|多定位源的自动综合]]&lt;br /&gt;
&lt;br /&gt;
[[Category:技术报告]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=CycleGUI&amp;diff=1014</id>
		<title>CycleGUI</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=CycleGUI&amp;diff=1014"/>
		<updated>2026-05-16T11:42:49Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
CycleGUI 是 MDCS 自研的 ''回合式 GUI 框架''：算法代码可以在任何线程、任何时间 ''cast'' 一个协程到 GUI 主线程；协程仅在 ''需要刷新界面''时被唤醒。这种&amp;quot;按需协程&amp;quot;模型让算法核心可以无负担地嵌入实时调试与可视化代码。&lt;br /&gt;
&lt;br /&gt;
CycleGUI is MDCS's homegrown '''cyclic-coroutine GUI framework'''. Algorithm code on any thread can ''cast'' a coroutine onto the GUI thread; the coroutine wakes only when its visuals need refreshing. The &amp;quot;lazy coroutine&amp;quot; model lets the algorithm core embed live-debug code without performance cost.&lt;br /&gt;
&lt;br /&gt;
== 设计动机 / Why ==&lt;br /&gt;
* 机器人算法（SLAM、规划、运动控制）需要 ''频繁、灵活''地可视化中间状态。&lt;br /&gt;
* 传统 GUI 框架要求把算法搬到 GUI 主线程或用观察者模式回调 —— 都侵入性强。&lt;br /&gt;
* CycleGUI 反转控制：算法在原线程跑，GUI 框架托管协程，只在 GUI 需要重绘时调用协程。&lt;br /&gt;
* 让 ''调试代码''与 ''算法代码''几乎无成本共存。&lt;br /&gt;
&lt;br /&gt;
* Robotics algorithms (SLAM, planning, motion) need to visualise intermediate state often and flexibly.&lt;br /&gt;
* Conventional GUI frameworks force the algorithm onto the UI thread or use observer callbacks — both invasive.&lt;br /&gt;
* CycleGUI inverts control: the algorithm stays on its thread; the GUI framework hosts the coroutine and pulls it only on repaint.&lt;br /&gt;
* Debug code and algorithm code coexist with near-zero cost.&lt;br /&gt;
&lt;br /&gt;
== 使用模型 / Usage model ==&lt;br /&gt;
&lt;br /&gt;
  算法代码 (任意线程)&lt;br /&gt;
       │&lt;br /&gt;
       ▼&lt;br /&gt;
   UI.GetPainter(&amp;quot;topic1&amp;quot;)&lt;br /&gt;
       │&lt;br /&gt;
       ▼&lt;br /&gt;
   painter.DrawDot3D(color, position);  ← 任意时刻调用&lt;br /&gt;
   painter.DrawLine3D(...);&lt;br /&gt;
       │&lt;br /&gt;
       ▼&lt;br /&gt;
  CycleGUI 主线程（按需）&lt;br /&gt;
       │&lt;br /&gt;
       ▼&lt;br /&gt;
   渲染到屏幕&lt;br /&gt;
&lt;br /&gt;
详见 [[Special:MyLanguage/如何使用CycleGUI快速开发UI界面|如何使用CycleGUI快速开发UI界面]] 的具体 API。&lt;br /&gt;
For specific APIs see [[Special:MyLanguage/如何使用CycleGUI快速开发UI界面|the usage guide]].&lt;br /&gt;
&lt;br /&gt;
== 核心抽象 / Core abstractions ==&lt;br /&gt;
* '''Painter''' — 一个命名的绘制目标（topic）；多次 `DrawXxx` 累积到同一画面。&lt;br /&gt;
* '''Layer''' — 多个 Painter 组合成一层；可控可见性。&lt;br /&gt;
* '''Cyclic coroutine''' — 一段被框架托管的 `IEnumerable&amp;lt;Cycle&amp;gt;`，每次 ''帧重绘''被执行一次。&lt;br /&gt;
* '''Two-way binding''' — UI 控件（按钮、滑条、输入框）通过协程的 `yield return WaitForXxx` 与算法同步。&lt;br /&gt;
&lt;br /&gt;
== 在 MDCS 中的应用 / Use within MDCS ==&lt;br /&gt;
* '''Detour''': 可视化 SLAM 中间状态（关键帧、闭环候选、协方差椭圆）。&lt;br /&gt;
* '''Clumsy''': 在 Movement 内部可视化 lidar 帧、检测器输出、目标姿态。&lt;br /&gt;
* '''SimpleComposer''': CAD 工具 + 调度时的车流可视化。&lt;br /&gt;
* '''仿真 / 回放''': 把回放数据投射到同一套 CycleGUI 视图。&lt;br /&gt;
&lt;br /&gt;
== 实现位置 / Where ==&lt;br /&gt;
源码：`D:\src\CycleGUI\`（独立仓库）。MDCS 通过 NuGet 引用。&lt;br /&gt;
Source: `D:\src\CycleGUI\` (standalone repo); MDCS depends via NuGet.&lt;br /&gt;
&lt;br /&gt;
== 关键特性 / Key features ==&lt;br /&gt;
* '''零侵入''': 不强制让算法上 GUI 线程。&lt;br /&gt;
* '''跨进程''': 一个 GUI 可以同时呈现多个进程的画面。&lt;br /&gt;
* '''可录制''': 所有 Painter 调用可序列化 → 离线回放。&lt;br /&gt;
* '''Web 版'''（实验）: 把绘制指令转 WebGL，浏览器查看。&lt;br /&gt;
&lt;br /&gt;
* Zero-intrusion — algorithm doesn't migrate to the UI thread.&lt;br /&gt;
* Cross-process — one GUI can render from multiple processes.&lt;br /&gt;
* Recordable — all Painter calls serialise; replay off-line.&lt;br /&gt;
* Web flavour (experimental) — render commands → WebGL, view in browser.&lt;br /&gt;
&lt;br /&gt;
== 与其它框架的对比 / Comparison ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 项 / Item !! CycleGUI !! ImGui !! Qt&lt;br /&gt;
|-&lt;br /&gt;
| 协程模型 / Coroutine || 是（按需唤醒）/ Yes (lazy) || 否（每帧重画）|| 否（事件驱动）&lt;br /&gt;
|-&lt;br /&gt;
| 跨进程 / Cross-process || 内建 || 需 RPC || 复杂&lt;br /&gt;
|-&lt;br /&gt;
| GUI 主线程要求 / UI-thread req || 无（cast 自动派发）|| 有 || 有&lt;br /&gt;
|-&lt;br /&gt;
| 学习曲线 / Curve || 中 || 低 || 高&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/如何使用CycleGUI快速开发UI界面|如何使用CycleGUI快速开发UI界面]]&lt;br /&gt;
* [[Special:MyLanguage/开发手册 - SimpleComposer界面开发 - CAD工具|开发手册 - SimpleComposer界面开发 - CAD工具]]&lt;br /&gt;
* [[Special:MyLanguage/LessokajiWeaver编译后处理工具|LessokajiWeaver编译后处理工具]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E8%BD%A6%E4%BD%93%E6%8A%BD%E8%B1%A1%E5%8E%9F%E7%90%86&amp;diff=1013</id>
		<title>车体抽象原理</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E8%BD%A6%E4%BD%93%E6%8A%BD%E8%B1%A1%E5%8E%9F%E7%90%86&amp;diff=1013"/>
		<updated>2026-05-16T11:26:59Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
MDCS 把&amp;quot;一台 AGV&amp;quot;抽象为三个层次的协作：'''Medulla'''（硬件层 / IO 平台），'''Clumsy'''（车体行为层 / 自动驾驶），'''Simple'''（车队层 / 调度与交管）。每层各自定义&amp;quot;车&amp;quot;的一个侧面，通过明确的数据契约串联。理解这套分层是开发 MDCS 插件、写适配代码与调试问题的前提。&lt;br /&gt;
&lt;br /&gt;
MDCS abstracts &amp;quot;an AGV&amp;quot; through three cooperating layers: '''Medulla''' (hardware / IO), '''Clumsy''' (vehicle behaviour / autopilot), '''Simple''' (fleet / scheduling). Each layer defines one facet of the vehicle and they connect through well-defined data contracts. Understanding this stack is foundational for any plugin / adaptation work.&lt;br /&gt;
&lt;br /&gt;
== 三层结构 / Three-layer view ==&lt;br /&gt;
&lt;br /&gt;
  ┌─────────────────────────────────────────────────────────────┐&lt;br /&gt;
  │  Simple  (Fleet plane)                                      │&lt;br /&gt;
  │  • SimpleComposer.RCS.Car / ClumsyCar / 自评估车 subclass    │&lt;br /&gt;
  │  • SimpleCore — DPS 交管 / 寻路 / 包络 / 可达性                │&lt;br /&gt;
  │  • TopazScript 任务脚本                                       │&lt;br /&gt;
  └────────────────┬────────────────────────────────────────────┘&lt;br /&gt;
                   │ 任务 / TopazScript / mission script&lt;br /&gt;
                   │ 上报 / Status (x,y,th,siteId,batt,...)&lt;br /&gt;
  ┌────────────────▼────────────────────────────────────────────┐&lt;br /&gt;
  │  Clumsy  (Vehicle behaviour plane)                          │&lt;br /&gt;
  │  • SimpleAgvInterface.Queue(...)                            │&lt;br /&gt;
  │  • MovementDefinition: SteeringLineFollowing,               │&lt;br /&gt;
  │    AutoFetchGood, AutoShelfFetching, ...                    │&lt;br /&gt;
  │  • DriveTask.WaitDriveTask(IEnumerable&amp;lt;bool&amp;gt;)                │&lt;br /&gt;
  │  • PilotDefinition&amp;lt;T&amp;gt; 自动驾驶宿主                            │&lt;br /&gt;
  └────────────────┬────────────────────────────────────────────┘&lt;br /&gt;
                   │ UpperIO 意图（vCmd, steerCmd, forkTgt, ...）&lt;br /&gt;
                   │ LowerIO 反馈（vEst, sensors, e-stop, ...）&lt;br /&gt;
  ┌────────────────▼────────────────────────────────────────────┐&lt;br /&gt;
  │  Medulla  (Hardware plane)                                  │&lt;br /&gt;
  │  • CartDefinition + [AsUpperIO]/[AsLowerIO]/[AsInitParam]    │&lt;br /&gt;
  │  • LadderLogic 周期性控制循环 (~50 ms)                        │&lt;br /&gt;
  │  • IOObject 体系（Lidar, Camera, Cart 都是）                  │&lt;br /&gt;
  │  • DObject 跨进程共享内存                                     │&lt;br /&gt;
  └─────────────────────────────────────────────────────────────┘&lt;br /&gt;
&lt;br /&gt;
== 各层的责任 / Layer responsibilities ==&lt;br /&gt;
=== Medulla — 硬件层 / Hardware plane ===&lt;br /&gt;
- 抽象具体硬件（CAN / Modbus / S7 / RS485 / USB）成 ''IO 字段''。&lt;br /&gt;
- 同步把上位指令写到硬件，把硬件状态读到字段。&lt;br /&gt;
- 不做&amp;quot;决策&amp;quot;，不知道&amp;quot;任务&amp;quot;。&lt;br /&gt;
- 关键文件：`D:\src\M2\OfficialPlugins\CartActivator\CartDefinition.cs`、`Attrs.cs`。&lt;br /&gt;
- 关键属性：`[AsUpperIO]`（命令向下流）、`[AsLowerIO]`（状态向上流）、`[AsInitParam]`（启动配置）、`[UseLadderLogic]`（周期循环）。&lt;br /&gt;
&lt;br /&gt;
- Abstracts hardware (CAN / Modbus / S7 / RS485 / USB) into ''IO fields''.&lt;br /&gt;
- Writes commands down, reads state up, on a fixed tick.&lt;br /&gt;
- Makes no &amp;quot;decisions&amp;quot; and is unaware of tasks.&lt;br /&gt;
- Source: `D:\src\M2\OfficialPlugins\CartActivator\CartDefinition.cs` / `Attrs.cs`.&lt;br /&gt;
&lt;br /&gt;
=== Clumsy — 车体行为层 / Vehicle behaviour plane ===&lt;br /&gt;
- 把&amp;quot;动作&amp;quot;（直行、原地转、自动取货、绕障）封装为 `MovementDefinition` 子类。&lt;br /&gt;
- 把 Movement 组合成&amp;quot;业务动作&amp;quot;如 `Fetch / Put / ExitSite`，挂在 `SimpleAgvInterface` 上。&lt;br /&gt;
- 不知道车队全局，只看到本车的位置、速度、IO。&lt;br /&gt;
- 任务通过 `SimpleAgvInterface.Queue(...)` 流水线执行。&lt;br /&gt;
- 关键文件：`D:\src\Clumsy\ClumsyCore\Pilot\MovementDefinition.cs:41`、`D:\src\Clumsy\ClumsyCore\Pilot\PilotDefinition.cs:91-122`、`D:\src\Clumsy\ClumsyCore\Interfaces\SimpleAgvInterface.cs`。&lt;br /&gt;
&lt;br /&gt;
- Packages a motion primitive as a `MovementDefinition` subclass with `IEnumerable&amp;lt;bool&amp;gt; Get()` ticked at `DriveTaskInterval` ms.&lt;br /&gt;
- Composes business actions (`Fetch / Put / ExitSite`) on top of `SimpleAgvInterface`.&lt;br /&gt;
- Sees only this vehicle's state.&lt;br /&gt;
&lt;br /&gt;
=== Simple — 车队层 / Fleet plane ===&lt;br /&gt;
- SimpleCore 提供调度内核：[[Special:MyLanguage/DPS调度算法详解|DPS]] 交管、寻路、包络、可达性、[[Special:MyLanguage/流场规划|流场]]。&lt;br /&gt;
- SimpleComposer 提供 UI 壳与车型 / 业务插件接口。&lt;br /&gt;
- 给单车下发的是 TopazScript（高层意图脚本），由 ClumsyCar 转给车载 Clumsy，或由自评估车在本地翻译为 RFID / Modbus。&lt;br /&gt;
- 关键文件：`D:\src\Simple\SimpleComposer\RCS\Car.cs:33` / `ClumsyCar.cs:34`、`D:\src\Simple\SimpleCore\PropType\AbstractCar.cs:346`、`BasicProps\AGVInterface.cs`。&lt;br /&gt;
&lt;br /&gt;
- SimpleCore: scheduling kernel (DPS traffic, pathfinding, envelopes, reachability, flow-field).&lt;br /&gt;
- SimpleComposer: UI shell + plugin host for car types and business scripts.&lt;br /&gt;
- Dispatches TopazScript mission scripts; ClumsyCar HTTP-posts to the on-board Clumsy adapter, while self-evaluating cars run `SelfEvaluating(agv, script)` locally and translate to vendor commands.&lt;br /&gt;
&lt;br /&gt;
== 上下位 IO 数据流 / Upper / Lower IO data flow ==&lt;br /&gt;
=== UpperIO（命令下行）/ Upper IO (commands going down) ===&lt;br /&gt;
'''上位''' = 调度 / Clumsy → 车体。典型上位字段：&lt;br /&gt;
&lt;br /&gt;
* `vCmd`, `steerCmd` — 速度 / 转向&lt;br /&gt;
* `forkHeightTgt`, `forkTiltTgt` — 叉齿&lt;br /&gt;
* `brakeOn`, `hornOn`, `ledColor`&lt;br /&gt;
* `jackTarget` — 顶升机构&lt;br /&gt;
&lt;br /&gt;
=== LowerIO（状态上行）/ Lower IO (status going up) ===&lt;br /&gt;
'''下位''' = 车体 → Clumsy / 调度。典型下位字段：&lt;br /&gt;
&lt;br /&gt;
* `vEst`, `steerEst` — 实测速度 / 转向&lt;br /&gt;
* `forkHeight`, `forkAtTarget`&lt;br /&gt;
* `batteryPercent`, `eStop`, `loadKg`&lt;br /&gt;
* 各类传感器（接近开关、负载、温度等）&lt;br /&gt;
&lt;br /&gt;
=== 为什么要分上下位 / Why split upper vs lower ===&lt;br /&gt;
MDCS 强制 ''命令 / 状态''解耦，使得：&lt;br /&gt;
* Clumsy 可以在上位写完命令后 ''立即向上汇报已下发''，不必等硬件确认；&lt;br /&gt;
* Medulla 的硬件层故障（如 PLC 通讯断）不会污染上层任务状态；&lt;br /&gt;
* 测试时可把整车换成模拟器，只要 IO 字段一致，上层代码无感知。&lt;br /&gt;
&lt;br /&gt;
The split enforces command/state decoupling: Clumsy reports &amp;quot;issued&amp;quot; immediately without waiting for hardware ack; Medulla failures don't poison higher-layer mission state; in simulation a vehicle can be swapped for a simulator as long as the IO fields match — the upper stack is unaware.&lt;br /&gt;
&lt;br /&gt;
== 任务下发完整链路 / Full mission-dispatch path ==&lt;br /&gt;
&lt;br /&gt;
# SimpleComposer 把业务任务 ''编译''为 TopazScript（路径分段 + 可执行动作）。&amp;lt;br /&amp;gt;SimpleComposer compiles a business job into TopazScript (path segments + executable actions).&lt;br /&gt;
# `Car.actualSendScript(script)` 被调度调用。&amp;lt;br /&amp;gt;The scheduler calls `Car.actualSendScript(script)`.&lt;br /&gt;
# '''ClumsyCar 分支''': HTTP POST 到车载 Clumsy；车载 `SelfEvaluating(agv, script)` 解释脚本 → 调 `AGV.Fetch / Put / Go` → `Queue(...)` → `Movement.Get()` → `DriveTask` → Medulla UpperIO。&amp;lt;br /&amp;gt;''ClumsyCar branch'': HTTP-POST to the on-board adapter; on-board `SelfEvaluating` interprets the script, calls AGV methods, queues Movements, drives Medulla IO.&lt;br /&gt;
# '''自评估车分支''': 调度本地运行 `SelfEvaluating(agv, script)`；nested `AGV : AGVInterface` 把 `agv.Go(...)` 翻译为 RFID / Modbus 命令发给车体 PLC。&amp;lt;br /&amp;gt;''Self-eval branch'': scheduler runs `SelfEvaluating` locally; the nested `AGV : AGVInterface` translates calls to vendor commands.&lt;br /&gt;
# 任意时刻车体心跳上报 `(x, y, th, siteId, ...)`；调度据此更新地图、推进任务。&amp;lt;br /&amp;gt;Heartbeat from the vehicle continuously reports pose + state; the scheduler updates and advances.&lt;br /&gt;
&lt;br /&gt;
== 实施建议 / Practical guidance ==&lt;br /&gt;
* '''先 Medulla''': 没硬件，上层一切免谈。先让 IO 表跑通（手动 set `vCmd=200` 看车动）。&lt;br /&gt;
* '''再 Clumsy''': IO 跑通后写 1 个 `SimpleAgvInterface` 子类、跑直线巡线。&lt;br /&gt;
* '''最后 Simple''': Clumsy 单车可控之后接入 SimpleComposer，做调度。&lt;br /&gt;
* '''测试每层''': Medulla 可以独立测；Clumsy 可以用 IO 模拟器（脚本里直接 set / get 字段）；SimpleCore 有单机调度仿真。&lt;br /&gt;
&lt;br /&gt;
* Order: Medulla → Clumsy → Simple. Don't write Simple-side code if Medulla isn't proven yet.&lt;br /&gt;
* Test each layer in isolation: Medulla via manual IO sets, Clumsy with an IO simulator, SimpleCore in standalone simulation.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/MDCS引擎适配机器人入门教学|MDCS引擎适配机器人入门教学]]&lt;br /&gt;
* [[Special:MyLanguage/Medulla|Medulla]] / [[Special:MyLanguage/Medulla-API|Medulla-API]]&lt;br /&gt;
* [[Special:MyLanguage/Clumsy-API|Clumsy-API]]&lt;br /&gt;
* [[Special:MyLanguage/Simple-API|Simple-API]]&lt;br /&gt;
* [[Special:MyLanguage/DObject|DObject]]&lt;br /&gt;
* [[Special:MyLanguage/AGV任务运行逻辑|AGV任务运行逻辑]]&lt;br /&gt;
* [[Special:MyLanguage/如何基于SimpleCore核心库进行调度系统开发|如何基于SimpleCore核心库进行调度系统开发]]&lt;br /&gt;
&lt;br /&gt;
[[Category:通识和入门教学]]&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E8%89%B2%E5%B8%A6%E5%92%8C%E7%A3%81%E6%9D%A1%E5%AF%BC%E8%88%AA&amp;diff=1012</id>
		<title>色带和磁条导航</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E8%89%B2%E5%B8%A6%E5%92%8C%E7%A3%81%E6%9D%A1%E5%AF%BC%E8%88%AA&amp;diff=1012"/>
		<updated>2026-05-16T11:26:57Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
色带和磁条导航是工业 AGV 最经典、最低成本的导航方式：在地面铺设一条 ''视觉色带''或 ''磁条''，车上装传感器实时测量与色带 / 磁条中线的横向偏差，反馈到转向回路实现自动巡线。MDCS 把这类车视为 [[Special:MyLanguage/如何基于SimpleCore核心库进行调度系统开发|自评估车]]：车体自行处理巡线，调度只下发&amp;quot;去哪个站点&amp;quot;的高层意图。&lt;br /&gt;
&lt;br /&gt;
Tape and magnetic-strip navigation is the most classic, lowest-cost industrial AGV navigation: lay coloured tape (vision) or a magnetic strip on the floor; the AGV uses a sensor to measure lateral offset from the strip and steers to keep it centred. MDCS treats these as [[Special:MyLanguage/如何基于SimpleCore核心库进行调度系统开发|self-evaluating vehicles]]: the cart does its own line-following, the fleet just dispatches &amp;quot;go to site X&amp;quot; intents.&lt;br /&gt;
&lt;br /&gt;
== 适用 / Use cases ==&lt;br /&gt;
* 低成本固定路径（仓储入库、装配线物料补给）&lt;br /&gt;
* 强干扰环境（金属反射多、纹理少，激光 SLAM 表现差）&lt;br /&gt;
* 老线改造（已有色带 / 磁条线路，复用基础设施）&lt;br /&gt;
* 安全要求极高、需要 ''物理路径''可视可控的场景&lt;br /&gt;
&lt;br /&gt;
* Low-cost fixed routes (binning, assembly-line restock).&lt;br /&gt;
* Environments hostile to laser SLAM (heavy metal reflection, no texture).&lt;br /&gt;
* Brown-field upgrades where tape / magnet routes already exist.&lt;br /&gt;
* Safety-critical use cases that demand a physically visible / controllable path.&lt;br /&gt;
&lt;br /&gt;
== 系统组成 / System composition ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 部件 / Part !! 色带导航 / Tape !! 磁条导航 / Magnetic strip&lt;br /&gt;
|-&lt;br /&gt;
| 路径基础设施 / Path media || 反光色带、贴在地面 / reflective coloured tape on the floor || 磁条，1–3 mm 厚，可埋入地面或贴面 / magnetic strip 1–3 mm thick&lt;br /&gt;
|-&lt;br /&gt;
| 车上传感器 / On-board sensor || 线扫相机或多点光电 / line camera or photo-array || 磁导航传感器（霍尔阵列）/ Hall-array&lt;br /&gt;
|-&lt;br /&gt;
| 站点标记 / Site marker || 二维码 / 反光异型 / RFID tag near the tape || RFID 标签（最常见）/ RFID tag&lt;br /&gt;
|-&lt;br /&gt;
| 控制器 / Controller || 厂商 PLC / MCU || 厂商 PLC / MCU&lt;br /&gt;
|-&lt;br /&gt;
| 通讯 / Comm to fleet || Wi-Fi + RFID 站点回报 || Wi-Fi + RFID 站点回报&lt;br /&gt;
|-&lt;br /&gt;
| 路径修改 / Route change || 重铺色带 || 重铺磁条&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
色带导航的优点：易铺设、易看见、可临时改路径；缺点：易污损。&amp;lt;br&amp;gt;&lt;br /&gt;
磁条优点：耐用，污垢不影响；缺点：金属物体附近误读，改线难。&lt;br /&gt;
&lt;br /&gt;
== MDCS 适配 / MDCS adaptation ==&lt;br /&gt;
'''要点''': 这类车在 SimpleCore / SimpleComposer 中是 ''自评估车''（不跑 MDCS 车载），调度的工作止于&amp;quot;高层意图 + 站点回报&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
The vehicle is a '''self-evaluating car''' from MDCS's point of view: no on-board Clumsy, no Medulla. The scheduler's job is to send intent and observe site reports.&lt;br /&gt;
&lt;br /&gt;
最小集成：&lt;br /&gt;
Minimal integration:&lt;br /&gt;
&lt;br /&gt;
# 在 SimpleComposer 中声明一个 `Car` 子类（不是 ClumsyCar），含 nested `AGV : AGVInterface`。&lt;br /&gt;
# `AGV.Go(srcId, dstId)` 把 ''站点意图''翻译为 ''RFID 命令帧''（或 Modbus 寄存器写入），通过 Wi-Fi 发送到车上 PLC。&lt;br /&gt;
# 车上 PLC 用 RFID 读到的站点 ID 自决定 ''下一步''（前进 / 等待 / 转向 / 停止），并把 RFID + 状态周期上报。&lt;br /&gt;
# 在 `Car.keepAlive()` 中解析回报，更新 `(x, y, th, siteId, batt, error)`。&lt;br /&gt;
# 站点上锁仍由 SimpleCore 的 `TryLock / Leave` 控制，但 ''是否真的进入''取决于车体识别到 RFID 标签。&lt;br /&gt;
&lt;br /&gt;
模板代码（重要细节见 `D:\src\cookbook\adaption-reference\苏州凌鸟\Scenes\AMRScene1\Cars\MNavCar.cs` 293 行）：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[CarType(Name = &amp;quot;TapeAGV&amp;quot;, Title = &amp;quot;色带 / 磁条 AGV / Tape or magstrip AGV&amp;quot;)]&lt;br /&gt;
public class TapeAGV : Car&lt;br /&gt;
{&lt;br /&gt;
    public string plcIp;&lt;br /&gt;
    public int    rfidLastSeen;&lt;br /&gt;
    public DateTime lastHeartbeat = DateTime.Now;&lt;br /&gt;
&lt;br /&gt;
    public class AGV : AGVInterface&lt;br /&gt;
    {&lt;br /&gt;
        private readonly TapeAGV car;&lt;br /&gt;
        public AGV(TapeAGV c) { car = c; }&lt;br /&gt;
&lt;br /&gt;
        public void Go(double sx, double sy, int srcId,&lt;br /&gt;
                       double dx, double dy, int dstId, int routeId)&lt;br /&gt;
        {&lt;br /&gt;
            Queue(&lt;br /&gt;
              async () =&amp;gt; { TryLock(srcId, routeId); },&lt;br /&gt;
              async () =&amp;gt;&lt;br /&gt;
              {&lt;br /&gt;
                  // 把&amp;quot;去往 dstId&amp;quot;翻译为 RFID 帧&lt;br /&gt;
                  await car.SendRfidGoCommand(dstId);&lt;br /&gt;
                  // 等车真的到了&lt;br /&gt;
                  while (car.rfidLastSeen != dstId) await Task.Delay(100);&lt;br /&gt;
              },&lt;br /&gt;
              async () =&amp;gt; { Leave(srcId); });&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    public override async Task keepAlive()&lt;br /&gt;
    {&lt;br /&gt;
        var status = await ReadPLC(plcIp);&lt;br /&gt;
        // 把 PLC 状态映射到 Car&lt;br /&gt;
        UpdatePose(status.X, status.Y, status.Heading);&lt;br /&gt;
        rfidLastSeen   = status.LastRfid;&lt;br /&gt;
        lastHeartbeat  = DateTime.Now;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 站点定位 / Site localisation ==&lt;br /&gt;
色带 / 磁条本身只能告诉车 ''横向偏差''，不能告诉车 ''我在路径上哪一段''。所以这类车必须沿路径布 ''RFID 标签''作为站点：&lt;br /&gt;
* 每个分叉前后必有 RFID。&lt;br /&gt;
* 长直线段每 5–10 m 一个 RFID 用于精确位姿恢复。&lt;br /&gt;
* 终点 / 工位前 RFID 触发停车 / 入位逻辑。&lt;br /&gt;
&lt;br /&gt;
Tape / strip only gives ''lateral offset''. RFID tags along the path provide ''which site I'm at'' — one before/after each branch, one every 5–10 m on long straight, one at each terminal / work position.&lt;br /&gt;
&lt;br /&gt;
== 与 MDCS 其它定位的混用 / Hybrid with other localisation ==&lt;br /&gt;
有些项目把色带 / 磁条导航 ''作为 SLAM 失败时的 fallback''：平时跑 [[Special:MyLanguage/激光SLAM建图|激光 SLAM]]，在已知劣化区段（如户外、强光、临时遮挡）切到磁条 fallback。技术上可行，但工程实现复杂（两套传感器、两套控制律、切换瞬间的姿态一致性）；除非现场要求否则不推荐。&lt;br /&gt;
&lt;br /&gt;
Some projects use tape / magstrip as a SLAM ''fallback'' on known-degraded segments. Technically feasible, engineering-complex (two sensors, two controllers, transition continuity). Avoid unless the deployment really demands it.&lt;br /&gt;
&lt;br /&gt;
== 调试与维护 / Debug &amp;amp; maintenance ==&lt;br /&gt;
* '''色带磨损''' / Tape wear: 每 3–6 个月巡线一次，发现 ≥ 5 cm 连续缺失立即重贴。&lt;br /&gt;
* '''磁条覆盖物''' / Magstrip cover: 磁条要避免被金属覆盖（货叉车叉齿掉落 / 临时金属板）。&lt;br /&gt;
* '''RFID 标签丢失''' / RFID loss: 每天上电后做一次 ''全路径轮询''；任何标签失效立即报警。&lt;br /&gt;
* '''传感器外参''' / Sensor extrinsics: 磁导航传感器中心要对齐车体中线 ±5 mm；偏太多巡线会偏。&lt;br /&gt;
&lt;br /&gt;
== 失败模式 / Failure modes ==&lt;br /&gt;
* '''丢线''' / Loss of tape: 车体应停 + 灯闪 + 报告 SimpleComposer。&lt;br /&gt;
* '''RFID 漏读''' / RFID miss: 心跳里包含 ''last_rfid + age''；调度可据此判断车是否仍在路径。&lt;br /&gt;
* '''重定位''' / Re-localise: 没有重定位概念 —— 失线后只能人工把车搬到任一 RFID 上。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/如何基于SimpleCore核心库进行调度系统开发|如何基于SimpleCore核心库进行调度系统开发]]&lt;br /&gt;
* [[Special:MyLanguage/纯二维码导航|纯二维码导航]] — 类似思路，地标改为二维码&lt;br /&gt;
* [[Special:MyLanguage/巡线行走|巡线行走]] — MDCS Clumsy 巡线（基于位姿，不依赖物理标记）&lt;br /&gt;
&lt;br /&gt;
[[Category:定位导航相关手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E7%BB%95%E9%9A%9C%E8%A1%8C%E8%B5%B0&amp;diff=1011</id>
		<title>绕障行走</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E7%BB%95%E9%9A%9C%E8%A1%8C%E8%B5%B0&amp;diff=1011"/>
		<updated>2026-05-16T11:26:53Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
&amp;quot;绕障行走&amp;quot;指 AGV 在 [[Special:MyLanguage/巡线行走|巡线行走]] 的基础上叠加 ''局部障碍感知与降速 / 短距偏避''能力。MDCS 中的绕障 ''不是动态重规划''（那是调度层 SimpleCore 的工作），而是 ''线段内的安全反应''：当前向激光检测到障碍时，先减速到停，必要时小幅横向避让；如果障碍持续存在 &amp;gt; 阈值时间，调度会接管做全局重路径。&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Obstacle-aware line following&amp;quot; adds ''local obstacle perception with deceleration and small lateral avoidance'' on top of [[Special:MyLanguage/巡线行走|line following]]. MDCS does '''not''' do dynamic global re-planning here — that's SimpleCore's job. The Movement-level logic is ''in-segment safety reaction'': decelerate-to-stop on a detected obstacle, optionally small lateral nudge; if the obstacle persists beyond a threshold, the scheduler takes over with a full reroute.&lt;br /&gt;
&lt;br /&gt;
== 与全局重规划的分工 / Division with global replanning ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 层 / Layer !! 触发 / Trigger !! 反应 / Reaction&lt;br /&gt;
|-&lt;br /&gt;
| Clumsy Movement (本页) || 前向激光 ROI 内有点 / Front lidar ROI has points || 减速 / 停车 / 小幅横向避让&lt;br /&gt;
|-&lt;br /&gt;
| SimpleCore 调度 || 障碍持续 &amp;gt; T_obstacle_stale (默认 8 s) || 重新规划该车的路径段&lt;br /&gt;
|-&lt;br /&gt;
| 全局地图重建 || 障碍很久不消失（&amp;gt; T_map_invalid） || 标记地图段为永久不可达&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The split is deliberate: Movement-level responses must be ≤ tens of milliseconds; scheduler-level replanning is hundreds of milliseconds and global. They cooperate via the per-segment &amp;quot;stuck&amp;quot; flag.&lt;br /&gt;
&lt;br /&gt;
== 关键参数 / Key parameters ==&lt;br /&gt;
绕障字段附加在 `SteeringLineFollowing` 之上（或它的&amp;quot;绕障变体&amp;quot;`SteeringLineFollowingWithAvoid`）：&lt;br /&gt;
&lt;br /&gt;
These fields are layered on top of `SteeringLineFollowing` (or its avoidance-aware sibling `SteeringLineFollowingWithAvoid`):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 字段 / Field !! 默认 / Default !! 含义 / Meaning&lt;br /&gt;
|-&lt;br /&gt;
| `obstacleROIStart` / `obstacleROIEnd` || 视场前向 ±15° || 障碍检测的角度区间 / FOV slice for obstacle detection&lt;br /&gt;
|-&lt;br /&gt;
| `obstacleROILength` || 1500 mm || 检测距离上限 / max distance considered&lt;br /&gt;
|-&lt;br /&gt;
| `obstacleSlowDist` || 1500 mm || 进入此距离开始降速 / start decelerating&lt;br /&gt;
|-&lt;br /&gt;
| `obstacleStopDist` || 400 mm || 在此距离停车 / hard stop&lt;br /&gt;
|-&lt;br /&gt;
| `obstacleMinPoints` || 4 || 多少个点视为&amp;quot;真障碍&amp;quot;（去抗 ghost）&lt;br /&gt;
|-&lt;br /&gt;
| `lateralAvoid` || `false` || 是否允许小幅横向偏避（仅全向车 / 双阿克曼蟹行可用）&lt;br /&gt;
|-&lt;br /&gt;
| `lateralAvoidMax` || 250 mm || 横向最大偏移 / max lateral nudge&lt;br /&gt;
|-&lt;br /&gt;
| `stuckTimeoutMs` || 8000 || 持续受阻多久后向调度告警 / report-stuck timeout&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 工作机制 / How it works ==&lt;br /&gt;
1. 每个 tick 读最新激光点云 `cachedLidar`（参见 [[Special:MyLanguage/2D激光雷达适配|2D 激光适配]]）。&lt;br /&gt;
2. 用 ROI 过滤：只关心角度在 `[obstacleROIStart, obstacleROIEnd]`、距离在 `[0, obstacleROILength]` 的点。&lt;br /&gt;
3. 统计 ROI 内点数 ≥ `obstacleMinPoints` 时认定&amp;quot;有障碍&amp;quot;，记录 ''最近距离'' d_obs。&lt;br /&gt;
4. 计算速度上限：&lt;br /&gt;
   - d_obs ≥ obstacleSlowDist  → basespeed&lt;br /&gt;
   - obstacleStopDist &amp;lt; d_obs &amp;lt; obstacleSlowDist → 线性插值降速&lt;br /&gt;
   - d_obs ≤ obstacleStopDist → 0（停车）&lt;br /&gt;
5. 如果 `lateralAvoid = true` 且持续受阻 &amp;gt; 500 ms，启用 ''小幅横向偏避''：把虚拟前瞻点向无障碍一侧偏 `lateralAvoidMax`，重新做纯跟踪。&lt;br /&gt;
6. 如果连续受阻 &amp;gt; `stuckTimeoutMs`，把 ''segment stuck'' 标志置 true → SimpleCore 调度立即重新寻路。&lt;br /&gt;
&lt;br /&gt;
1. Each tick read `cachedLidar` from the front lidar.&lt;br /&gt;
2. ROI filter on the angle/distance slice.&lt;br /&gt;
3. Points ≥ `obstacleMinPoints` → mark obstacle, record nearest distance d_obs.&lt;br /&gt;
4. Speed cap: linear ramp between `obstacleStopDist` (0) and `obstacleSlowDist` (basespeed).&lt;br /&gt;
5. If `lateralAvoid` is on, after 500 ms of blockage shift the look-ahead point sideways.&lt;br /&gt;
6. After `stuckTimeoutMs` continuous blockage, flag the segment as stuck → scheduler replans.&lt;br /&gt;
&lt;br /&gt;
== 调用样例 / Example ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public void DriveAvoiding(double sx, double sy, double dx, double dy)&lt;br /&gt;
{&lt;br /&gt;
    Queue(async () =&amp;gt;&lt;br /&gt;
    {&lt;br /&gt;
        DriveTask.WaitDriveTask(new SteeringLineFollowingWithAvoid&lt;br /&gt;
        {&lt;br /&gt;
            srcX = sx, srcY = sy, dstX = dx, dstY = dy,&lt;br /&gt;
            basespeed = 800,&lt;br /&gt;
&lt;br /&gt;
            obstacleSlowDist  = 1500,&lt;br /&gt;
            obstacleStopDist  = 400,&lt;br /&gt;
            obstacleMinPoints = 4,&lt;br /&gt;
            lateralAvoid      = false,   // 普通阿克曼车关闭&lt;br /&gt;
            stuckTimeoutMs    = 8000&lt;br /&gt;
        }.Follow());&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 调试要点 / Tuning ==&lt;br /&gt;
* '''误触发停车''' / False positives → 增大 `obstacleMinPoints`（地面反射或飞虫的少量点会被忽略）。&lt;br /&gt;
* '''反应迟钝''' / Slow to react → 减小 `obstacleSlowDist` 接近 `obstacleStopDist`，但太接近会跌停（无平滑减速）。&lt;br /&gt;
* '''横向避让撞两侧''' / Lateral nudge into walls → 减小 `lateralAvoidMax`；或在调度可达性中声明 ''两侧不可避''。&lt;br /&gt;
* '''反复 stuck / replan 抖动''' / Flap between stuck / unstuck → 增大 `stuckTimeoutMs`，避免短暂遮挡引发重路径。&lt;br /&gt;
&lt;br /&gt;
== 全向车与 蟹行避让 / Omni &amp;amp; crab avoidance ==&lt;br /&gt;
全向车与双阿克曼车型可以 ''真'' 横向避让（不改变车头朝向就能侧移）。对应 Movement 是 `OmniLineFollowingWithAvoid` 与 `CrabLineFollowingWithAvoid`，它们的 `lateralAvoidMax` 可以放宽到 600 mm。&lt;br /&gt;
&lt;br /&gt;
Omni and double-Ackermann vehicles can truly translate laterally without yawing. Use `OmniLineFollowingWithAvoid` / `CrabLineFollowingWithAvoid`; `lateralAvoidMax` can be opened up to 600 mm safely.&lt;br /&gt;
&lt;br /&gt;
== 安全 / Safety ==&lt;br /&gt;
绕障 Movement '''不是安全 e-stop''' —— 它的反应时间是 50 ms 量级（Movement tick），而安全 e-stop 必须 &amp;lt; 10 ms 并 ''独立于 Movement 逻辑''。`obstacleStopDist` 之外还必须有：&lt;br /&gt;
&lt;br /&gt;
The Movement is '''not''' a safety e-stop — its reaction is at the ~50 ms tick. Always provide:&lt;br /&gt;
&lt;br /&gt;
* 激光雷达紧急停止区（&amp;lt; 300 mm 内任何点）—— 接到 Medulla 上位 `eStop` 信号上。&lt;br /&gt;
* 物理安全条 / 急停按钮。&lt;br /&gt;
* SIL-2 / PL-d 的功能安全设备（重载场景）。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/巡线行走|巡线行走]]&lt;br /&gt;
* [[Special:MyLanguage/车体抽象原理|车体抽象原理]]&lt;br /&gt;
* [[Special:MyLanguage/可达性状态编程|可达性状态编程]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - 寻路启发器功能|寻路启发器功能]] — 调度侧重规划&lt;br /&gt;
&lt;br /&gt;
[[Category:运动控制使用手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E7%89%B5%E5%BC%95%E8%BD%A6%E9%80%82%E9%85%8D%E6%A1%88%E4%BE%8B&amp;diff=1010</id>
		<title>牵引车适配案例</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E7%89%B5%E5%BC%95%E8%BD%A6%E9%80%82%E9%85%8D%E6%A1%88%E4%BE%8B&amp;diff=1010"/>
		<updated>2026-05-16T11:26:53Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
牵引车（也称双阿克曼车型、tow-truck、tractor）的关键特征是 ''有非完整约束''（不能瞬时横移），加上 ''后挂多节拖车''时存在 ''逆向跟踪难''与 ''折叠/失稳'' 风险。MDCS 通过专用 Movement 与额外的拖车包络处理这些问题。&lt;br /&gt;
&lt;br /&gt;
The tractor / double-Ackermann (tow-truck) vehicle has '''non-holonomic constraints''' (can't translate laterally) and, when towing trailers, the difficulties of reverse-tracking, jackknife avoidance, and trailer swing. MDCS handles these via dedicated Movements and trailer envelopes.&lt;br /&gt;
&lt;br /&gt;
参考：浙江中力 (`D:\src\cookbook\adaption-reference\浙江中力\`) 等拖车 / 牵引适配案例。&lt;br /&gt;
References: Zhejiang Zhongli (`adaption-reference\浙江中力\`) and similar.&lt;br /&gt;
&lt;br /&gt;
== 车型变体 / Variants ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 子型 / Sub-type !! 说明 / Description&lt;br /&gt;
|-&lt;br /&gt;
| 单阿克曼 / Single-Ackermann || 前轮转向，后轮驱动，无拖车 / Front-steer, rear-drive, no trailer&lt;br /&gt;
|-&lt;br /&gt;
| 双阿克曼 / Double-Ackermann || 前后都可转向（蟹行）/ Both axles steer (can crab-walk)&lt;br /&gt;
|-&lt;br /&gt;
| 牵引 + 1 挂 / Tractor + 1 trailer || 单个铰接点 / single hitch&lt;br /&gt;
|-&lt;br /&gt;
| 牵引 + N 挂 / Tractor + N trailers || 多铰接，每节带额外的运动学约束&lt;br /&gt;
|-&lt;br /&gt;
| 双向牵引 / Bidirectional tractor || 前后都能作为牵引头 / can lead from either end&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 1. Medulla 适配 / Medulla side ==&lt;br /&gt;
关键 IO（以双阿克曼为例）：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class TowCart : CartDefinition&lt;br /&gt;
{&lt;br /&gt;
    [AsInitParam] public string canIface = &amp;quot;can0&amp;quot;;&lt;br /&gt;
    [AsInitParam] public float  wheelBase = 1450;   // mm&lt;br /&gt;
    [AsInitParam] public float  trackFront = 1100;&lt;br /&gt;
    [AsInitParam] public float  trackRear  = 1100;&lt;br /&gt;
    [AsInitParam] public bool   doubleAckermann = true;&lt;br /&gt;
&lt;br /&gt;
    [AsUpperIO] public float vCmd;&lt;br /&gt;
    [AsUpperIO] public float steerFrontCmd;     // 前轮转角 / rad&lt;br /&gt;
    [AsUpperIO] public float steerRearCmd;      // 后轮转角 / rad  (sign for crab vs co-turn)&lt;br /&gt;
    [AsUpperIO] public bool  brakeOn;&lt;br /&gt;
    [AsUpperIO] public bool  hitchEngaged;      // 挂钩控制 / hitch control&lt;br /&gt;
&lt;br /&gt;
    [AsLowerIO] public float vEst;&lt;br /&gt;
    [AsLowerIO] public float steerFrontEst, steerRearEst;&lt;br /&gt;
    [AsLowerIO] public float hitchAngle1, hitchAngle2, hitchAngle3; // 每节拖车的折角&lt;br /&gt;
    [AsLowerIO] public bool  hitchOK;&lt;br /&gt;
    [AsLowerIO] public bool  eStop;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
铰接角 / Hitch angles：每节拖车与前一节的相对角度，是逆向跟踪与失稳预警的关键状态。`hitchAngleN` 用 IMU 双轴 或 单点编码器测。&lt;br /&gt;
&lt;br /&gt;
The hitch angle of each trailer relative to the one in front is the key state for reverse-tracking and jackknife warning. Measure via dual-axis IMUs or a hitch encoder.&lt;br /&gt;
&lt;br /&gt;
== 2. Clumsy 自动驾驶适配 / Clumsy side ==&lt;br /&gt;
=== 关键 Movement ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Movement !! 用途 / Use&lt;br /&gt;
|-&lt;br /&gt;
| `SteeringLineFollowing` || 正向行驶（单 / 双阿克曼通用）/ forward driving&lt;br /&gt;
|-&lt;br /&gt;
| `SteeringLineFollowingReverse` || 逆向行驶（带 trailer）/ reverse with trailer&lt;br /&gt;
|-&lt;br /&gt;
| `CrabLineFollowing` || 蟹行（仅双阿克曼）/ crab walk (double-Ackermann only)&lt;br /&gt;
|-&lt;br /&gt;
| `TractorTurnAround` || 调头动作（多段曲线）/ multi-segment U-turn&lt;br /&gt;
|-&lt;br /&gt;
| `JackknifeGuard` || 守护线程：检测 hitch 角度过大时刹停 / guard thread for excessive hitch angle&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== 逆向跟踪（reverse-track with trailer）===&lt;br /&gt;
带 N 节拖车做逆向跟踪时，运动学是 ''非线性、不稳定''。MDCS 采用：&lt;br /&gt;
&lt;br /&gt;
Reverse-tracking with N trailers is nonlinear and unstable. MDCS uses:&lt;br /&gt;
&lt;br /&gt;
# 在路径规划时 ''禁止''带挂逆向超过 5 m。&lt;br /&gt;
# 仅允许在专用的 ''逆向缓冲区''做带挂倒车，且车速 ≤ 100 mm/s。&lt;br /&gt;
# 控制器：以 ''最后一节拖车''的姿态为状态，通过非线性逆向控制律解算前轴转角。&lt;br /&gt;
# 任何 hitch 角度 ≥ 30° 立即停车 + 报警。&lt;br /&gt;
&lt;br /&gt;
# Path planning forbids reverse-with-trailer &amp;gt; 5 m.&lt;br /&gt;
# Reverse-with-trailer only permitted in dedicated reverse buffer zones at ≤ 100 mm/s.&lt;br /&gt;
# Controller takes the rear-most trailer pose as the state and inverts to front-axle angle.&lt;br /&gt;
# Any hitch angle ≥ 30° → e-stop + alarm.&lt;br /&gt;
&lt;br /&gt;
=== 调头 / U-turn ===&lt;br /&gt;
不带挂的牵引车 ''必须用最小转弯半径以内的曲线段拼接''来调头，常见 K-turn 或 U-turn。MDCS 的 `TractorTurnAround` Movement 包含一个解析的多段 Dubin's 路径生成器，已考虑 wheelBase + minTurnRadius。&lt;br /&gt;
&lt;br /&gt;
A tractor without a trailer needs a multi-segment path within its minimum turning radius. The `TractorTurnAround` Movement uses an analytic Dubin's path generator parameterised by wheelBase and minTurnRadius.&lt;br /&gt;
&lt;br /&gt;
== 3. SimpleComposer 端 / SimpleComposer side ==&lt;br /&gt;
=== 拖车包络 / Trailer envelope ===&lt;br /&gt;
牵引 + N 挂的包络必须由 ''拖车折角''计算 —— 直行时是简单的长矩形，弯路时各节会摆向不同方向，整体包络变成 ''扇形''。&lt;br /&gt;
&lt;br /&gt;
The envelope of a tractor + N trailers is shaped by '''hitch angles'''. Straight-line travel gives a long rectangle; turning causes each segment to swing differently, producing a fan-shaped envelope. SimpleComposer dynamically recomputes this from hitch-angle readings.&lt;br /&gt;
&lt;br /&gt;
=== 调度约束 / Scheduling constraints ===&lt;br /&gt;
* ''最小转弯半径''声明在车型属性中，调度寻路时排除半径过小的弯。&lt;br /&gt;
* ''带挂''与 ''不带挂''是两种不同的可达性状态（[[Special:MyLanguage/可达性状态编程|可达性状态编程]]）。&lt;br /&gt;
* 拖车数量影响交管锁；多挂车一次锁连续多个站点。&lt;br /&gt;
&lt;br /&gt;
* Minimum turn radius declared in car props; pathfinder excludes tighter turns.&lt;br /&gt;
* Towing vs not-towing are two reachability states.&lt;br /&gt;
* Multi-trailer cars take multi-site locks at once.&lt;br /&gt;
&lt;br /&gt;
== 4. 标定与调试 / Calibration &amp;amp; tuning ==&lt;br /&gt;
# '''前轮 / 后轮转角极性''' / Steer polarity: 单独测各轴的左 / 右极性，确认与 IMU 一致。&lt;br /&gt;
# '''单阿克曼直线度''' / Single-Ackermann straight: 直行 10 m 后侧向偏差 &amp;lt; 30 mm。&lt;br /&gt;
# '''双阿克曼蟹行''' / Crab walk: 命令 30° 蟹行 5 m，实际方向偏差 &amp;lt; 1°。&lt;br /&gt;
# '''拖车铰接角度''' / Hitch zero: 静态直拖时 hitchAngle 应 = 0；偏 &amp;gt; 1° 必须重做 IMU 零点。&lt;br /&gt;
# '''极限工况''' / Boundary: 满载 + 满拖 + 最小半径转弯，确认无失稳。&lt;br /&gt;
&lt;br /&gt;
# Steer polarity per axle; confirm IMU sign agreement.&lt;br /&gt;
# Single-Ack straight-line: &amp;lt; 30 mm drift over 10 m.&lt;br /&gt;
# Double-Ack crab-walk: 30° crab × 5 m → heading drift &amp;lt; 1°.&lt;br /&gt;
# Hitch zero: static straight tow → hitchAngle = 0 ± 1°.&lt;br /&gt;
# Boundary: fully-loaded, full trailer train, min-radius turn → no jackknife.&lt;br /&gt;
&lt;br /&gt;
== 5. 常见问题 / Common pitfalls ==&lt;br /&gt;
* '''逆向失控''' / Reverse instability: 拖车超 2 节时控制律不收敛；改成 ''分段倒车''（每次只倒 0.5 m 复位）。&lt;br /&gt;
* '''弯道挂车摆动''' / Trailer swing on curves: 把 `basespeed` 在弯道前先降到 50%，并加大 ''弯道前提早转向''时间。&lt;br /&gt;
* '''hitch 编码器漂''' / Hitch encoder drift: 检查机械连接 + IMU 零点；每天上电后做一次自检。&lt;br /&gt;
* '''带挂掉头空间不足''' / Insufficient room for U-turn: 调度寻路前必须知道拖车节数，否则 `TractorTurnAround` 会规划出穿墙路径。&lt;br /&gt;
&lt;br /&gt;
== 6. 双向牵引 / Bidirectional tractor ==&lt;br /&gt;
双向牵引车前后都可拖挂；切换&amp;quot;主拉端&amp;quot;时，Detour 的车体坐标需要同步 ''翻转 180°''，并切换前向 / 后向激光雷达为 SLAM 主源。&lt;br /&gt;
&lt;br /&gt;
A bidirectional tractor can pull from either end. When swapping the &amp;quot;lead end&amp;quot;, flip the vehicle frame 180° in Detour and switch the primary SLAM lidar from front to rear.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/MDCS引擎适配机器人入门教学|MDCS引擎适配机器人入门教学]]&lt;br /&gt;
* [[Special:MyLanguage/叉车适配案例|叉车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/全向车适配案例|全向车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/潜伏顶升车(KIVA类小车)适配案例|潜伏顶升车(KIVA类小车)适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/可达性状态编程|可达性状态编程]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - Simple:包络如何定义|Simple:包络如何定义]]&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E6%BD%9C%E4%BC%8F%E9%A1%B6%E5%8D%87%E8%BD%A6(KIVA%E7%B1%BB%E5%B0%8F%E8%BD%A6)%E9%80%82%E9%85%8D%E6%A1%88%E4%BE%8B&amp;diff=1009</id>
		<title>潜伏顶升车(KIVA类小车)适配案例</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E6%BD%9C%E4%BC%8F%E9%A1%B6%E5%8D%87%E8%BD%A6(KIVA%E7%B1%BB%E5%B0%8F%E8%BD%A6)%E9%80%82%E9%85%8D%E6%A1%88%E4%BE%8B&amp;diff=1009"/>
		<updated>2026-05-16T11:26:51Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
潜伏顶升车（也称 KIVA 类小车、料架搬运车）是从料架下方钻入、顶升后整体搬运料架到目的地的低矮 AGV。结构最简，但对 ''Detour 定位精度''与 ''[[Special:MyLanguage/使用自动对准料架功能|自动对准料架]]'' 算法要求最高。&lt;br /&gt;
&lt;br /&gt;
The lift-AGV (also &amp;quot;KIVA-class&amp;quot; after Amazon's KIVA) is a low-profile AGV that slides under a rack, jacks it up, and carries it. Mechanically simplest, but most demanding on [[Special:MyLanguage/Detour简介|Detour]] precision and the [[Special:MyLanguage/使用自动对准料架功能|rack-alignment]] algorithm.&lt;br /&gt;
&lt;br /&gt;
参考：浙江迈睿 KIVA 适配（`D:\src\cookbook\adaption-reference\浙江迈睿\CartAdapters\kiva\`）以及苏州艾吉威的对应 ClumsyPilot 实现。&lt;br /&gt;
References: Zhejiang Mairui KIVA adapter (`D:\src\cookbook\adaption-reference\浙江迈睿\CartAdapters\kiva\`) plus Suzhou Eswei's ClumsyPilot.&lt;br /&gt;
&lt;br /&gt;
== 硬件清单 / Hardware ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 部件 / Part !! 典型规格 / Typical spec&lt;br /&gt;
|-&lt;br /&gt;
| 底盘 / Chassis || 双驱动差速（差速旋转） / Two-wheel differential&lt;br /&gt;
|-&lt;br /&gt;
| 驱动电机 / Drive motor || BLDC 200–600 W × 2&lt;br /&gt;
|-&lt;br /&gt;
| 顶升机构 / Jack || 电动丝杆 / liquid jack；行程 30–100 mm&lt;br /&gt;
|-&lt;br /&gt;
| 雷达 / Lidar || 顶面双向 2D（前后扫描）或单向 + 360° 反光板&lt;br /&gt;
|-&lt;br /&gt;
| IMU / IMU || 6-DoF，必备&lt;br /&gt;
|-&lt;br /&gt;
| 接近开关 / Proximity || 料架腿检测辅助 / aux for leg detection&lt;br /&gt;
|-&lt;br /&gt;
| 通讯 / Comm || 工控 PC 本体 + Wi-Fi 5&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
不需要叉齿 / 倾角 / 复杂 PLC，硬件 IO 极少；MDCS 适配的核心难度在 ''钻入精度''与 ''带架原地旋转''。&lt;br /&gt;
&lt;br /&gt;
No fork / tilt / complex PLC; minimal hardware IO. The MDCS adaptation difficulty is in ''slide-under accuracy'' and ''in-place rotation while carrying''.&lt;br /&gt;
&lt;br /&gt;
== 1. Medulla 适配 / Medulla side ==&lt;br /&gt;
最小 IO 表 / Minimal IO surface:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class KivaCart : CartDefinition&lt;br /&gt;
{&lt;br /&gt;
    [AsInitParam] public string motorPort = &amp;quot;COM3&amp;quot;;&lt;br /&gt;
    [AsInitParam] public float  wheelBase  = 460;      // mm, between drive wheels&lt;br /&gt;
    [AsInitParam] public float  jackStroke = 80;       // mm&lt;br /&gt;
&lt;br /&gt;
    [AsUpperIO] public float vLeft, vRight;   // 双驱差速指令 / per-wheel cmd&lt;br /&gt;
    [AsUpperIO] public float jackTarget;       // 0 = down, jackStroke = up&lt;br /&gt;
    [AsUpperIO] public bool  led;              // 顶部状态灯 / status LED&lt;br /&gt;
&lt;br /&gt;
    [AsLowerIO] public float vLeftEst, vRightEst;&lt;br /&gt;
    [AsLowerIO] public float jackHeight;&lt;br /&gt;
    [AsLowerIO] public bool  jackAtTarget;&lt;br /&gt;
    [AsLowerIO] public bool  eStop;&lt;br /&gt;
    [AsLowerIO] public int   batteryPercent;&lt;br /&gt;
    [AsLowerIO] public bool  legProx;          // 接近开关：腿在我下面 / leg under me&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
ladder logic 同样 50 ms 周期；KIVA 一般通过厂商 USB-CAN 或 RS485 与轮控板通讯（具体协议见厂商手册）。&lt;br /&gt;
&lt;br /&gt;
The ladder logic runs at 50 ms; talk to the wheel controllers over the vendor's USB-CAN or RS485 channel.&lt;br /&gt;
&lt;br /&gt;
== 2. Clumsy 自动驾驶适配 / Clumsy side ==&lt;br /&gt;
关键 Movement / Key movements:&lt;br /&gt;
&lt;br /&gt;
* `SteeringLineFollowing` — 巡线到 ''料架前 anchor''&lt;br /&gt;
* `AutoShelfFetching_ManyLegs` — 自动钻入并对准料架（见 [[Special:MyLanguage/使用自动对准料架功能|使用自动对准料架功能]]）&lt;br /&gt;
* `JackUp(targetHeight)` — 顶升包封，等待 `jackAtTarget` 为 true&lt;br /&gt;
* `InPlaceRotate(angle)` — 带架原地差速旋转（必须在 jack-up 后启用 ''带载包络''）&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class AGV : SimpleAgvInterface&lt;br /&gt;
{&lt;br /&gt;
    public void PickRack(double anchorX, double anchorY, double rackX, double rackY)&lt;br /&gt;
    {&lt;br /&gt;
        Queue(&lt;br /&gt;
          async () =&amp;gt;&lt;br /&gt;
          {&lt;br /&gt;
              DriveTask.WaitDriveTask(new SteeringLineFollowing&lt;br /&gt;
              {&lt;br /&gt;
                  srcX = currentX, srcY = currentY,&lt;br /&gt;
                  dstX = anchorX,  dstY = anchorY,&lt;br /&gt;
                  basespeed = 800&lt;br /&gt;
              }.Follow());&lt;br /&gt;
          },&lt;br /&gt;
          async () =&amp;gt;&lt;br /&gt;
          {&lt;br /&gt;
              // Slide under + auto-align (从 anchor 切到 rack centre)&lt;br /&gt;
              DriveTask.WaitDriveTask(new AutoShelfFetching_ManyLegs&lt;br /&gt;
              {&lt;br /&gt;
                  aX = anchorX, aY = anchorY,&lt;br /&gt;
                  bX = rackX,   bY = rackY,&lt;br /&gt;
                  initSpeed = 600, inShelfSpeed = 200,&lt;br /&gt;
                  stopDist = 30, width = Configuration.conf.rackLegSpan,&lt;br /&gt;
                  startingSlot = 10&lt;br /&gt;
              }.Get());&lt;br /&gt;
          },&lt;br /&gt;
          async () =&amp;gt;&lt;br /&gt;
          {&lt;br /&gt;
              // Jack up&lt;br /&gt;
              SetUpperIO(&amp;quot;jackTarget&amp;quot;, Configuration.conf.jackStroke);&lt;br /&gt;
              await WaitForLowerIO(&amp;quot;jackAtTarget&amp;quot;, 4000);&lt;br /&gt;
              carryingRack = true;&lt;br /&gt;
          });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    public void DropRack(double targetX, double targetY)&lt;br /&gt;
    {&lt;br /&gt;
        Queue(&lt;br /&gt;
          async () =&amp;gt;&lt;br /&gt;
          {&lt;br /&gt;
              DriveTask.WaitDriveTask(new SteeringLineFollowing&lt;br /&gt;
              {&lt;br /&gt;
                  srcX = currentX, srcY = currentY,&lt;br /&gt;
                  dstX = targetX, dstY = targetY,&lt;br /&gt;
                  basespeed = 500    // 带载减速 / slow when loaded&lt;br /&gt;
              }.Follow());&lt;br /&gt;
          },&lt;br /&gt;
          async () =&amp;gt;&lt;br /&gt;
          {&lt;br /&gt;
              SetUpperIO(&amp;quot;jackTarget&amp;quot;, 0);&lt;br /&gt;
              await WaitForLowerIO(&amp;quot;jackAtTarget&amp;quot;, 4000);&lt;br /&gt;
              carryingRack = false;&lt;br /&gt;
          },&lt;br /&gt;
          async () =&amp;gt;&lt;br /&gt;
          {&lt;br /&gt;
              // 后退 200 mm 退出料架 / withdraw 200 mm&lt;br /&gt;
              DriveTask.WaitDriveTask(new SteeringLineFollowingReverse&lt;br /&gt;
              {&lt;br /&gt;
                  srcX = targetX, srcY = targetY,&lt;br /&gt;
                  dstX = targetX - 0.2 * Math.Cos(currentTh),&lt;br /&gt;
                  dstY = targetY - 0.2 * Math.Sin(currentTh),&lt;br /&gt;
                  basespeed = 200&lt;br /&gt;
              }.ReverseFollow());&lt;br /&gt;
          });&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 3. SimpleComposer 端 / SimpleComposer side ==&lt;br /&gt;
顶升车 ClumsyCar 子类需要 ''携带料架时的扩展包络''（料架长宽通常大于车体）：&lt;br /&gt;
&lt;br /&gt;
The ClumsyCar subclass needs an alternate ''carried-rack envelope'' (rack is wider/longer than the AGV):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[CarType(Name = &amp;quot;KivaCart&amp;quot;, Title = &amp;quot;KIVA 顶升车 / KIVA lift-AGV&amp;quot;)]&lt;br /&gt;
public class KivaCar : ClumsyCar&lt;br /&gt;
{&lt;br /&gt;
    public bool carryingRack =&amp;gt; /* 从车上心跳读 / from heartbeat */;&lt;br /&gt;
&lt;br /&gt;
    public override Envelope GetActiveEnvelope()&lt;br /&gt;
    {&lt;br /&gt;
        return carryingRack ? carriedRackEnvelope : base.GetActiveEnvelope();&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 4. 标定与调试 / Calibration &amp;amp; tuning ==&lt;br /&gt;
# '''轮距与轮径标定''' / Track + wheel diameter: 做正反 5 m 直线，比较 Detour 估的位移与实际，差异 &amp;gt; 2% 必须改 wheel radius 配置。&lt;br /&gt;
# '''差速旋转误差''' / Rotation error: 原地转 360° × 10，累计偏差 &amp;lt; 5°。误差大 → 轮距错。&lt;br /&gt;
# '''料架腿检测先验''' / Leg priors: 真测料架腿距，写入 `width`。&lt;br /&gt;
# '''顶升时长''' / Jack timing: `inshelfMillis` 比顶升时长长 1 s，避免还没顶完就开走。&lt;br /&gt;
# '''反光板 / SLAM 切换''' / Reflector / SLAM switch: 反光板辅助定位时务必检查 KIVA 顶面雷达视野，料架钻入后是否仍能看到反光板。&lt;br /&gt;
&lt;br /&gt;
# Wheel-track + diameter cal: drive 5 m fwd/back; if Detour pose differs &amp;gt; 2%, fix the radius.&lt;br /&gt;
# Rotation error: spin 360° ×10 in place; cumulative drift &amp;lt; 5° is acceptable.&lt;br /&gt;
# Leg priors: write real measured leg span into the `width` field.&lt;br /&gt;
# Jack timing: `inshelfMillis` should be 1 s longer than the jack-up time.&lt;br /&gt;
# SLAM vs reflector: confirm the top-mounted lidar still sees reflectors after the rack is on top.&lt;br /&gt;
&lt;br /&gt;
== 5. 常见问题 / Common pitfalls ==&lt;br /&gt;
* '''钻入时撞腿''' / Hits leg on entry: 检测器 `blobDist` 太大或 anchor 站点离料架太近。&lt;br /&gt;
* '''顶升后偏摆''' / Sway after jack: 料架重心偏，或 jack 行程未完全到位；查 `jackAtTarget` 信号。&lt;br /&gt;
* '''带架转弯失败''' / Rotation fails with rack: 带载包络未启用，调度允许了无法通过的窄通道。&lt;br /&gt;
* '''返回原料架放回失败''' / Re-dock to same rack fails: anchor 与 rack 站点坐标在 SLAM 漂移后不一致；定期 [[Special:MyLanguage/标定与校准|重标定]]。&lt;br /&gt;
&lt;br /&gt;
== 6. 通讯与节拍 / Comm &amp;amp; throughput ==&lt;br /&gt;
* 控制周期：50 ms（Medulla LadderLogic）/ Control cycle: 50 ms.&lt;br /&gt;
* 心跳：100 ms（向 SimpleComposer 报位置 + jackHeight）/ Heartbeat: 100 ms.&lt;br /&gt;
* 取放节拍参考：单次（取-搬-放）约 35–50 s（含 anchor 站到 anchor 站直线 8 m）。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/MDCS引擎适配机器人入门教学|MDCS引擎适配机器人入门教学]]&lt;br /&gt;
* [[Special:MyLanguage/叉车适配案例|叉车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/全向车适配案例|全向车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/牵引车适配案例|牵引车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/使用自动对准料架功能|使用自动对准料架功能]]&lt;br /&gt;
* [[Special:MyLanguage/自动识别工位并取放货|自动识别工位并取放货]]&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%B7%A1%E7%BA%BF%E8%A1%8C%E8%B5%B0&amp;diff=1008</id>
		<title>巡线行走</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%B7%A1%E7%BA%BF%E8%A1%8C%E8%B5%B0&amp;diff=1008"/>
		<updated>2026-05-16T11:26:50Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
&amp;quot;巡线行走&amp;quot;是 MDCS Clumsy 最基础的运动原语：给一个起点 (srcX, srcY) 和终点 (dstX, dstY)，AGV 沿直线段从起点驶到终点。它由 `SteeringLineFollowing`（正向）和 `SteeringLineFollowingReverse`（反向）两个 `MovementDefinition` 子类实现，所有其它复杂动作（取放货、绕障、双车联动）都是在它之上组合而成。&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Line following&amp;quot; is the most basic Clumsy motion primitive: given (srcX, srcY) and (dstX, dstY), the AGV runs a straight segment between them. It's implemented by `SteeringLineFollowing` (forward) and `SteeringLineFollowingReverse` (reverse), both `MovementDefinition` subclasses. Every higher action (pick/place, obstacle avoidance, twin-car coordination) composes on top.&lt;br /&gt;
&lt;br /&gt;
文件 / Files: `D:\src\Clumsy\ClumsyCore\Legacy\SteeringLineFollowing.cs`、`SteeringLineFollowingReverse.cs`。&lt;br /&gt;
&lt;br /&gt;
== 使用场景 / When to use ==&lt;br /&gt;
* 站点对点的直线行驶（无需绕障 / 无需精对位）。&lt;br /&gt;
* 取放货的 ''粗对位''阶段（开 [[Special:MyLanguage/自动识别工位并取放货|工位识别]] 之前的逼近）。&lt;br /&gt;
* 反向出库（用 `Reverse` 变体）。&lt;br /&gt;
* 简单调度的最小可用配置。&lt;br /&gt;
&lt;br /&gt;
* Point-to-point straight driving on uncongested aisles.&lt;br /&gt;
* Coarse-approach stage before [[Special:MyLanguage/自动识别工位并取放货|station-aware pickup]].&lt;br /&gt;
* Reverse withdraw (the `Reverse` variant).&lt;br /&gt;
* Minimum config for a quick-running scheduler.&lt;br /&gt;
&lt;br /&gt;
== 关键字段 / Key fields ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 字段 / Field !! 类型 !! 含义 / Meaning&lt;br /&gt;
|-&lt;br /&gt;
| `srcX`, `srcY` || `double` || 起点 / segment start (mm)&lt;br /&gt;
|-&lt;br /&gt;
| `dstX`, `dstY` || `double` || 终点 / segment end (mm)&lt;br /&gt;
|-&lt;br /&gt;
| `basespeed` || `int` || 巡航速度 / cruise speed (mm/s)&lt;br /&gt;
|-&lt;br /&gt;
| `lookAhead` || `float` || 纯跟踪前瞻距离 / pure-pursuit look-ahead (mm)&lt;br /&gt;
|-&lt;br /&gt;
| `dFactor` || `float` || 横向偏差转化为转向的增益 / lateral-error → steer gain&lt;br /&gt;
|-&lt;br /&gt;
| `slowdown` || `bool` || 临近终点时降速 / decelerate near goal (true by default)&lt;br /&gt;
|-&lt;br /&gt;
| `finSpeed` || `int` || 终点最低速度（slowdown 触发后）/ final speed when slowdown active&lt;br /&gt;
|-&lt;br /&gt;
| `slowdownDist` || `float` || 进入减速段的距离阈值 / decel-zone size (mm)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
`SteeringLineFollowingReverse` 字段完全一致；语义为车头朝向 ''不变''、车体后退到目标。&lt;br /&gt;
`SteeringLineFollowingReverse` has the same fields; the car drives in reverse while keeping its heading unchanged.&lt;br /&gt;
&lt;br /&gt;
== 工作机制 / How it works ==&lt;br /&gt;
1. `MovementDefinition.Get()` 是一个 ''可枚举的 bool 序列''，每个 tick 返回一次 `true`（表示继续），直到完成时返回 `false` / 退出枚举。&lt;br /&gt;
2. `DriveTask.WaitDriveTask(IEnumerable&amp;lt;bool&amp;gt;)` 把这个序列每隔 `Configuration.conf.DriveTaskInterval` ms 调一次，并阻塞调用方直到序列结束。&lt;br /&gt;
3. 每个 tick 内部：&lt;br /&gt;
   - 读当前位姿 `(x, y, th)`（来自 Detour，已订阅）。&lt;br /&gt;
   - 计算前瞻点（沿目标线段往前 `lookAhead`）。&lt;br /&gt;
   - 用纯跟踪法求转向角；`vCmd` 取 `basespeed`（若进入减速段则衰减到 `finSpeed`）。&lt;br /&gt;
   - 写到 Medulla 上位 IO。&lt;br /&gt;
4. 当车体距 `(dstX, dstY)` 小于阈值（默认 50 mm），返回 `false` 结束。&lt;br /&gt;
&lt;br /&gt;
1. `MovementDefinition.Get()` returns an `IEnumerable&amp;lt;bool&amp;gt;` ticked once per control cycle; `true` = keep going, end-of-iteration = done.&lt;br /&gt;
2. `DriveTask.WaitDriveTask(...)` ticks at `Configuration.conf.DriveTaskInterval` ms and blocks the caller until the iteration ends.&lt;br /&gt;
3. Each tick: read pose → compute look-ahead point → pure-pursuit steer → write `vCmd` / `steerCmd`.&lt;br /&gt;
4. Exits when within ~50 mm of `(dstX, dstY)`.&lt;br /&gt;
&lt;br /&gt;
== 调用样例 / Example ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public void DriveStraight(double sx, double sy, double dx, double dy)&lt;br /&gt;
{&lt;br /&gt;
    Queue(async () =&amp;gt;&lt;br /&gt;
    {&lt;br /&gt;
        DriveTask.WaitDriveTask(new SteeringLineFollowing&lt;br /&gt;
        {&lt;br /&gt;
            srcX = sx, srcY = sy,&lt;br /&gt;
            dstX = dx, dstY = dy,&lt;br /&gt;
            basespeed = 600,&lt;br /&gt;
            lookAhead = 500,&lt;br /&gt;
            dFactor   = 1.2f,&lt;br /&gt;
            slowdown  = true,&lt;br /&gt;
            finSpeed  = 100&lt;br /&gt;
        }.Follow());&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 调试要点 / Tuning ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 现象 / Symptom !! 调整 / Adjustment&lt;br /&gt;
|-&lt;br /&gt;
| 路径偏移 / Drifts off the line || 增大 `dFactor`（默认 1.2），但太大会震荡&lt;br /&gt;
|-&lt;br /&gt;
| 在终点震荡 / Oscillates at goal || 启用 `slowdown` 并增大 `slowdownDist`&lt;br /&gt;
|-&lt;br /&gt;
| 终点冲过 / Overshoots || 降低 `basespeed` 或减小 `finSpeed`&lt;br /&gt;
|-&lt;br /&gt;
| 弯道内切 / Cuts inside corner (短线段段拼接时) || 减小 `lookAhead`&lt;br /&gt;
|-&lt;br /&gt;
| 慢速时跟不准 / Poor tracking at low speed || `dFactor` 与速度成反比；低速时增大&lt;br /&gt;
|-&lt;br /&gt;
| 反向跑歪 / Reverse drifts || 检查后向激光雷达视野；可能未启用，导致姿态误差累积&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 与其它 Movement 的衔接 / Composing with other Movements ==&lt;br /&gt;
* 取货 / 放货前的接近 — 后接 `AutoFetchGood` / `AutoShelfFetching_ManyLegs`。&lt;br /&gt;
* 绕障 — 见 [[Special:MyLanguage/绕障行走|绕障行走]]。&lt;br /&gt;
* 反向出库 — 用 `SteeringLineFollowingReverse`，搭配同样的字段。&lt;br /&gt;
* 长路径 — SimpleComposer 调度会把长路径切成若干段 LineFollowing 拼接，每段一个 `srcX/Y → dstX/Y`。&lt;br /&gt;
&lt;br /&gt;
== 注意 / Caveat ==&lt;br /&gt;
LineFollowing 是 ''几何意义''上的直线段跟踪，''与车型无关''。但具体 Movement 内部隐含假设：阿克曼 / 差速车型可以 ''转向即改向''。对全向车应改用 `OmniLineFollowing`（见 [[Special:MyLanguage/全向车适配案例|全向车适配案例]]），它允许车身朝向独立于运动方向。&lt;br /&gt;
&lt;br /&gt;
`SteeringLineFollowing` is a geometric line-segment tracker but implicitly assumes Ackermann / differential kinematics. For omni vehicles use `OmniLineFollowing` (heading independent of motion direction) — see the omni case study.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/绕障行走|绕障行走]]&lt;br /&gt;
* [[Special:MyLanguage/车体抽象原理|车体抽象原理]]&lt;br /&gt;
* [[Special:MyLanguage/Clumsy-API|Clumsy-API]]&lt;br /&gt;
&lt;br /&gt;
[[Category:运动控制使用手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%A6%82%E4%BD%95%E5%9F%BA%E4%BA%8ESimpleCore%E6%A0%B8%E5%BF%83%E5%BA%93%E8%BF%9B%E8%A1%8C%E8%B0%83%E5%BA%A6%E7%B3%BB%E7%BB%9F%E5%BC%80%E5%8F%91&amp;diff=1007</id>
		<title>如何基于SimpleCore核心库进行调度系统开发</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%A6%82%E4%BD%95%E5%9F%BA%E4%BA%8ESimpleCore%E6%A0%B8%E5%BF%83%E5%BA%93%E8%BF%9B%E8%A1%8C%E8%B0%83%E5%BA%A6%E7%B3%BB%E7%BB%9F%E5%BC%80%E5%8F%91&amp;diff=1007"/>
		<updated>2026-05-16T11:26:47Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
MDCS 的车队调度被分为两层：'''SimpleCore'''（核心算法库，包含交管、寻路、包络、可达性、DPS 等内核算法）与 '''SimpleComposer'''（基于 SimpleCore 的 UI 壳层，包含 CAD 工具、可视化、插件宿主）。本页面对开发者：你想自己写一个调度应用（或在 SimpleComposer 之外用 SimpleCore 内核）该如何开始。&lt;br /&gt;
&lt;br /&gt;
MDCS fleet management is split into two layers: '''SimpleCore''' (algorithm library — traffic control, pathfinding, envelopes, reachability, DPS) and '''SimpleComposer''' (a UI shell built on SimpleCore — CAD tool, visualisation, plugin host). This page targets developers writing their own scheduling app on top of SimpleCore, or extending SimpleComposer with a custom car / mission type.&lt;br /&gt;
&lt;br /&gt;
== 何时基于 SimpleCore 开发 / When to use SimpleCore directly ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 场景 / Scenario !! 建议 / Recommendation&lt;br /&gt;
|-&lt;br /&gt;
| 集成进非 SimpleComposer 的现成系统 || 直接用 SimpleCore，把 UI 留给原系统&lt;br /&gt;
|-&lt;br /&gt;
| 给 SimpleComposer 加车型 / 业务 || 写插件在 SimpleComposer 上挂载，不必自己写调度&lt;br /&gt;
|-&lt;br /&gt;
| 算法研究（新交管 / 新寻路） || SimpleCore，作为算法宿主&lt;br /&gt;
|-&lt;br /&gt;
| 单工位单车 || 都不需要，写一个小循环够了&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 核心抽象 / Core abstractions ==&lt;br /&gt;
=== Car 体系 / Car hierarchy ===&lt;br /&gt;
SimpleCore 把&amp;quot;车&amp;quot;按照 ''是否使用 MDCS 车载系统'' 分为两支：&lt;br /&gt;
SimpleCore splits &amp;quot;cars&amp;quot; into two branches by whether they use the MDCS on-vehicle stack:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 类 / Class !! 文件 / File !! 用途 / Use&lt;br /&gt;
|-&lt;br /&gt;
| `AbstractCar` || `D:\src\Simple\SimpleCore\PropType\AbstractCar.cs:346` || 调度可见的车的最小契约 / minimal scheduling contract&lt;br /&gt;
|-&lt;br /&gt;
| `Car` || `D:\src\Simple\SimpleComposer\RCS\Car.cs:33` || SimpleComposer 中的&amp;quot;车&amp;quot;基类 / Composer-side base&lt;br /&gt;
|-&lt;br /&gt;
| `ClumsyCar : Car` || `D:\src\Simple\SimpleComposer\RCS\ClumsyCar.cs:34` || 跑 MDCS 车载（Medulla + Clumsy）的车 / runs full MDCS on-vehicle stack&lt;br /&gt;
|-&lt;br /&gt;
| 自评估 `Car`（自定义子类）/ Self-eval `Car` (custom subclass) || 各 vendor 适配代码 / vendor adapters || PLC / 磁导航 / 二维码 类无车载 PC 的车 / cars without on-board MDCS (PLC, magstrip, QR)&lt;br /&gt;
|-&lt;br /&gt;
| `AGVInterface` || `D:\src\Simple\SimpleCore\BasicProps\AGVInterface.cs` || 任务脚本调用的 API / API surface called by mission scripts&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== AGVInterface 与 Queue 机制 ===&lt;br /&gt;
`AGVInterface` 暴露给任务脚本（TopazScript）的核心方法是 `Queue(params Func&amp;lt;Task&amp;gt;[] actions)` —— ''超标量'' 异步流水线：&lt;br /&gt;
&lt;br /&gt;
The core method on `AGVInterface` exposed to mission scripts is `Queue(params Func&amp;lt;Task&amp;gt;[] actions)` — a &amp;quot;super-scalar&amp;quot; async pipeline:&lt;br /&gt;
&lt;br /&gt;
* 每个 Func 是一个 ''行 / row''，按提交顺序进入流水线。&lt;br /&gt;
* 同一行内的代码顺序执行。&lt;br /&gt;
* 上一行未完成前，下一行不会开始 —— 但下一行可在上一行最后一个 await 后立即 schedule，达成宏观流水。&lt;br /&gt;
* `WaitAsync(int pipe)` 让调用方等到指定行完成再继续。&lt;br /&gt;
&lt;br /&gt;
* Each Func is a '''row''' in the pipeline.&lt;br /&gt;
* Code within a row runs sequentially.&lt;br /&gt;
* The next row only starts after the previous row's last await, but its prelude can overlap, giving macro-level pipelining.&lt;br /&gt;
* `WaitAsync(int pipe)` lets the caller wait for a specific row.&lt;br /&gt;
&lt;br /&gt;
=== TryLock / Leave — 交管 / Traffic control ===&lt;br /&gt;
`TryLock(int siteID)` / `Leave(int siteID)` 是 SimpleCore 交管在 ''每个 mission 脚本里''的钩子。任务脚本在进入一个交管区前必须 `TryLock`；离开时 `Leave`。SimpleCore 的内核保证：&lt;br /&gt;
&lt;br /&gt;
`TryLock(siteID)` / `Leave(siteID)` are SimpleCore's traffic-control hooks ''per mission script''. Scripts must `TryLock` before entering a controlled segment and `Leave` on exit. SimpleCore's kernel guarantees:&lt;br /&gt;
&lt;br /&gt;
* 无死锁（DPS 算法，见 [[Special:MyLanguage/DPS调度算法详解|DPS调度算法详解]]）&amp;lt;br /&amp;gt;Deadlock freedom (via DPS — see the DPS page).&lt;br /&gt;
* 公平（按到达顺序授予锁）&amp;lt;br /&amp;gt;Fairness — locks granted in arrival order.&lt;br /&gt;
* 兼容性（同一锁可被声明 ''可兼容车型'' 一起持有）&amp;lt;br /&amp;gt;Compatibility — a lock can be held simultaneously by compatible car types.&lt;br /&gt;
&lt;br /&gt;
== 写一个 ClumsyCar 子类 / Subclassing ClumsyCar ==&lt;br /&gt;
ClumsyCar 用于车上跑完整 MDCS（Clumsy 自动驾驶 + Medulla 硬件抽象）的情况。最小子类骨架：&lt;br /&gt;
&lt;br /&gt;
ClumsyCar is the right base when the vehicle runs the full MDCS stack on-board. Minimal subclass:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using SimpleComposer.RCS;&lt;br /&gt;
&lt;br /&gt;
namespace Vendor.Carts&lt;br /&gt;
{&lt;br /&gt;
    [CarType(Name = &amp;quot;MyCart&amp;quot;, Title = &amp;quot;我的车型 / MyCart&amp;quot;)]&lt;br /&gt;
    public class MyCart : ClumsyCar&lt;br /&gt;
    {&lt;br /&gt;
        public MyCart() : base()&lt;br /&gt;
        {&lt;br /&gt;
            conf = &amp;quot;configs/MyCart.config&amp;quot;;&lt;br /&gt;
            api_retries = 3;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        public override async Task actualSendScript(string script)&lt;br /&gt;
        {&lt;br /&gt;
            // 把 TopazScript 通过 HTTP 发到车载 Clumsy 适配器 / POST to on-board Clumsy adapter&lt;br /&gt;
            var url = $&amp;quot;http://{address}:30080/run&amp;quot;;&lt;br /&gt;
            var resp = await HttpClient.PostAsync(url, new StringContent(script));&lt;br /&gt;
            resp.EnsureSuccessStatusCode();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        public override async Task keepAlive()&lt;br /&gt;
        {&lt;br /&gt;
            // 心跳 + 状态轮询 / heartbeat + status poll&lt;br /&gt;
            var status = await HttpClient.GetStringAsync($&amp;quot;http://{address}:30080/status&amp;quot;);&lt;br /&gt;
            UpdateStatusFromJson(status);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
参考样例（80 行可读完）：`D:\src\cookbook\adaption-reference\法睿兰达\CartAdapters\FY2208008\Scenes\AMRScene\ProjNameCar.cs`。&lt;br /&gt;
Reference (80 lines, easy to read end-to-end): `D:\src\cookbook\adaption-reference\法睿兰达\CartAdapters\FY2208008\Scenes\AMRScene\ProjNameCar.cs`.&lt;br /&gt;
&lt;br /&gt;
== 写一个自评估 Car 子类 / Subclassing Car for self-eval vehicles ==&lt;br /&gt;
自评估车（磁导航 / 色带 / PLC 控制 / 二维码 等）的车载没有 MDCS，调度只能发&amp;quot;高层意图&amp;quot;，由 ''SimpleCore 侧'' 的 AGV 接口翻译成厂商命令，再通过 RFID / Modbus / HTTP 下发。&lt;br /&gt;
&lt;br /&gt;
Self-evaluating vehicles (magnetic-strip, tape, PLC-controlled, QR-only) don't run MDCS on-board. The scheduler dispatches &amp;quot;high-level intents&amp;quot;, and a ''fleet-side'' `AGV : AGVInterface` translates them into vendor commands (RFID frames, Modbus, HTTP) and reads back `(x, y, th, siteID)` via `keepAlive()`.&lt;br /&gt;
&lt;br /&gt;
经典样例：`D:\src\cookbook\adaption-reference\苏州凌鸟\Scenes\AMRScene1\Cars\MNavCar.cs`（293 行，含双向导航 + RFID 命令队列 + 启发式约束容器 `MNavCarHeuristics`）。&lt;br /&gt;
&lt;br /&gt;
Canonical example: `D:\src\cookbook\adaption-reference\苏州凌鸟\Scenes\AMRScene1\Cars\MNavCar.cs` (293 lines — two-way navigation, RFID command queue, `MNavCarHeuristics` for no-go turns / forced reversals).&lt;br /&gt;
&lt;br /&gt;
骨架 / Skeleton:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[CarType(Name = &amp;quot;MyMagCart&amp;quot;, Title = &amp;quot;磁导航车 / Magnetic-strip cart&amp;quot;)]&lt;br /&gt;
public class MyMagCart : Car&lt;br /&gt;
{&lt;br /&gt;
    public string address;&lt;br /&gt;
    public int rfidPort = 502;&lt;br /&gt;
&lt;br /&gt;
    public class AGV : AGVInterface&lt;br /&gt;
    {&lt;br /&gt;
        private readonly MyMagCart car;&lt;br /&gt;
        public AGV(MyMagCart c) { car = c; }&lt;br /&gt;
&lt;br /&gt;
        public override bool TryLock(int siteID, int route_id) { /* SimpleCore 内置 / inherit */ return base.TryLock(siteID, route_id); }&lt;br /&gt;
        public override bool Leave(int siteID) { return base.Leave(siteID); }&lt;br /&gt;
&lt;br /&gt;
        public void Go(double sx, double sy, int sid, double dx, double dy, int did, int routeId)&lt;br /&gt;
        {&lt;br /&gt;
            Queue(&lt;br /&gt;
              async () =&amp;gt;&lt;br /&gt;
              {&lt;br /&gt;
                  // 在锁拿到后才发指令 / send intent only after lock granted&lt;br /&gt;
                  if (!TryLock(did, routeId)) return;&lt;br /&gt;
                  car.SendRFIDGoCommand(did);&lt;br /&gt;
              },&lt;br /&gt;
              async () =&amp;gt;&lt;br /&gt;
              {&lt;br /&gt;
                  // 等到车真的到了 / wait until reported arrival&lt;br /&gt;
                  while (car.lastReportedSiteId != did) await Task.Delay(100);&lt;br /&gt;
                  Leave(sid);&lt;br /&gt;
              });&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    public override async Task actualSendScript(string script) { /* 自评估车通常不发 TopazScript */ }&lt;br /&gt;
    public override async Task keepAlive()&lt;br /&gt;
    {&lt;br /&gt;
        // Modbus / TCP 读位置并写入 (x, y, th, siteId) / poll position&lt;br /&gt;
        var (x, y, th, siteId) = await PollPLCAsync(address);&lt;br /&gt;
        UpdatePose(x, y, th); lastReportedSiteId = siteId;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 任务下发 / Mission dispatch flow ==&lt;br /&gt;
&lt;br /&gt;
  调度内核 → SegmentPlan.Compile() → TopazScript（一段脚本）&lt;br /&gt;
                ↓&lt;br /&gt;
       CarProgram.script  (放入 Car.status.programs.forecasted)&lt;br /&gt;
                ↓&lt;br /&gt;
   Car.actualSendScript(script)&lt;br /&gt;
                ↓ (ClumsyCar)               ↓ (self-eval)&lt;br /&gt;
      HTTP POST 到车载                直接在调度侧本地跑 SelfEvaluating(agv, script)&lt;br /&gt;
                ↓                             ↓&lt;br /&gt;
   车载 SelfEvaluating(agv, script)    AGV.Go / AGV.Queue 等映射到 RFID/Modbus 命令&lt;br /&gt;
                ↓&lt;br /&gt;
      Clumsy Movement 执行 → 物理运动&lt;br /&gt;
&lt;br /&gt;
== Reachability 与包络 / Reachability &amp;amp; envelope ==&lt;br /&gt;
SimpleCore 提供 ''可达性状态机''（见 [[Special:MyLanguage/可达性状态编程|可达性状态编程]]）与 ''包络声明''（见 [[Special:MyLanguage/使用手册 - Simple:包络如何定义|包络如何定义]]）。你的 Car 子类一般只需要：&lt;br /&gt;
&lt;br /&gt;
* 声明默认包络（无载货状态的车体包络）。&lt;br /&gt;
* 在带载 / 升叉 / 顶升等状态时声明 ''备用包络''，由 `HaveExtraEnvelope` 状态触发。&lt;br /&gt;
* 在不可达的路径段上声明 ''可达性规则''（例如：单向通道、最低剩余电量、强制掉头）。&lt;br /&gt;
&lt;br /&gt;
A Car subclass typically: (1) declares the empty envelope, (2) declares alternate envelopes triggered by carry / fork-up / jack-up states, (3) declares reachability rules on path segments (one-way, min battery, mandatory reverse-out).&lt;br /&gt;
&lt;br /&gt;
== 用作算法宿主：在 SimpleComposer 之外用 SimpleCore ==&lt;br /&gt;
SimpleCore 是普通 .NET 库（`D:\src\Simple\SimpleCore\`），可以独立引用。最小集成：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using SimpleCore;&lt;br /&gt;
&lt;br /&gt;
var world = new World(); // 加载站点 + 路径图 / load sites + path graph&lt;br /&gt;
world.LoadFromJson(File.ReadAllText(&amp;quot;map.json&amp;quot;));&lt;br /&gt;
&lt;br /&gt;
var car1 = new MyAbstractCarImpl();   // 实现 AbstractCar 接口&lt;br /&gt;
var car2 = new MyAbstractCarImpl();&lt;br /&gt;
world.AddCar(car1); world.AddCar(car2);&lt;br /&gt;
&lt;br /&gt;
var planner = new DPSPlanner(world);&lt;br /&gt;
var mission = planner.Plan(&lt;br /&gt;
    from: world.GetSite(&amp;quot;A&amp;quot;),&lt;br /&gt;
    to:   world.GetSite(&amp;quot;Z&amp;quot;));&lt;br /&gt;
&lt;br /&gt;
foreach (var step in mission.Steps)&lt;br /&gt;
{&lt;br /&gt;
    Console.WriteLine($&amp;quot;car={step.Car} site={step.Site} action={step.Action}&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= 算法 / Algorithms =&lt;br /&gt;
SimpleCore 内置：&lt;br /&gt;
SimpleCore ships with:&lt;br /&gt;
&lt;br /&gt;
* DPS 调度（动态规划交管）—— `[[Special:MyLanguage/DPS调度算法详解|DPS调度算法详解]]`&lt;br /&gt;
* 寻路启发器 —— `[[Special:MyLanguage/使用手册 - 寻路启发器功能|寻路启发器功能]]`&lt;br /&gt;
* 流场规划 —— `[[Special:MyLanguage/流场规划|流场规划]]`&lt;br /&gt;
* 网络流业务规划 —— `[[Special:MyLanguage/基于网络流的业务规划器|基于网络流的业务规划器]]`&lt;br /&gt;
&lt;br /&gt;
== 调试 / Debugging ==&lt;br /&gt;
* 用 [[Special:MyLanguage/CycleGUI|CycleGUI]] 把交管锁、车辆状态、路径片段实时画出来。&lt;br /&gt;
* SimpleComposer 的 ''Trace 面板'' 显示每条任务的 Queue 行执行轨迹。&lt;br /&gt;
* 死锁怀疑时打开 ''DPS 决策可视化''（DPS 算法会输出每次决策的剪枝树）。&lt;br /&gt;
&lt;br /&gt;
* Use [[Special:MyLanguage/CycleGUI|CycleGUI]] to render locks, car state, path segments live.&lt;br /&gt;
* SimpleComposer's ''Trace panel'' shows the Queue-row execution timeline per mission.&lt;br /&gt;
* When deadlock is suspected, enable ''DPS decision visualisation''.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/AGV任务运行逻辑|AGV任务运行逻辑]]&lt;br /&gt;
* [[Special:MyLanguage/调度内核运行原理|调度内核运行原理]]&lt;br /&gt;
* [[Special:MyLanguage/DPS调度算法详解|DPS调度算法详解]]&lt;br /&gt;
* [[Special:MyLanguage/Simple-API|Simple-API]]&lt;br /&gt;
* [[Special:MyLanguage/开发手册 - SimpleComposer界面开发 - CAD工具|SimpleComposer界面开发]]&lt;br /&gt;
* [[Special:MyLanguage/使用手册 - Simple:从使用到开发|Simple:从使用到开发]]&lt;br /&gt;
&lt;br /&gt;
[[Category:二次开发相关说明]]&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%8F%89%E8%BD%A6%E9%80%82%E9%85%8D%E6%A1%88%E4%BE%8B&amp;diff=1006</id>
		<title>叉车适配案例</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%8F%89%E8%BD%A6%E9%80%82%E9%85%8D%E6%A1%88%E4%BE%8B&amp;diff=1006"/>
		<updated>2026-05-16T11:26:45Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
叉车（含牵引车、堆高车）是仓储 AMR 中最复杂的车型 —— 既要做 [[Special:MyLanguage/巡线行走|巡线行走]]、又要做 [[Special:MyLanguage/自动识别托盘取放货|托盘取放]]、再加上叉齿升降与平衡。本页用一台真实在产叉车（凌鸟 LB14）的源码 (`D:\src\cookbook\adaption-reference\苏州凌鸟\CartAdapters\LB14\`，~330 行可读) 串起完整适配流程。&lt;br /&gt;
&lt;br /&gt;
Forklifts (incl. tractors, stackers) are the most complex class of warehouse AMR — they do [[Special:MyLanguage/巡线行走|line-following]], [[Special:MyLanguage/自动识别托盘取放货|pallet pick-and-place]], plus fork lift / tilt control and balance. This page walks through a real adaptation using Linbir LB14 source (`D:\src\cookbook\adaption-reference\苏州凌鸟\CartAdapters\LB14\`, ~330 LOC) as the running example.&lt;br /&gt;
&lt;br /&gt;
== 硬件清单 / Hardware ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 部件 / Part !! 典型规格 / Typical spec&lt;br /&gt;
|-&lt;br /&gt;
| 底盘 / Chassis || 三轮（前驱单舵 + 后双轮被动）/ Three-wheel (front steer + 2 rear passive)&lt;br /&gt;
|-&lt;br /&gt;
| 驱动电机 / Drive motor || 永磁同步 1.5–4 kW&lt;br /&gt;
|-&lt;br /&gt;
| 转向编码器 / Steer encoder || 绝对值，分辨率 ≥ 14 bit&lt;br /&gt;
|-&lt;br /&gt;
| 叉齿马达 / Fork motor || 液压泵 + 阀，行程 100–3000 mm&lt;br /&gt;
|-&lt;br /&gt;
| 倾角传感 / Tilt sensor || IMU + 阀块行程编码&lt;br /&gt;
|-&lt;br /&gt;
| 前向激光 / Front lidar || 2D，高度对齐叉孔 / 2D at fork height&lt;br /&gt;
|-&lt;br /&gt;
| 后向激光（含侧扫）/ Rear lidar || 用于反向行驶 + 双车联动 V 槽观测 / reverse + V-groove for [[Special:MyLanguage/双车/多车联动|twin-car coordination]]&lt;br /&gt;
|-&lt;br /&gt;
| 负载传感 / Load sensor || 叉齿底油压或称重模块 / hydraulic pressure or load-cell&lt;br /&gt;
|-&lt;br /&gt;
| 通讯 / Comm || PLC 经 RS485 / Modbus 或专用 CAN / PLC via RS485 / Modbus, or proprietary CAN&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 1. Medulla 适配 / Medulla side ==&lt;br /&gt;
关键文件：`D:\src\cookbook\adaption-reference\苏州凌鸟\CartAdapters\LB14\MedullaAdapter\`&lt;br /&gt;
- `LB14Cart.cs` — `CartDefinition` 子类，声明 IO 与 LadderLogic&lt;br /&gt;
- `Omron485.cs` / `OmronRoutine.cs` — RS485 报文层（与欧姆龙 PLC 通讯）&lt;br /&gt;
- `LB14Remote.cs` — 与厂商遥控手柄的协议&lt;br /&gt;
&lt;br /&gt;
最少需要的 IO 声明 / Minimum IO surface:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class LB14Cart : CartDefinition&lt;br /&gt;
{&lt;br /&gt;
    [AsInitParam] public string plcIp = &amp;quot;192.168.0.100&amp;quot;;&lt;br /&gt;
    [AsInitParam] public int    plcPort = 502;&lt;br /&gt;
    [AsInitParam] public float  wheelBase = 1320; // mm&lt;br /&gt;
    [AsInitParam] public float  forkOffset = 760; // tine root to drive wheel&lt;br /&gt;
&lt;br /&gt;
    // 上位 IO（调度 / Clumsy 下发的意图）/ Upper IO (intent flowing down)&lt;br /&gt;
    [AsUpperIO] public float vCmd;            // 前进速度 / forward velocity (mm/s)&lt;br /&gt;
    [AsUpperIO] public float steerCmd;        // 转向角 / steer angle (rad)&lt;br /&gt;
    [AsUpperIO] public float forkHeightTgt;   // 叉齿目标高度 / fork target height&lt;br /&gt;
    [AsUpperIO] public float forkTiltTgt;     // 叉门架倾角 / fork tilt&lt;br /&gt;
    [AsUpperIO] public bool  brakeOn;&lt;br /&gt;
    [AsUpperIO] public bool  hornOn;&lt;br /&gt;
&lt;br /&gt;
    // 下位 IO（硬件回报的状态）/ Lower IO (status rising up)&lt;br /&gt;
    [AsLowerIO] public float vEst;&lt;br /&gt;
    [AsLowerIO] public float steerEst;&lt;br /&gt;
    [AsLowerIO] public float forkHeight;&lt;br /&gt;
    [AsLowerIO] public bool  forkAtTarget;&lt;br /&gt;
    [AsLowerIO] public float loadKg;&lt;br /&gt;
    [AsLowerIO] public bool  eStop;&lt;br /&gt;
    [AsLowerIO] public int   batteryPercent;&lt;br /&gt;
&lt;br /&gt;
    public override void Init()&lt;br /&gt;
    {&lt;br /&gt;
        // 1) 连接 PLC，握手 / connect &amp;amp; handshake&lt;br /&gt;
        plcConn = OmronRoutine.Connect(plcIp, plcPort);&lt;br /&gt;
&lt;br /&gt;
        // 2) 注册看门狗 / register watchdog (LadderLogic ticks every ~50 ms)&lt;br /&gt;
        plcConn.AttachStateCallback(OnPLCStatus);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    [UseLadderLogic(IntervalMs = 50)]&lt;br /&gt;
    public class ControlLoop : LadderLogic&amp;lt;LB14Cart&amp;gt;&lt;br /&gt;
    {&lt;br /&gt;
        public override void Run(LB14Cart self)&lt;br /&gt;
        {&lt;br /&gt;
            // 把 Upper IO 写到 PLC&lt;br /&gt;
            self.plcConn.WriteRegisters(&lt;br /&gt;
                vCmd:     (int)(self.vCmd * 10),&lt;br /&gt;
                steerCmd: (int)(self.steerCmd * 10000),&lt;br /&gt;
                forkTgt:  (int)self.forkHeightTgt&lt;br /&gt;
            );&lt;br /&gt;
            // 读 Lower IO&lt;br /&gt;
            var s = self.plcConn.ReadStatus();&lt;br /&gt;
            self.vEst         = s.vEst / 10f;&lt;br /&gt;
            self.steerEst     = s.steerEst / 10000f;&lt;br /&gt;
            self.forkHeight   = s.forkHeight;&lt;br /&gt;
            self.forkAtTarget = s.forkAtTarget;&lt;br /&gt;
            self.loadKg       = s.loadKg;&lt;br /&gt;
            self.eStop        = s.eStop;&lt;br /&gt;
            self.batteryPercent = s.battery;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 2. Clumsy 自动驾驶适配 / Clumsy autopilot side ==&lt;br /&gt;
关键文件：`D:\src\cookbook\adaption-reference\苏州凌鸟\CartAdapters\LB14\ClumsyPilot\`&lt;br /&gt;
- `AGV.cs` (255 行) — `SimpleAgvInterface` 子类，业务级动作 `Fetch / Put / ExitSite / ReverseGo1`&lt;br /&gt;
- `Movements.cs` — 叉车特有的 `MovementDefinition` 子类&lt;br /&gt;
- `Configs.cs` / `CSGTypeF3CDef.cs` — 参数配置&lt;br /&gt;
&lt;br /&gt;
叉车特有的 Movement / Movements specific to forklifts:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class AGV : SimpleAgvInterface&lt;br /&gt;
{&lt;br /&gt;
    public int baseSpeed = (int) Configuration.conf.basicSpeed;&lt;br /&gt;
    private SteeringLineFollowing       lastDriver;&lt;br /&gt;
    private SteeringLineFollowingReverse lastDriverR;&lt;br /&gt;
&lt;br /&gt;
    // 取货 / Fetch&lt;br /&gt;
    public void Fetch(double srcX, double srcY, int srcid,&lt;br /&gt;
                     double dstX, double dstY, int dstid,&lt;br /&gt;
                     double pivotX, double pivotY)&lt;br /&gt;
    {&lt;br /&gt;
        Queue(&lt;br /&gt;
          async () =&amp;gt;&lt;br /&gt;
          {&lt;br /&gt;
              // 1) 巡线到 srcX/Y（叉孔正前方）&lt;br /&gt;
              DriveTask.WaitDriveTask(new SteeringLineFollowing&lt;br /&gt;
              {&lt;br /&gt;
                  srcX = currentX, srcY = currentY,&lt;br /&gt;
                  dstX = srcX,     dstY = srcY,&lt;br /&gt;
                  basespeed = baseSpeed&lt;br /&gt;
              }.Follow());&lt;br /&gt;
          },&lt;br /&gt;
          async () =&amp;gt;&lt;br /&gt;
          {&lt;br /&gt;
              // 2) 升叉到取货高度&lt;br /&gt;
              SetUpperIO(&amp;quot;forkHeightTgt&amp;quot;, Configuration.conf.fetchHeight);&lt;br /&gt;
              await WaitForLowerIO(&amp;quot;forkAtTarget&amp;quot;, timeout: 5000);&lt;br /&gt;
          },&lt;br /&gt;
          async () =&amp;gt;&lt;br /&gt;
          {&lt;br /&gt;
              // 3) 自动识别托盘并对位&lt;br /&gt;
              DriveTask.WaitDriveTask(new AutoFetchGood&lt;br /&gt;
              {&lt;br /&gt;
                  aX = srcX, aY = srcY, bX = dstX, bY = dstY,&lt;br /&gt;
                  initSpeed = 500, inShelfSpeed = 150, stopDist = 80&lt;br /&gt;
              }.Get());&lt;br /&gt;
          },&lt;br /&gt;
          async () =&amp;gt;&lt;br /&gt;
          {&lt;br /&gt;
              // 4) 升叉离地 50 mm&lt;br /&gt;
              SetUpperIO(&amp;quot;forkHeightTgt&amp;quot;, forkHeight + 50);&lt;br /&gt;
              await WaitForLowerIO(&amp;quot;forkAtTarget&amp;quot;, 4000);&lt;br /&gt;
          },&lt;br /&gt;
          async () =&amp;gt;&lt;br /&gt;
          {&lt;br /&gt;
              // 5) 倒退离开工位&lt;br /&gt;
              DriveTask.WaitDriveTask(new SteeringLineFollowingReverse&lt;br /&gt;
              {&lt;br /&gt;
                  srcX = dstX, srcY = dstY,&lt;br /&gt;
                  dstX = srcX, dstY = srcY,&lt;br /&gt;
                  basespeed = (int)(baseSpeed * 0.6)&lt;br /&gt;
              }.ReverseFollow());&lt;br /&gt;
          });&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 3. SimpleComposer 端 / SimpleComposer side ==&lt;br /&gt;
ClumsyCar 子类的最简实现见 [[Special:MyLanguage/如何基于SimpleCore核心库进行调度系统开发|SimpleCore 开发指南]]。叉车多了一个 ''带载包络'' 状态：&lt;br /&gt;
&lt;br /&gt;
A ClumsyCar subclass is minimal; the extra piece for forklifts is a '''carrying-load envelope''' state:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
[CarType(Name = &amp;quot;LB14&amp;quot;, Title = &amp;quot;凌鸟LB14 叉车 / Linbir LB14 forklift&amp;quot;)]&lt;br /&gt;
public class LB14 : ClumsyCar&lt;br /&gt;
{&lt;br /&gt;
    public override Envelope GetActiveEnvelope()&lt;br /&gt;
    {&lt;br /&gt;
        if (carryingLoad)&lt;br /&gt;
            return loadedEnvelope;        // 带载包络 / loaded envelope (longer)&lt;br /&gt;
        if (forkHeight &amp;gt; 500)&lt;br /&gt;
            return highRiseEnvelope;      // 高位包络（重心抬高）&lt;br /&gt;
        return base.GetActiveEnvelope();&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 4. 调试与标定 / Calibration &amp;amp; tuning ==&lt;br /&gt;
按下列顺序：&lt;br /&gt;
Order:&lt;br /&gt;
&lt;br /&gt;
# '''基本运动''' / Basic motion: 站点对点行驶，调 `basespeed`、`steerCmd` 极性。&lt;br /&gt;
# '''激光雷达外参''' / Lidar extrinsics: 用 [[Special:MyLanguage/标定激光雷达外参|标定激光雷达外参]]。&lt;br /&gt;
# '''叉齿水平''' / Fork-tine horizontality: 用水平仪 + IMU 读数对齐。&lt;br /&gt;
# '''托盘检测先验''' / Pallet priors: 真实托盘测量 `pocketWidth`，写入 CAD。&lt;br /&gt;
# '''带载试运行''' / Loaded run-in: 不同载重 + 偏心载下跑同一路径，确认偏移收敛。&lt;br /&gt;
# '''高位放料''' / High-rise placement: 仅在 0–500–1500–2500 mm 多个高度测稳定性。&lt;br /&gt;
&lt;br /&gt;
== 5. 常见问题 / Common pitfalls ==&lt;br /&gt;
* '''液压响应滞后导致超调''' / Hydraulic lag → 在 Movement 中加 ''下一目标提前 200 ms'' 的预设。&lt;br /&gt;
* '''叉孔对齐偏''' / Pocket misalign → 90% 是激光雷达水平度问题。&lt;br /&gt;
* '''带载横向漂''' / Lateral drift loaded → 检查 `wheelBase`、`forkOffset` 标定。&lt;br /&gt;
* '''反向行驶失败''' / Reverse line-follow fails → 后向激光遮挡或 `lastDriverR` 没设置成员。&lt;br /&gt;
* '''高位倒料''' / Tip-over at height → 高位包络未声明，调度允许了限速过高的路径。&lt;br /&gt;
&lt;br /&gt;
== 6. 双车联动叉车 / Twin-car forklifts ==&lt;br /&gt;
叉车做 [[Special:MyLanguage/双车/多车联动|双车联动]] 时，作为 ''领头''常见，跟随车需用后向激光雷达观测领头叉车背面的 V 槽。详见双车联动页。&lt;br /&gt;
&lt;br /&gt;
When a forklift participates in [[Special:MyLanguage/双车/多车联动|twin-car coordination]], it is typically the leader; the follower observes the leader's rear-mounted V-groove via rear lidar. See the twin-car page for details.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/MDCS引擎适配机器人入门教学|MDCS引擎适配机器人入门教学]] — 入门&lt;br /&gt;
* [[Special:MyLanguage/潜伏顶升车(KIVA类小车)适配案例|潜伏顶升车(KIVA类小车)适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/全向车适配案例|全向车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/牵引车适配案例|牵引车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/使用叉车自动取托盘功能|使用叉车自动取托盘功能]]&lt;br /&gt;
* [[Special:MyLanguage/使用叉车料架堆叠功能|使用叉车料架堆叠功能]]&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E5%85%A8%E5%90%91%E8%BD%A6%E9%80%82%E9%85%8D%E6%A1%88%E4%BE%8B&amp;diff=1005</id>
		<title>全向车适配案例</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E5%85%A8%E5%90%91%E8%BD%A6%E9%80%82%E9%85%8D%E6%A1%88%E4%BE%8B&amp;diff=1005"/>
		<updated>2026-05-16T11:26:44Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
全向车（mecanum 麦克纳姆轮 或 4 独立舵轮）能 ''任意方向平移 + 独立旋转''，无非完整约束，适合狭窄场景。MDCS 中典型全向车是大重载（10 T+）平板搬运车，常出现在 [[Special:MyLanguage/双车/多车联动|双车联动]] 场景。&lt;br /&gt;
&lt;br /&gt;
Omni vehicles (mecanum-wheel or 4-independent-steered-wheel) can ''translate in any direction + rotate independently'' — no non-holonomic constraints, ideal for tight spaces. The typical MDCS omni is a 10 t+ heavy-load flat-bed for [[Special:MyLanguage/双车/多车联动|twin-car]] operations.&lt;br /&gt;
&lt;br /&gt;
参考：浙江联核（`D:\src\cookbook\adaption-reference\浙江联核\new\`）、卓一全向叉车（`D:\src\cookbook\adaption-reference\卓一\zhuoyiomniforklift\`）。&lt;br /&gt;
References: Zhejiang Lianhe (`adaption-reference\浙江联核\new\`), Zhuoyi omni-forklift.&lt;br /&gt;
&lt;br /&gt;
== 硬件清单 / Hardware ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 部件 / Part !! 麦克纳姆 / Mecanum !! 四舵轮 / 4-steered&lt;br /&gt;
|-&lt;br /&gt;
| 驱动轮数 / Drive wheels || 4 (左前 / 右前 / 左后 / 右后 全独立)  || 4 独立驱动 + 4 独立转向&lt;br /&gt;
|-&lt;br /&gt;
| 编码器 / Encoders || 每轮一个 / 1 per wheel || 每轮 2 个（驱 + 转）&lt;br /&gt;
|-&lt;br /&gt;
| IMU || 必备 / required || 必备 / required&lt;br /&gt;
|-&lt;br /&gt;
| 控制板 / Controller || 单板可控 4 路 BLDC || 通常每对驱 / 转一对一块板&lt;br /&gt;
|-&lt;br /&gt;
| 重载承重 / Payload || 0.3–3 T || 5–20 T&lt;br /&gt;
|-&lt;br /&gt;
| 适用场景 / Use case || 中等重量、清洁机器人 || 重载、长货物&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 1. Medulla 适配 / Medulla side ==&lt;br /&gt;
全向车的关键是 ''速度合成''：把 `(vx, vy, omega)` 拆成 4 路轮速 / 转向角，下发到电机。这通常在 LadderLogic 内做：&lt;br /&gt;
&lt;br /&gt;
The crux is ''velocity synthesis'': decompose `(vx, vy, omega)` into 4-wheel speeds / steer angles. Do it inside the LadderLogic loop:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class OmniCart : CartDefinition&lt;br /&gt;
{&lt;br /&gt;
    [AsInitParam] public string canDevice = &amp;quot;USBCAN0&amp;quot;;&lt;br /&gt;
    [AsInitParam] public float  wheelBase = 1600; // mm (front-rear distance)&lt;br /&gt;
    [AsInitParam] public float  trackWidth = 1200; // mm&lt;br /&gt;
    [AsInitParam] public float  wheelRadius = 100; // mm&lt;br /&gt;
&lt;br /&gt;
    // 上位 IO — 三轴速度&lt;br /&gt;
    [AsUpperIO] public float vxCmd;        // 前向 / forward&lt;br /&gt;
    [AsUpperIO] public float vyCmd;        // 横向 / lateral (left positive)&lt;br /&gt;
    [AsUpperIO] public float omegaCmd;     // 转向角速度 / yaw rate&lt;br /&gt;
&lt;br /&gt;
    // 下位 IO — 4 路状态&lt;br /&gt;
    [AsLowerIO] public float vEstW1, vEstW2, vEstW3, vEstW4;&lt;br /&gt;
    [AsLowerIO] public bool  eStop;&lt;br /&gt;
&lt;br /&gt;
    [UseLadderLogic(IntervalMs = 50)]&lt;br /&gt;
    public class Loop : LadderLogic&amp;lt;OmniCart&amp;gt;&lt;br /&gt;
    {&lt;br /&gt;
        public override void Run(OmniCart s)&lt;br /&gt;
        {&lt;br /&gt;
            // 麦克纳姆轮 inverse kinematics（标准 4 轮，X 型布局）&lt;br /&gt;
            var L = (s.wheelBase + s.trackWidth) / 2f;&lt;br /&gt;
            var w1 = s.vxCmd - s.vyCmd - L * s.omegaCmd;   // 左前&lt;br /&gt;
            var w2 = s.vxCmd + s.vyCmd + L * s.omegaCmd;   // 右前&lt;br /&gt;
            var w3 = s.vxCmd + s.vyCmd - L * s.omegaCmd;   // 左后&lt;br /&gt;
            var w4 = s.vxCmd - s.vyCmd + L * s.omegaCmd;   // 右后&lt;br /&gt;
&lt;br /&gt;
            // 转换到 RPM 并下发&lt;br /&gt;
            s.can.SendWheelRPM(&lt;br /&gt;
                w1 / s.wheelRadius,&lt;br /&gt;
                w2 / s.wheelRadius,&lt;br /&gt;
                w3 / s.wheelRadius,&lt;br /&gt;
                w4 / s.wheelRadius);&lt;br /&gt;
&lt;br /&gt;
            // 读回 4 路速度&lt;br /&gt;
            var status = s.can.ReadStatus();&lt;br /&gt;
            s.vEstW1 = status.w1; s.vEstW2 = status.w2;&lt;br /&gt;
            s.vEstW3 = status.w3; s.vEstW4 = status.w4;&lt;br /&gt;
            s.eStop  = status.eStop;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
四舵轮变体在上述基础上再加 4 路 steer angle，并把 (vx, vy, omega) → (4 个轮速 + 4 个转向角) 的解算改为 ''每个轮的方向''指向行驶切线 + ''每个轮的轮速'' 等于切线速率。&lt;br /&gt;
&lt;br /&gt;
The 4-steered variant adds 4 steer-angle outputs and solves so each wheel's direction points tangent to its instantaneous motion arc.&lt;br /&gt;
&lt;br /&gt;
== 2. Clumsy 自动驾驶适配 / Clumsy side ==&lt;br /&gt;
全向车需要 ''全向版本''的 Movement —— 不能直接用 `SteeringLineFollowing`（它假设阿克曼 / 差速）。建议家族：&lt;br /&gt;
&lt;br /&gt;
Omni vehicles need ''omni variants'' of the Movements — `SteeringLineFollowing` assumes Ackermann / differential. The recommended family:&lt;br /&gt;
&lt;br /&gt;
* `OmniLineFollowing` — 直线，可指定 ''移动朝向''与 ''车头朝向''独立&lt;br /&gt;
* `OmniArcFollowing` — 圆弧&lt;br /&gt;
* `OmniInPlaceRotate` — 不动平移、原地旋转&lt;br /&gt;
* `OmniSlotFollowing` — 替代 `SlotFollowing` 用于料架跟随&lt;br /&gt;
&lt;br /&gt;
差异点：全向车可以 ''横向接近'' 工位（车头不变向，车身横向移动到工位），常用于狭窄取料场景。&lt;br /&gt;
&lt;br /&gt;
The signature feature: omni can ''side-approach'' a station (car heading fixed, body slides sideways onto target), useful in narrow pick aisles.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public void SideApproachShelf(double sx, double sy, double tx, double ty)&lt;br /&gt;
{&lt;br /&gt;
    Queue(async () =&amp;gt;&lt;br /&gt;
    {&lt;br /&gt;
        DriveTask.WaitDriveTask(new OmniLineFollowing&lt;br /&gt;
        {&lt;br /&gt;
            srcX = sx, srcY = sy, dstX = tx, dstY = ty,&lt;br /&gt;
            holdHeading = currentTh,   // 保持车头方向&lt;br /&gt;
            basespeed = 300&lt;br /&gt;
        }.Follow());&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 3. SimpleComposer 端 / SimpleComposer side ==&lt;br /&gt;
全向车的包络是 ''圆盘形''（旋转无差异），调度需要的可达性比阿克曼车型多很多。建议在 SimpleComposer 中给全向车型设置：&lt;br /&gt;
* 较小的包络半径（无前后偏置）&lt;br /&gt;
* 更宽松的可达性（无需考虑转向半径）&lt;br /&gt;
&lt;br /&gt;
The omni envelope is a disk (rotation symmetric); reachability is much more permissive than Ackermann. Configure: small disk envelope, no turning-radius constraint in pathfinding.&lt;br /&gt;
&lt;br /&gt;
== 4. 标定与调试 / Calibration &amp;amp; tuning ==&lt;br /&gt;
# '''轮间距与轮径''' / Track + wheel radius —— 每轮独立测，麦克纳姆轮的 ''滚轴角度''（45° 或 -45°）必须配对正确。&lt;br /&gt;
# '''轮间打滑''' / Slip: 同向运行时四轮速差应 &amp;lt; 2%。&lt;br /&gt;
# '''横向移动直线度''' / Sideway straightness: 横移 5 m，偏离 &amp;lt; 50 mm。否则有一轮打滑或方向错。&lt;br /&gt;
# '''带载性能''' / Loaded behaviour: 满载时麦克纳姆轮容易&amp;quot;挤压&amp;quot;地面，横向速度损耗严重，要建立载重-补偿表。&lt;br /&gt;
&lt;br /&gt;
# Per-wheel track + radius — for mecanum the ''roller angle'' (±45°) sign must be correct per corner.&lt;br /&gt;
# Slip: in straight-fwd motion, the 4-wheel speed spread &amp;lt; 2%.&lt;br /&gt;
# Side-shift straightness: 5 m of pure lateral; lateral deviation &amp;lt; 50 mm.&lt;br /&gt;
# Loaded behaviour: heavy loads cause mecanum rollers to scrub; build a load → speed-compensation table.&lt;br /&gt;
&lt;br /&gt;
== 5. 常见问题 / Common pitfalls ==&lt;br /&gt;
* '''横向移动不直''' / Side motion drifts: 通常是滚轴角度对应错（左前 / 右前 / 左后 / 右后 4 个角的滚轴角应为 -45° / +45° / +45° / -45° X 型布局）。&lt;br /&gt;
* '''重载时减速过快''' / Decelerates too fast loaded: 载重 → 加速度上限映射需重做。&lt;br /&gt;
* '''原地旋转有平移''' / In-place rotation drifts: 轮径 / 轮间距标定不准。&lt;br /&gt;
* '''麦克纳姆滚轴磨损快''' / Mecanum rollers wear quickly: 改 4 舵轮设计或限制带载横移用例。&lt;br /&gt;
&lt;br /&gt;
== 6. 在双车联动中作为成员 / In twin-car coordination ==&lt;br /&gt;
[[Special:MyLanguage/双车/多车联动|重载双车联动]] 默认全向车作为主从两台。全向车的 V 形特征槽通常装在车体侧面（联动时两车横向并排，槽侧对槽侧）。&lt;br /&gt;
&lt;br /&gt;
The default twin-car coordination configuration uses two omni AGVs side-by-side, with V-grooves on the lateral faces (side-to-side, not tail-to-tail) so each can observe the other while moving.&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/MDCS引擎适配机器人入门教学|MDCS引擎适配机器人入门教学]]&lt;br /&gt;
* [[Special:MyLanguage/叉车适配案例|叉车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/潜伏顶升车(KIVA类小车)适配案例|潜伏顶升车(KIVA类小车)适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/牵引车适配案例|牵引车适配案例]]&lt;br /&gt;
* [[Special:MyLanguage/双车/多车联动|双车/多车联动]]&lt;br /&gt;
&lt;br /&gt;
[[Category:开发手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E8%AF%86%E5%88%AB%E6%96%99%E6%A1%86%E5%B9%B6%E5%A0%86%E5%9E%9B%E6%8B%86%E5%9E%9B&amp;diff=1004</id>
		<title>识别料框并堆垛拆垛</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E8%AF%86%E5%88%AB%E6%96%99%E6%A1%86%E5%B9%B6%E5%A0%86%E5%9E%9B%E6%8B%86%E5%9E%9B&amp;diff=1004"/>
		<updated>2026-05-16T11:15:22Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
&amp;quot;识别料框并堆垛拆垛&amp;quot;是面向 ''金属 / 塑料料框'' 的自动化动作集合：识别料框上沿与开口、堆码到指定高度、按节拍拆垛到指定层。料框相比标准托盘特征更复杂（边沿薄、四角圆滑、可能堆叠 4–6 层），因此需要 3D 感知 + 多帧融合。&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Kit-bin stacking / de-stacking&amp;quot; handles ''metal / plastic kit-bins'' — pickup, stack to a target height, or remove top bins to a target tier. Bins are harder than pallets to detect (thin lips, rounded corners, stacks up to 4–6 layers), so we use 3D sensing + multi-frame fusion.&lt;br /&gt;
&lt;br /&gt;
== 与料架 / 托盘的区别 / Difference from racks / pallets ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 项 / Item !! 标准托盘 / Pallet !! 料框 / Kit-bin&lt;br /&gt;
|-&lt;br /&gt;
| 几何特征 / Features || 两个叉孔 + 边沿 || 上沿矩形 + 内空腔 + 边沿圆弧&lt;br /&gt;
|-&lt;br /&gt;
| 叉齿可入位 / Fork pockets || 是 / Yes || 否（叉齿夹外沿或抓取）/ No (clamp or grab)&lt;br /&gt;
|-&lt;br /&gt;
| 堆叠层数 / Stack height || 2–3 层 / 2–3 || 4–6 层 / 4–6&lt;br /&gt;
|-&lt;br /&gt;
| 主用感知 / Sensing || 2D 激光 || 3D 激光 / 深度相机 + 2D 辅助&lt;br /&gt;
|-&lt;br /&gt;
| 主用 Detector || `LidarDetectPallet` || `LidarDetect3DBin`, `CamDetectBinRim`&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 工作流程 / Pipeline ==&lt;br /&gt;
=== 堆垛 / Stack ===&lt;br /&gt;
# 巡线到 ''下层 Anchor'' 站点（距堆位约 1.5 m）。&lt;br /&gt;
# 3D 激光扫描堆区，提取 ''最顶层料框''的上沿矩形（4 个角点 + 1 个朝向）。&lt;br /&gt;
# 计算放置位姿 = 最顶层上沿姿态 + 设计层高（典型 250 mm）。&lt;br /&gt;
# 升叉到 ''计算放置高度 + 30 mm''；可升降激光重新检测目标层确认。&lt;br /&gt;
# 横向 + 朝向精对位（≤ ±10 mm / ±0.5°）。&lt;br /&gt;
# 缓慢降叉直到接触检测信号（如 `forkLoadDetected` 反转）。&lt;br /&gt;
# 倒车离开。&lt;br /&gt;
&lt;br /&gt;
=== 拆垛 / De-stack ===&lt;br /&gt;
# 巡线到 ''下层 Anchor'' 站点。&lt;br /&gt;
# 3D 扫描 → 选定 ''要取的层''（默认最顶层；可指定）。&lt;br /&gt;
# 升叉到 ''目标层下沿 - 10 mm''。&lt;br /&gt;
# 抓取机构闭合 / 叉齿前推；触发 `binGripDetected`。&lt;br /&gt;
# 升叉 50 mm 提离堆。&lt;br /&gt;
# 倒车离开。&lt;br /&gt;
# 巡线到目标释放位 → 落料 → 完成。&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
// Stacking step 3 sketch&lt;br /&gt;
var top = new LidarDetect3DBin&lt;br /&gt;
{&lt;br /&gt;
    expectedWidth  = 600,&lt;br /&gt;
    expectedDepth  = 400,&lt;br /&gt;
    minConfidence  = 0.7f&lt;br /&gt;
}.Detect(latest3DCloud);&lt;br /&gt;
&lt;br /&gt;
var placeX = top.X;&lt;br /&gt;
var placeY = top.Y;&lt;br /&gt;
var placeZ = top.Z + binLayerHeight + 30;  // 30 mm safety margin&lt;br /&gt;
&lt;br /&gt;
await Lift(placeZ);&lt;br /&gt;
await DriveTask.WaitDriveTask(new BinPlaceMovement&lt;br /&gt;
{&lt;br /&gt;
    targetX = placeX, targetY = placeY,&lt;br /&gt;
    targetTheta = top.Theta&lt;br /&gt;
}.Get());&lt;br /&gt;
await Lift(placeZ - binLayerHeight - 30);   // settle&lt;br /&gt;
await OpenGrip();&lt;br /&gt;
await Reverse(1.5);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 多帧融合 / Multi-frame fusion ==&lt;br /&gt;
单帧 3D 激光的上沿点云因遮挡 / 反射常有缺失。MDCS 采用 ''K 帧合并 + 矩形拟合''：&lt;br /&gt;
&lt;br /&gt;
A single 3D frame often misses bin rims due to occlusion / reflection. MDCS uses ''K-frame merge + rectangle fit'':&lt;br /&gt;
&lt;br /&gt;
# 在 Approach 站点静态停车，连续读 K 帧（典型 K = 5）。&lt;br /&gt;
# 合并点云后去除地面 + 远处点 ROI 过滤。&lt;br /&gt;
# 在 ROI 内做矩形 RANSAC 拟合，输出矩形中心 + 朝向 + 平面高度。&lt;br /&gt;
# 若 ''confidence &amp;lt; 0.7''，把车前进 100 mm 再扫一轮（避免遮挡）。&lt;br /&gt;
&lt;br /&gt;
== 关键 Detector / Key detectors ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Detector !! 文件 / File !! 用途 / Use&lt;br /&gt;
|-&lt;br /&gt;
| `LidarDetect3DBin` || `Clumsy\ClumsyDance\Detectors\LidarDetect3DBin.cs` || 3D 激光：从单 / 多帧扫描中提 bin&lt;br /&gt;
|-&lt;br /&gt;
| `CamDetectBinRim` || `Clumsy\ClumsyDance\Detectors\CamDetectBinRim.cs` || 深度相机 + 2D 辅助&lt;br /&gt;
|-&lt;br /&gt;
| `LidarDetect2DTier` || `Clumsy\ClumsyDance\Detectors\LidarDetect2DTier.cs` || 升降 2D 激光：按高度切层&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 调试要点 / Tuning ==&lt;br /&gt;
* '''3D 点云稀疏 / 3D cloud too sparse''' → 增加 K 帧合并数，或检查 3D 雷达 [[Special:MyLanguage/3D激光雷达适配|适配]] 中的扫描频率。&lt;br /&gt;
* '''料框边沿反光强烈 / Rim glints''' → 调高强度阈值，过滤 specular spike。&lt;br /&gt;
* '''抓取后晃动 / Bin swings after grab''' → 检查抓爪夹紧力；落料前留 2 s 稳定时间。&lt;br /&gt;
* '''层高漂移 / Tier-height drift over many stacks''' → 每次堆完做一次重新 3D 扫描，更新堆顶基准。&lt;br /&gt;
&lt;br /&gt;
== 安全 / Safety ==&lt;br /&gt;
* 堆叠中倒塌风险随堆高指数上升；4 层以上必须 ''每次堆完都重新读 IMU 与上沿姿态''，姿态偏差 &amp;gt; 1° 立即停。&lt;br /&gt;
* 料框拆垛过程中下层料框可能因震动错位，必须在 ''每次拆垛后''重新扫一次堆顶。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/自动识别工位并取放货|自动识别工位并取放货]]&lt;br /&gt;
* [[Special:MyLanguage/使用叉车料架堆叠功能|使用叉车料架堆叠功能]]&lt;br /&gt;
* [[Special:MyLanguage/托盘识别|托盘识别]]&lt;br /&gt;
* [[Special:MyLanguage/3D激光雷达适配|3D激光雷达适配]]&lt;br /&gt;
&lt;br /&gt;
[[Category:特殊技术方案]]&lt;br /&gt;
[[Category:运动控制使用手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E8%AE%BE%E5%A4%87%E8%B7%9F%E9%9A%8F%E8%81%94%E5%8A%A8&amp;diff=1003</id>
		<title>设备跟随联动</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E8%AE%BE%E5%A4%87%E8%B7%9F%E9%9A%8F%E8%81%94%E5%8A%A8&amp;diff=1003"/>
		<updated>2026-05-16T11:15:21Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
&amp;quot;设备跟随联动&amp;quot;指 AGV 跟随 ''非 AGV 设备''（手推叉车、人工拖车、卷料车、料箱等）协同移动的功能。与 [[Special:MyLanguage/双车/多车联动|双车 / 多车联动]] 的区别：本场景中 ''领头是设备 / 人''，不属于 MDCS 调度网络，没有可控信号；跟随车只能依靠 ''视觉 + 激光''观测领头物体并跟随。&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Device-following coordination&amp;quot; lets an AGV shadow a '''non-AGV device''' (manual trolley, hand-pushed forklift, coil cart, kit-box, …). Compared to [[Special:MyLanguage/双车/多车联动|twin-car coordination]], the leader here is a piece of equipment or a person — it's outside the MDCS scheduling network and has no controllable signal. The follower can only ''observe'' the leader via vision / lidar and respond.&lt;br /&gt;
&lt;br /&gt;
== 应用场景 / Scenarios ==&lt;br /&gt;
* '''人工卸车 + AGV 接驳''': 工人在卡车上把货物推到边沿，AGV 在地面跟随接货位。&lt;br /&gt;
* '''卷料车跟随 / Coil-cart shadowing''': 操作工推卷料车从一道工序到下一道，AGV 跟随承担载货。&lt;br /&gt;
* '''跨工序流转 / Inter-process transfer''': 装配线上的料箱由工艺人员沿不规则路径推动，AGV 自动跟随至工位。&lt;br /&gt;
* '''拖车协同 / Trailer-tow assist''': 牵引车前牵，AGV 在后段加推支撑（适用于长拖车）。&lt;br /&gt;
&lt;br /&gt;
== 与已有联动方案的差异 / What makes it different ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 项 / Item !! 双车联动 / Twin-car !! 设备跟随联动 / Device-follow&lt;br /&gt;
|-&lt;br /&gt;
| 领头 / Leader || 同样是 MDCS AGV / Another MDCS AGV || 任意非 AGV 设备 / 人 / Any device / human&lt;br /&gt;
|-&lt;br /&gt;
| 通讯 / Comms || AP Wi-Fi 心跳 / AP heartbeat || 无通讯，纯视觉 + 激光 / None — vision / lidar only&lt;br /&gt;
|-&lt;br /&gt;
| 任务下发 / Task dispatch || 调度直接派单 / Scheduler dispatches both || 领头无法接收任务；调度只能管跟随车&lt;br /&gt;
|-&lt;br /&gt;
| 路径 / Path || 调度规划 / Scheduler-planned || 领头自由路径 / Leader free-form&lt;br /&gt;
|-&lt;br /&gt;
| 速度 / Speed || 调度约束 / Scheduler-bounded || 跟随者跟领头实时速度（带上限）/ Follower matches leader (with cap)&lt;br /&gt;
|-&lt;br /&gt;
| 失联处理 / Lost-leader || 联动解体 / Decouple || 跟随者就地停 / Stop in place&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== 实现方案 / Implementation ==&lt;br /&gt;
=== 感知 / Perception ===&lt;br /&gt;
跟随车需要在领头设备上识别一个稳定的 ''视觉锚点'' — 几种可选：&lt;br /&gt;
The follower needs a stable ''visual anchor'' on the leader. Options:&lt;br /&gt;
&lt;br /&gt;
* '''V 槽 / V-groove''' — 在设备背面装 1 个 V 槽（同 [[Special:MyLanguage/双车/多车联动|双车联动]]）。2D 激光检测，最便宜可靠。&lt;br /&gt;
* '''AprilTag / ArUco''' — 在设备背面贴一个易识别 fiducial；前向相机检测。&lt;br /&gt;
* '''3D 模板''' — 用 3D 点云模板匹配（领头形状已知，如标准卷料车）。&lt;br /&gt;
* '''腿对检测 / Leg-pair''' — 领头有两条立柱（如标准托盘车），用 `LidarDetectTray` 检测。&lt;br /&gt;
&lt;br /&gt;
推荐组合：V 槽 + AprilTag 冗余；任一可识别即继续，两者全丢 ≥ 2 s 则停车。&lt;br /&gt;
&lt;br /&gt;
Recommended combination: V-groove + AprilTag redundancy. If either is found, continue. If both lose lock for ≥ 2 s, stop.&lt;br /&gt;
&lt;br /&gt;
=== 跟随控制器 / Follow controller ===&lt;br /&gt;
跟随车的运动控制本质是一个 ''常距点对点跟踪器''，目标点 = 领头锚点 - (固定偏移)。&lt;br /&gt;
The follower's motion control is essentially a constant-distance point tracker, with the target = leader anchor − fixed offset.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public class DeviceFollow : MovementDefinition&lt;br /&gt;
{&lt;br /&gt;
    public float standoff = 1500;        // 距领头多远 / standoff (mm)&lt;br /&gt;
    public float maxSpeed = 600;&lt;br /&gt;
    public float ahesionGain = 0.8f;     // 横向 / 朝向校正强度&lt;br /&gt;
&lt;br /&gt;
    public override IEnumerable&amp;lt;bool&amp;gt; Get()&lt;br /&gt;
    {&lt;br /&gt;
        var detector = new LidarDetectTray&lt;br /&gt;
        {&lt;br /&gt;
            blobDist = 100,&lt;br /&gt;
            BlobPtCount = 3&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var lostFrames = 0;&lt;br /&gt;
        while (true)&lt;br /&gt;
        {&lt;br /&gt;
            var lead = detector.DetectLeader();   // 领头锚点&lt;br /&gt;
            if (lead == null)&lt;br /&gt;
            {&lt;br /&gt;
                if (++lostFrames &amp;gt; 50)   // 50 ticks ≈ 2 s&lt;br /&gt;
                {&lt;br /&gt;
                    DriveStop();&lt;br /&gt;
                    yield break;&lt;br /&gt;
                }&lt;br /&gt;
                yield return true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            lostFrames = 0;&lt;br /&gt;
&lt;br /&gt;
            var dist = lead.Range;&lt;br /&gt;
            var lateral = lead.Lateral;&lt;br /&gt;
            var theta  = lead.Heading;&lt;br /&gt;
&lt;br /&gt;
            // 控制律 / control law&lt;br /&gt;
            var fwd = MathF.Min(maxSpeed, (dist - standoff) * 1.2f);&lt;br /&gt;
            var turn = -(lateral * ahesionGain + theta * 0.5f);&lt;br /&gt;
&lt;br /&gt;
            SetDriveCommand(fwd, turn);&lt;br /&gt;
            yield return true;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 安全与边界 / Safety guards ===&lt;br /&gt;
* '''跟随距离上下限 / Standoff bounds''': 太近会撞领头，太远会丢锁；推荐 1.0–2.5 m。&lt;br /&gt;
* '''加速度上限 / Accel bound''': 领头突然提速时跟随车不能瞬时跟上；加上 0.5 m/s² 上限。&lt;br /&gt;
* '''转向角速度上限 / Angular vel bound''': 领头急转 → 跟随车保持不超过 0.5 rad/s。&lt;br /&gt;
* '''与人接触 / Contact with humans''': 跟随车的紧急停止线必须独立于跟随逻辑；激光雷达 1 m 内有任何障碍物立即触发 e-stop。&lt;br /&gt;
&lt;br /&gt;
== 调度集成 / Scheduling integration ==&lt;br /&gt;
* SimpleComposer 把&amp;quot;跟随设备&amp;quot; 状态视为一个特殊 mission：调度不规划跟随车的路径，只是 ''把跟随车锁定''在跟随状态。&lt;br /&gt;
* `Car.CurrentMission = Following(&amp;quot;DeviceXX&amp;quot;)`；调度看到这个状态时不会派其它任务给该 AGV。&lt;br /&gt;
* 跟随过程中跟随车汇报自己的实时位姿；调度可视化展示 ''领头-跟随'' 链路。&lt;br /&gt;
&lt;br /&gt;
SimpleComposer treats &amp;quot;follow-device&amp;quot; as a special mission state. The scheduler does not plan a path; it just locks the AGV into following mode and won't dispatch other tasks until the follow ends.&lt;br /&gt;
&lt;br /&gt;
== 启动 / 结束 / Start &amp;amp; end ==&lt;br /&gt;
* '''启动 / Start''': 通过 SimpleComposer 上的&amp;quot;启动设备跟随&amp;quot;按钮；选定要跟随的设备类型；车开始尝试识别。&lt;br /&gt;
* '''确认 / Confirm''': 识别到锚点后车体调整姿态并停在 standoff 距离上；车灯绿色提示。&lt;br /&gt;
* '''结束 / End''': 操作工通过手柄按&amp;quot;结束跟随&amp;quot;，或跟随车到达预设终点站点（参数化）。&lt;br /&gt;
&lt;br /&gt;
== 调试要点 / Tuning ==&lt;br /&gt;
* '''跟随抖动 / Jitter''': 调低 `ahesionGain`，或在检测端加 EWMA 滤波。&lt;br /&gt;
* '''领头突然消失 / Sudden lost-lock''': 缩短 `lostFrames` 阈值（默认 50 ticks ≈ 2 s）。&lt;br /&gt;
* '''拐弯时甩出 / Slips on turn''': 把 `maxSpeed` 降到 400 mm/s，或在领头加 AprilTag 增强检测稳定性。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/双车/多车联动|双车 / 多车联动]]&lt;br /&gt;
* [[Special:MyLanguage/联动天眼系统进行装卸车|联动天眼系统进行装卸车]]&lt;br /&gt;
* [[Special:MyLanguage/巡线行走|巡线行走]] — 跟随的退化情况&lt;br /&gt;
&lt;br /&gt;
[[Category:特殊技术方案]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
	<entry>
		<id>https://wiki2.lessokaji.com/index.php?title=%E8%87%AA%E5%8A%A8%E8%AF%86%E5%88%AB%E6%89%98%E7%9B%98%E5%8F%96%E6%94%BE%E8%B4%A7&amp;diff=1002</id>
		<title>自动识别托盘取放货</title>
		<link rel="alternate" type="text/html" href="https://wiki2.lessokaji.com/index.php?title=%E8%87%AA%E5%8A%A8%E8%AF%86%E5%88%AB%E6%89%98%E7%9B%98%E5%8F%96%E6%94%BE%E8%B4%A7&amp;diff=1002"/>
		<updated>2026-05-16T11:15:20Z</updated>

		<summary type="html">&lt;p&gt;Artheru：​Initial bilingual draft (auto-published)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;languages/&amp;gt;&lt;br /&gt;
== 概述 / Overview ==&lt;br /&gt;
&amp;quot;自动识别托盘取放货&amp;quot;是 [[Special:MyLanguage/自动识别工位并取放货|自动识别工位并取放货]] 的特例：工位本身是一个独立的托盘（pallet）而非固定货架，车体到位后通过激光雷达识别托盘的 ''两个插孔''（叉车场景）或 ''两个边缘特征''（顶升 / 平板场景），完成对位 + 升降 + 离场。&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Auto-pallet pick &amp;amp; place&amp;quot; specialises [[Special:MyLanguage/自动识别工位并取放货|station-aware pick &amp;amp; place]] to pallets. The &amp;quot;station&amp;quot; is the pallet itself, not a fixed rack. Once the vehicle is roughly in front, the lidar locates the pallet's '''two fork-pockets''' (forklifts) or its '''two side edges''' (lift / flat-bed vehicles), then the car runs the alignment / lift / withdraw sequence.&lt;br /&gt;
&lt;br /&gt;
== 与货架取放的差异 / Difference from rack pick ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! 项 / Item !! 货架（KIVA 类）/ Rack (KIVA) !! 托盘 / Pallet&lt;br /&gt;
|-&lt;br /&gt;
| 特征点 / Features || 4 条立腿 / 4 legs || 2 个叉孔或 2 条边 / 2 fork pockets or 2 edges&lt;br /&gt;
|-&lt;br /&gt;
| 进入方向 / Entry side || 任意（钻入式）/ any (slide-under) || 固定一侧（短边）/ fixed short edge&lt;br /&gt;
|-&lt;br /&gt;
| 升降高度 / Lift travel || 30–80 mm（顶升）/ 30–80 mm (jack) || 100–1800 mm（叉齿）/ 100–1800 mm (forks)&lt;br /&gt;
|-&lt;br /&gt;
| 误差容忍 / Tolerance || ±30 mm / ±2° || ±15 mm / ±1.5°（叉孔机械约束）&lt;br /&gt;
|-&lt;br /&gt;
| 主用 Movement || `AutoShelfFetching_ManyLegs` || `AutoFetchGood`, `PutGood`&lt;br /&gt;
|-&lt;br /&gt;
| 主用 Detector || `LidarDetectTray` (3-腿) / 4-腿 || `LidarDetectPallet`, `LidarBlobPairDetector`&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
托盘场景对车体姿态的容忍度比货架低，因为叉齿与叉孔的机械配合余量只有 10–15 mm；调试时务必先用 [[Special:MyLanguage/标定与校准|标定与校准]] 把外参打齐。&lt;br /&gt;
&lt;br /&gt;
Pallet pickup tolerates less pose error than rack pickup because the fork-pocket/fork-tine clearance is only 10–15 mm. Always level the lidar and verify extrinsics via [[Special:MyLanguage/标定与校准|the calibration guide]] before tuning.&lt;br /&gt;
&lt;br /&gt;
== 工作流程 / Pipeline ==&lt;br /&gt;
# '''粗对位 / Coarse approach''': SimpleComposer 把 AGV 派到托盘前的 ''anchor 站点''，使用普通巡线 / 路径跟踪。&amp;lt;br /&amp;gt;SimpleComposer dispatches the AGV to the anchor site in front of the pallet; ordinary line / path follow.&lt;br /&gt;
# '''粗检测 / Coarse detect''': 启动 `LidarDetectPallet`，从前方激光帧中找出托盘的两个特征（叉孔中心 or 边缘中点）的近似位置。&amp;lt;br /&amp;gt;Start `LidarDetectPallet` to extract approximate centers of the two pallet features from one or two lidar frames.&lt;br /&gt;
# '''精对位跟踪 / Fine alignment''': 用检测出的两个点拟合托盘中轴线，`AutoFetchGood`（或 `PutGood`）开启跟踪，逼近期望停靠位姿。&amp;lt;br /&amp;gt;Fit the pallet's midline from the two feature points; `AutoFetchGood` (or `PutGood`) opens a tracking thread to drive toward the docking pose.&lt;br /&gt;
# '''升降 / Lift''': 到位后通过 `[[Special:MyLanguage/Medulla-API|Medulla 上位 IO]]` 触发叉齿 / 顶升机构动作。&amp;lt;br /&amp;gt;On dock, fire the fork / jack actuator through Medulla upper-IO.&lt;br /&gt;
# '''离场 / Withdraw''': 倒车（叉车）或推车前进（顶升）退出托盘正前方区域。&amp;lt;br /&amp;gt;Reverse out (forklift) or roll forward (jack) to clear.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public void PickPallet(double approachX, double approachY, double palletX, double palletY)&lt;br /&gt;
{&lt;br /&gt;
    Queue(&lt;br /&gt;
      async () =&amp;gt;&lt;br /&gt;
      {&lt;br /&gt;
          // 1. Coarse approach&lt;br /&gt;
          DriveTask.WaitDriveTask(new SteeringLineFollowing&lt;br /&gt;
          {&lt;br /&gt;
              srcX = currentX, srcY = currentY,&lt;br /&gt;
              dstX = approachX, dstY = approachY,&lt;br /&gt;
              basespeed = 800&lt;br /&gt;
          }.Follow());&lt;br /&gt;
      },&lt;br /&gt;
      async () =&amp;gt;&lt;br /&gt;
      {&lt;br /&gt;
          // 2 + 3. Detect + fine alignment (encapsulated in AutoFetchGood)&lt;br /&gt;
          DriveTask.WaitDriveTask(new AutoFetchGood&lt;br /&gt;
          {&lt;br /&gt;
              aX = approachX, aY = approachY,&lt;br /&gt;
              bX = palletX,   bY = palletY,&lt;br /&gt;
              initSpeed   = 500,&lt;br /&gt;
              inShelfSpeed= 150,&lt;br /&gt;
              stopDist    = 80,           // tight: pocket / tine clearance&lt;br /&gt;
              width       = 800           // pallet pocket span&lt;br /&gt;
          }.Get());&lt;br /&gt;
      },&lt;br /&gt;
      async () =&amp;gt;&lt;br /&gt;
      {&lt;br /&gt;
          // 4. Lift via Medulla upper-IO (fork at 200 mm)&lt;br /&gt;
          MedullaAPI.SetUpperIO(&amp;quot;forkHeight&amp;quot;, 200);&lt;br /&gt;
          await WaitForUpperIO(&amp;quot;forkAtTarget&amp;quot;, timeoutMs: 5000);&lt;br /&gt;
      },&lt;br /&gt;
      async () =&amp;gt;&lt;br /&gt;
      {&lt;br /&gt;
          // 5. Withdraw 1.5 m&lt;br /&gt;
          DriveTask.WaitDriveTask(new SteeringLineFollowingReverse&lt;br /&gt;
          {&lt;br /&gt;
              srcX = palletX, srcY = palletY,&lt;br /&gt;
              dstX = approachX, dstY = approachY,&lt;br /&gt;
              basespeed = 400&lt;br /&gt;
          }.ReverseFollow());&lt;br /&gt;
      });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 调试要点 / Tuning ==&lt;br /&gt;
* '''叉孔检测假阳性 / False positive on fork pockets''': 缩窄 `LidarDetectPallet.widthMin/widthMax` 区间至 ±50 mm。&lt;br /&gt;
* '''托盘姿态偏 / Pallet pose drift''': 检查激光雷达水平度，重做 [[Special:MyLanguage/标定激光雷达外参|外参标定]]。&lt;br /&gt;
* '''叉齿与叉孔刮擦 / Tines scrape pocket walls''': 适当增大 `stopDist`（保留 20–30 mm 余量），或把 `inShelfSpeed` 降到 100 mm/s。&lt;br /&gt;
* '''码垛环境光斑干扰 / Beam glints in stacking yards''': 给托盘正面贴一段宽 50 mm 的吸光胶带可显著改善。&lt;br /&gt;
&lt;br /&gt;
== 相关页面 / See also ==&lt;br /&gt;
* [[Special:MyLanguage/自动识别工位并取放货|自动识别工位并取放货]]&lt;br /&gt;
* [[Special:MyLanguage/托盘识别|托盘识别]] — 检测器的纯算法细节&lt;br /&gt;
* [[Special:MyLanguage/使用叉车自动取托盘功能|使用叉车自动取托盘功能]] — 终端使用手册视角&lt;br /&gt;
* [[Special:MyLanguage/识别料框并堆垛拆垛|识别料框并堆垛拆垛]]&lt;br /&gt;
&lt;br /&gt;
[[Category:运动控制使用手册]]&lt;/div&gt;</summary>
		<author><name>Artheru</name></author>
	</entry>
</feed>