Files
HkVisionPro/HkVisionPro.App/Automation/AutoAssemblyWorkflowController.cs

806 lines
28 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using Stateless;
using VM.PlatformSDKCS;
using VM.Core;
namespace HkVisionPro.App.Automation
{
/// <summary>
/// 自动装配流程状态枚举。
/// </summary>
public enum AutoAssemblyState
{
/// <summary>
/// 空闲状态,尚未启动自动流程。
/// </summary>
Idle,
/// <summary>
/// A 层检测中。
/// </summary>
LayerAInspecting,
/// <summary>
/// B 层检测中。
/// </summary>
LayerBInspecting,
/// <summary>
/// C 层检测中。
/// </summary>
LayerCInspecting,
/// <summary>
/// 单件产品 A/B/C 全部通过。
/// </summary>
ProductCompleted,
/// <summary>
/// 自动流程已停止。
/// </summary>
Stopped,
/// <summary>
/// 自动流程故障状态。
/// </summary>
Faulted,
}
/// <summary>
/// 自动装配流程状态通知事件参数。
/// </summary>
public sealed class AutoAssemblyStatusChangedEventArgs : EventArgs
{
/// <summary>
/// 当前状态。
/// </summary>
public AutoAssemblyState State { get; }
/// <summary>
/// 状态说明文本。
/// </summary>
public string Message { get; }
/// <summary>
/// 当前产品序号(从 1 开始)。
/// </summary>
public int ProductIndex { get; }
/// <summary>
/// 当前激活流程名称。
/// </summary>
public string ActiveProcedureName { get; }
/// <summary>
/// 当前稳定计时已累计毫秒数。
/// </summary>
public int StableElapsedMilliseconds { get; }
/// <summary>
/// 稳定计时目标毫秒数。
/// </summary>
public int StableTargetMilliseconds { get; }
/// <summary>
/// 状态变化事件参数构造函数。
/// </summary>
/// <param name="state">当前状态。</param>
/// <param name="message">状态说明。</param>
/// <param name="productIndex">当前产品序号(从 1 开始)。</param>
/// <param name="activeProcedureName">当前流程名称。</param>
/// <param name="stableElapsedMilliseconds">当前稳定计时累计毫秒数。</param>
/// <param name="stableTargetMilliseconds">稳定计时目标毫秒数。</param>
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;
}
}
/// <summary>
/// 自动装配流程配置参数。
/// </summary>
public sealed class AutoAssemblyOptions
{
/// <summary>
/// A 层流程名称。
/// </summary>
public string ProcedureAName { get; set; }
/// <summary>
/// B 层流程名称。
/// </summary>
public string ProcedureBName { get; set; }
/// <summary>
/// C 层流程名称。
/// </summary>
public string ProcedureCName { get; set; }
/// <summary>
/// 判定 OK 的整型输出名。
/// </summary>
public string OkIntOutputName { get; set; }
/// <summary>
/// 判定 OK 的第二个整型输出名(当配置后,需与第一个整型输出同时满足目标值才判定为 OK
/// </summary>
public string OkIntOutputName2 { get; set; }
/// <summary>
/// 判定 OK 的整型目标值。
/// </summary>
public int OkIntValue { get; set; }
/// <summary>
/// OK 持续稳定时间(毫秒)。
/// </summary>
public int StableOkMilliseconds { get; set; }
/// <summary>
/// 校验配置参数有效性。
/// </summary>
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。");
}
}
}
/// <summary>
/// 自动装配流程状态机触发器。
/// </summary>
internal enum AutoAssemblyTrigger
{
/// <summary>
/// 启动自动流程。
/// </summary>
Start,
/// <summary>
/// 当前层流程结果持续 OK 达标。
/// </summary>
StableOkReached,
/// <summary>
/// 停止自动流程。
/// </summary>
Stop,
/// <summary>
/// 单件产品完成后进入下一件。
/// </summary>
NextProduct,
/// <summary>
/// 自动流程异常。
/// </summary>
Fault,
}
/// <summary>
/// 自动装配流程控制器:
/// 基于 Stateless 状态机实现 A -> B -> C 顺序检测,并以“连续 OK 达标时长”驱动层间切换。
/// </summary>
public sealed class AutoAssemblyWorkflowController : IDisposable
{
/// <summary>
/// 并发互斥锁。
/// </summary>
private readonly object _syncRoot = new object();
/// <summary>
/// 自动流程配置。
/// </summary>
private readonly AutoAssemblyOptions _options;
/// <summary>
/// 日志输出委托。
/// </summary>
private readonly Action<string> _logger;
/// <summary>
/// 外部渲染回调(可选)。
/// </summary>
private readonly Action<VmProcedure, string> _renderResultAction;
/// <summary>
/// Stateless 状态机。
/// </summary>
private readonly StateMachine<AutoAssemblyState, AutoAssemblyTrigger> _stateMachine;
/// <summary>
/// 当前激活流程。
/// </summary>
private VmProcedure _activeProcedure;
/// <summary>
/// 当前流程首次出现 OK 的起始时间UTC
/// </summary>
private DateTime _okBeginUtc;
/// <summary>
/// 当前是否处于 OK 稳定计时中。
/// </summary>
private bool _isOkTiming;
/// <summary>
/// 控制器是否处于运行中。
/// </summary>
private bool _isRunning;
/// <summary>
/// 控制器是否已释放。
/// </summary>
private bool _isDisposed;
/// <summary>
/// 最近一次“输出缺失”日志时间(用于限流)。
/// </summary>
private DateTime _lastMissingOutputLogUtc;
/// <summary>
/// 最近一次稳定计时进度上报时间(用于限流)。
/// </summary>
private DateTime _lastStableProgressReportUtc;
/// <summary>
/// VisionMaster: 模块已处于连续执行中的错误码IMVS_EC_MODULE_CONTINUE_EXECUTE
/// </summary>
private const int VmErrorCodeModuleContinueExecute = -536870127;
/// <summary>
/// 当前产品序号(从 1 开始)。
/// </summary>
private int _currentProductIndex;
/// <summary>
/// 状态变化事件。
/// </summary>
public event EventHandler<AutoAssemblyStatusChangedEventArgs> StatusChanged;
/// <summary>
/// 当前状态。
/// </summary>
public AutoAssemblyState CurrentState => _stateMachine.State;
/// <summary>
/// 是否正在自动运行。
/// </summary>
public bool IsRunning => _isRunning;
/// <summary>
/// 自动装配流程控制器构造函数。
/// </summary>
/// <param name="options">自动流程参数。</param>
/// <param name="logger">日志输出委托(可为空)。</param>
/// <param name="renderResultAction">渲染回调(可为空)。</param>
public AutoAssemblyWorkflowController(
AutoAssemblyOptions options,
Action<string> logger,
Action<VmProcedure, string> 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, AutoAssemblyTrigger>(AutoAssemblyState.Idle);
ConfigureStateMachine();
}
/// <summary>
/// 启动自动装配流程。
/// </summary>
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;
}
}
}
/// <summary>
/// 停止自动装配流程。
/// </summary>
/// <param name="reason">停止原因。</param>
public void Stop(string reason)
{
lock (_syncRoot)
{
if (_isDisposed)
{
return;
}
if (!_isRunning)
{
return;
}
FireIfPermitted(AutoAssemblyTrigger.Stop);
_isRunning = false;
PublishStatus(AutoAssemblyState.Stopped, $"自动流程停止:{reason}");
}
}
/// <summary>
/// 释放控制器资源。
/// </summary>
public void Dispose()
{
lock (_syncRoot)
{
if (_isDisposed)
{
return;
}
try
{
Stop("控制器释放");
DeactivateCurrentProcedure("控制器释放");
}
finally
{
_isDisposed = true;
}
}
}
/// <summary>
/// 配置状态机。
/// </summary>
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);
}
/// <summary>
/// 配置层级检测状态。
/// </summary>
/// <param name="state">状态机状态。</param>
/// <param name="procedureName">流程名称。</param>
/// <param name="layerDisplayName">层级显示名。</param>
/// <param name="nextState">连续 OK 达标后的下一状态。</param>
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);
}
/// <summary>
/// 进入层级检测状态时激活对应流程。
/// </summary>
/// <param name="state">目标状态。</param>
/// <param name="procedureName">流程名称。</param>
/// <param name="layerDisplayName">层级显示名。</param>
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}");
}
/// <summary>
/// 关闭当前激活流程并解绑回调。
/// </summary>
/// <param name="reason">关闭原因。</param>
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;
}
}
/// <summary>
/// 流程执行结束回调:用于判定 OK 稳定时长,并驱动状态机切换。
/// </summary>
/// <param name="sender">回调事件源。</param>
/// <param name="e">标准事件参数。</param>
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);
}
}
}
/// <summary>
/// 判定当前流程结果是否为 OK。
/// 判定规则Result1 和 Result2 两个整型输出都等于目标值,才判定为 OK。
/// </summary>
/// <param name="procedure">流程对象。</param>
/// <param name="sourceName">命中的输出名。</param>
/// <param name="sourceValue">命中的输出值。</param>
/// <returns>是否判定为 OK。</returns>
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() : "<missing>")}/{(hasSecondInt ? secondIntValue.ToString() : "<missing>")}";
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;
}
/// <summary>
/// 尝试读取整型输出。
/// </summary>
/// <param name="procedure">流程对象。</param>
/// <param name="outputName">输出名。</param>
/// <param name="value">输出值。</param>
/// <returns>读取是否成功。</returns>
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;
}
}
/// <summary>
/// 判断异常是否为“模块已处于连续执行”错误。
/// </summary>
/// <param name="ex">异常对象。</param>
/// <returns>若是连续执行中错误则返回 true。</returns>
private static bool IsModuleContinueExecuteVmException(Exception ex)
{
if (ex == null)
{
return false;
}
var vmException = VmExceptionTool.GetVmException(ex);
return vmException != null && vmException.errorCode == VmErrorCodeModuleContinueExecute;
}
/// <summary>
/// 触发状态更新事件。
/// </summary>
/// <param name="state">状态。</param>
/// <param name="message">消息。</param>
/// <param name="writeLog">是否写入日志。</param>
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);
}
}
/// <summary>
/// 依据状态返回中文显示文本。
/// </summary>
/// <param name="state">状态枚举。</param>
/// <returns>中文描述。</returns>
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 "空闲";
}
}
/// <summary>
/// 若当前状态允许指定触发器,则触发状态机。
/// </summary>
/// <param name="trigger">触发器。</param>
private void FireIfPermitted(AutoAssemblyTrigger trigger)
{
if (_stateMachine.CanFire(trigger))
{
_stateMachine.Fire(trigger);
}
}
/// <summary>
/// 输出日志(允许日志委托为空)。
/// </summary>
/// <param name="message">日志内容。</param>
private void WriteLog(string message)
{
_logger?.Invoke(message);
}
/// <summary>
/// 已释放保护。
/// </summary>
private void ThrowIfDisposed()
{
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(AutoAssemblyWorkflowController));
}
}
}
}