EXPERIENCE.md 16 KB

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<TModel> Model 挂配置([Serializable] 随 .aas 序列化);RegistryOrDebug 取公式值
输出声明 OutputField(Key, Type) Key(不是 Name)——下游公式 &{节点.Key} 绑定
执行器 FlowExecutor SkipReset 保留输入变量;EndNodeEncountered 结束分支
结果注册表 ResultRegistry SetInputVariable/GetValue;组合模块「输入.变量」注入
序列化 BinaryFormatter + FlowSerializationBinder 跨程序集类型迁移按全名兜底解析

1.4 插件开发套路(新节点 5 分钟)

[Serializable]
[Plugin("显示名", PluginCategory.视觉模块, typeof(MyModel), "图标Kind",
    NodeShape = NodeCategory.Normal, IsSubFlowNode = true, VisionCategory = VisionPlugin.检测识别,
    Description = "...")]
public class MyPlugin : BasePlugin<MyModel>
{
    public override List<OutputField> DeclareOutputs() { ... }   // 输出契约(绑定树可见)
    public override NodeRunStatus PluginRun(CancellationToken ct, out Dictionary<string, object> 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 的真根因)

// 错误:包裹托管数组 + 立即解除固定 → 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)—— 防御式集成

  • 原生运行库不可用是常态(客户机/远程环境):HalconWindowHOperatorSet.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 使用要点(属性面板零代码的关键)

坑:

  • [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 工具程序集;桌面副本记得同步重打包。
特性 用途 备注
[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 属性