# TeamAAS 开发经验汇总 > 面向后续维护/扩展者。先读 `Docs/ARCHITECTURE.md`(架构蓝图)与本文档; > 每轮迭代的详细过程见 `Docs/NIGHT_LOG.md`(追加轮①~㉒)。 > 技术栈:.NET Framework 4.8 / WPF / Prism.DryIoc / HandyControl 3.5.1 / PropertyGridLib 1.1.2 / NLog。 --- ## 一、软件整体框架 ### 1.1 五层架构(自下而上) ``` ┌─────────────────────────────────────────────────────────┐ │ 5. UI(客户交付层) │ │ TeamAAS —— 主程序壳(导航/主页/产品管理),按客户裁剪 │ ├─────────────────────────────────────────────────────────┤ │ 4. 插件层(开放给工程师写代码的层) │ │ TeamAAS.Plugins/* │ │ ├─ Plugins.Standard(逻辑/文件/通讯批量等通用节点) │ │ ├─ Plugins.Feeder(供料) │ │ ├─ Plugins.Vpp / .Vpp.Detection / .Vpp.Measurement │ │ ├─ Plugins.Vm / Plugins.Vm.Module │ │ └─ Plugins.Halcon / .Halcon.{ImageProcessing, │ │ Detection, Measurement, Logic} │ ├─────────────────────────────────────────────────────────┤ │ 3. 流程引擎 + 视觉引擎 + 设备内核(地基的核心) │ │ TeamAAS.FlowEngine(流程画布/执行器/插件框架/公式绑定) │ │ TeamAAS.VisualEngine(视觉引擎发现与切换) │ │ TeamAAS.{Camera,Communication,Feeder,Motion,Robot, │ │ Database}(设备内核,统一接口 + 调试页) │ ├─────────────────────────────────────────────────────────┤ │ 2. SDK / 公共(跨插件契约,只依赖无业务) │ │ TeamAAS.SDK(图层总线 Vision/、设备契约 Devices/) │ │ TeamAAS.Global(日志/对话框/本地化/主题) │ │ TeamAAS.Shared │ ├─────────────────────────────────────────────────────────┤ │ 1. 宿主(ExE/win-x64 部署目录:EXE + Runtime + Config) │ └─────────────────────────────────────────────────────────┘ ``` 依赖方向永远向下:插件引用 Core(Core 聚合 SDK/Global/FlowEngine), SDK 不依赖任何上层;平台 SDK(Cognex/海康/HALCON)只被对应插件引用。 ### 1.2 运行时部署结构(ExE/win-x64) ``` ExE/win-x64/ ├─ TeamAAS.exe / .config ├─ Config/ Products/ Recipe/ Logs/ ← 配置与数据(根目录白名单) └─ Runtime/ ← 其余全部下沉到这里 ├─ *.dll ← 宿主/引擎/SDK 程序集 ├─ Plugins/ ← 插件根(递归扫描) │ ├─ Plugins.Standard.dll / Plugins.Vpp.dll / Plugins.Vm.dll / Plugins.Feeder.dll │ ├─ HalconPlugins/ (Plugins.Halcon.* + halcondotnet.dll) │ ├─ VppPlugins/ (Plugins.Vpp.Detection/Measurement) │ ├─ VmPlugins/ (Plugins.Vm.Module) │ └─ Vpp9.0/ Vm4.4/ dll/x64/ ← 平台原生/托管运行库 ``` 关键机制: - **PostBuild.ps1**(TeamAAS 工程):把生成输出搬进 Runtime;插件分类工程各自 Copy 到 `EXE/win-x64/Plugins/<平台目录>`,PostBuild 再整体移到 `Runtime/Plugins`。 - **PluginLoader.LoadFrom**:递归扫描 `Runtime/Plugins/**`,只装载 `Plugins.*.dll`,同名 DLL 去重。 - **AssemblyResolve**(App.xaml.cs):缺程序集时在 `RuntimeDir` 递归查找 —— 平台运行库 (Vpp9.0/Vm4.4)就位即可用,不装全局。 - **SetupNativeDllSearchPath**:native(halcon.dll 等)指向 `Runtime/dll/x64`。 ### 1.3 流程引擎核心概念(TeamAAS.FlowEngine) | 概念 | 类型 | 说明 | |---|---|---| | 流程图 | `FlowGraph` | 节点+连线;组合模块持有 `SubGraph` | | 节点 | `FlowNode` | PluginId→插件实例;Registry=流程结果注册表 | | 插件契约 | `IFlowNodePlugin` | InitRun/PluginRun/DeclareOutputs/GetModel | | 插件基类 | `BasePlugin` | Model 挂配置([Serializable] 随 .aas 序列化);RegistryOrDebug 取公式值 | | 输出声明 | `OutputField(Key, Type)` | **Key**(不是 Name)——下游公式 `&{节点.Key}` 绑定 | | 执行器 | `FlowExecutor` | SkipReset 保留输入变量;EndNodeEncountered 结束分支 | | 结果注册表 | `ResultRegistry` | SetInputVariable/GetValue;组合模块「输入.变量」注入 | | 序列化 | BinaryFormatter + `FlowSerializationBinder` | 跨程序集类型迁移按全名兜底解析 | ### 1.4 插件开发套路(新节点 5 分钟) ```csharp [Serializable] [Plugin("显示名", PluginCategory.视觉模块, typeof(MyModel), "图标Kind", NodeShape = NodeCategory.Normal, IsSubFlowNode = true, VisionCategory = VisionPlugin.检测识别, Description = "...")] public class MyPlugin : BasePlugin { public override List DeclareOutputs() { ... } // 输出契约(绑定树可见) public override NodeRunStatus PluginRun(CancellationToken ct, out Dictionary res) { ... } } ``` - 模型属性用 `PropertyGridLib` 特性标注([Category]/[DisplayName]/[Description]/[Button]/ [MultiSelect]/[NumberSlider]/[FilePath]/[CollectionEditor]/[FormulaEditor])——**属性面板零 UI 代码**; - 子流程专用节点加 `IsSubFlowNode = true`(主工具箱不显示,只进对应平台子流程编辑器); - 显示层三种形态:PropertyGrid 属性编辑 / 自定义视图(ViewType)/ 组合模块编辑器(SubGraph)。 --- ## 二、视觉平台集成经验(三平台统一模式) ### 2.1 统一模式 每个视觉平台 = **主插件工程**(引擎/显示/工具封装/容器节点)+ **分类算子工程**(`Plugins.<平台>.<功能>`), 部署到 `Runtime/Plugins/<平台目录>/`。编辑器 = FlowEditorView(子流程工具箱按平台过滤) + 右侧大显示窗 (注入替换「节点信息」区)。 ### 2.2 VisionPro(Vpp)—— 本轮踩坑最多的平台 ⚠️ **① 程序集是懒加载的** CogBlobTool 所在的 `Cognex.VisionPro.Blob.dll` 等,应用没显式用到之前不在 AppDomain, 反射按名扫描永远找不到。`FindToolType/FindControlType` 找不到时先 `EnsureCognexAssembliesLoaded()`(Assembly.Load 触发 AssemblyResolve → Runtime 递归解析)再重扫。 **部署提醒**:`Runtime/Vpp9.0` 必须含 Blob/PMAlign/Caliper 工具程序集 (本机安装的 `ReferencedAssemblies` 目录里有,bin 目录没有——这版很特殊)。 **② 显示控件 RenderEngine 必须切 GDI** `CogRecordDisplay` 默认硬件加速渲染,向日葵远程/部分驱动下**整块白屏** (本机实测:WinForms/WPF 都正常、仅 Cog 显示白屏)。修复 = `RenderEngine = CogGDIRenderEngine(1)`。 **注意该属性只能在句柄创建后读写**(否则 InvalidOperationException)。 统一入口 `CogGdiRender.Apply(control)`:递归找带 RenderEngine 属性的 Cog 控件,HandleCreated 时机切换; 动态创建的面板(如 ToolBlockEditV2 内部)用短周期 DispatcherTimer 重扫兜底。 **③ CogImage8Root 悬空指针(黑图/结果恒 0 的真根因)** ```csharp // 错误:包裹托管数组 + 立即解除固定 → GC 移动数组后读到悬空指针 root.Initialize(w, h, handle.AddrOfPinnedObject(), stride, null); grey.SetRoot(root); // handle.Free() 之后 → 全黑 // 正确:走 Bitmap 拷贝路径(该版 VisionPro 无 SetRootCopier/Freeze API,编译器证实) using (var bmp = img.ToBitmap()) return new CogImage8Grey(bmp); ``` **④ Blob 测量是 GetMeasure 机制,不是属性** `CogBlobResult` 上没有 Area/CenterMassX 属性——40 项官方测量 (CogBlobMeasureConstants:Label/Area/Perimeter/CenterMassX/Y/Inertia*/Elongation/Angle/ Acircularity*/BoundingBox*×3 族/NotClipped)全部经 `GetMeasure(枚举)` 查询, 且**必须在工具的 Measurements 页启用**,否则抛"测量未计算"。 → 终端树对这类对象注入「测量项」分支(路径 `...@Measure:Area`),取值走 GetMeasureValue。 **⑤ 结果集合的数据在方法里,不在属性里** `CogBlobResults`(Results 的类型)**零属性**,数据全在无参方法:GetBlobs()/GetBlobMeasure(...)/ CreateBlobImage()/GetTopLevelBlobs()...——树/路径解析必须支持「无参 Get*/Create* 方法段」, 否则 Results 节点必然断层。 **⑥ 编辑控件命名规则**:工具 `CogBlobTool` ↔ 编辑控件 `CogBlobEditV2`(**去掉 Tool**), 且可能在不同程序集(ToolGroup.Controls)。查找按"原名+去名"两套候选、跨程序集扫描。 **⑦ 预设输出终端(PresetTerminals)** 各工具在插件类里定义「中文名 + 属性路径」的常用结果(Blob:面积1..3/重心X2 Y2/X3 Y3; PMAlign:第 2/3 目标 分数/位置/角度;Caliper:分数),基类自动声明为输出并每次运行赋值。 **预设路径首次预勾选时逐条实测**(GetPathStrict 区分"路径不存在"与"值为 null"),只保留真实存在的。 **⑧ 终端路径体系(三方必须一致)** 树节点 Tag = 存储的勾选路径 = 运行取值路径。格式:方法段带 `()`、集合 `[n]`、 测量 `@Measure:名`。`NormalizePath` 负责旧格式(属性式)一次性迁移;GetPath 解析段名时剥 `()`、 GetMember 带"无属性时尝试同名 GetXxx() 方法"兜底。**任何一处格式漂移 = 预勾选不回显或输出 null**。 ### 2.3 HALCON(Halcon)—— 防御式集成 - **原生运行库不可用是常态**(客户机/远程环境):`HalconWindow` 用 `HOperatorSet.GetSystem("version")` 探测 → 失败显示占位文案(画布/属性编辑不受影响),**绝不闪退**; - WPF 软件渲染环境(向日葵远程 + 双显卡)下 HSmartWindowControlWPF 渲染正常(已实测), 但显示 part 依赖布局完成 —— 首帧尺寸为 0 时挂 LayoutUpdated 补做适配; - uint2/real 图像显示前按 MinMaxGray 拉伸到 byte(HALCON 显示 uint2 的 100 ≈ 全黑); - Blob 测量启用对照:HALCON 是算子参数直出(阈值分割直接给区域),无 VisionPro 式测量启用问题。 ### 2.4 VisionMaster(Vm)—— 隔离层 + 资源守护 - SDK 引用集中在 `VmSdk\`(Private=False),运行时由本机 VM 4.4 提供;所有 SDK 调用经 `VmRuntime` 适配层(LoadSolution/GetProcedure/SetInputs/Run/GetOutputs/GetLayers); - **VM 的 WPF 控件会向 Application 级资源注入字符串画刷污染 HandyControl 主题** → `VmResourceGuard`(1.5s 轮询 watchdog + 主题快照急救)+ 显示端坚持用 WinForms 渲染控件; - 方案句柄缓存 + 路径变化自动重载;输出图层发布到 VisionLayerStore 供编辑器显示。 ### 2.5 远程/显卡环境的两个全局坑(已修复并记录) 1. **所有 WPF 程序白屏** = WPF 硬件加速在远程会话失败 → `HKCU\Software\Microsoft\AvalonGraphics\DisableHWAcceleration=1`(仅 WPF 软渲染,无需重启)。 还原:`reg delete ... /v DisableHWAcceleration /f`。彻底解决 = 升级显卡驱动/向日葵。 2. **Cognex 显示白屏**(WPF 正常、仅 Cog 黑/白)= 同一类远程渲染问题 → RenderEngine 切 GDI(见 2.2②)。 --- ## 三、编辑器显示/输出体系(Vpp/Halcon/Vm 三编辑器) ### 3.1 布局 子流程编辑器(FlowEditorView,工具箱按平台过滤:Vision/VisionVpp/VisionVm 三种 Scope) + 右侧「视觉显示」面板(注入替换 FlowEditorView 的「节点信息」区,ReplaceNodeInfoPanel; 执行历史/执行结果保留)。 ### 3.2 图层总线(TeamAAS.SDK.Vision) - 节点运行 → `VisionLayerStore.Publish(LayerPublishArgs{FlowId,NodeName,RunStamp,PreferredLayer,Layers})`; - RunStamp 批次整体替换防叠加;Disposer 钩子释放平台对象(HObject 等); - `LayerComboHelper`:BuildEntries(下拉条目)/ResolveLayers(选中层+底图回退—— 区域类节点自动垫最近发布的图像层)/FindLatestImageLayer。 ### 3.3 VPP 编辑器(简化版,当前形态) - 显示端**永远跟随最新发布**(无筛选下拉——输出内容已在终端编辑器配置); - 画布点选节点 → 看该节点最新图; - 图层内容为 ICogRecord 时走 **CogRecordDisplay 原生渲染**(图像+图形叠加=官方观感)。 --- ## 四、PropertyGridLib 1.1.2 使用要点(属性面板零代码的关键) | 特性 | 用途 | 备注 | |---|---|---| | `[Button("文本", nameof(方法))]` | 属性面板按钮 | 方法 public 无参/单参;**宿主方法内自己 try/catch** | | `[MultiSelect(typeof(Provider))]` | 多选勾选列表 | Provider : IMultiSelectProvider 返回候选项;属性 List | | `[TypeConverter]` 标准值 | 单选下拉 | GetStandardValuesSupported=true;**不要依赖 context.Instance 是模型本体**(可能传包装项,用 PluginLoader.SelectFlow 兜底) | | `[CollectionEditor]` | 集合编辑弹窗 | 远程环境下弹窗可能不显示(本机实测)→ **多选优先用 [MultiSelect]** | | `[NumberSlider]` / `[FilePath]` / `[DirectoryPath]` | 滑块/路径 | | | `[FormulaEditor(typeof(Provider))]` | 公式绑定 | FormulaBound 属性 | 坑: - **[NonSerialized] 字段的初始化器在 BinaryFormatter 反序列化时不执行**(不走构造器) ——所有"运行时注入的委托"必须在使用处判空并给默认实现(NativeEditorOpener 教训); - 重写基类属性访问修饰符要一致(protected virtual → public override 会 CS0507); - 集合编辑器弹窗在部分远程环境不显示 → 复杂选择改用 MultiSelect 或专用按钮+自绘弹窗。 --- ## 五、调试心法(本项目反复验证有效的套路) 1. **最小程序隔离**:疑难显示问题先做 10 行最小复现程序(csc 直编 + PrintWindow 截图), 把"环境问题"和"代码问题"一刀切开——本轮 VPP 白屏/HALCON 黑屏都是这样定位的; 2. **日志先行**:AppLogger 覆盖插件加载/执行失败/模块实例化失败; AssemblyScanner 实例化失败从静默改为告警后,"引擎没起来"类问题一眼可见; 3. **反射实证**:不确定第三方 API 时,PowerShell 反射列出真实成员(注意挂 AssemblyResolve 且防递归栈溢出)——CogBlobMeasureConstants/GetMeasure 就是这么发现的; 4. **编码坑**:PowerShell 跑中文 ps1 必须 ASCII 或 BOM;cmd 批处理路径反斜杠; 元数据字符串是 UTF-16(字节搜索中文串要转 UTF-16 序列); 5. **编译**:Git Bash 不可用时的替代链 = node_repl → 写 build.cmd → cmd /c 后台执行 → 轮询 result.txt;MSBuild 路径用 vswhere/目录探测(VS 18 Insiders); 6. **每次改完必做**:构建 → 产物时间戳/符号校验 → 重启程序。 --- ## 六、遗留事项 / 下一步建议 - git 分支提交(shell 恢复后 `git checkout -b feature/modularization`,工作区改动全量入库); - `ExE.zip`(167MB,用户自建)确认后删除; - Vp 预设终端按需扩充(每个工具先在原生界面启用对应 Measurements); - FindCircle/FindLine 暂无额外预设;PMAlign 多目标预设引用 Results[1..2],越界输出 null 属正常; - Halcon/VM 编辑器若也要"去筛选下拉、跟随最新运行",参照 VppFlowEditorView 简化版套用; - 部署清单:Vpp9.0 必须含 Blob/PMAlign/Caliper 工具程序集;桌面副本记得同步重打包。