From 5000ccd1bafb6389afcf47c3a65c4c45d947090b Mon Sep 17 00:00:00 2001 From: Tyrone CT Date: Wed, 25 Mar 2026 10:31:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=8F=AF=E4=BB=A5=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=EF=BC=8C=E6=9C=AA=E4=B8=A5=E6=A0=BC=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .windsurf/rules/pro1.md | 14 + HkVisionPro.App/App.config | 32 +- .../AutoAssemblyWorkflowController.cs | 805 ++++++++++++++++++ HkVisionPro.App/HkVisionPro.App.csproj | 1 + HkVisionPro.App/MainForm.Designer.cs | 93 +- HkVisionPro.App/MainForm.cs | 595 +++++++++++-- 6 files changed, 1443 insertions(+), 97 deletions(-) create mode 100644 .windsurf/rules/pro1.md create mode 100644 HkVisionPro.App/Automation/AutoAssemblyWorkflowController.cs diff --git a/.windsurf/rules/pro1.md b/.windsurf/rules/pro1.md new file mode 100644 index 0000000..8b5bced --- /dev/null +++ b/.windsurf/rules/pro1.md @@ -0,0 +1,14 @@ +--- +trigger: always_on +--- +## 我先开发一个视觉的检测系统,使用海康的硬件和算子二次开发 + +### 被检测的产品放到工位上然后工人进行组装,安装一些部件,此时调用A层(底板) 的VisionMaster解决方案的A流程进行实时检测(具体检测内容由A流程进行定义),工人所有部件安装完毕后并且A流程能检测到的结果是所有都安装OK时,视觉检测都OK的状态维持2秒,代表A层板部件安装和检测完毕,进行提示,程序切入VisionMaster解决方案的B流程(具体检测内容由B流程进行定义)进行实时检测,工人放盖板到B层继续部件安装,工人所有部件安装完毕后并且B流程能检测到的结果是所有都安装OK时,视觉检测都OK的状态维持2秒,代表B层板部件安装和检测完毕,进行提示,程序切入VisionMaster解决方案的C流程,依次类推,C层板部件安装和检测OK完毕,整个产品组装完毕,总结果提示OK,循环进入下一个产品的组装。 + +### 如果A流程的部件安装没有完成(那么视觉检测A流程的结果肯定也是NG),是不允许进入B流程的,即是上一个流程没完成不允许进入下一个流程,整个过程在不出现异常时是自动运行的 + +### 流程管理我使用Stateless状态机进行管理 + +### VisionMaster解决方案里面我会按照约定放置好A B C 三个流程,供程序调用 + +### 不要把逻辑放到Main界面中,界面里面很多的按钮代表手动的已经开发好了,自动的按照上面描述的流程进行开发 diff --git a/HkVisionPro.App/App.config b/HkVisionPro.App/App.config index b1ac8fd..a604f5c 100644 --- a/HkVisionPro.App/App.config +++ b/HkVisionPro.App/App.config @@ -5,16 +5,28 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/HkVisionPro.App/Automation/AutoAssemblyWorkflowController.cs b/HkVisionPro.App/Automation/AutoAssemblyWorkflowController.cs new file mode 100644 index 0000000..5109fff --- /dev/null +++ b/HkVisionPro.App/Automation/AutoAssemblyWorkflowController.cs @@ -0,0 +1,805 @@ +using System; +using Stateless; +using VM.PlatformSDKCS; +using VM.Core; + +namespace HkVisionPro.App.Automation +{ + /// + /// 自动装配流程状态枚举。 + /// + public enum AutoAssemblyState + { + /// + /// 空闲状态,尚未启动自动流程。 + /// + Idle, + + /// + /// A 层检测中。 + /// + LayerAInspecting, + + /// + /// B 层检测中。 + /// + LayerBInspecting, + + /// + /// C 层检测中。 + /// + LayerCInspecting, + + /// + /// 单件产品 A/B/C 全部通过。 + /// + ProductCompleted, + + /// + /// 自动流程已停止。 + /// + Stopped, + + /// + /// 自动流程故障状态。 + /// + Faulted, + } + + /// + /// 自动装配流程状态通知事件参数。 + /// + public sealed class AutoAssemblyStatusChangedEventArgs : EventArgs + { + /// + /// 当前状态。 + /// + public AutoAssemblyState State { get; } + + /// + /// 状态说明文本。 + /// + public string Message { get; } + + /// + /// 当前产品序号(从 1 开始)。 + /// + public int ProductIndex { get; } + + /// + /// 当前激活流程名称。 + /// + public string ActiveProcedureName { get; } + + /// + /// 当前稳定计时已累计毫秒数。 + /// + public int StableElapsedMilliseconds { get; } + + /// + /// 稳定计时目标毫秒数。 + /// + public int StableTargetMilliseconds { get; } + + /// + /// 状态变化事件参数构造函数。 + /// + /// 当前状态。 + /// 状态说明。 + /// 当前产品序号(从 1 开始)。 + /// 当前流程名称。 + /// 当前稳定计时累计毫秒数。 + /// 稳定计时目标毫秒数。 + public AutoAssemblyStatusChangedEventArgs( + AutoAssemblyState state, + string message, + int productIndex = 0, + string activeProcedureName = "", + int stableElapsedMilliseconds = 0, + int stableTargetMilliseconds = 0) + { + State = state; + Message = message ?? string.Empty; + ProductIndex = productIndex < 0 ? 0 : productIndex; + ActiveProcedureName = activeProcedureName ?? string.Empty; + StableElapsedMilliseconds = stableElapsedMilliseconds < 0 ? 0 : stableElapsedMilliseconds; + StableTargetMilliseconds = stableTargetMilliseconds < 0 ? 0 : stableTargetMilliseconds; + } + } + + /// + /// 自动装配流程配置参数。 + /// + public sealed class AutoAssemblyOptions + { + /// + /// A 层流程名称。 + /// + public string ProcedureAName { get; set; } + + /// + /// B 层流程名称。 + /// + public string ProcedureBName { get; set; } + + /// + /// C 层流程名称。 + /// + public string ProcedureCName { get; set; } + + /// + /// 判定 OK 的整型输出名。 + /// + public string OkIntOutputName { get; set; } + + /// + /// 判定 OK 的第二个整型输出名(当配置后,需与第一个整型输出同时满足目标值才判定为 OK)。 + /// + public string OkIntOutputName2 { get; set; } + + /// + /// 判定 OK 的整型目标值。 + /// + public int OkIntValue { get; set; } + + /// + /// OK 持续稳定时间(毫秒)。 + /// + public int StableOkMilliseconds { get; set; } + + /// + /// 校验配置参数有效性。 + /// + public void Validate() + { + if (string.IsNullOrWhiteSpace(ProcedureAName) || string.IsNullOrWhiteSpace(ProcedureBName) || string.IsNullOrWhiteSpace(ProcedureCName)) + { + throw new ArgumentException("自动装配流程名称配置无效,请检查 A/B/C 流程名称。", nameof(AutoAssemblyOptions)); + } + + if (string.IsNullOrWhiteSpace(OkIntOutputName) || string.IsNullOrWhiteSpace(OkIntOutputName2)) + { + throw new ArgumentException("自动装配判定输出配置无效,请检查 Result1/Result2 输出名称。", nameof(AutoAssemblyOptions)); + } + + if (StableOkMilliseconds <= 0) + { + throw new ArgumentOutOfRangeException(nameof(StableOkMilliseconds), "稳定判定时长必须大于 0。"); + } + } + + } + + /// + /// 自动装配流程状态机触发器。 + /// + internal enum AutoAssemblyTrigger + { + /// + /// 启动自动流程。 + /// + Start, + + /// + /// 当前层流程结果持续 OK 达标。 + /// + StableOkReached, + + /// + /// 停止自动流程。 + /// + Stop, + + /// + /// 单件产品完成后进入下一件。 + /// + NextProduct, + + /// + /// 自动流程异常。 + /// + Fault, + } + + /// + /// 自动装配流程控制器: + /// 基于 Stateless 状态机实现 A -> B -> C 顺序检测,并以“连续 OK 达标时长”驱动层间切换。 + /// + public sealed class AutoAssemblyWorkflowController : IDisposable + { + /// + /// 并发互斥锁。 + /// + private readonly object _syncRoot = new object(); + + /// + /// 自动流程配置。 + /// + private readonly AutoAssemblyOptions _options; + + /// + /// 日志输出委托。 + /// + private readonly Action _logger; + + /// + /// 外部渲染回调(可选)。 + /// + private readonly Action _renderResultAction; + + /// + /// Stateless 状态机。 + /// + private readonly StateMachine _stateMachine; + + /// + /// 当前激活流程。 + /// + private VmProcedure _activeProcedure; + + /// + /// 当前流程首次出现 OK 的起始时间(UTC)。 + /// + private DateTime _okBeginUtc; + + /// + /// 当前是否处于 OK 稳定计时中。 + /// + private bool _isOkTiming; + + /// + /// 控制器是否处于运行中。 + /// + private bool _isRunning; + + /// + /// 控制器是否已释放。 + /// + private bool _isDisposed; + + /// + /// 最近一次“输出缺失”日志时间(用于限流)。 + /// + private DateTime _lastMissingOutputLogUtc; + + /// + /// 最近一次稳定计时进度上报时间(用于限流)。 + /// + private DateTime _lastStableProgressReportUtc; + + /// + /// VisionMaster: 模块已处于连续执行中的错误码(IMVS_EC_MODULE_CONTINUE_EXECUTE)。 + /// + private const int VmErrorCodeModuleContinueExecute = -536870127; + + /// + /// 当前产品序号(从 1 开始)。 + /// + private int _currentProductIndex; + + /// + /// 状态变化事件。 + /// + public event EventHandler StatusChanged; + + /// + /// 当前状态。 + /// + public AutoAssemblyState CurrentState => _stateMachine.State; + + /// + /// 是否正在自动运行。 + /// + public bool IsRunning => _isRunning; + + /// + /// 自动装配流程控制器构造函数。 + /// + /// 自动流程参数。 + /// 日志输出委托(可为空)。 + /// 渲染回调(可为空)。 + public AutoAssemblyWorkflowController( + AutoAssemblyOptions options, + Action logger, + Action renderResultAction) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _options.Validate(); + + _logger = logger; + _renderResultAction = renderResultAction; + _okBeginUtc = DateTime.MinValue; + _lastMissingOutputLogUtc = DateTime.MinValue; + _lastStableProgressReportUtc = DateTime.MinValue; + _currentProductIndex = 1; + + _stateMachine = new StateMachine(AutoAssemblyState.Idle); + ConfigureStateMachine(); + } + + /// + /// 启动自动装配流程。 + /// + public void Start() + { + lock (_syncRoot) + { + ThrowIfDisposed(); + + if (_isRunning) + { + return; + } + + _currentProductIndex = 1; + _okBeginUtc = DateTime.MinValue; + _isOkTiming = false; + _lastStableProgressReportUtc = DateTime.MinValue; + _isRunning = true; + try + { + FireIfPermitted(AutoAssemblyTrigger.Start); + } + catch + { + // 启动阶段若状态机进入失败,需回滚运行状态,避免外部误判“已运行”。 + _isRunning = false; + DeactivateCurrentProcedure("启动失败回滚"); + throw; + } + } + } + + /// + /// 停止自动装配流程。 + /// + /// 停止原因。 + public void Stop(string reason) + { + lock (_syncRoot) + { + if (_isDisposed) + { + return; + } + + if (!_isRunning) + { + return; + } + + FireIfPermitted(AutoAssemblyTrigger.Stop); + _isRunning = false; + PublishStatus(AutoAssemblyState.Stopped, $"自动流程停止:{reason}"); + } + } + + /// + /// 释放控制器资源。 + /// + public void Dispose() + { + lock (_syncRoot) + { + if (_isDisposed) + { + return; + } + + try + { + Stop("控制器释放"); + DeactivateCurrentProcedure("控制器释放"); + } + finally + { + _isDisposed = true; + } + } + } + + /// + /// 配置状态机。 + /// + private void ConfigureStateMachine() + { + _stateMachine.Configure(AutoAssemblyState.Idle) + .Permit(AutoAssemblyTrigger.Start, AutoAssemblyState.LayerAInspecting) + .Permit(AutoAssemblyTrigger.Stop, AutoAssemblyState.Stopped); + + ConfigureLayerState(AutoAssemblyState.LayerAInspecting, _options.ProcedureAName, "A层", AutoAssemblyState.LayerBInspecting); + ConfigureLayerState(AutoAssemblyState.LayerBInspecting, _options.ProcedureBName, "B层", AutoAssemblyState.LayerCInspecting); + ConfigureLayerState(AutoAssemblyState.LayerCInspecting, _options.ProcedureCName, "C层", AutoAssemblyState.ProductCompleted); + + _stateMachine.Configure(AutoAssemblyState.ProductCompleted) + .OnEntry(() => + { + PublishStatus(AutoAssemblyState.ProductCompleted, "整机检测结果:OK,进入下一产品。"); + _currentProductIndex++; + if (_isRunning) + { + FireIfPermitted(AutoAssemblyTrigger.NextProduct); + } + }) + .Permit(AutoAssemblyTrigger.NextProduct, AutoAssemblyState.LayerAInspecting) + .Permit(AutoAssemblyTrigger.Stop, AutoAssemblyState.Stopped) + .Permit(AutoAssemblyTrigger.Fault, AutoAssemblyState.Faulted); + + _stateMachine.Configure(AutoAssemblyState.Stopped) + .OnEntry(() => + { + _isRunning = false; + DeactivateCurrentProcedure("状态机停止"); + }) + .Permit(AutoAssemblyTrigger.Start, AutoAssemblyState.LayerAInspecting); + + _stateMachine.Configure(AutoAssemblyState.Faulted) + .OnEntry(() => + { + _isRunning = false; + DeactivateCurrentProcedure("状态机故障"); + PublishStatus(AutoAssemblyState.Faulted, "自动流程发生异常,已停止。"); + }) + .Permit(AutoAssemblyTrigger.Start, AutoAssemblyState.LayerAInspecting) + .Permit(AutoAssemblyTrigger.Stop, AutoAssemblyState.Stopped); + } + + /// + /// 配置层级检测状态。 + /// + /// 状态机状态。 + /// 流程名称。 + /// 层级显示名。 + /// 连续 OK 达标后的下一状态。 + private void ConfigureLayerState( + AutoAssemblyState state, + string procedureName, + string layerDisplayName, + AutoAssemblyState nextState) + { + _stateMachine.Configure(state) + .OnEntry(() => ActivateLayerProcedure(state, procedureName, layerDisplayName)) + .OnExit(() => DeactivateCurrentProcedure($"离开{layerDisplayName}检测")) + .Permit(AutoAssemblyTrigger.StableOkReached, nextState) + .Permit(AutoAssemblyTrigger.Stop, AutoAssemblyState.Stopped) + .Permit(AutoAssemblyTrigger.Fault, AutoAssemblyState.Faulted); + } + + /// + /// 进入层级检测状态时激活对应流程。 + /// + /// 目标状态。 + /// 流程名称。 + /// 层级显示名。 + private void ActivateLayerProcedure(AutoAssemblyState state, string procedureName, string layerDisplayName) + { + if (string.IsNullOrWhiteSpace(procedureName)) + { + throw new InvalidOperationException($"{layerDisplayName}流程名称为空,无法启动自动检测。"); + } + + DeactivateCurrentProcedure($"切换到{layerDisplayName}"); + + var procedure = VmSolution.Instance[procedureName] as VmProcedure; + if (procedure == null) + { + throw new InvalidOperationException($"未找到自动流程:{procedureName}({layerDisplayName})。"); + } + + _activeProcedure = procedure; + _okBeginUtc = DateTime.MinValue; + _isOkTiming = false; + _lastStableProgressReportUtc = DateTime.MinValue; + + _activeProcedure.OnWorkEndStatusCallBack -= ActiveProcedure_OnWorkEndStatusCallBack; + _activeProcedure.OnWorkEndStatusCallBack += ActiveProcedure_OnWorkEndStatusCallBack; + _activeProcedure.ContinuousRunEnable = true; + try + { + _activeProcedure.Run(); + } + catch (Exception ex) + { + if (IsModuleContinueExecuteVmException(ex)) + { + WriteLog($"自动流程[{procedureName}]已处于连续执行状态,沿用现有执行链路。\n若需重启请先停止当前连续执行。"); + } + else + { + throw; + } + } + + PublishStatus(state, $"{layerDisplayName}检测中(连续OK保持{_options.StableOkMilliseconds}ms后切换)。"); + WriteLog($"自动流程切换到{layerDisplayName}:{procedureName}"); + } + + /// + /// 关闭当前激活流程并解绑回调。 + /// + /// 关闭原因。 + private void DeactivateCurrentProcedure(string reason) + { + if (_activeProcedure == null) + { + _okBeginUtc = DateTime.MinValue; + _isOkTiming = false; + _lastStableProgressReportUtc = DateTime.MinValue; + return; + } + + try + { + _activeProcedure.OnWorkEndStatusCallBack -= ActiveProcedure_OnWorkEndStatusCallBack; + _activeProcedure.ContinuousRunEnable = false; + WriteLog($"自动流程停用:{_activeProcedure.Name}({reason})"); + } + catch (Exception ex) + { + WriteLog($"停用自动流程异常:{ex.Message}"); + } + finally + { + _activeProcedure = null; + _okBeginUtc = DateTime.MinValue; + _isOkTiming = false; + _lastStableProgressReportUtc = DateTime.MinValue; + } + } + + /// + /// 流程执行结束回调:用于判定 OK 稳定时长,并驱动状态机切换。 + /// + /// 回调事件源。 + /// 标准事件参数。 + private void ActiveProcedure_OnWorkEndStatusCallBack(object sender, EventArgs e) + { + lock (_syncRoot) + { + if (_isDisposed || !_isRunning) + { + return; + } + + var procedure = sender as VmProcedure; + if (procedure == null || !ReferenceEquals(procedure, _activeProcedure)) + { + return; + } + + try + { + _renderResultAction?.Invoke(procedure, $"自动流程[{GetStateDisplayText(_stateMachine.State)}]"); + + var isOk = EvaluateProcedureResultIsOk(procedure, out var sourceName, out var sourceValue); + if (!isOk) + { + if (_isOkTiming) + { + _isOkTiming = false; + _okBeginUtc = DateTime.MinValue; + _lastStableProgressReportUtc = DateTime.MinValue; + PublishStatus(_stateMachine.State, $"{GetStateDisplayText(_stateMachine.State)}检测 NG({sourceName}={sourceValue}),继续等待。"); + } + + return; + } + + if (!_isOkTiming) + { + _isOkTiming = true; + _okBeginUtc = DateTime.UtcNow; + _lastStableProgressReportUtc = DateTime.UtcNow; + PublishStatus(_stateMachine.State, $"{GetStateDisplayText(_stateMachine.State)}检测 OK,开始稳定计时:0/{_options.StableOkMilliseconds}ms。"); + return; + } + + var elapsedMs = (DateTime.UtcNow - _okBeginUtc).TotalMilliseconds; + if (elapsedMs < _options.StableOkMilliseconds) + { + if ((DateTime.UtcNow - _lastStableProgressReportUtc).TotalMilliseconds >= 500) + { + _lastStableProgressReportUtc = DateTime.UtcNow; + var currentMs = (int)Math.Max(0, elapsedMs); + PublishStatus( + _stateMachine.State, + $"{GetStateDisplayText(_stateMachine.State)}检测 OK,稳定计时:{currentMs}/{_options.StableOkMilliseconds}ms。", + false); + } + + return; + } + + _isOkTiming = false; + _okBeginUtc = DateTime.MinValue; + _lastStableProgressReportUtc = DateTime.MinValue; + PublishStatus(_stateMachine.State, $"{GetStateDisplayText(_stateMachine.State)}检测连续 OK 达标,准备切换下一层。"); + FireIfPermitted(AutoAssemblyTrigger.StableOkReached); + } + catch (Exception ex) + { + WriteLog($"自动流程回调异常:{ex.Message}"); + FireIfPermitted(AutoAssemblyTrigger.Fault); + } + } + } + + /// + /// 判定当前流程结果是否为 OK。 + /// 判定规则:Result1 和 Result2 两个整型输出都等于目标值,才判定为 OK。 + /// + /// 流程对象。 + /// 命中的输出名。 + /// 命中的输出值。 + /// 是否判定为 OK。 + private bool EvaluateProcedureResultIsOk(VmProcedure procedure, out string sourceName, out string sourceValue) + { + sourceName = string.Empty; + sourceValue = string.Empty; + + if (procedure == null) + { + return false; + } + + var hasFirstInt = TryReadOutputInt(procedure, _options.OkIntOutputName, out var firstIntValue); + var secondIntValue = 0; + var hasSecondInt = TryReadOutputInt(procedure, _options.OkIntOutputName2, out secondIntValue); + + sourceName = $"{_options.OkIntOutputName}/{_options.OkIntOutputName2}"; + sourceValue = $"{(hasFirstInt ? firstIntValue.ToString() : "")}/{(hasSecondInt ? secondIntValue.ToString() : "")}"; + if (hasFirstInt && hasSecondInt) + { + return firstIntValue == _options.OkIntValue && secondIntValue == _options.OkIntValue; + } + + if ((DateTime.UtcNow - _lastMissingOutputLogUtc).TotalSeconds >= 5) + { + _lastMissingOutputLogUtc = DateTime.UtcNow; + WriteLog($"自动流程未读到判定输出,已尝试 Int[{_options.OkIntOutputName}/{_options.OkIntOutputName2}]。"); + } + + return false; + } + + /// + /// 尝试读取整型输出。 + /// + /// 流程对象。 + /// 输出名。 + /// 输出值。 + /// 读取是否成功。 + private static bool TryReadOutputInt(VmProcedure procedure, string outputName, out int value) + { + value = 0; + + if (procedure == null || string.IsNullOrWhiteSpace(outputName)) + { + return false; + } + + try + { + var output = procedure.ModuResult.GetOutputInt(outputName); + if (output.pIntVal == null || output.pIntVal.Length == 0) + { + return false; + } + + value = output.pIntVal[0]; + return true; + } + catch + { + return false; + } + } + + /// + /// 判断异常是否为“模块已处于连续执行”错误。 + /// + /// 异常对象。 + /// 若是连续执行中错误则返回 true。 + private static bool IsModuleContinueExecuteVmException(Exception ex) + { + if (ex == null) + { + return false; + } + + var vmException = VmExceptionTool.GetVmException(ex); + return vmException != null && vmException.errorCode == VmErrorCodeModuleContinueExecute; + } + + /// + /// 触发状态更新事件。 + /// + /// 状态。 + /// 消息。 + /// 是否写入日志。 + private void PublishStatus(AutoAssemblyState state, string message, bool writeLog = true) + { + var elapsedMs = 0; + if (_isOkTiming && _okBeginUtc != DateTime.MinValue) + { + elapsedMs = (int)Math.Max(0, (DateTime.UtcNow - _okBeginUtc).TotalMilliseconds); + if (_options.StableOkMilliseconds > 0) + { + elapsedMs = Math.Min(elapsedMs, _options.StableOkMilliseconds); + } + } + + var statusArgs = new AutoAssemblyStatusChangedEventArgs( + state, + message, + _currentProductIndex, + _activeProcedure?.Name ?? string.Empty, + elapsedMs, + _options.StableOkMilliseconds); + + StatusChanged?.Invoke(this, statusArgs); + if (writeLog) + { + WriteLog(message); + } + } + + /// + /// 依据状态返回中文显示文本。 + /// + /// 状态枚举。 + /// 中文描述。 + private static string GetStateDisplayText(AutoAssemblyState state) + { + switch (state) + { + case AutoAssemblyState.LayerAInspecting: + return "A层"; + case AutoAssemblyState.LayerBInspecting: + return "B层"; + case AutoAssemblyState.LayerCInspecting: + return "C层"; + case AutoAssemblyState.ProductCompleted: + return "整机"; + case AutoAssemblyState.Faulted: + return "故障"; + case AutoAssemblyState.Stopped: + return "已停止"; + default: + return "空闲"; + } + } + + /// + /// 若当前状态允许指定触发器,则触发状态机。 + /// + /// 触发器。 + private void FireIfPermitted(AutoAssemblyTrigger trigger) + { + if (_stateMachine.CanFire(trigger)) + { + _stateMachine.Fire(trigger); + } + } + + /// + /// 输出日志(允许日志委托为空)。 + /// + /// 日志内容。 + private void WriteLog(string message) + { + _logger?.Invoke(message); + } + + /// + /// 已释放保护。 + /// + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(AutoAssemblyWorkflowController)); + } + } + } +} diff --git a/HkVisionPro.App/HkVisionPro.App.csproj b/HkVisionPro.App/HkVisionPro.App.csproj index 1c84c66..9162cf4 100644 --- a/HkVisionPro.App/HkVisionPro.App.csproj +++ b/HkVisionPro.App/HkVisionPro.App.csproj @@ -585,6 +585,7 @@ + Form diff --git a/HkVisionPro.App/MainForm.Designer.cs b/HkVisionPro.App/MainForm.Designer.cs index 08b86a2..88ffca6 100644 --- a/HkVisionPro.App/MainForm.Designer.cs +++ b/HkVisionPro.App/MainForm.Designer.cs @@ -40,6 +40,7 @@ this.label4 = new System.Windows.Forms.Label(); this.vmProcedureConfigControl1 = new VMControls.Winform.Release.VmProcedureConfigControl(); this.groupBox2 = new System.Windows.Forms.GroupBox(); + this.btnAutoRun = new System.Windows.Forms.Button(); this.btnProExecStop = new System.Windows.Forms.Button(); this.btnProExecAlway = new System.Windows.Forms.Button(); this.btnProExecOnce = new System.Windows.Forms.Button(); @@ -57,17 +58,20 @@ this.pictureBox1 = new System.Windows.Forms.PictureBox(); this.groupBox6 = new System.Windows.Forms.GroupBox(); this.vmParamsConfigControl1 = new VMControls.Winform.Release.VmParamsConfigControl(); + this.groupBox5 = new System.Windows.Forms.GroupBox(); + this.lblRunState = new System.Windows.Forms.Label(); this.groupBox1.SuspendLayout(); this.groupBox2.SuspendLayout(); this.groupBox3.SuspendLayout(); this.groupBox4.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); this.groupBox6.SuspendLayout(); + this.groupBox5.SuspendLayout(); this.SuspendLayout(); // // btnLoadSolution // - this.btnLoadSolution.Location = new System.Drawing.Point(103, 93); + this.btnLoadSolution.Location = new System.Drawing.Point(114, 52); this.btnLoadSolution.Name = "btnLoadSolution"; this.btnLoadSolution.Size = new System.Drawing.Size(127, 38); this.btnLoadSolution.TabIndex = 0; @@ -77,24 +81,25 @@ // // txtSolutionAddress // - this.txtSolutionAddress.Location = new System.Drawing.Point(103, 20); + this.txtSolutionAddress.Location = new System.Drawing.Point(101, 23); this.txtSolutionAddress.Name = "txtSolutionAddress"; this.txtSolutionAddress.Size = new System.Drawing.Size(393, 23); this.txtSolutionAddress.TabIndex = 2; // // btnSelctedSolution // - this.btnSelctedSolution.Location = new System.Drawing.Point(369, 49); + this.btnSelctedSolution.BackColor = System.Drawing.Color.Transparent; + this.btnSelctedSolution.Location = new System.Drawing.Point(10, 52); this.btnSelctedSolution.Name = "btnSelctedSolution"; - this.btnSelctedSolution.Size = new System.Drawing.Size(127, 38); + this.btnSelctedSolution.Size = new System.Drawing.Size(100, 38); this.btnSelctedSolution.TabIndex = 3; this.btnSelctedSolution.Text = "选择文件方案"; - this.btnSelctedSolution.UseVisualStyleBackColor = true; + this.btnSelctedSolution.UseVisualStyleBackColor = false; this.btnSelctedSolution.Click += new System.EventHandler(this.btnSelctedSolution_Click); // // btnExecutSolution // - this.btnExecutSolution.Location = new System.Drawing.Point(236, 93); + this.btnExecutSolution.Location = new System.Drawing.Point(244, 52); this.btnExecutSolution.Name = "btnExecutSolution"; this.btnExecutSolution.Size = new System.Drawing.Size(127, 38); this.btnExecutSolution.TabIndex = 4; @@ -104,7 +109,7 @@ // // btnSaveSolution // - this.btnSaveSolution.Location = new System.Drawing.Point(369, 93); + this.btnSaveSolution.Location = new System.Drawing.Point(372, 52); this.btnSaveSolution.Name = "btnSaveSolution"; this.btnSaveSolution.Size = new System.Drawing.Size(127, 38); this.btnSaveSolution.TabIndex = 5; @@ -120,9 +125,9 @@ this.groupBox1.Controls.Add(this.btnExecutSolution); this.groupBox1.Controls.Add(this.txtSolutionAddress); this.groupBox1.Controls.Add(this.btnLoadSolution); - this.groupBox1.Location = new System.Drawing.Point(882, 4); + this.groupBox1.Location = new System.Drawing.Point(882, 58); this.groupBox1.Name = "groupBox1"; - this.groupBox1.Size = new System.Drawing.Size(505, 144); + this.groupBox1.Size = new System.Drawing.Size(505, 105); this.groupBox1.TabIndex = 6; this.groupBox1.TabStop = false; this.groupBox1.Text = "方案操作"; @@ -130,7 +135,7 @@ // label3 // this.label3.AutoSize = true; - this.label3.Location = new System.Drawing.Point(30, 23); + this.label3.Location = new System.Drawing.Point(28, 26); this.label3.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); this.label3.Name = "label3"; this.label3.Size = new System.Drawing.Size(68, 17); @@ -159,16 +164,16 @@ // // vmProcedureConfigControl1 // - this.vmProcedureConfigControl1.Dock = System.Windows.Forms.DockStyle.Left; - this.vmProcedureConfigControl1.Location = new System.Drawing.Point(20, 60); + this.vmProcedureConfigControl1.Location = new System.Drawing.Point(20, 440); this.vmProcedureConfigControl1.Margin = new System.Windows.Forms.Padding(2); this.vmProcedureConfigControl1.Name = "vmProcedureConfigControl1"; - this.vmProcedureConfigControl1.Size = new System.Drawing.Size(439, 781); + this.vmProcedureConfigControl1.Size = new System.Drawing.Size(439, 440); this.vmProcedureConfigControl1.TabIndex = 7; // TODO: “”的代码生成失败,原因是出现异常“无效的基元类型: System.IntPtr。请考虑使用 CodeObjectCreateExpression。”。 // // groupBox2 // + this.groupBox2.Controls.Add(this.btnAutoRun); this.groupBox2.Controls.Add(this.cmbProcdure); this.groupBox2.Controls.Add(this.label4); this.groupBox2.Controls.Add(this.btnProExecStop); @@ -177,13 +182,23 @@ this.groupBox2.Controls.Add(this.btnProDel); this.groupBox2.Controls.Add(this.btnProExpo); this.groupBox2.Controls.Add(this.btnProImport); - this.groupBox2.Location = new System.Drawing.Point(882, 151); + this.groupBox2.Location = new System.Drawing.Point(881, 167); this.groupBox2.Name = "groupBox2"; this.groupBox2.Size = new System.Drawing.Size(505, 145); this.groupBox2.TabIndex = 7; this.groupBox2.TabStop = false; this.groupBox2.Text = "流程操作"; // + // btnAutoRun + // + this.btnAutoRun.Location = new System.Drawing.Point(11, 53); + this.btnAutoRun.Name = "btnAutoRun"; + this.btnAutoRun.Size = new System.Drawing.Size(75, 64); + this.btnAutoRun.TabIndex = 9; + this.btnAutoRun.Text = "自动运行"; + this.btnAutoRun.UseVisualStyleBackColor = true; + this.btnAutoRun.Click += new System.EventHandler(this.btnAutoRun_Click); + // // btnProExecStop // this.btnProExecStop.Location = new System.Drawing.Point(372, 97); @@ -251,7 +266,7 @@ this.groupBox3.Controls.Add(this.btnModuleRenderResult); this.groupBox3.Controls.Add(this.btnModuleExec); this.groupBox3.Controls.Add(this.btnModuleBindingPar); - this.groupBox3.Location = new System.Drawing.Point(882, 302); + this.groupBox3.Location = new System.Drawing.Point(881, 314); this.groupBox3.Name = "groupBox3"; this.groupBox3.Size = new System.Drawing.Size(505, 112); this.groupBox3.TabIndex = 9; @@ -311,19 +326,19 @@ // this.listBox1.FormattingEnabled = true; this.listBox1.ItemHeight = 17; - this.listBox1.Location = new System.Drawing.Point(882, 422); + this.listBox1.Location = new System.Drawing.Point(882, 431); this.listBox1.Name = "listBox1"; - this.listBox1.Size = new System.Drawing.Size(516, 429); + this.listBox1.Size = new System.Drawing.Size(516, 446); this.listBox1.TabIndex = 10; // // groupBox4 // this.groupBox4.Controls.Add(this.pictureBox1); - this.groupBox4.Location = new System.Drawing.Point(463, 2); + this.groupBox4.Location = new System.Drawing.Point(463, 58); this.groupBox4.Margin = new System.Windows.Forms.Padding(2); this.groupBox4.Name = "groupBox4"; this.groupBox4.Padding = new System.Windows.Forms.Padding(2); - this.groupBox4.Size = new System.Drawing.Size(414, 412); + this.groupBox4.Size = new System.Drawing.Size(414, 376); this.groupBox4.TabIndex = 11; this.groupBox4.TabStop = false; this.groupBox4.Text = "视图窗口"; @@ -333,18 +348,18 @@ this.pictureBox1.Dock = System.Windows.Forms.DockStyle.Fill; this.pictureBox1.Location = new System.Drawing.Point(2, 18); this.pictureBox1.Name = "pictureBox1"; - this.pictureBox1.Size = new System.Drawing.Size(410, 392); + this.pictureBox1.Size = new System.Drawing.Size(410, 356); this.pictureBox1.TabIndex = 0; this.pictureBox1.TabStop = false; // // groupBox6 // this.groupBox6.Controls.Add(this.vmParamsConfigControl1); - this.groupBox6.Location = new System.Drawing.Point(463, 422); + this.groupBox6.Location = new System.Drawing.Point(463, 432); this.groupBox6.Margin = new System.Windows.Forms.Padding(2); this.groupBox6.Name = "groupBox6"; this.groupBox6.Padding = new System.Windows.Forms.Padding(2); - this.groupBox6.Size = new System.Drawing.Size(409, 439); + this.groupBox6.Size = new System.Drawing.Size(409, 447); this.groupBox6.TabIndex = 12; this.groupBox6.TabStop = false; this.groupBox6.Text = "参数配置"; @@ -357,14 +372,35 @@ this.vmParamsConfigControl1.ModuleSource = null; this.vmParamsConfigControl1.Name = "vmParamsConfigControl1"; this.vmParamsConfigControl1.ParamsConfig = null; - this.vmParamsConfigControl1.Size = new System.Drawing.Size(405, 419); + this.vmParamsConfigControl1.Size = new System.Drawing.Size(405, 427); this.vmParamsConfigControl1.TabIndex = 0; // + // groupBox5 + // + this.groupBox5.Controls.Add(this.lblRunState); + this.groupBox5.Location = new System.Drawing.Point(23, 58); + this.groupBox5.Name = "groupBox5"; + this.groupBox5.Size = new System.Drawing.Size(435, 374); + this.groupBox5.TabIndex = 13; + this.groupBox5.TabStop = false; + this.groupBox5.Text = "自动运行结果"; + // + // lblRunState + // + this.lblRunState.AutoSize = true; + this.lblRunState.Font = new System.Drawing.Font("微软雅黑", 21.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lblRunState.Location = new System.Drawing.Point(32, 85); + this.lblRunState.Name = "lblRunState"; + this.lblRunState.Size = new System.Drawing.Size(133, 39); + this.lblRunState.TabIndex = 0; + this.lblRunState.Text = "运行状态"; + // // MainForm // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 17F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(1400, 861); + this.ClientSize = new System.Drawing.Size(1400, 900); + this.Controls.Add(this.groupBox5); this.Controls.Add(this.groupBox6); this.Controls.Add(this.groupBox4); this.Controls.Add(this.listBox1); @@ -376,7 +412,9 @@ this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.Margin = new System.Windows.Forms.Padding(4); this.Name = "MainForm"; - this.Text = "主窗体"; + this.Style = ReaLTaiizor.Enum.Poison.ColorStyle.Black; + this.Text = "机器视觉系统ORPAON-v26.1"; + this.Theme = ReaLTaiizor.Enum.Poison.ThemeStyle.Default; this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); this.Load += new System.EventHandler(this.MainForm_Load); this.groupBox1.ResumeLayout(false); @@ -388,6 +426,8 @@ this.groupBox4.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit(); this.groupBox6.ResumeLayout(false); + this.groupBox5.ResumeLayout(false); + this.groupBox5.PerformLayout(); this.ResumeLayout(false); } @@ -422,6 +462,9 @@ private System.Windows.Forms.ComboBox cmbProcdure; private System.Windows.Forms.Label label4; private System.Windows.Forms.PictureBox pictureBox1; + private System.Windows.Forms.GroupBox groupBox5; + private System.Windows.Forms.Label lblRunState; + private System.Windows.Forms.Button btnAutoRun; } } diff --git a/HkVisionPro.App/MainForm.cs b/HkVisionPro.App/MainForm.cs index e865cb4..3628676 100644 --- a/HkVisionPro.App/MainForm.cs +++ b/HkVisionPro.App/MainForm.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Configuration; +using HkVisionPro.App.Automation; using ReaLTaiizor.Enum.Poison; using ReaLTaiizor.Forms; using ReaLTaiizor.Manager; @@ -86,30 +87,30 @@ namespace HkVisionPro.App /// private bool _isClosingResourcesReleased; - /// - /// 流程字符串输出名称(支持配置,默认 out)。 - /// - private readonly string _procedureOutputCodeName; - - /// - /// 流程整数输出名称(支持配置,默认 out0)。 - /// - private readonly string _procedureOutputNumberName; - /// /// 渲染图像输出名称(支持配置,默认 Image)。 /// private readonly string _renderImageOutputName; /// - /// 渲染 ROI 输出名称(支持配置,默认 ROI)。 + /// 渲染主 ROI 输出名称(支持配置,默认 InputR1)。 /// - private readonly string _renderRoiOutputName; + private readonly string _renderRoiOutputName1; /// - /// 渲染文本输出名称(支持配置,默认 code)。 + /// 渲染次 ROI 输出名称(支持配置,默认 InputR2)。 /// - private readonly string _renderCodeOutputName; + private readonly string _renderRoiOutputName2; + + /// + /// 渲染判定结果1整型输出名称(支持配置,默认 Result1)。 + /// + private readonly string _renderResultIntOutputName1; + + /// + /// 渲染判定结果2整型输出名称(支持配置,默认 Result2)。 + /// + private readonly string _renderResultIntOutputName2; /// /// VisionMaster: 方案正在加载中的错误码(IMVS_EC_SOLUTION_LOADING)。 @@ -121,6 +122,21 @@ namespace HkVisionPro.App /// private const int VmErrorCodeModuleContinueExecute = -536870127; + /// + /// 自动装配状态机配置。 + /// + private readonly AutoAssemblyOptions _autoAssemblyOptions; + + /// + /// 自动装配流程控制器(Stateless 状态机)。 + /// + private readonly AutoAssemblyWorkflowController _autoAssemblyController; + + /// + /// 方案加载完成后是否自动启动自动装配流程。 + /// + private readonly bool _autoRunEnabledAfterSolutionLoaded; + /// /// ReaLTaiizor Poison 样式管理器(用于统一暗色主题风格元数据)。 /// @@ -171,11 +187,20 @@ namespace HkVisionPro.App InitializeComponent(); _poisonStyleManager = components == null ? new PoisonStyleManager() : new PoisonStyleManager(components); _solutionManager = new SolutionManager(); - _procedureOutputCodeName = GetAppSettingOrDefault("ProcedureOutputCodeName", "out"); - _procedureOutputNumberName = GetAppSettingOrDefault("ProcedureOutputNumberName", "out0"); - _renderImageOutputName = GetAppSettingOrDefault("RenderImageOutputName", "Image"); - _renderRoiOutputName = GetAppSettingOrDefault("RenderRoiOutputName", "ROI"); - _renderCodeOutputName = GetAppSettingOrDefault("RenderCodeOutputName", "code"); + _renderImageOutputName = GetAppSettingOrDefault("RenderImageOutputName", "ImageData0"); + _renderRoiOutputName1 = GetAppSettingOrDefault("RenderRoiOutputName", "InputR1"); + _renderRoiOutputName2 = GetAppSettingOrDefault("RenderRoiOutputName2", "InputR2"); + _renderResultIntOutputName1 = GetAppSettingOrDefault("RenderResultIntOutputName1", "Result1"); + _renderResultIntOutputName2 = GetAppSettingOrDefault("RenderResultIntOutputName2", "Result2"); + _autoAssemblyOptions = BuildAutoAssemblyOptions(); + _autoRunEnabledAfterSolutionLoaded = GetAppSettingBoolOrDefault("AutoRunEnabledAfterSolutionLoaded", true); + + _autoAssemblyController = new AutoAssemblyWorkflowController( + _autoAssemblyOptions, + WriteLog, + (procedure, triggerSource) => RenderProcedureResultToPictureBox(procedure, triggerSource)); + + _autoAssemblyController.StatusChanged += AutoAssemblyController_StatusChanged; RegisterSolutionCallbacks(); RegisterVmSolutionCallbacks(); @@ -187,11 +212,276 @@ namespace HkVisionPro.App pictureBox1.BackColor = Color.Black; InitializeDarkTheme(); + InitializeAutoRunStatusPanel(); UpdateUiState(false); txtSolutionAddress.Text = string.Empty; } + /// + /// 初始化自动运行结果显示面板。 + /// + private void InitializeAutoRunStatusPanel() + { + lblRunState.AutoSize = false; + lblRunState.Dock = DockStyle.Fill; + lblRunState.TextAlign = ContentAlignment.MiddleCenter; + + var initMessage = _autoRunEnabledAfterSolutionLoaded + ? "自动流程待启动(等待方案加载)" + : "自动流程已禁用(配置开关关闭)"; + + UpdateAutoRunResultDisplay(AutoAssemblyState.Idle, initMessage, 0, string.Empty, 0, _autoAssemblyOptions.StableOkMilliseconds); + SyncAutoRunButtonState(); + } + + /// + /// 自动装配状态变化事件:刷新“自动运行结果”显示。 + /// + /// 事件源对象。 + /// 状态变化参数。 + private void AutoAssemblyController_StatusChanged(object sender, AutoAssemblyStatusChangedEventArgs e) + { + if (e == null) + { + return; + } + + SyncAutoRunButtonState(); + + UpdateAutoRunResultDisplay( + e.State, + e.Message, + e.ProductIndex, + e.ActiveProcedureName, + e.StableElapsedMilliseconds, + e.StableTargetMilliseconds); + } + + /// + /// 更新自动运行结果显示控件(线程安全)。 + /// + /// 自动流程状态。 + /// 状态描述。 + /// 当前产品序号(从 1 开始)。 + /// 当前流程名。 + /// 稳定计时已累计毫秒数。 + /// 稳定计时目标毫秒数。 + private void UpdateAutoRunResultDisplay( + AutoAssemblyState state, + string message, + int productIndex, + string activeProcedureName, + int stableElapsedMilliseconds, + int stableTargetMilliseconds) + { + if (!IsHandleCreated || IsDisposed) + { + return; + } + + void UpdateAction() + { + lblRunState.ForeColor = GetAutoRunStateColor(state); + lblRunState.Text = BuildAutoRunDisplayText( + state, + message, + productIndex, + activeProcedureName, + stableElapsedMilliseconds, + stableTargetMilliseconds); + } + + if (lblRunState.InvokeRequired) + { + lblRunState.BeginInvoke(new Action(UpdateAction)); + } + else + { + UpdateAction(); + } + } + + /// + /// 构造自动运行结果展示文本。 + /// + /// 自动流程状态。 + /// 状态描述。 + /// 当前产品序号。 + /// 当前流程名。 + /// 稳定计时累计毫秒数。 + /// 稳定计时目标毫秒数。 + /// 用于显示的多行文本。 + private static string BuildAutoRunDisplayText( + AutoAssemblyState state, + string message, + int productIndex, + string activeProcedureName, + int stableElapsedMilliseconds, + int stableTargetMilliseconds) + { + var displayProductIndex = productIndex <= 0 ? 1 : productIndex; + var displayProcedure = string.IsNullOrWhiteSpace(activeProcedureName) ? "-" : activeProcedureName; + var elapsed = Math.Max(0, stableElapsedMilliseconds); + var target = Math.Max(0, stableTargetMilliseconds); + if (target > 0) + { + elapsed = Math.Min(elapsed, target); + } + + return $"{GetAutoRunStateTitle(state)}\r\n产品序号:#{displayProductIndex}\r\n当前流程:{displayProcedure}\r\n稳定计时:{elapsed}/{target} ms\r\n状态描述:{message}"; + } + + /// + /// 获取自动状态标题文本。 + /// + /// 自动流程状态。 + /// 状态标题。 + private static string GetAutoRunStateTitle(AutoAssemblyState state) + { + switch (state) + { + case AutoAssemblyState.LayerAInspecting: + return "A层检测中"; + case AutoAssemblyState.LayerBInspecting: + return "B层检测中"; + case AutoAssemblyState.LayerCInspecting: + return "C层检测中"; + case AutoAssemblyState.ProductCompleted: + return "整机检测OK"; + case AutoAssemblyState.Faulted: + return "自动流程异常"; + case AutoAssemblyState.Stopped: + return "自动流程已停止"; + default: + return "自动流程空闲"; + } + } + + /// + /// 获取自动状态标题颜色。 + /// + /// 自动流程状态。 + /// 状态颜色。 + private static Color GetAutoRunStateColor(AutoAssemblyState state) + { + switch (state) + { + case AutoAssemblyState.LayerAInspecting: + return Color.FromArgb(64, 196, 255); + case AutoAssemblyState.LayerBInspecting: + return Color.FromArgb(255, 179, 71); + case AutoAssemblyState.LayerCInspecting: + return Color.FromArgb(170, 132, 255); + case AutoAssemblyState.ProductCompleted: + return Color.FromArgb(120, 220, 130); + case AutoAssemblyState.Faulted: + return Color.FromArgb(255, 99, 99); + case AutoAssemblyState.Stopped: + return Color.FromArgb(200, 200, 200); + default: + return DarkThemeForeColor; + } + } + + /// + /// 按配置构建自动装配流程参数。 + /// + /// 自动流程配置对象。 + private AutoAssemblyOptions BuildAutoAssemblyOptions() + { + return new AutoAssemblyOptions + { + ProcedureAName = GetAppSettingOrDefault("AutoRunProcedureAName", "A"), + ProcedureBName = GetAppSettingOrDefault("AutoRunProcedureBName", "B"), + ProcedureCName = GetAppSettingOrDefault("AutoRunProcedureCName", "C"), + OkIntOutputName = GetAppSettingOrDefault("AutoRunOkIntOutputName", _renderResultIntOutputName1), + OkIntOutputName2 = GetAppSettingOrDefault("AutoRunOkIntOutputName2", _renderResultIntOutputName2), + OkIntValue = GetAppSettingIntOrDefault("AutoRunOkIntValue", 1), + StableOkMilliseconds = GetAppSettingIntOrDefault("AutoRunStableMilliseconds", 2000), + }; + } + + /// + /// 方案加载成功后按配置启动自动装配流程。 + /// + private void StartAutoAssemblyIfConfigured() + { + if (!_autoRunEnabledAfterSolutionLoaded) + { + UpdateAutoRunResultDisplay( + AutoAssemblyState.Idle, + "自动流程已禁用(AutoRunEnabledAfterSolutionLoaded=false)", + 0, + string.Empty, + 0, + _autoAssemblyOptions.StableOkMilliseconds); + return; + } + + if (!_isSolutionLoaded) + { + return; + } + + try + { + // 切入自动模式前清理手动执行上下文,避免两套执行链路并发冲突。 + StopContinuousRunIfNeeded("切换到自动装配流程"); + UnbindProcedureWorkEndCallback(); + _currentProcedure = null; + + _autoAssemblyController.Start(); + } + catch (Exception ex) + { + HandleException("启动自动装配流程", ex); + } + } + + /// + /// 停止自动装配流程(若运行中)。 + /// + /// 停止原因。 + private void StopAutoAssemblyIfRunning(string reason) + { + try + { + _autoAssemblyController.Stop(reason); + SyncAutoRunButtonState(); + } + catch (Exception ex) + { + WriteLog($"停止自动装配流程异常:{ex.Message}"); + } + } + + /// + /// 同步自动运行按钮文本(线程安全)。 + /// 运行中显示“停止自动”,未运行显示“自动运行”。 + /// + private void SyncAutoRunButtonState() + { + if (!IsHandleCreated || IsDisposed) + { + return; + } + + void UpdateAction() + { + btnAutoRun.Text = _autoAssemblyController.IsRunning ? "停止自动" : "自动运行"; + } + + if (btnAutoRun.InvokeRequired) + { + btnAutoRun.BeginInvoke(new Action(UpdateAction)); + } + else + { + UpdateAction(); + } + } + /// /// 初始化 ReaLTaiizor 暗色主题,并将原生 WinForms 控件统一成暗色风格。 /// @@ -635,6 +925,10 @@ namespace HkVisionPro.App _activeBcrTool = null; } + StopAutoAssemblyIfRunning("窗体关闭释放资源"); + _autoAssemblyController.StatusChanged -= AutoAssemblyController_StatusChanged; + _autoAssemblyController.Dispose(); + StopContinuousRunIfNeeded("窗体关闭释放资源"); UnbindProcedureWorkEndCallback(); _currentProcedure = null; @@ -702,6 +996,7 @@ namespace HkVisionPro.App _isSolutionLoaded = true; BindFirstModuleToRenderControl(); UpdateProcedureList(); + StartAutoAssemblyIfConfigured(); WriteLog($"方案加载成功:{_currentSolutionPath}"); MessageBox.Show(this, "方案加载成功。", "加载方案", MessageBoxButtons.OK, MessageBoxIcon.Information); @@ -709,6 +1004,7 @@ namespace HkVisionPro.App else { _isSolutionLoaded = false; + StopAutoAssemblyIfRunning("方案加载失败"); ResetProcedureAndModuleState(); MessageBox.Show(this, $"方案加载失败,状态码:{endInfo.nStatus}", "加载方案", MessageBoxButtons.OK, MessageBoxIcon.Error); } @@ -716,6 +1012,7 @@ namespace HkVisionPro.App catch (Exception ex) { _isSolutionLoaded = false; + StopAutoAssemblyIfRunning("加载方案回调异常"); ResetProcedureAndModuleState(); HandleException("加载方案回调处理", ex); } @@ -831,23 +1128,29 @@ namespace HkVisionPro.App try { - // 字符串读取仅尝试字符串候选名,避免对 int 输出调用 GetOutputString 引发 SDK 内部异常。 - var code = GetProcedureOutputString(procedure, _renderCodeOutputName, _procedureOutputCodeName, "out0", "code", "Code"); - // 整型读取仅尝试整型候选名。 - var codeNum = GetProcedureOutputInt(procedure, _procedureOutputNumberName, "out"); - var bitmap = GetProcedureOutputBitmap(procedure, _renderImageOutputName, "ImageData", "Image", "image"); + var result1 = GetProcedureOutputInt(procedure, _renderResultIntOutputName1); + var result2 = GetProcedureOutputInt(procedure, _renderResultIntOutputName2); + var isResult1Ok = string.Equals(result1?.Trim(), "1", StringComparison.Ordinal); + var isResult2Ok = string.Equals(result2?.Trim(), "1", StringComparison.Ordinal); + var pairResult = isResult1Ok && isResult2Ok ? "OK" : "NG"; + var overlayText = $"R1={result1}, R2={result2}, Total={pairResult}"; + + var bitmap = GetProcedureOutputBitmap(procedure, _renderImageOutputName); if (bitmap == null) { - WriteLog($"{triggerSource}未读取到可渲染图像,输出名候选:{_renderImageOutputName}/ImageData/Image/image"); + WriteLog($"{triggerSource}未读取到可渲染图像,固定输出名:{_renderImageOutputName}"); return; } try { - var roiList = GetProcedureOutputBoxList(procedure, _renderRoiOutputName, "InputROI", "rect", "outline", "ROI", "roi"); - var renderBitmap = DrawROIOnBitmap(bitmap, roiList, Color.Cyan, 2, code); + var primaryRoiList = GetProcedureOutputBoxList(procedure, _renderRoiOutputName1); + var secondaryRoiList = GetProcedureOutputBoxList(procedure, _renderRoiOutputName2); + var mergedRoiList = MergeRoiLists(primaryRoiList, secondaryRoiList); + var roiColor = pairResult == "OK" ? Color.Lime : Color.Red; + var renderBitmap = DrawROIOnBitmap(bitmap, mergedRoiList, roiColor, 2, overlayText); SetPictureBoxImage(renderBitmap); - WriteLog($"{triggerSource}渲染完成:Code={code},CodeNumber={codeNum},ROI数={roiList.Count}"); + WriteLog($"{triggerSource}渲染完成:Result1={result1},Result2={result2},Total={pairResult},ROI数={mergedRoiList.Count}"); } finally { @@ -930,6 +1233,32 @@ namespace HkVisionPro.App return new List(); } + /// + /// 合并多组 ROI 列表,避免流程多目标输出时遗漏展示。 + /// + /// 待合并 ROI 列表集合。 + /// 合并后的 ROI 列表。 + private static List MergeRoiLists(params List[] roiGroups) + { + var merged = new List(); + if (roiGroups == null || roiGroups.Length == 0) + { + return merged; + } + + foreach (var roiGroup in roiGroups) + { + if (roiGroup == null || roiGroup.Count == 0) + { + continue; + } + + merged.AddRange(roiGroup); + } + + return merged; + } + /// /// 在图像上绘制 ROI 轮廓和识别文本。 /// @@ -1148,11 +1477,13 @@ namespace HkVisionPro.App btnModuleBindingPar.Enabled = canOperateLoadedSolution; btnModuleExec.Enabled = canOperateLoadedSolution; btnModuleRenderResult.Enabled = canOperateLoadedSolution; + btnAutoRun.Enabled = canOperateLoadedSolution; cmbProcdure.Enabled = canOperateLoadedSolution; cmbModule.Enabled = canOperateLoadedSolution; // 保持连续执行按钮文案与状态一致。 btnProExecAlway.Text = _isContinuousRunning ? "停止连续" : "连续执行"; + SyncAutoRunButtonState(); } /// @@ -1301,6 +1632,54 @@ namespace HkVisionPro.App } } + /// + /// 从 AppSettings 读取整型配置,失败时返回默认值。 + /// + /// 配置键名。 + /// 默认值。 + /// 解析后的整型值。 + private static int GetAppSettingIntOrDefault(string key, int defaultValue) + { + try + { + var value = ConfigurationManager.AppSettings[key]; + if (string.IsNullOrWhiteSpace(value)) + { + return defaultValue; + } + + return int.TryParse(value.Trim(), out var parsedValue) ? parsedValue : defaultValue; + } + catch + { + return defaultValue; + } + } + + /// + /// 从 AppSettings 读取布尔配置,失败时返回默认值。 + /// + /// 配置键名。 + /// 默认值。 + /// 解析后的布尔值。 + private static bool GetAppSettingBoolOrDefault(string key, bool defaultValue) + { + try + { + var value = ConfigurationManager.AppSettings[key]; + if (string.IsNullOrWhiteSpace(value)) + { + return defaultValue; + } + + return bool.TryParse(value.Trim(), out var parsedValue) ? parsedValue : defaultValue; + } + catch + { + return defaultValue; + } + } + /// /// 尝试获取当前选中的流程对象。 /// @@ -1411,6 +1790,7 @@ namespace HkVisionPro.App /// private void ResetProcedureAndModuleState() { + StopAutoAssemblyIfRunning("重置流程与模块状态"); StopContinuousRunIfNeeded("重置流程与模块状态"); UnbindProcedureWorkEndCallback(); _currentProcedure = null; @@ -1427,40 +1807,6 @@ namespace HkVisionPro.App ClearPictureBoxImage(); } - /// - /// 读取流程字符串输出(多候选容错读取)。 - /// - private string GetProcedureOutputString(VmProcedure procedure, params string[] outputNames) - { - if (procedure == null || outputNames == null || outputNames.Length == 0) - { - return string.Empty; - } - - foreach (var outputName in outputNames) - { - if (string.IsNullOrWhiteSpace(outputName)) - { - continue; - } - - try - { - var output = procedure.ModuResult.GetOutputString(outputName); - if (output.astStringVal != null && output.astStringVal.Length > 0) - { - return output.astStringVal[0].strValue; - } - } - catch - { - // 忽略输出不存在等异常,保持流程主链路稳定。 - } - } - - return string.Empty; - } - /// /// 读取流程整型输出(多候选容错读取)。 /// @@ -1660,6 +2006,12 @@ namespace HkVisionPro.App } } + /// + /// 导入流程按钮事件:从外部 .prc 文件导入流程到当前方案,并刷新流程列表。 + /// 该方法包含完整的前置状态检查(忙碌态、方案是否已加载)与异常处理,避免 UI 重入导致的状态错乱。 + /// + /// 事件源控件(导入按钮)。 + /// 标准事件参数。 private void btnProImport_Click(object sender, EventArgs e) { if (_isBusy) @@ -1693,6 +2045,7 @@ namespace HkVisionPro.App procedurePath = dialog.FileName; } + // 通过 VM SDK 加载流程文件到当前方案上下文。 VmProcedure.Load(procedurePath); UpdateProcedureList(); WriteLog($"流程导入成功:{procedurePath}"); @@ -1708,6 +2061,11 @@ namespace HkVisionPro.App } } + /// + /// 导出流程按钮事件:将当前选中的流程导出为独立 .prc 文件。 + /// + /// 事件源控件(导出按钮)。 + /// 标准事件参数。 private void btnProExpo_Click(object sender, EventArgs e) { if (_isBusy) @@ -1739,6 +2097,7 @@ namespace HkVisionPro.App try { UpdateUiState(true); + // 仅导出当前选中流程,不影响方案内其它流程。 procedure.SaveAs(dialog.FileName); WriteLog($"流程导出成功:{dialog.FileName}"); MessageBox.Show(this, "流程导出成功。", "导出流程", MessageBoxButtons.OK, MessageBoxIcon.Information); @@ -1754,6 +2113,11 @@ namespace HkVisionPro.App } } + /// + /// 删除流程按钮事件:删除当前选中的流程,并同步清理界面绑定状态(模块列表、参数面板、渲染图像)。 + /// + /// 事件源控件(删除按钮)。 + /// 标准事件参数。 private void btnProDel_Click(object sender, EventArgs e) { if (_isBusy) @@ -1781,6 +2145,7 @@ namespace HkVisionPro.App try { UpdateUiState(true); + // 从方案中删除流程,并清理当前流程回调绑定,避免悬挂引用。 VmSolution.Instance.DeleteOneProcedure(selectedProcedureName); UnbindProcedureWorkEndCallback(); _currentProcedure = null; @@ -1800,6 +2165,11 @@ namespace HkVisionPro.App } } + /// + /// 执行一次按钮事件:对当前选中流程执行单次运行,并在流程结束回调中刷新渲染结果。 + /// + /// 事件源控件(执行一次按钮)。 + /// 标准事件参数。 private void btnProExecOnce_Click(object sender, EventArgs e) { if (_isBusy) @@ -1820,6 +2190,7 @@ namespace HkVisionPro.App try { UpdateUiState(true); + // 执行单次前先停掉连续执行,保证流程运行模式互斥。 StopContinuousRunIfNeeded("执行单次流程"); UnbindProcedureWorkEndCallback(); _currentProcedure = procedure; @@ -1838,6 +2209,13 @@ namespace HkVisionPro.App } } + /// + /// 连续执行按钮事件(启停一体): + /// - 若当前未连续运行,则启动选中流程的连续执行; + /// - 若当前已连续运行,则先停止,再根据选择情况决定是否切换到新流程继续运行。 + /// + /// 事件源控件(连续执行按钮)。 + /// 标准事件参数。 private void btnProExecAlway_Click(object sender, EventArgs e) { if (_isBusy) @@ -1873,6 +2251,7 @@ namespace HkVisionPro.App } } + // 显式开启 SDK 连续执行标记,并重新绑定流程结束回调。 procedure.ContinuousRunEnable = true; UnbindProcedureWorkEndCallback(); _currentProcedure = procedure; @@ -1899,6 +2278,11 @@ namespace HkVisionPro.App } } + /// + /// 停止执行按钮事件:在连续执行模式下,主动停止当前流程连续运行并同步按钮状态。 + /// + /// 事件源控件(停止按钮)。 + /// 标准事件参数。 private void btnProExecStop_Click(object sender, EventArgs e) { if (_isBusy) @@ -1917,10 +2301,16 @@ namespace HkVisionPro.App return; } + // 仅处理连续执行停止,不触发新的执行请求。 StopContinuousRunIfNeeded("用户点击停止按钮"); UpdateUiState(false); } + /// + /// 绑定参数按钮事件:将当前模块绑定到参数配置控件,供用户查看或调整模块参数。 + /// + /// 事件源控件(绑定参数按钮)。 + /// 标准事件参数。 private void btnModuleBindingPar_Click(object sender, EventArgs e) { if (_isBusy) @@ -1940,6 +2330,7 @@ namespace HkVisionPro.App try { + // 将模块对象注入参数控件,参数控件内部负责参数树加载与编辑。 vmParamsConfigControl1.ModuleSource = module; WriteLog($"绑定模块参数成功:{cmbProcdure.Text}.{cmbModule.Text}"); } @@ -1949,6 +2340,12 @@ namespace HkVisionPro.App } } + /// + /// 执行模块按钮事件:执行当前模块,并尝试刷新流程渲染结果; + /// 若为条码模块,则自动注册条码结果回调用于实时日志输出。 + /// + /// 事件源控件(执行模块按钮)。 + /// 标准事件参数。 private void btnModuleExec_Click(object sender, EventArgs e) { if (_isBusy) @@ -1970,6 +2367,7 @@ namespace HkVisionPro.App { UpdateUiState(true); + // 先执行模块本体,再根据当前流程输出刷新界面渲染。 module.Run(); if (TryGetSelectedProcedure(out var procedure)) { @@ -1983,6 +2381,7 @@ namespace HkVisionPro.App _activeBcrTool = null; } + // 若当前模块支持条码结果回调,启用回调并做去重绑定。 var bcrTool = module as IMVSBcrModuTool; if (bcrTool != null) { @@ -2007,6 +2406,11 @@ namespace HkVisionPro.App } } + /// + /// 渲染结果按钮事件:不重新执行模块,仅根据当前流程最新输出重绘图像/ROI/识别结果。 + /// + /// 事件源控件(渲染结果按钮)。 + /// 标准事件参数。 private void btnModuleRenderResult_Click(object sender, EventArgs e) { if (_isBusy) @@ -2068,6 +2472,11 @@ namespace HkVisionPro.App } } + /// + /// 模块下拉展开事件:在用户展开模块下拉框时动态刷新模块列表,确保显示与当前流程一致。 + /// + /// 事件源控件(模块下拉框)。 + /// 标准事件参数。 private void cmbModule_DropDown(object sender, EventArgs e) { if (_isBusy || !_isSolutionLoaded) @@ -2086,6 +2495,11 @@ namespace HkVisionPro.App } } + /// + /// 流程下拉展开事件:在用户展开流程下拉框时动态刷新流程列表,避免旧缓存造成显示不一致。 + /// + /// 事件源控件(流程下拉框)。 + /// 标准事件参数。 private void cmbProcdure_DropDown(object sender, EventArgs e) { if (_isBusy || !_isSolutionLoaded) @@ -2114,9 +2528,66 @@ namespace HkVisionPro.App ReleaseSdkResources(); } + /// + /// 主窗体加载事件。 + /// 当前初始化逻辑已在构造函数中完成(主题、回调、UI 状态), + /// 因此此处暂不添加额外业务处理,预留后续扩展入口。 + /// + /// 事件源控件(主窗体)。 + /// 标准事件参数。 private void MainForm_Load(object sender, EventArgs e) { } + + /// + /// 自动运行按钮事件。 + /// 点击后执行自动流程启停切换: + /// - 未运行:启动自动状态机; + /// - 运行中:停止自动状态机。 + /// + /// 事件源控件(自动运行按钮)。 + /// 标准事件参数。 + private void btnAutoRun_Click(object sender, EventArgs e) + { + if (_isBusy) + { + return; + } + + if (!EnsureSolutionLoaded("自动运行")) + { + return; + } + + try + { + UpdateUiState(true); + + if (_autoAssemblyController.IsRunning) + { + StopAutoAssemblyIfRunning("用户点击自动运行按钮停止"); + WriteLog("自动装配流程已停止(按钮触发)。"); + return; + } + + // 启动自动流程前清理手动流程执行上下文,确保执行链路互斥。 + StopContinuousRunIfNeeded("点击自动运行按钮"); + UnbindProcedureWorkEndCallback(); + _currentProcedure = null; + + _autoAssemblyController.Start(); + SyncAutoRunButtonState(); + WriteLog("自动装配流程已启动(按钮触发)。"); + } + catch (Exception ex) + { + HandleException("自动运行按钮操作", ex); + } + finally + { + UpdateUiState(false); + } + } } }