VmFlowNodes.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Linq;
  5. using System.Text.RegularExpressions;
  6. using System.Threading;
  7. using PropertyGridLib.Attributes;
  8. using PropertyGridLib.Controls;
  9. using TeamAAS.FlowEditor.Execution;
  10. using TeamAAS.FlowEditor.Models;
  11. using TeamAAS.FlowEditor.Plugins;
  12. using TeamAAS.FlowEngine.FormulaData;
  13. namespace Plugins.Vm
  14. {
  15. // ─────────────────────────────────────────────────────────────────────
  16. // VM(VisionMaster)流程容器 —— 镜像 HalconFlowPlugin/VppFlowPlugin 的"平台子流程"模式:
  17. // 主流程拖一个「VM流程」容器节点,双击进入 VM 专用子流程编辑器(工具箱只显
  18. // Vm模块 的算子),配置输入/输出映射后循环执行子图。
  19. // ─────────────────────────────────────────────────────────────────────
  20. /// <summary>
  21. /// VM 流程容器节点。镜像 HalconFlowPlugin 的行为:
  22. /// 配置输入映射(把主流程图像等注入子流程)→ 运行 VM 子流程(可循环)→ 收集输出映射回主流程。
  23. /// 双击打开 <see cref="Views.VmFlowEditorView"/>:一个只含 VM 算子(按 VisionPlugin 分类)的流程编辑器。
  24. /// </summary>
  25. [Serializable]
  26. [Plugin("VM流程", PluginCategory.Vm模块, typeof(Models.VmFlowModel), typeof(Views.VmFlowEditorView),
  27. "CameraIris", NodeShape = NodeCategory.Group,
  28. Description = "VisionMaster 视觉子流程容器 - 图像经输入映射进入,内部用 VM 模块算子处理,结果映射回主流程")]
  29. public class VmFlowPlugin : BasePlugin<Models.VmFlowModel>
  30. {
  31. public override List<OutputField> DeclareOutputs()
  32. {
  33. var list = new List<OutputField>();
  34. if (Model?.ModuleOutputs != null)
  35. {
  36. foreach (var kv in Model.ModuleOutputs)
  37. {
  38. if (string.IsNullOrWhiteSpace(kv.OutName)) continue;
  39. list.Add(new OutputField(kv.OutName, typeof(object)));
  40. }
  41. }
  42. list.Add(new OutputField("实际循环次数", typeof(int)));
  43. list.Add(new OutputField("循环设置", typeof(string)));
  44. list.Add(new OutputField("结果", typeof(bool)));
  45. list.Add(new OutputField("Message", typeof(string)));
  46. return list;
  47. }
  48. public override bool InitPlugin()
  49. {
  50. Model.EnsureSubGraph();
  51. return true;
  52. }
  53. [Newtonsoft.Json.JsonIgnore]
  54. public int CurrentCount { get; set; } = 0;
  55. public override NodeRunStatus PluginRun(CancellationToken token, out Dictionary<string, object> results)
  56. {
  57. var res = new Dictionary<string, object>();
  58. results = res;
  59. Model.EnsureSubGraph();
  60. NodeRunStatus Fail(string message)
  61. {
  62. res["Error"] = message;
  63. res["结果"] = false;
  64. Log(1, $"VM 流程失败: {message}", TeamAAS.LogLevel.Error);
  65. return NodeRunStatus.Failed;
  66. }
  67. if (Model.LoopCount == 0)
  68. {
  69. Log(2, "循环次数为 0,跳过 VM 流程执行");
  70. res["结果"] = true;
  71. res["实际循环次数"] = 0;
  72. res["循环设置"] = "0";
  73. return NodeRunStatus.Skipped;
  74. }
  75. if (Model.SubGraph.Nodes.Count == 0)
  76. return Fail("VM 子流程为空(双击节点进入编辑器拖入 VM 模块算子)");
  77. Log(3, $"VM 流程开始,循环设置={(Model.LoopCount == -1 ? "无限" : Model.LoopCount.ToString())},输入 {Model.ModuleInputs?.Count ?? 0} 个、输出 {Model.ModuleOutputs?.Count ?? 0} 个、子节点 {Model.SubGraph.Nodes.Count} 个");
  78. Model.IsBreakRequested = false;
  79. int maxLoops = Model.LoopCount == -1 ? int.MaxValue : Model.LoopCount;
  80. int actualLoops = 0;
  81. try
  82. {
  83. for (int i = 0; i < maxLoops; i++)
  84. {
  85. if (token.IsCancellationRequested) break;
  86. CurrentCount = i + 1;
  87. foreach (var node in Model.SubGraph.Nodes)
  88. {
  89. node.Status = NodeRunStatus.NotStarted;
  90. node.CostTime = 0;
  91. }
  92. // ResetAllNodes 会 ClearFlow 清掉子流程所有结果(含刚注册的输入),
  93. // 所以用 SkipReset=true 保留输入变量;节点状态由上面的循环手动复位(与 GroupPlugin 一致)
  94. RegisterInputs();
  95. var executor = new FlowExecutor(Model.SubGraph) { SkipReset = true };
  96. executor.EndNodeEncountered += () => { Model.IsBreakRequested = true; };
  97. executor.ExecuteAsync(token).Wait();
  98. actualLoops++;
  99. if (Model.IsBreakRequested)
  100. {
  101. res["Message"] = $"第 {i + 1} 次循环遇到结束节点";
  102. Log(2, $"第 {i + 1} 次循环遇到结束节点,跳出循环");
  103. break;
  104. }
  105. }
  106. // 收集输出:把子流程内部节点结果映射回主流程
  107. SaveOutputs(res);
  108. res["实际循环次数"] = actualLoops;
  109. res["循环设置"] = Model.LoopCount == -1 ? "无限" : Model.LoopCount.ToString();
  110. if (token.IsCancellationRequested)
  111. {
  112. Log(1, $"VM 流程被取消,已执行 {actualLoops} 次循环", TeamAAS.LogLevel.Warning);
  113. res["结果"] = false;
  114. return NodeRunStatus.Failed;
  115. }
  116. res["结果"] = true;
  117. Log(2, $"VM 流程执行完成,实际循环 {actualLoops} 次");
  118. return NodeRunStatus.Success;
  119. }
  120. catch (Exception ex)
  121. {
  122. return Fail(ex.Message);
  123. }
  124. }
  125. /// <summary>
  126. /// 把 ModuleInputs 映射到子流程注册表的「输入」区,内部节点通过 &amp;{输入.变量名} 引用。
  127. /// </summary>
  128. private void RegisterInputs()
  129. {
  130. if (Model?.ModuleInputs == null || Model.ModuleInputs.Count == 0) return;
  131. var subGraphId = Model.SubGraph.GraphId;
  132. var flowId = GetModel.FlowId;
  133. foreach (var input in Model.ModuleInputs)
  134. {
  135. if (string.IsNullOrWhiteSpace(input.InputName)) continue;
  136. if (input.SourceValue == null) continue;
  137. object value = null;
  138. if (!string.IsNullOrWhiteSpace(input.SourceValue.Formula))
  139. value = RegistryOrDebug.ResolveFormula(flowId, input.SourceValue.Formula);
  140. else
  141. value = input.SourceValue.Value;
  142. RegistryOrDebug.SetInputVariable(subGraphId, input.InputName, value);
  143. Log(4, $"输入[{input.InputName}] ← {(value?.GetType().Name ?? "null")}");
  144. }
  145. }
  146. /// <summary>把每个 ModuleOutput 的公式在子流程内解析后写入结果(键=OutName)。</summary>
  147. private void SaveOutputs(Dictionary<string, object> results)
  148. {
  149. if (Model?.ModuleOutputs == null) return;
  150. foreach (var kv in Model.ModuleOutputs)
  151. {
  152. if (string.IsNullOrWhiteSpace(kv.OutName)) continue;
  153. object value = ResolveFromSubGraph(kv.OutValue?.Formula, kv.OutValue?.Value);
  154. results[kv.OutName] = value;
  155. Log(4, $"输出[{kv.OutName}] = {(value?.ToString() ?? "null")}");
  156. }
  157. }
  158. /// <summary>
  159. /// 解析 &amp;{节点名.属性名}(从子流程节点本地 RawResults 读),
  160. /// 或 &amp;{流程名.节点名.属性名}(回退到注册表);无公式时返回 fallback。
  161. /// </summary>
  162. private object ResolveFromSubGraph(string formula, object fallback)
  163. {
  164. if (string.IsNullOrEmpty(formula)) return fallback;
  165. var m2 = Regex.Match(formula, @"^&\{(?<node>[^.}]+)\.(?<prop>[^.}]+)\}$");
  166. if (m2.Success)
  167. {
  168. var nodeName = m2.Groups["node"].Value;
  169. var propName = m2.Groups["prop"].Value;
  170. // 「输入」伪节点直接查注册表
  171. if (nodeName == "输入")
  172. return RegistryOrDebug.GetValue(Model.SubGraph.GraphId, "输入", propName);
  173. var node = Model.SubGraph?.Nodes?.FirstOrDefault(n => n.NodeName == nodeName);
  174. if (node?.RawResults != null && node.RawResults.TryGetValue(propName, out var val))
  175. return val;
  176. return null;
  177. }
  178. var m3 = Regex.Match(formula, @"^&\{(?<flow>[^.}]+)\.(?<node>[^.}]+)\.(?<prop>[^.}]+)\}$");
  179. if (m3.Success)
  180. return RegistryOrDebug.GetValue(m3.Groups["flow"].Value, m3.Groups["node"].Value, m3.Groups["prop"].Value);
  181. return formula;
  182. }
  183. }
  184. }
  185. namespace Plugins.Vm.Models
  186. {
  187. using TeamAAS.FlowEditor.Plugins;
  188. /// <summary>
  189. /// VM 流程容器节点模型。镜像 HalconFlowModel:持有一张子流程图 <see cref="SubGraph"/>,
  190. /// 支持把主流程前序节点的输出(尤其是图像)映射进来,并把子流程结果映射回主流程。
  191. /// 子流程内部的节点是 VM 专用算子(按 VisionPlugin 分类),不含主流程节点。
  192. /// </summary>
  193. [Serializable]
  194. public class VmFlowModel : BasePluginModel
  195. {
  196. [Category("I.参数")]
  197. [DisplayName("1.输入参数")]
  198. [Description("把主流程前序节点的输出映射进来(图像通常映射为「图像」);子流程内部节点用 &{输入.变量名} 引用")]
  199. [CollectionEditor(CanAdd = true, CanRemove = true, CanSort = true)]
  200. public List<VmFlowInput> ModuleInputs { get; set; } = new List<VmFlowInput>();
  201. [Category("I.参数")]
  202. [DisplayName("2.输出参数")]
  203. [Description("把子流程内部节点的结果映射回主流程(如结果键值、OK/NG)")]
  204. [CollectionEditor(CanAdd = true, CanRemove = true, CanSort = true)]
  205. public List<VmFlowOutput> ModuleOutputs { get; set; } = new List<VmFlowOutput>();
  206. private int _loopCount = 1;
  207. [Category("II.循环设置")]
  208. [DisplayName("1.循环次数")]
  209. [Description("循环次数。-1=无限循环, 0=跳过不执行, 其他=按次数执行")]
  210. public int LoopCount
  211. {
  212. get => _loopCount;
  213. set => _loopCount = value;
  214. }
  215. [Browsable(false)]
  216. public FlowGraph SubGraph { get; set; }
  217. [Browsable(false)]
  218. public bool IsBreakRequested { get; set; } = false;
  219. public void EnsureSubGraph()
  220. {
  221. if (SubGraph == null)
  222. SubGraph = new FlowGraph { GraphName = ToolName ?? "VM流程", GraphId = NodeId };
  223. }
  224. }
  225. /// <summary>容器输入映射项:主流程来源值 → 子流程内 &amp;{输入.InputName}。</summary>
  226. [Serializable]
  227. public class VmFlowInput
  228. {
  229. public VmFlowInput()
  230. {
  231. SourceValue = new FormulaBound<object>();
  232. }
  233. [Category("输入参数")]
  234. [DisplayName("变量名")]
  235. [Description("自定义变量名,内部节点通过 &{输入.变量名} 引用(图像一般命名为「图像」)")]
  236. public string InputName { get; set; } = "图像";
  237. [Category("输入参数")]
  238. [DisplayName("来源值")]
  239. [Description("从主流程前序节点的输出结果中选择(运行时自动读取最新值)")]
  240. [FormulaEditor(typeof(InputSourceDataProvider))]
  241. public FormulaBound<object> SourceValue { get; set; }
  242. public override string ToString() => InputName;
  243. }
  244. /// <summary>容器输出映射项:子流程内 &amp;{节点名.属性名} → 主流程 OutName。</summary>
  245. [Serializable]
  246. public class VmFlowOutput
  247. {
  248. public VmFlowOutput()
  249. {
  250. OutValue = new FormulaBound<object>();
  251. }
  252. private string _outName = string.Empty;
  253. [Category("输出参数")]
  254. [DisplayName("参数名")]
  255. [Description("输出到主流程的参数名(下游节点据此绑定)")]
  256. public string OutName
  257. {
  258. get => _outName;
  259. set => _outName = value;
  260. }
  261. [Category("输出参数")]
  262. [DisplayName("参数值")]
  263. [Description("从子流程内部节点的输出中选择(如 &{VM模块1.分数}、&{VM模块1.结果})")]
  264. [FormulaEditor(typeof(PluginDataProvider))]
  265. public FormulaBound<object> OutValue { get; set; }
  266. public override string ToString() => OutName;
  267. }
  268. }