using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using Cognex.VisionPro; using PropertyGridLib.Attributes; using PropertyGridLib.Controls; using TeamAAS.FlowEditor.Execution; using TeamAAS.FlowEditor.Models; using TeamAAS.FlowEditor.Plugins; using TeamAAS.FlowEngine.FormulaData; using TeamAAS.Vision; namespace Plugins.Vpp { // ───────────────────────────────────────────────────────────────────── // VPP(VisionPro)流程容器 —— 镜像 HalconFlowPlugin 的"平台子流程"模式: // 主流程拖一个「VPP流程」容器节点,双击进入 VPP 专用子流程编辑器(工具箱只显 // Vpp模块 的算子),配置输入/输出映射后循环执行子图。 // ───────────────────────────────────────────────────────────────────── /// /// VPP 编辑预览挂接点:容器节点运行前开启新一轮运行批次(换 RunStamp + 清图层总线, /// 只保留当前运行的图层),Vp 算子节点运行后发布图层,VPP 流程编辑器右侧大显示窗消费。 /// public static class VppEditorPreview { private static long _runSeq; /// 当前运行批次号(同批次重复发布=整体替换)。 public static string RunStamp { get; private set; } = "0"; /// 容器节点新一轮运行开始:批次号+1 并清空图层总线。 public static void BeginRunCycle() { _runSeq++; RunStamp = _runSeq.ToString(); VisionLayerStore.Instance.ClearAll(); } /// /// 发布工具的【运行记录】为图层集 —— 镜像 VisionPro 原生下拉的体验: /// 一条记录一个图层(InputImage/Histogram/BlobImage/BlobImageUnfiltered...), /// 图层内容就是 ICogRecord 本身,编辑器选中后交给 Cognex 原生显示控件渲染 /// (图像与图形叠加按 VisionPro 的方式原样呈现)。 /// 返回:图层名 → 记录对象(保持树序;供节点作为输出往下游传递 + 模型学习图层清单)。 /// public static Dictionary ShowRecord(BasePluginModel model, object rootRecord, string preferredLayer = null) { var layerContents = new Dictionary(StringComparer.OrdinalIgnoreCase); if (model == null || rootRecord == null) return layerContents; try { var args = new LayerPublishArgs { FlowId = model.FlowId, NodeName = model.NodeName, RunStamp = RunStamp, PreferredLayer = string.IsNullOrWhiteSpace(preferredLayer) ? null : preferredLayer, }; var seen = new HashSet(); WalkRecord(rootRecord, null, 0, args, seen); foreach (var l in args.Layers) layerContents[l.Name] = l.Content; if (args.Layers.Count > 0) VisionLayerStore.Instance.Publish(args); } catch { /* 图层发布失败不影响流程 */ } return layerContents; } /// 递归把记录树展开为图层(根记录在最前 = 工具默认合成视图;子记录按路径命名防重名)。 /// 注意 ICogRecord 的名字属性是 RecordKey(即 VisionPro 下拉里的 "InputImage" 等显示名)。 private static void WalkRecord(object recObj, string parentPath, int depth, LayerPublishArgs args, HashSet seen) { var rec = recObj as ICogRecord; if (rec == null || !seen.Add(rec) || depth > 3) return; if (depth > 0) { var leaf = string.IsNullOrWhiteSpace(rec.RecordKey) ? "图层" + (args.Layers.Count + 1) : rec.RecordKey.Trim(); var name = depth == 1 ? leaf : parentPath + "." + leaf; if (args.Layers.Any(l => string.Equals(l.Name, name, StringComparison.OrdinalIgnoreCase))) name = name + " (" + args.Layers.Count + ")"; var isImage = rec.Content is ICogImage || (rec.ContentType != null && typeof(ICogImage).IsAssignableFrom(rec.ContentType)); args.Layers.Add(new LayerItem(name, isImage ? LayerKind.Image : LayerKind.Other, rec, owned: false)); } var subs = rec.SubRecords; if (subs == null) return; var path = depth == 0 ? (rec.RecordKey ?? "LastRun") : (parentPath + "." + (rec.RecordKey ?? "N")); foreach (ICogRecord sub in subs) WalkRecord(sub, path, depth + 1, args, seen); } } /// /// VPP 流程容器节点。镜像 HalconFlowPlugin 的行为: /// 配置输入映射(把主流程图像等注入子流程)→ 运行 VPP 子流程(可循环)→ 收集输出映射回主流程。 /// 双击打开 :一个只含 VPP 算子(按 VisionPlugin 分类)的流程编辑器。 /// [Serializable] [Plugin("VPP流程", PluginCategory.Vpp模块, typeof(Models.VppFlowModel), typeof(Views.VppFlowEditorView), "ImageFilterCenterFocus", NodeShape = NodeCategory.Group, Description = "VisionPro 视觉子流程容器 - 图像经输入映射进入,内部用 VPP 算子处理,结果映射回主流程")] public class VppFlowPlugin : BasePlugin { public override List DeclareOutputs() { var list = new List(); if (Model?.ModuleOutputs != null) { foreach (var kv in Model.ModuleOutputs) { if (string.IsNullOrWhiteSpace(kv.OutName)) continue; list.Add(new OutputField(kv.OutName, typeof(object))); } } list.Add(new OutputField("实际循环次数", typeof(int))); list.Add(new OutputField("循环设置", typeof(string))); list.Add(new OutputField("结果", typeof(bool))); list.Add(new OutputField("Message", typeof(string))); return list; } public override bool InitPlugin() { Model.EnsureSubGraph(); return true; } [Newtonsoft.Json.JsonIgnore] public int CurrentCount { get; set; } = 0; public override NodeRunStatus PluginRun(CancellationToken token, out Dictionary results) { var res = new Dictionary(); results = res; Model.EnsureSubGraph(); NodeRunStatus Fail(string message) { res["Error"] = message; res["结果"] = false; Log(1, $"VPP 流程失败: {message}", TeamAAS.LogLevel.Error); return NodeRunStatus.Failed; } if (Model.LoopCount == 0) { Log(2, "循环次数为 0,跳过 VPP 流程执行"); res["结果"] = true; res["实际循环次数"] = 0; res["循环设置"] = "0"; return NodeRunStatus.Skipped; } if (Model.SubGraph.Nodes.Count == 0) return Fail("VPP 子流程为空(双击节点进入编辑器拖入 VPP 算子)"); Log(3, $"VPP 流程开始,循环设置={(Model.LoopCount == -1 ? "无限" : Model.LoopCount.ToString())},输入 {Model.ModuleInputs?.Count ?? 0} 个、输出 {Model.ModuleOutputs?.Count ?? 0} 个、子节点 {Model.SubGraph.Nodes.Count} 个"); Model.IsBreakRequested = false; int maxLoops = Model.LoopCount == -1 ? int.MaxValue : Model.LoopCount; int actualLoops = 0; // 新一轮运行:换图层批次并清掉上一轮遗留图层(显示窗只保留当前运行的图层) VppEditorPreview.BeginRunCycle(); try { for (int i = 0; i < maxLoops; i++) { if (token.IsCancellationRequested) break; CurrentCount = i + 1; foreach (var node in Model.SubGraph.Nodes) { node.Status = NodeRunStatus.NotStarted; node.CostTime = 0; } // ResetAllNodes 会 ClearFlow 清掉子流程所有结果(含刚注册的输入), // 所以用 SkipReset=true 保留输入变量;节点状态由上面的循环手动复位(与 GroupPlugin 一致) RegisterInputs(); var executor = new FlowExecutor(Model.SubGraph) { SkipReset = true }; executor.EndNodeEncountered += () => { Model.IsBreakRequested = true; }; executor.ExecuteAsync(token).Wait(); actualLoops++; if (Model.IsBreakRequested) { res["Message"] = $"第 {i + 1} 次循环遇到结束节点"; Log(2, $"第 {i + 1} 次循环遇到结束节点,跳出循环"); break; } } // 收集输出:把子流程内部节点结果映射回主流程 SaveOutputs(res); res["实际循环次数"] = actualLoops; res["循环设置"] = Model.LoopCount == -1 ? "无限" : Model.LoopCount.ToString(); if (token.IsCancellationRequested) { Log(1, $"VPP 流程被取消,已执行 {actualLoops} 次循环", TeamAAS.LogLevel.Warning); res["结果"] = false; return NodeRunStatus.Failed; } res["结果"] = true; Log(2, $"VPP 流程执行完成,实际循环 {actualLoops} 次"); return NodeRunStatus.Success; } catch (Exception ex) { return Fail(ex.Message); } } /// /// 把 ModuleInputs 映射到子流程注册表的「输入」区,内部节点通过 &{输入.变量名} 引用。 /// private void RegisterInputs() { if (Model?.ModuleInputs == null || Model.ModuleInputs.Count == 0) return; var subGraphId = Model.SubGraph.GraphId; var flowId = GetModel.FlowId; foreach (var input in Model.ModuleInputs) { if (string.IsNullOrWhiteSpace(input.InputName)) continue; if (input.SourceValue == null) continue; object value = null; if (!string.IsNullOrWhiteSpace(input.SourceValue.Formula)) value = RegistryOrDebug.ResolveFormula(flowId, input.SourceValue.Formula); else value = input.SourceValue.Value; RegistryOrDebug.SetInputVariable(subGraphId, input.InputName, value); Log(4, $"输入[{input.InputName}] ← {(value?.GetType().Name ?? "null")}"); } } /// 把每个 ModuleOutput 的公式在子流程内解析后写入结果(键=OutName)。 private void SaveOutputs(Dictionary results) { if (Model?.ModuleOutputs == null) return; foreach (var kv in Model.ModuleOutputs) { if (string.IsNullOrWhiteSpace(kv.OutName)) continue; object value = ResolveFromSubGraph(kv.OutValue?.Formula, kv.OutValue?.Value); results[kv.OutName] = value; Log(4, $"输出[{kv.OutName}] = {(value?.ToString() ?? "null")}"); } } /// /// 解析 &{节点名.属性名}(从子流程节点本地 RawResults 读), /// 或 &{流程名.节点名.属性名}(回退到注册表);无公式时返回 fallback。 /// private object ResolveFromSubGraph(string formula, object fallback) { if (string.IsNullOrEmpty(formula)) return fallback; var m2 = Regex.Match(formula, @"^&\{(?[^.}]+)\.(?[^.}]+)\}$"); if (m2.Success) { var nodeName = m2.Groups["node"].Value; var propName = m2.Groups["prop"].Value; // 「输入」伪节点直接查注册表 if (nodeName == "输入") return RegistryOrDebug.GetValue(Model.SubGraph.GraphId, "输入", propName); var node = Model.SubGraph?.Nodes?.FirstOrDefault(n => n.NodeName == nodeName); if (node?.RawResults != null && node.RawResults.TryGetValue(propName, out var val)) return val; return null; } var m3 = Regex.Match(formula, @"^&\{(?[^.}]+)\.(?[^.}]+)\.(?[^.}]+)\}$"); if (m3.Success) return RegistryOrDebug.GetValue(m3.Groups["flow"].Value, m3.Groups["node"].Value, m3.Groups["prop"].Value); return formula; } } } namespace Plugins.Vpp.Models { using TeamAAS.FlowEditor.Plugins; /// /// VPP 流程容器节点模型。镜像 HalconFlowModel:持有一张子流程图 , /// 支持把主流程前序节点的输出(尤其是图像)映射进来,并把子流程结果映射回主流程。 /// 子流程内部的节点是 VPP 专用算子(按 VisionPlugin 分类),不含主流程节点。 /// [Serializable] public class VppFlowModel : BasePluginModel { [Category("I.参数")] [DisplayName("1.输入参数")] [Description("把主流程前序节点的输出映射进来(图像通常映射为「图像」);子流程内部节点用 &{输入.变量名} 引用")] [CollectionEditor(CanAdd = true, CanRemove = true, CanSort = true)] public List ModuleInputs { get; set; } = new List(); [Category("I.参数")] [DisplayName("2.输出参数")] [Description("把子流程内部节点的结果映射回主流程(如分数、位置、OK/NG)")] [CollectionEditor(CanAdd = true, CanRemove = true, CanSort = true)] public List ModuleOutputs { get; set; } = new List(); private int _loopCount = 1; [Category("II.循环设置")] [DisplayName("1.循环次数")] [Description("循环次数。-1=无限循环, 0=跳过不执行, 其他=按次数执行")] public int LoopCount { get => _loopCount; set => _loopCount = value; } [Browsable(false)] public FlowGraph SubGraph { get; set; } [Browsable(false)] public bool IsBreakRequested { get; set; } = false; public void EnsureSubGraph() { if (SubGraph == null) SubGraph = new FlowGraph { GraphName = ToolName ?? "VPP流程", GraphId = NodeId }; } } /// 容器输入映射项:主流程来源值 → 子流程内 &{输入.InputName}。 [Serializable] public class VppFlowInput { public VppFlowInput() { SourceValue = new FormulaBound(); } [Category("输入参数")] [DisplayName("变量名")] [Description("自定义变量名,内部节点通过 &{输入.变量名} 引用(图像一般命名为「图像」)")] public string InputName { get; set; } = "图像"; [Category("输入参数")] [DisplayName("来源值")] [Description("从主流程前序节点的输出结果中选择(运行时自动读取最新值)")] [FormulaEditor(typeof(InputSourceDataProvider))] public FormulaBound SourceValue { get; set; } public override string ToString() => InputName; } /// 容器输出映射项:子流程内 &{节点名.属性名} → 主流程 OutName。 [Serializable] public class VppFlowOutput { public VppFlowOutput() { OutValue = new FormulaBound(); } private string _outName = string.Empty; [Category("输出参数")] [DisplayName("参数名")] [Description("输出到主流程的参数名(下游节点据此绑定)")] public string OutName { get => _outName; set => _outName = value; } [Category("输出参数")] [DisplayName("参数值")] [Description("从子流程内部节点的输出中选择(如 &{Vp模板匹配1.分数}、&{Vp找圆1.半径})")] [FormulaEditor(typeof(PluginDataProvider))] public FormulaBound OutValue { get; set; } public override string ToString() => OutName; } }