# MotionFramework **Repository Path**: ralfchen1978/motion-framework ## Basic Information - **Project Name**: MotionFramework - **Description**: 运动控制卡演示框架+模拟卡 顺便给自己找个工作 - **Primary Language**: C# - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-14 - **Last Updated**: 2026-09-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 通用运动控制框架(C# / .NET 9 / WPF + Prism 9) 一套**与运动控制卡品牌解耦**的通用运动控制框架。上层业务只面向 `IAxis` / `IAxisGroup` 等统一抽象,底层通过插件化适配器对接各家运控卡(固高 GTS、雷赛 DMC、仿真)。 **加一个品牌 = 放一个 DLL**,宿主代码零改动。 配套一份 8 页面的 WPF 工业 HMI(仪表盘 / 轴调试 / 轴组插补 / 轨迹视图 / 安全·IO / 配置与插件 / 实时调度器 / 日志),并且**没有硬件也能完整跑通**——仿真适配器保证 逻辑可测、可演示、可回归。 --- ## 1. 快速开始 ```bash # 构建(0 警告 0 错误) dotnet build MotionFramework.sln -c Debug # 无头冒烟:50 项断言,无需任何界面与硬件 dotnet run --project tests/Motion.SmokeTest # 启动界面 dotnet run --project src/Motion.App # 界面冒烟:8 个页面逐页截图 + 插补跳转检查,自动退出(CI / 提交前用) cd src/Motion.App/bin/Debug/net9.0-windows MOTION_SNAPSHOT_DIR=snapshots MOTION_SNAPSHOT_EXIT=1 ./Motion.App.exe ``` 首次运行会自动在 `config/motion.config.json` 生成默认配置(1 张仿真卡 + X/Y/Z/R 四轴), 开箱即跑。界面冒烟会先在仿真卡上真实跑一遍 **使能 → 回零 → 直线插补 → 圆弧插补**, 让截图里是真实数据而不是空壳(可用 `MOTION_SNAPSHOT_DEMO=0` 关闭)。 **启动系统不等于连接控制卡**:点「启动系统」只拉起软实时心跳(轮询 / 安全评估 / setpoint 下发), 控制卡要在仪表盘的控制器卡片上手动点「连接这张卡」(或页头「连接全部控制卡」)。 这是刻意的 —— 上位机启动就自动接通动力,等于省掉了急停之后的第一道人工确认。 未连接的卡既不会报通信故障,也不会参与安全评估。 接真实运控卡时多一个开关(厂商 SDK 基本都是 32 位): ```bash dotnet build MotionFramework.sln -p:MotionRealHardware=true # 切 x86 ``` --- ## 2. 分层架构 依赖方向**只能向下**(`App → Framework → Core`,适配器同时实现 `Core` 的抽象)。 `Motion.Core` 不引用任何具体硬件、不引用 WPF。 ```mermaid graph TD subgraph L1["应用层 Motion.App(WPF + Prism 9)"] A1["8 个页面(MVVM)"] A2["MotionHostService 宿主 / 日志 / UI 调度"] end subgraph L2["框架核心层 Motion.Framework"] B1["MotionSystem 组合根"] B2["AxisBase / MotionControllerBase / AxisGroup"] B3["SoftRealtimeScheduler · LookAhead · Interpolators"] B4["LockFreeCommandQueue · SafetyMonitor · AdapterPluginLoader"] end subgraph L3["设备抽象层 Motion.Core"] C1["IAxis · IAxisGroup · IMotionController"] C2["IInterpolator · IMotionAdapter · IErrorMapper"] C3["IRealtimeScheduler · ICoordinateTransform · ISafetyMonitor"] C4["单位 / 状态机 / 错误码 / 几何 / 配置模型"] end subgraph L4["适配器层"] D1["Motion.Adapters.Simulation"] D2["Motion.Adapters.Googol(gts.dll)"] D3["Motion.Adapters.Leadshine(LTDMC.dll)"] end L5["硬件层:运控卡 / 伺服 / 驱动器(P/Invoke 到原生 DLL)"] L1 --> L2 --> L3 L2 -. "反射 + AssemblyLoadContext" .-> L4 L4 -->|实现抽象| L3 L4 --> L5 ``` **关键设计决策** | 决策 | 原因 | | --- | --- | | 命令与状态分离 | 上层通过队列下发命令,状态从 `Poll` 快照读;多线程不再直接碰轴对象 | | 停止不进队列 | `StopMode` 走 volatile 高优先通路,急停不能被前面排队的运动命令挡住 | | C# 只做软实时 | Windows 不是硬实时系统。C# 负责 1~10ms 软实时调度与粗插补,硬实时交给卡 / PLC | | 安全独立于软件 | 急停 / STO / 硬限位是硬件回路,软件只监控 + 执行保护性动作(重力轴先抱闸) | | 适配器必须声明能力 | 卡是否支持卡上插补、是否支持回零,由能力集声明,框架据此决定降级策略并**说明原因** | | 连接失败绝不静默降级 | 配置了真卡却没连上,宁可保留失败控制器让现场看到原因,也不偷偷换成仿真 | --- ## 3. 工程结构 ``` MotionFramework/ ├── Directory.Build.props # 全局属性 + MotionRealHardware(x86) 开关 ├── MotionFramework.sln ├── docs/ # 设计文档(见下方索引) ├── src/ │ ├── Motion.Core/ # 设备抽象层:接口 + 单位/状态/错误码/几何/配置模型 │ │ ├── Abstractions/ # IAxisGroup/IMotionAdapter/IMotionController/IHostServices │ │ ├── Commands/ # MotionCommand + 长时命令所有权移交 │ │ ├── Configuration/ # motion.config.json 的强类型模型 │ │ ├── Hardware/ # DriverSearchPath + DriverProbe(驱动前检) │ │ ├── Units/ # UnitConverter(工程单位 ↔ 脉冲) │ │ ├── Geometry.cs # MotionPoint/Point2D/Point3D │ │ └── Interpolation.cs # TrajectorySegment/IBInterpolation 契约 │ ├── Motion.Framework/ # 框架核心层 │ │ ├── Composition/ # MotionSystem + MotionSystemBuilder(组合根) │ │ ├── Controllers/ # AxisBase / MotionControllerBase │ │ ├── AxisGroup/ # AxisGroup + RealtimeInterpolationKernel │ │ ├── Commands/ # LockFreeCommandQueue / MotionCommandLoop │ │ ├── Scheduling/ # SoftRealtimeScheduler │ │ ├── Planning/ # VelocityProfile(梯形/S 曲线)/ LookAhead(前瞻) │ │ ├── Interpolation/ # 直线 / 圆弧 / 样条 │ │ ├── StateMachine/ # AxisStateMachine(8 态) │ │ ├── Safety/ # SafetyMonitor(软限位 / 跟随误差 / 软急停) │ │ ├── Diagnostics/ # TrajectoryRecorder │ │ └── Plugins/ # AdapterPluginLoader(三级发现) │ ├── Motion.Adapters.Simulation/# 仿真适配器(无硬件跑通全部逻辑) │ ├── Motion.Adapters.Googol/ # 固高 GTS:GtsNative + GoogolAdapter + GoogolAxis │ ├── Motion.Adapters.Leadshine/ # 雷赛 DMC:LtdmcNative + LeadshineAdapter + ... │ └── Motion.App/ # 应用层(WPF + Prism 9) │ ├── Views/ ViewModels/ Controls/ Converters/ Themes/ Services/ │ └── drivers/{品牌}/ # 原生驱动 DLL 约定目录(见其 README) └── tests/Motion.SmokeTest/ # 无头冒烟:48 项断言 ``` --- ## 4. 统一约定(这是"通用"的基础) **单位**(上层只见工程单位,脉冲换算全部锁在适配器里) | 量 | 单位 | 说明 | | --- | --- | --- | | 位置 | mm / deg | 由 `AxisConfig.Unit` 决定 | | 速度 | mm/s | | | 加速度 | mm/s² | | | 角度 | deg | | | 脉冲当量 | 脉冲/mm | `PulseEquivalent = EncoderResolution × GearRatio / LeadScrew` | **轴状态机**(8 态,所有品牌统一) ```mermaid stateDiagram-v2 [*] --> Disabled Disabled --> Standby: 使能 Standby --> Homing: 回零 Homing --> Ready: 回零完成 Standby --> Ready: 无回零需求 Ready --> Moving: 定位 / 点动 Ready --> Synchronized: 轴组插补 Moving --> Ready: 到位 Synchronized --> Ready: 插补结束 Moving --> Stopped: 受控 / 减速停止 Synchronized --> Stopped: 同步停止 Stopped --> Ready: 安全复位 Ready --> Disabled: 下使能 Homing --> Error: 超时 / 无原点 Ready --> Error: 跟随误差 / 驱动报警 Moving --> Error: 跟随误差 / 驱动报警 Error --> Standby: 复位 ``` **线程**(详见 `docs/02`) | 线程 | 优先级 | 职责 | | --- | --- | --- | | 软实时线程 | Highest(可提升) | 周期回调:状态轮询 → 安全评估 → 插补 setpoint 下发 → 轨迹采样 | | 命令线程 | Normal(LongRunning) | 消费无锁队列,执行使能 / 回零 / 定位等长时命令 | | UI 线程 | Normal | 定时刷新快照(默认 200~250ms),绝不阻塞在运动命令上 | | 厂商回调 / 后台 | 由适配器决定 | 只允许投递到队列或 `IUiDispatcher`,不直接改状态 | --- ## 5. 文档索引 | 文档 | 内容 | | --- | --- | | [docs/01-架构设计.md](docs/01-架构设计.md) | 分层职责、核心接口、类关系图、关键设计决策与取舍 | | [docs/02-线程模型与时序.md](docs/02-线程模型与时序.md) | 线程清单、启停/插补/停止/急停时序图、实时性实测数据 | | [docs/03-适配器接入指南.md](docs/03-适配器接入指南.md) | 接入新品牌的完整步骤、代码骨架、错误映射、验收清单 | | [docs/04-验证与缺陷修复记录.md](docs/04-验证与缺陷修复记录.md) | 48 项冒烟覆盖范围、界面冒烟、已修缺陷与根因、**已知限制与卡上插补路径的验证缺口(含一处已定位的疑似单位换算缺陷)** | | [src/Motion.App/drivers/README.md](src/Motion.App/drivers/README.md) | 驱动 DLL 目录约定与位宽 / 伴随 DLL 陷阱 | --- ## 6. 常见问题(都是运行时才会暴露的坑,已踩过) | 现象 | 根因 | 处理 | | --- | --- | --- | | 窗口起不来,`XamlParseException: Cannot find non-neutral culture related to 'en-us'` | `InvariantGlobalization=true` 与 WPF 不兼容(任何带 Converter 的绑定首次求值即炸) | App 工程覆写 `false` | | `无法对只读属性 "X" 进行 TwoWay 绑定` | `ProgressBar.Value` / `TextBox.Text` 默认 TwoWay | 显式 `Mode=OneWay` | | 导航到某页面抛 `NullReferenceException` | WPF 在 **XAML 加载阶段**就求值 `CanExecute`,此时选中项还是 null | 属性写成可空(`Selected?.Axis`),动作里用 `Axis!` | | 界面能打开,但内容区一片空白 | 在 Prism 区域创建(`ContentControl.Loaded`)之前导航,失败是静默的 | 导航内部等区域就绪再试(见 `ShellViewModel.TryNavigate`) | | 顶栏数据永远是初始值 | Shell 由 `CreateShell()` 创建,不是区域导航目标,`INavigationAware` 永不触发 | 显式调用 `ViewModelBase.Activate()` | | 实时周期只能跑到设定值的 1/3,抖动好几毫秒 | 等待分支也推进了时间轴 + Windows 定时器默认 15.6ms 分辨率 | 等待分支不推进 `next`;`timeBeginPeriod(1)` | | 配置了真卡却跑起来像仿真 | 泛化 catch 把连接异常吞掉后静默降级 | 框架禁止静默降级:保留失败控制器 + 写清原因 | | **系统一启动就报「通信中断」并触发软急停** | 安全监控把「未连接」当成「通信中断」——`!IsConnected` 同时覆盖了"还没连"和"连了又断" | 用 `IMotionController.IsCommunicationLost`(曾连上过 && 现在断开)判断;未连接的控制器直接跳过评估 | | **轨迹视图不显示实时曲线(画面定格)** | 重绘版本号由 `Samples.Count` 算出,环形缓冲写满后条数恒定 → 版本号不再变 → 停止重绘 | 版本号改用**单调递增计数器**(`BumpPlot()`),与"内容变了没有"解耦 | | **拷好 DLL 再点「探测驱动 DLL」没反应** | `DriverProbe` 有静态缓存,启动时探测的 `Missing` 被缓存且不会过期 | 手动探测前 `DriverProbe.ClearCache()`;缓存里只有「可用」结果能短路返回 | | 顶栏按钮高低不齐(差 8px) | `ButtonBase` 默认 `Margin="0,0,8,8"`(为页面内按钮组设计),顶栏里被个别按钮覆盖成 `0` | 顶栏按钮逐个显式指定 `Width/Height/Margin` | | 所有按钮操作都"没有反馈" | 命令包装器在操作**成功之后**清空 `StatusMessage`,把 action 里刚设的成功提示一起擦掉 | 改为在操作**开始前**清空,保留 action 内设置的提示 | --- ## 7. 非目标(明确不做的) - **不追求 C# 硬实时插补**。Windows 上做不到,硬实时交回卡 / PLC。 - **不替代安全 PLC / STO / 急停硬件回路**。软件只监控与执行保护性停机,安全等级由硬件保证。 - **不做工艺逻辑**(点胶路径规划、视觉算法等)。框架只提供运动能力与扩展点 (`ICoordinateTransform` 已留好像素 → 机器人 → 轴坐标的接口)。