Merge branch 'develop' into feature/single-session-login

# Conflicts:
#	MECF.Framework.UI.Client/CenterViews/Modules/PM/PMProcessViewModel.cs
This commit is contained in:
SL 2023-09-30 17:36:10 +08:00
commit 47863a000d
60 changed files with 2324 additions and 959 deletions

View File

@ -20,6 +20,7 @@ namespace Aitex.Core.RT.IOCore
: base(name, index, values)
{
_floatValues = floatValues;
type = IOType.AI;
}
}
}

View File

@ -25,6 +25,7 @@ namespace Aitex.Core.RT.IOCore
{
setter = SetValueSafe;
_floatValues = floatValues;
type = IOType.AO;
}
private void SetValueSafe(int index, short value)

View File

@ -20,6 +20,7 @@ namespace Aitex.Core.RT.IOCore
: base(name, index, values)
{
rawAccessor = new IOAccessor<bool>(name, index, raws);
type = IOType.DI;
}
}
}

View File

@ -15,7 +15,8 @@ namespace Aitex.Core.RT.IOCore
: base(name, index, values)
{
setter = SetValueSafe;
}
type = IOType.DO;
}
public bool SetValue(bool value, out string reason)
{

View File

@ -7,6 +7,8 @@ namespace Aitex.Core.RT.IOCore
{
protected string name;
protected IOType type;
protected string addr;
protected int index;
@ -33,6 +35,8 @@ namespace Aitex.Core.RT.IOCore
public string Name => name;
public IOType Type => type;
public int Index => index;
public int BlockOffset { get; set; }

View File

@ -3,6 +3,8 @@ namespace Aitex.Core.RT.IOCore;
public interface IIOAccessor
{
string Name { get; }
IOType Type { get; }
int Index { get; }

View File

@ -0,0 +1,78 @@
using System.Collections.Generic;
namespace Aitex.Core.RT.IOCore;
/// <summary>
/// 互锁动作接口。
/// </summary>
public interface IInterlockAction
{
#region Properties
/// <summary>
/// 返回当前动作的名称。
/// <remarks>
/// 通常返回当前动作对应的DO名称。
/// </remarks>
/// </summary>
string ActionName { get; }
/// <summary>
/// 返回当前动作的限制条件。
/// </summary>
IEnumerable<IInterlockLimit> Limits { get; }
/// <summary>
/// 返回当前动作限制条件中的“逻辑与”组。
/// <remarks>
/// “逻辑与”组由多个Limit对象构成若其中任意一个Limit命中则当前组命中。
/// </remarks>
/// </summary>
IEnumerable<IEnumerable<IInterlockLimit>> LogicOrGroups { get; }
#endregion
#region Methods
/// <summary>
/// 判断当前动作是否和给定的条件一致,若一致,则表明当前动作与特定条件的动作为同一动作。
/// </summary>
/// <remarks>
/// 对比时忽略OO名称大小写。
/// </remarks>>
/// <param name="doName">DO名称</param>
/// <param name="value">DO目标输出</param>
/// <returns></returns>
bool IsSame(string doName, bool value);
/// <summary>
/// 增加一个限制条件。
/// </summary>
/// <param name="limit"></param>
void AddLimit(IInterlockLimit limit);
/// <summary>
/// 增加一个“逻辑与”组。
/// </summary>
/// <param name="group"></param>
void AddLogicOrGroup(List<IInterlockLimit> group);
/// <summary>
/// 判断当前动作是否允许执行。
/// <remarks>
/// 当前动作的所有Limit均命中时允许执行。
/// </remarks>
/// </summary>
/// <param name="reason"></param>
/// <returns></returns>
bool CanDo(out string reason);
/// <summary>
/// 扫描线程周期性执行的任务。
/// </summary>
void Monitor();
#endregion
}

View File

@ -17,20 +17,35 @@ public interface IInterlockLimit
string UniqueId { get; }
/// <summary>
/// 返回互锁限制条件名称。
/// 返回当前Limit对应的IO的名称。
/// </summary>
string Name { get; }
/// <summary>
/// 返回当前Limit描述用于输出用户信息。
/// </summary>
string Description { get; }
/// <summary>
/// 返回互锁限制条件触发原因。
/// </summary>
string LimitReason { get; }
/// <summary>
/// 返回守护条件满足时输出的信息。
/// </summary>
string DaemonReason { get; }
/// <summary>
/// 返回当前互锁条件提示信息。
/// </summary>
string Tip { get; }
/// <summary>
/// 返回该Limit是否对Action进行反转
/// </summary>
bool IgnoreReverse { get; }
/*/// <summary>
/// 判断两个互锁限制条件是否相等。
/// </summary>
@ -56,7 +71,6 @@ public interface IInterlockLimit
/// <returns></returns>
bool CanDo(out string reason);
/// <summary>
/// 获取指定互锁限制条件中限制条件的内容。
/// </summary>

View File

@ -0,0 +1,33 @@
namespace Aitex.Core.RT.IOCore;
/// <summary>
/// 互锁限制条件数据供应器。
/// </summary>
public interface IInterlockLimitDataProvider
{
/// <summary>
/// 返回供应器名称。
/// <remarks>
/// 对于IO类型的Provider返回IO名称例如PMx.DO_xxxxxx。
/// <br/>
/// 对于DataPoll类型的Provider返回拉取的数据路径。
/// </remarks>
/// </summary>
string Name { get; }
/// <summary>
/// 返回供应器名称。
/// <remarks>
/// 对于IO类型的Provider返回包含地址的IO名称例如(DO-xx)PMx.DO_xxxxxx。
/// <br/>
/// 对于DataPoll类型的Provider返回拉取的数据路径。
/// </remarks>
/// </summary>
string Description { get; }
/// <summary>
/// 获取数据。
/// </summary>
/// <returns></returns>
object GetValue();
}

View File

@ -0,0 +1,46 @@
using System.Collections.Generic;
namespace Aitex.Core.RT.IOCore.Interlock.Actions
{
public class InterlockAction : InterlockActionBase
{
public InterlockAction(string module, DOAccessor doItem, bool value, string tip, Dictionary<string, string> cultureTip)
: base(module, doItem, value, tip, cultureTip)
{
}
#region Methods
/// <summary>
/// 如果命中某个互锁限制条件则将DO电平恢复到Action定义的反向电平。
/// </summary>
/// <param name="reason">执行恢复电平动作的信息。</param>
/// <returns>
/// True执行了电平恢复操作False未操作电平
/// </returns>
public bool TryReverse(out string reason)
{
reason = string.Empty;
// 如果DO当前电平不等于Action定义的电平则啥也不干
if (_do.Value != _actionValue)
return false;
// 如果DO已经输出Action定义的电平则反向
if (_do.SetValue(!_actionValue, out reason))
reason =
$"Interlock Force set DO-{_do.IoTableIndex}({_do.Name}) = [{((!_actionValue) ? "ON" : "OFF")}]";
return true;
}
public override void Monitor()
{
throw new System.NotSupportedException();
}
#endregion
}
}

View File

@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Aitex.Core.RT.IOCore.Interlock.Actions
{
public abstract class InterlockActionBase : IInterlockAction
{
#region Variables
private readonly List<List<IInterlockLimit>> _logicOrGroups;
private readonly List<IInterlockLimit> _limits;
private readonly string _tip;
private Dictionary<string, string> _cultureTip;
protected readonly string _module;
protected readonly DOAccessor _do;
protected readonly bool _actionValue;
#endregion
#region Constructors
protected InterlockActionBase(string module, DOAccessor doItem, bool value, string tip, Dictionary<string, string> cultureTip)
{
_module = module;
_logicOrGroups = new();
_limits = new();
_do = doItem;
_actionValue = value;
_tip = tip;
_cultureTip = cultureTip;
}
#endregion
#region Properties
/// <summary>
/// <inheritdoc cref="IInterlockAction.ActionName"/>
/// </summary>
public string ActionName => (_do != null) ? _do.Name : "";
/// <summary>
/// <inheritdoc cref="IInterlockAction.Limits"/>
/// </summary>
public IEnumerable<IInterlockLimit> Limits => _limits.AsReadOnly();
/// <summary>
/// <inheritdoc cref="IInterlockAction.LogicOrGroups"/>
/// </summary>
public IEnumerable<IEnumerable<IInterlockLimit>> LogicOrGroups => _logicOrGroups.AsReadOnly();
#endregion
#region Methods
/// <summary>
/// <inheritdoc cref="IInterlockAction.AddLimit"/>
/// </summary>
public void AddLimit(IInterlockLimit limit)
{
_limits.Add(limit);
}
/// <summary>
/// <inheritdoc cref="IInterlockAction.AddLogicOrGroup"/>
/// </summary>
public void AddLogicOrGroup(List<IInterlockLimit> orGroup)
{
_logicOrGroups.Add(orGroup);
}
/// <summary>
/// <inheritdoc cref="IInterlockAction.IsSame"/>
/// </summary>
public bool IsSame(string doName, bool value)
{
return string.Equals(doName, _do.Name, StringComparison.CurrentCultureIgnoreCase)
&& _actionValue == value;
}
/// <summary>
/// <inheritdoc cref="IInterlockAction.CanDo"/>
/// </summary>
public bool CanDo(out string reason)
{
reason = string.Empty;
var sbReason = new StringBuilder();
var result = true;
foreach (var limit in _limits)
{
if (!limit.CanDo(out var limitReason))
{
sbReason.AppendLine(limitReason);
result = false;
}
}
// 如果有互锁被触发,则打印互锁信息
if (!result)
{
sbReason.Insert(0,
$"Interlock triggered, DO-{_do.IoTableIndex}({_do.Name}) can not be [{(_actionValue ? "ON" : "OFF")}]{(string.IsNullOrEmpty(_tip) ? "" : $",{_tip}")} \r\n");
reason = sbReason.ToString().TrimEnd('\r', '\n');
}
return result;
}
/// <summary>
/// <inheritdoc cref="IInterlockAction.Monitor"/>
/// </summary>
public abstract void Monitor();
/// <inheritdoc cref="object.ToString()"/>
public override string ToString()
{
return $"{ActionName}, Action={_actionValue}, Limit Count={Limits.Count()}, OR Group Count={LogicOrGroups.Count()}";
}
#endregion
}
}

View File

@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Aitex.Core.RT.Event;
using Aitex.Core.RT.Log;
using Aitex.Core.Util;
using TypeDefLimitList = System.Collections.Generic.List<Aitex.Core.RT.IOCore.IInterlockLimit>;
namespace Aitex.Core.RT.IOCore.Interlock.Actions;
public class InterlockDaemonAction : InterlockActionBase
{
#region Variables
private readonly R_TRIG _rTrigSetDoFailed = new();
#endregion
#region Constructors
public InterlockDaemonAction(string module, DOAccessor doItem, bool value, string tip, Dictionary<string, string> cultureTip)
: base(module, doItem, value, tip, cultureTip)
{
}
#endregion
#region Methods
/// <summary>
/// 扫描当前动作的条件并设置相关DO的输出状态。
/// </summary>
public override void Monitor()
{
var info = new StringBuilder();
// 检查所有的Limit是否满足要求
foreach (var limit in Limits)
{
if (!limit.CanDo(out _))
return;
info.AppendLine($"{limit.DaemonReason}");
}
// 扫描OR定义的Limit组每个组中只要有一个Limit命中则继续测试下个OR组否则直接退出。
foreach (var group in LogicOrGroups)
{
var hitLimit = group.FirstOrDefault(limit => limit.CanDo(out _));
if (hitLimit == null) // 当前OR组中没用命中的Limit退出Monitor
return;
info.AppendLine($"{hitLimit.DaemonReason}");
}
// 如果DO输出已经满足要求则直接退出
if (_do.Value == _actionValue)
return;
// 所有的Limit和OR逻辑组均命中
var succeeded = _do.SetValue(_actionValue, out var reason);
_rTrigSetDoFailed.CLK = !succeeded;
if (succeeded)
{
info.Insert(0,
$"Interlock Daemon Force set DO-{_do.IoTableIndex}({_do.Name}) = [{((_actionValue) ? "ON" : "OFF")}] Due to\r\n");
EV.PostInfoLog( _module, info.ToString().TrimEnd('\r', '\n'));
_rTrigSetDoFailed.CLK = false;
}
else if(_rTrigSetDoFailed.Q)
{
// 第一次Set DO错误时打印日志
LOG.Error($"Interlock Daemon set {_do.Name} failed, {reason}");
}
}
#endregion
}

View File

@ -1,41 +0,0 @@
using System.Collections.Generic;
namespace Aitex.Core.RT.IOCore;
/// <summary>
/// 基于AI判断的互锁限制条件。
/// </summary>
internal class AiLimit : InterlockLimit<AIAccessor, double>
{
#region Constructors
public AiLimit(AIAccessor io, string value, string tip, Dictionary<string, string> cultureTip)
: base(io, value, tip, cultureTip)
{
LimitRange = new InterlockLimitRangeDouble(value);
}
#endregion
#region Properties
private InterlockLimitRangeDouble LimitRange { get; }
public override double CurrentValue => Io.FloatValue;
public override string LimitReason =>
$"AI-{Io.IoTableIndex}({Io.Name}) out of range [{LimitRange}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
#endregion
#region Methods
protected override bool CheckInRange()
{
return LimitRange.CheckIsInRange(CurrentValue);
}
#endregion
}

View File

@ -1,41 +0,0 @@
using System.Collections.Generic;
namespace Aitex.Core.RT.IOCore;
/// <summary>
/// 基于AO判断的互锁限制条件。
/// </summary>
internal class AoLimit : InterlockLimit<AOAccessor, double>
{
#region Constructors
public AoLimit(AOAccessor io, string value, string tip, Dictionary<string, string> cultureTip)
: base(io, value, tip, cultureTip)
{
LimitRange = new InterlockLimitRangeDouble(value);
}
#endregion
#region Properties
private InterlockLimitRangeDouble LimitRange { get; }
public override double CurrentValue => Io.FloatValue;
public override string LimitReason =>
$"AO-{Io.IoTableIndex}({Io.Name}) out of range [{LimitRange}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
#endregion
#region Methods
protected override bool CheckInRange()
{
return LimitRange.CheckIsInRange(CurrentValue);
}
#endregion
}

View File

@ -0,0 +1,19 @@
using System;
namespace Aitex.Core.RT.IOCore.Interlock.DataProvider;
public class AiValueProvider : IoValueProviderBase<AIAccessor>, IInterlockLimitDataProvider
{
public AiValueProvider(AIAccessor io) : base(io)
{
}
/// <summary>
/// 返回指定AO的FloatValue。
/// </summary>
/// <returns></returns>
public override object GetValue()
{
return Convert.ToDouble(Io.FloatValue);
}
}

View File

@ -0,0 +1,17 @@
namespace Aitex.Core.RT.IOCore.Interlock.DataProvider;
public class AoValueProvider : IoValueProviderBase<AOAccessor>, IInterlockLimitDataProvider
{
public AoValueProvider(AOAccessor io) : base(io)
{
}
/// <summary>
/// 返回指定AI的FloatValue。
/// </summary>
/// <returns></returns>
public override object GetValue()
{
return Io.FloatValue;
}
}

View File

@ -0,0 +1,30 @@
using System;
using Aitex.Core.RT.DataCenter;
namespace Aitex.Core.RT.IOCore.Interlock.DataProvider;
public class BoolDataPollProvider : DataPollProviderBase<bool>, IInterlockLimitDataProvider
{
public BoolDataPollProvider(string dataPath) : base(dataPath)
{
}
/// <summary>
/// <inheritdoc cref="DataPollProviderBase{T}.HandleGetValue"/>
/// </summary>
/// <returns></returns>
/// <exception cref="NullReferenceException"></exception>
/// <exception cref="InvalidCastException"></exception>
protected override object HandleGetValue()
{
var value = DATA.Poll(DataPath);
if (value == null)
throw new NullReferenceException("null is returned.");
if (bool.TryParse(value.ToString(), out var bValue))
return bValue;
throw new InvalidCastException($"the return value {value} is not bool.");
}
}

View File

@ -0,0 +1,90 @@
using System;
using Aitex.Core.RT.Log;
using Aitex.Core.Util;
namespace Aitex.Core.RT.IOCore.Interlock.DataProvider;
public abstract class DataPollProviderBase<T> : IInterlockLimitDataProvider
{
#region Variables
private readonly R_TRIG _rTrigPollFailed = new();
#endregion
#region Constructors
/// <summary>
/// 实例化数据拉取器
/// </summary>
/// <param name="dataPath"></param>
protected DataPollProviderBase(string dataPath)
{
DataPath = dataPath;
}
#endregion
#region Properties
/// <summary>
/// <inheritdoc cref="DataPollProviderBase{T}.Name"/>
/// </summary>
public string Name => DataPath;
/// <summary>
/// <inheritdoc cref="DataPollProviderBase{T}.Description"/>
/// </summary>
public string Description => DataPath;
/// <summary>
/// 返回当前数据拉取器需要拉取的数据路径。
/// <remarks>
/// 注意路径中不要包含Module名称。
/// </remarks>
/// </summary>
protected string DataPath { get; }
#endregion
#region Methods
/// <summary>
/// 需由派生类实现的获取数据的方法。
/// </summary>
/// <returns></returns>
protected abstract object HandleGetValue();
/// <summary>
/// 获取数据。
/// </summary>
/// <returns></returns>
public object GetValue()
{
try
{
var value = HandleGetValue();
_rTrigPollFailed.CLK = false;
return value;
}
catch (Exception ex)
{
_rTrigPollFailed.CLK = true;
if (_rTrigPollFailed.Q)
{
LOG.Error($"DataPollProvider poll data {DataPath} failed, {ex.Message}");
}
return null;
}
}
public override string ToString()
{
return Name;
}
#endregion
}

View File

@ -0,0 +1,18 @@
namespace Aitex.Core.RT.IOCore.Interlock.DataProvider;
public class DiValueProvider : IoValueProviderBase<DIAccessor>, IInterlockLimitDataProvider
{
public DiValueProvider(DIAccessor io) : base(io)
{
}
/// <summary>
/// 返回指定DI的FloatValue。
/// </summary>
/// <returns></returns>
public override object GetValue()
{
return Io.Value;
}
}

View File

@ -0,0 +1,17 @@
namespace Aitex.Core.RT.IOCore.Interlock.DataProvider;
public class DoValueProvider : IoValueProviderBase<DOAccessor>, IInterlockLimitDataProvider
{
public DoValueProvider(DOAccessor io) : base(io)
{
}
/// <summary>
/// 返回指定DO的FloatValue。
/// </summary>
/// <returns></returns>
public override object GetValue()
{
return Io.Value;
}
}

View File

@ -0,0 +1,23 @@
using System;
using Aitex.Core.RT.DataCenter;
namespace Aitex.Core.RT.IOCore.Interlock.DataProvider;
public class DoubleDataPollProvider : DataPollProviderBase<double>, IInterlockLimitDataProvider
{
public DoubleDataPollProvider(string dataPath) : base(dataPath)
{
}
protected override object HandleGetValue()
{
var value = DATA.Poll(DataPath);
if (value == null)
throw new NullReferenceException("unable to poll data, null is returned.");
if (double.TryParse(value.ToString(), out var dValue))
return dValue;
throw new InvalidCastException($"the return value {value} is not a number.");
}
}

View File

@ -0,0 +1,64 @@
using System;
using Aitex.Core.Util;
namespace Aitex.Core.RT.IOCore.Interlock.DataProvider;
public abstract class IoValueProviderBase<TAccessor> : IInterlockLimitDataProvider
where TAccessor: IIOAccessor
{
#region Variables
private readonly R_TRIG _rTrigPollFailed = new();
#endregion
#region Constructors
/// <summary>
/// 实例化数据拉取器
/// </summary>
/// <param name="io"></param>
/// <param name="module"></param>
protected IoValueProviderBase(TAccessor io)
{
Io = io;
}
#endregion
#region Properties
/// <summary>
/// <inheritdoc cref="DataPollProviderBase{T}.Name"/>
/// </summary>
public string Name => Io.Name;
/// <summary>
/// <inheritdoc cref="DataPollProviderBase{T}.Description"/>
/// </summary>
public string Description => $"{Io.Type}-{Io.IoTableIndex}({Io.Name})";
/// <summary>
/// 返回当前数据拉取器需要拉取的数据路径。
/// <remarks>
/// 注意路径中不要包含Module名称。
/// </remarks>
/// </summary>
public TAccessor Io { get; }
#endregion
#region Methods
/// <summary>
/// 获取数据。
/// </summary>
/// <returns></returns>
public virtual object GetValue()
{
throw new NotImplementedException();
}
#endregion
}

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
namespace Aitex.Core.RT.IOCore
{
/// <summary>
/// 基于DI判断的互锁限制条件。
/// </summary>
internal class DiLimit : InterlockLimit<DIAccessor, bool>
{
public override bool CurrentValue => Io.Value;
public override string LimitReason =>
$"DI-{Io.IoTableIndex}({Io.Name}) = [{(Io.Value ? "ON" : "OFF")}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
public DiLimit(DIAccessor diItem, string value, string tip, Dictionary<string, string> cultureTip)
: base(diItem, value, tip, cultureTip)
{
if (bool.TryParse(value, out var limitValue))
LimitValue = limitValue;
else
throw new InvalidCastException($"unable to convert {value} to boolean.");
}
protected override bool CheckInRange()
{
return CurrentValue == LimitValue;
}
}
}

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
namespace Aitex.Core.RT.IOCore
{
/// <summary>
/// 基于AO判断的互锁限制条件。
/// </summary>
internal class DoLimit : InterlockLimit<DOAccessor, bool>
{
public override bool CurrentValue => Io.Value;
public override string LimitReason =>
$"DO-{Io.IoTableIndex}({Io.Name}) = [{(Io.Value ? "ON" : "OFF")}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
public DoLimit(DOAccessor doItem, string value, string tip, Dictionary<string, string> cultureTip)
: base(doItem, value, tip, cultureTip)
{
if (bool.TryParse(value, out var limitValue))
LimitValue = limitValue;
else
throw new InvalidCastException($"unable to convert {value} to boolean.");
}
protected override bool CheckInRange()
{
return CurrentValue == LimitValue;
}
}
}

View File

@ -1,86 +0,0 @@
using System.Collections.Generic;
using System.Text;
namespace Aitex.Core.RT.IOCore
{
internal class InterlockAction
{
private readonly List<IInterlockLimit> _limits;
private readonly DOAccessor _do;
private readonly bool _actionValue;
private readonly string _tip;
private Dictionary<string, string> _cultureTip;
public string ActionName => (_do != null) ? _do.Name : "";
public InterlockAction(DOAccessor doItem, bool value, string tip, Dictionary<string, string> cultureTip,
List<IInterlockLimit> limits)
{
_do = doItem;
_actionValue = value;
_tip = tip;
_cultureTip = cultureTip;
_limits = limits;
}
public bool IsSame(string doName, bool value)
{
return doName == _do.Name && _actionValue == value;
}
/// <summary>
/// 如果命中某个互锁限制条件则将DO电平恢复到Action定义的反向电平。
/// </summary>
/// <param name="reason">执行恢复电平动作的信息。</param>
/// <returns>
/// True执行了电平恢复操作False未操作电平
/// </returns>
public bool TryReverse(out string reason)
{
reason = string.Empty;
// 如果DO当前电平不等于Action定义的电平则啥也不干
if (_do.Value != _actionValue)
return false;
// 如果DO已经输出Action定义的电平则反向
if (_do.SetValue(!_actionValue, out reason))
reason = $"Interlock Force set DO-{_do.IoTableIndex}({_do.Name}) = [{((!_actionValue) ? "ON" : "OFF")}]";
return true;
}
public bool CanDo(out string reason)
{
reason = string.Empty;
var sbReason = new StringBuilder();
var result = true;
foreach (var limit in _limits)
{
if (!limit.CanDo(out var limitReason))
{
sbReason.AppendLine(limitReason);
result = false;
}
}
// 如果有互锁被触发,则打印互锁信息
if (!result)
{
sbReason.Insert(0,
$"Interlock triggered, DO-{_do.IoTableIndex}({_do.Name}) can not be [{(_actionValue ? "ON" : "OFF")}]{(string.IsNullOrEmpty(_tip) ? "" : $",{_tip}")} \r\n");
reason = sbReason.ToString().TrimEnd('\r', '\n');
}
return result;
}
public override string ToString()
{
return $"{ActionName}, Action={_actionValue}, Limit Count={_limits.Count}";
}
}
}

View File

@ -0,0 +1,30 @@
using System.Diagnostics;
using System.Linq;
using Aitex.Core.RT.IOCore.Interlock.Actions;
using MECF.Framework.Common.Equipment;
namespace Aitex.Core.RT.IOCore.Interlock;
public class InterlockDaemonManager : InterlockManagerBase<InterlockDaemonAction>
{
public InterlockDaemonManager()
{
RootNodeName = "Daemon";
}
/// <summary>
/// <inheritdoc cref="InterlockManagerBase{TAction}.Monitor()"/>
/// </summary>
public override void Monitor()
{
// 按Module扫描Interlock Limit
foreach (var moduleName in _dicActionsPerModule.Keys.ToList())
{
Debug.Assert(moduleName != ModuleName.UnDefined,
$"Interlock Manager CanSetDo() undesired module name {ModuleName.UnDefined}");
foreach (var action in _dicActionsPerModule[moduleName].ToList())
action.Monitor();
}
}
}

View File

@ -1,164 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics;
using Aitex.Core.Util;
namespace Aitex.Core.RT.IOCore
{
/// <summary>
/// 互锁限制条件。
/// </summary>
public abstract class InterlockLimit<TAccessor, TValue> : IInterlockLimit
where TAccessor: IIOAccessor where TValue : struct
{
#region Variables
private Dictionary<string, string> _cultureTip;
private readonly R_TRIG _trigger;
#endregion
#region Constructor
/// <summary>
/// 互锁限制条件的构造函数。
/// </summary>
/// <param name="io">IO对象实例。</param>
/// <param name="value">当前限制条件中的IO状态。</param>
/// <param name="tip">默认语言提示信息。</param>
/// <param name="cultureTip">多国语言提示信息。</param>
/// <exception cref="InvalidIoNameException">IO名称错误前缀不是“DI_”、”DO_“、”AI_“或"AO_".</exception>
protected InterlockLimit(TAccessor io, string value, string tip, Dictionary<string, string> cultureTip)
{
Debug.Assert(io != null, "The IO Accessor object can not be null.");
_trigger = new R_TRIG();
Io = io;
Name = io.Name;
Tip = tip;
_cultureTip = cultureTip;
UniqueId = $"{io.Name}.{value}";
}
#endregion
#region Properties
protected TAccessor Io { get; }
/// <summary>
/// 返回互锁限制条件的唯一识别码。
/// <remarks>
/// 该唯一识别码用于创建字典时作为字典的Key值使用。
/// <br/>
/// 该值由Name+LimitValue组成。
/// </remarks>
/// </summary>
public string UniqueId { get; }
/// <summary>
/// 返回互锁限制条件名称。
/// </summary>
public string Name { get; }
/// <summary>
/// 返回
/// </summary>
public abstract TValue CurrentValue { get; }
/// <summary>
/// 返回当前互锁限制条件触发的原因。
/// </summary>
public abstract string LimitReason { get; }
/// <summary>
/// 返回当前互锁条件的信号约束值。
/// </summary>
public TValue LimitValue { get; protected set; }
/// <summary>
/// 返回当前互锁条件提示信息。
/// </summary>
public string Tip { get; }
#endregion
#region Methods
/*
/// <summary>
/// 判断两个互锁限制条件是否相等。
/// </summary>
/// <param name="interlockLimit">待比较的互锁限制条件。</param>
/// <returns>
/// <para>True: 相同False不同。</para>
/// </returns>
public bool IsSame(object interlockLimit)
{
if (interlockLimit is not InterlockLimit<TAccessor, TValue> li)
return false;
return Name == li.Name && li.LimitValue == LimitValue;
}*/
/// <summary>
/// 返回互锁限制监测的信号当前值和期望值不相等的条件是否触发。
/// <remarks>
/// 捕获当前值和期望值不相等信号的上升沿当上升沿到达时触发输出Q。
/// </remarks>
/// </summary>
/// <returns></returns>
public bool IsTriggered()
{
_trigger.CLK = !CheckInRange();
return _trigger.Q;
}
/// <summary>
/// 根据互锁条件判断是否允许DO输出。
/// </summary>
/// <param name="reason">如果禁止DO输出返回互锁原因。</param>
/// <returns></returns>
public bool CanDo(out string reason)
{
reason = string.Empty;
if (CheckInRange())
return true;
reason = LimitReason;
return false;
}
/// <summary>
/// 获取指定互锁限制条件中限制条件的内容。
/// </summary>
/// <returns></returns>
public virtual string GetLimitValue()
{
return LimitValue.ToString();
}
/// <summary>
/// 获取指定互锁限制条件中当前IO状态。
/// </summary>
/// <returns></returns>
public virtual string GetCurrentValue()
{
return CurrentValue.ToString();
}
/// <summary>
/// 检查当前值和是否超出限制值范围。
/// </summary>
/// <returns>True: 在范围内; False超出范围。</returns>
protected abstract bool CheckInRange();
/// <summary>
/// <inheritdoc />
/// </summary>
/// <returns></returns>
public override string ToString()
{
return $"{Name}, Limit={LimitValue}";
}
#endregion
}
}

View File

@ -1,19 +1,13 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Xml;
using Aitex.Core.RT.Event;
using Aitex.Core.RT.Log;
using Aitex.Core.RT.IOCore.Interlock.Actions;
using Aitex.Core.RT.SCCore;
using Aitex.Core.Util;
using MECF.Framework.Common.Equipment;
using DictLimitToActionMap =
System.Collections.Generic.Dictionary<Aitex.Core.RT.IOCore.IInterlockLimit, System.Collections.Generic.List<Aitex.Core.RT.IOCore.InterlockAction>>;
namespace Aitex.Core.RT.IOCore
namespace Aitex.Core.RT.IOCore.Interlock
{
/// <summary>
/// 互锁管理器。
@ -24,17 +18,15 @@ namespace Aitex.Core.RT.IOCore
/// 置为Action节点Value属性中定义电平的反向电平。
/// </remarks>
/// </summary>
public class InterlockManager : Singleton<InterlockManager>
public class InterlockManager : InterlockManagerBase<InterlockAction>
{
#region Variables
private readonly List<InterlockAction> _lstActions;
private readonly DictLimitToActionMap _dicLimitToActionMap;
private static List<IIOAccessor> _diMap;
private static List<IIOAccessor> _doMap;
private static List<IIOAccessor> _aiMap;
private static List<IIOAccessor> _aoMap;
/// <summary>
/// 以Module为单位当设置该Module所属的DO并触发互锁时是否输出Info而不是Warning。
/// </summary>
private Dictionary<ModuleName, bool> _dicModulePostInfo;
#endregion
#region Constructors
@ -44,197 +36,112 @@ namespace Aitex.Core.RT.IOCore
/// </summary>
public InterlockManager()
{
_lstActions = new List<InterlockAction>();
_dicLimitToActionMap = new DictLimitToActionMap();
RootNodeName = "Interlock";
_dicModulePostInfo = new();
}
#endregion
#region Methods
/// <summary>
/// 初始化互锁管理器。
/// </summary>
/// <param name="interlockFile">互锁配置文件。</param>
/// <param name="configFie">互锁配置文件。</param>
/// <param name="doMap">DO点表。</param>
/// <param name="diMap">DI点表。</param>
/// <param name="aoMap">AO点表。</param>
/// <param name="aiMap">AI点表。</param>
/// <param name="reason">初始化失败原因。</param>
/// <returns></returns>
public bool Initialize(string interlockFile,
public override bool Initialize(string configFie,
Dictionary<string, DOAccessor> doMap,
Dictionary<string, DIAccessor> diMap,
Dictionary<string, AIAccessor> aiMap,
Dictionary<string, AOAccessor> aoMap,
Dictionary<string, DIAccessor> diMap,
Dictionary<string, AIAccessor> aiMap,
Dictionary<string, AOAccessor> aoMap,
out string reason)
{
reason = "";
_doMap = doMap.Values.Cast<IIOAccessor>().ToList();
_diMap = diMap.Values.Cast<IIOAccessor>().ToList();
_aiMap = aiMap.Values.Cast<IIOAccessor>().ToList();
_aoMap = aoMap.Values.Cast<IIOAccessor>().ToList();
var sbReason = new StringBuilder();
try
{
var doc = new XmlDocument();
doc.Load(interlockFile);
var xmlNode = doc.SelectSingleNode("Interlock");
if(xmlNode == null) // 如果Interlock节点不存在
{
var err =
$"Failed to load interlock file, the node 'Interlock' is not found in file {interlockFile}.";
sbReason.AppendLine(err);
LOG.Error(err);
return false;
}
foreach (XmlNode childNode in xmlNode.ChildNodes)
{
// 遍历'Action'节点
if (childNode.NodeType == XmlNodeType.Comment || childNode is not XmlElement xmlElement)
continue;
if (xmlElement.Name != "Action")
{
if (xmlElement.NodeType != XmlNodeType.Comment)
LOG.Write("interlock config file contains no comments content, " + xmlElement.InnerXml);
continue;
}
// Action节点仅支持对DO进行配置
if (!xmlElement.HasAttribute("do") || !xmlElement.HasAttribute("value"))
{
sbReason.AppendLine("action node has no [do] or [value] attribute");
continue;
}
var doName = xmlElement.GetAttribute("do");
var doValue = Convert.ToBoolean(xmlElement.GetAttribute("value"));
var tip = string.Empty;
var dicTips = new Dictionary<string, string>();
var lstLimit = new List<IInterlockLimit>();
if (!doMap.ContainsKey(doName))
{
sbReason.AppendLine("action node " + doName + " no such DO defined");
continue;
}
// 获取DO实例
var doAction = doMap[doName];
if (doAction == null)
{
// 如果DO不存在则读取下一个Action节点
sbReason.AppendLine("action node " + doName + " no such DO defined");
continue;
}
if (xmlElement.HasAttribute("tip"))
tip = xmlElement.GetAttribute("tip");
if (xmlElement.HasAttribute("tip.zh-CN"))
dicTips["zh-CN"] = xmlElement.GetAttribute("tip.zh-CN");
if (xmlElement.HasAttribute("tip.en-US"))
dicTips["en-US"] = xmlElement.GetAttribute("tip.en-US");
// 遍历Action下的Limit节点
foreach (XmlElement nodeLimit in xmlElement.ChildNodes)
{
// 获取InterlockLimit对象。
var limit = CreateInterlockLimit(nodeLimit, out var err);
if(limit != null)
lstLimit.Add(limit);
else
sbReason.AppendLine(err);
}
// 创建InterlockAction对象
var newAction = new InterlockAction(doAction, doValue, tip, dicTips, lstLimit);
_lstActions.Add(newAction);
// 创建InterlockLimit到被使用的InterlockAction映射字典
foreach (var limit in lstLimit)
{
// 检查InterlockLimit是否已经存在于字典中
var limitInDic = _dicLimitToActionMap.Keys.FirstOrDefault(x => x.UniqueId == limit.UniqueId);
Debug.Assert(limitInDic != null, "The condition should never be hit");
// 存在则插入InterlockAction不存在则新建
if (limitInDic != null)
_dicLimitToActionMap[limitInDic].Add(newAction);
else
{
_dicLimitToActionMap[limit] = new List<InterlockAction> { newAction };
}
}
}
}
catch (Exception ex)
{
sbReason.AppendLine(ex.Message);
}
if (sbReason.Length > 0)
{
reason = sbReason.ToString().TrimEnd('\n', '\r');
var succeeded = base.Initialize(configFie, doMap, diMap, aiMap, aoMap, out reason);
if (!succeeded)
return false;
}
_dicModulePostInfo = _dicLimitsPerModule.Keys.ToDictionary(x => x, x => false);
return true;
}
/// <summary>
/// 背景扫描线程执行的任务。
/// </summary>
public void Monitor()
{
// 如果系统设置中旁路了互锁,则不监测互锁条件
if (SC.ContainsItem("System.BypassInterlock") && SC.GetValue<bool>("System.BypassInterlock"))
return;
// 遍历每一个InterlockLimit对象。
public override void Monitor()
{
// 按Module扫描Interlock Limit
foreach (var moduleName in _dicLimitsPerModule.Keys.ToList())
{
Debug.Assert(moduleName != ModuleName.UnDefined,
$"Interlock Manager CanSetDo() undesired module name {ModuleName.UnDefined}");
// 检查当前Module是否旁路Interlock
var isBypassInterlock = GetScBypassInterlockValue(moduleName);
if (isBypassInterlock)
continue;
foreach (var limit in _dicLimitsPerModule[moduleName].ToList())
{
var limits = _dicLimitToActionMap.Keys.ToList();
foreach (var limit in limits)
{
// 如果互锁没被触发
if (!limit.IsTriggered())
continue;
// 如果互锁没被触发
if (!limit.IsTriggered())
continue;
var reverseInfo = new StringBuilder();
var module = "System";
var reverseInfo = new StringBuilder();
var module = "System";
var actions = _dicLimitToActionMap[limit].ToList();
foreach (var action in actions)
{
// 尝试根据Action定义复位该互锁限制条件对应的所有DO的电平
if (action.TryReverse(out var reason))
{
var ss = action.ActionName.Split('.');
if (ss.Length > 1 && ModuleHelper.IsPm(ss[0]))
module = ss[0];
reverseInfo.AppendLine(reason);
}
}
// 如果PM腔有被恢复的DO则打印信息并报警。
if (reverseInfo.Length > 0)
{
reverseInfo.Insert(0,
$"Due to the {limit.Tip}, {limit.Name} is not [{limit.GetLimitValue()}]\r\n");
var actions = _dicLimitToActionMap[limit].ToList();
foreach (var action in actions)
{
// 尝试根据Action定义复位该互锁限制条件对应的所有DO的电平
if (((InterlockAction)action).TryReverse(out var reason))
{
var ss = action.ActionName.Split('.');
if (ss.Length > 1 && ModuleHelper.IsPm(ss[0]))
module = ss[0];
EV.PostWarningLog(module, reverseInfo.ToString().TrimEnd('\r', '\n'));
}
}
}
reverseInfo.AppendLine(reason);
}
}
/// <summary>
// 如果PM腔有被恢复的DO则打印信息并报警。
if (reverseInfo.Length > 0)
{
reverseInfo.Insert(0,
$"Due to the {limit.Tip}, {limit.Description} is not [{limit.GetLimitValue()}]\r\n");
// Post事件的类型 Info还是Warning
_dicModulePostInfo.TryGetValue(moduleName, out var isPostInfo);
if (isPostInfo)
EV.PostInfoLog(module, reverseInfo.ToString().TrimEnd('\r', '\n'));
else
EV.PostWarningLog(module, reverseInfo.ToString().TrimEnd('\r', '\n'));
}
}
}
}
/// <summary>
/// 设置指定Module的Interlock打印信息等级。
/// </summary>
/// <param name="module">模组名称</param>
/// <param name="isPostInfo">是否以Info等级打印信息</param>
public void SetEventLevel(string module, bool isPostInfo)
{
var moduleName = ModuleHelper.Converter(module);
if (_dicModulePostInfo.ContainsKey(moduleName))
_dicModulePostInfo[moduleName] = isPostInfo;
}
/// <summary>
/// 对指定的DO的操作是否满足互锁条件。
/// </summary>
/// <param name="doName">待操作的DO名称。</param>
@ -251,10 +158,15 @@ namespace Aitex.Core.RT.IOCore
public bool CanSetDo(string doName, bool onOff, out string reason)
{
reason = string.Empty;
if (SC.ContainsItem("System.BypassInterlock") && SC.GetValue<bool>("System.BypassInterlock"))
{
var moduleName = GetModuleFromIo(doName);
Debug.Assert(moduleName != ModuleName.UnDefined,
$"Interlock Manager CanSetDo() undesired module name {ModuleName.UnDefined}");
var isBypassInterlock = GetScBypassInterlockValue(moduleName);
if (isBypassInterlock)
return true;
}
foreach (var action in _lstActions.Where(action => action.IsSame(doName, onOff)))
@ -264,209 +176,33 @@ namespace Aitex.Core.RT.IOCore
return true;
}
/// <summary>
/// 创建互锁显示条件对象
/// 获取系统配置中指定Module的ByPassInterlock参数设置值
/// </summary>
/// <remarks>
/// 注意对于同一个IO、相同状态的互锁限制条件该函数确保仅生成一个实例不会针对同一条件创建不同实例。
/// </remarks>
/// <param name="ioType">IO类型请参考<see cref="IOType"/>。</param>
/// <param name="ioName">IO名称。</param>
/// <param name="limitValue">当前限制条件中的IO状态。</param>
/// <param name="tip">默认语言提示信息。</param>
/// <param name="cultureTip">多国语言提示信息。</param>
/// <param name="reason">创建失败并返回null的原因。</param>
/// <returns>互锁限制条件对象的实例。</returns>
private IInterlockLimit CreateInterlockLimit(IOType ioType, string ioName, string limitValue,
string tip, Dictionary<string, string> cultureTip, out string reason)
{
Debug.Assert(Enum.TryParse<IOType>(ioType.ToString(), out _),"Undefined IO Type");
Debug.Assert(!string.IsNullOrEmpty(ioName),"IO Name can not be empty");
Debug.Assert(!string.IsNullOrEmpty(limitValue),"LimitValue can not be empty");
reason = "";
// 创建一个InterlockLimit实例。
IIOAccessor io;
IInterlockLimit limit;
try
{
io = GetIoByIoType(ioType, ioName);
}
catch (IoNotFoundException)
{
reason = $"limit node {ioName} no such {ioType} defined";
return null;
}
catch (InvalidIoTypeExeption)
{
reason = $"limit node {ioName} no such io type defined";
return null;
}
switch (ioType)
{
case IOType.DI:
limit = new DiLimit((DIAccessor)io, limitValue, tip, cultureTip);
break;
case IOType.DO:
limit = new DoLimit((DOAccessor)io, limitValue, tip, cultureTip);
break;
case IOType.AI:
limit = new AiLimit((AIAccessor)io, limitValue, tip, cultureTip);
break;
case IOType.AO:
limit = new AoLimit((AOAccessor)io, limitValue, tip, cultureTip);
break;
default:
throw new InvalidIoTypeExeption();
}
// 检查limit是否已经存在。
var limitExist = _dicLimitToActionMap.Keys.FirstOrDefault(x => x.UniqueId == limit.UniqueId);
// 如果不存在,则返回新创建的对象。
if (limitExist == null)
{
_dicLimitToActionMap.Add(limit, new List<InterlockAction>());
return limit;
}
// 返回已存在的对象。
return limitExist;
}
/// <summary>
/// 通过XML配置创建互锁限制条件对象。
/// </summary>
/// <param name="xmlNodeLimit">包含Limit定义的Xml节点。</param>
/// <param name="reason">创建失败并返回null的原因。</param>
/// <returns></returns>
private IInterlockLimit CreateInterlockLimit(XmlElement xmlNodeLimit, out string reason)
{
reason = "";
// 检查节点名称是否为Limit
if (xmlNodeLimit.Name != "Limit") // 节点名称不是Limit
{
reason = "the name of xml node is not 'Limit'";
if (xmlNodeLimit.NodeType != XmlNodeType.Comment)
{
reason = "interlock config file contains no comments content, " + xmlNodeLimit.InnerXml;
LOG.Write(reason);
}
return null;
}
// 节点不包含didoaiao或者不包含value属性
string limitIoName;
IOType limitIoType;
if (xmlNodeLimit.HasAttribute("di"))
{
limitIoName = xmlNodeLimit.GetAttribute("di");
limitIoType = IOType.DI;
}
else if (xmlNodeLimit.HasAttribute("do"))
{
limitIoName = xmlNodeLimit.GetAttribute("do");
limitIoType = IOType.DO;
}
else if (xmlNodeLimit.HasAttribute("ai"))
{
limitIoName = xmlNodeLimit.GetAttribute("ai");
limitIoType = IOType.AI;
}
else if (xmlNodeLimit.HasAttribute("ao"))
{
limitIoName = xmlNodeLimit.GetAttribute("ao");
limitIoType = IOType.AO;
}
else
{
reason = "limit node lack of di/do/ai/ao/io attribute";
return null;
}
string limitValue;
if (xmlNodeLimit.HasAttribute("value"))
limitValue = xmlNodeLimit.GetAttribute("value");
else
{
reason = "limit node lack of value attribute";
return null;
}
var tip = string.Empty;
var dicLimitTips = new Dictionary<string, string>();
if (xmlNodeLimit.HasAttribute("tip"))
tip = xmlNodeLimit.GetAttribute("tip");
if (xmlNodeLimit.HasAttribute("tip.zh-CN"))
{
dicLimitTips["zh-CN"] = xmlNodeLimit.GetAttribute("tip.zh-CN");
if (string.IsNullOrEmpty(tip))
tip = dicLimitTips["zh-CN"];
}
if (xmlNodeLimit.HasAttribute("tip.en-US"))
{
dicLimitTips["en-US"] = xmlNodeLimit.GetAttribute("tip.en-US");
if (string.IsNullOrEmpty(tip))
tip = dicLimitTips["en-US"];
}
var limit = CreateInterlockLimit(limitIoType, limitIoName, limitValue, tip, dicLimitTips, out reason);
return limit;
}
/// <summary>
/// 根据给定的IO类型和IO名称从IO列表中获取IO对象实例。
/// </summary>
/// <param name="type">IO类型请参考<see cref="IOType"/>。</param>
/// <param name="ioName">IO名称。</param>
/// <returns></returns>
/// <exception cref="InvalidIoTypeExeption">无效的IO类型。</exception>
/// <exception cref="IoNotFoundException">未找到IO。</exception>
private static IIOAccessor GetIoByIoType(IOType type, string ioName)
{
List<IIOAccessor> dictMap;
switch (type)
{
case IOType.DI:
dictMap = _diMap;
break;
case IOType.DO:
dictMap = _doMap;
break;
case IOType.AI:
dictMap = _aiMap;
break;
case IOType.AO:
dictMap = _aoMap;
break;
default:
throw new InvalidIoTypeExeption();
}
var io = dictMap.FirstOrDefault(x => x.Name == ioName);
if (io != null)
return io;
throw new IoNotFoundException(type, ioName);
}
/// <param name="module">
/// 模组名称, <see cref="ModuleName"/>
/// </param>
/// <returns></returns>
public static bool GetScBypassInterlockValue(ModuleName module)
{
return GetScBypassInterlockValue(module.ToString());
}
/// <summary>
/// 获取系统配置中指定Module的ByPassInterlock参数设置值。
/// </summary>
/// <param name="module">模组名称</param>
/// <returns></returns>
public static bool GetScBypassInterlockValue(string module)
{
if (ModuleHelper.IsPm(module))
return SC.SafeGetValue($"PM.{module}.BypassInterlock", false);
else
return SC.SafeGetValue($"{module}.BypassInterlock", false);
}
#endregion
}
}

View File

@ -0,0 +1,494 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceModel.Configuration;
using System.Text;
using System.Xml;
using Aitex.Core.RT.IOCore.Interlock.DataProvider;
using Aitex.Core.RT.IOCore.Interlock.Limits;
using Aitex.Core.RT.Log;
using MECF.Framework.Common.Equipment;
namespace Aitex.Core.RT.IOCore.Interlock;
public abstract class InterlockManagerBase<TAction>
where TAction : IInterlockAction
{
#region Variables
private static List<IIOAccessor> _diMap;
private static List<IIOAccessor> _doMap;
private static List<IIOAccessor> _aiMap;
private static List<IIOAccessor> _aoMap;
protected string RootNodeName;
protected readonly List<TAction> _lstActions;
/// <summary>
/// 每个Limit对应的DoAction集合。
/// </summary>
protected readonly Dictionary<IInterlockLimit, List<IInterlockAction>> _dicLimitToActionMap;
/// <summary>
/// 每个Module对应的DoAction集合。
/// </summary>
protected readonly Dictionary<ModuleName, List<TAction>> _dicActionsPerModule;
/// <summary>
/// 每个Module对应的Limit集合。
/// </summary>
protected readonly Dictionary<ModuleName, List<IInterlockLimit>> _dicLimitsPerModule;
#endregion
#region Constructors
/// <summary>
/// 互锁管理器的构造函数。
/// </summary>
protected InterlockManagerBase()
{
_lstActions = new();
_dicActionsPerModule = new();
_dicLimitToActionMap = new ();
_dicLimitsPerModule = new ();
}
#endregion
#region Methods
/// <summary>
/// 初始化互锁管理器。
/// </summary>
/// <param name="configFile">互锁配置文件。</param>
/// <param name="doMap">DO点表。</param>
/// <param name="diMap">DI点表。</param>
/// <param name="aoMap">AO点表。</param>
/// <param name="aiMap">AI点表。</param>
/// <param name="reason">初始化失败原因。</param>
/// <returns></returns>
public virtual bool Initialize(string configFile,
Dictionary<string, DOAccessor> doMap,
Dictionary<string, DIAccessor> diMap,
Dictionary<string, AIAccessor> aiMap,
Dictionary<string, AOAccessor> aoMap,
out string reason)
{
reason = "";
if (string.IsNullOrEmpty(configFile))
{
reason = "interlock daemon config file does not specified";
return false;
}
if (!File.Exists(configFile))
{
reason = $"interlock daemon config file {configFile} does not exist";
return false;
}
_doMap = doMap.Values.Cast<IIOAccessor>().ToList();
_diMap = diMap.Values.Cast<IIOAccessor>().ToList();
_aiMap = aiMap.Values.Cast<IIOAccessor>().ToList();
_aoMap = aoMap.Values.Cast<IIOAccessor>().ToList();
var sbReason = new StringBuilder();
try
{
var doc = new XmlDocument();
doc.Load(configFile);
var xmlNode = doc.SelectSingleNode(RootNodeName);
if (xmlNode == null) // 如果Interlock节点不存在
{
var err =
$"Failed to load interlock daemon file, the node 'Daemon' is not found in file {configFile}.";
sbReason.AppendLine(err);
LOG.Error(err);
return false;
}
foreach (XmlNode childNode in xmlNode.ChildNodes)
{
// 遍历'Action'节点
if (!CheckIsXmlElement(childNode, out var actionNode))
continue;
if (actionNode.Name != "Action")
{
if (actionNode.NodeType != XmlNodeType.Comment)
LOG.Write("interlock daemon file contains no comments content, " + actionNode.InnerXml);
continue;
}
// Action节点仅支持对DO进行配置
if (!actionNode.HasAttribute("do") || !actionNode.HasAttribute("value"))
{
sbReason.AppendLine("action node has no [do] or [value] attribute");
continue;
}
var doName = actionNode.GetAttribute("do");
var doValue = Convert.ToBoolean(actionNode.GetAttribute("value"));
var tip = string.Empty;
var dicTips = new Dictionary<string, string>();
if (!doMap.ContainsKey(doName))
{
sbReason.AppendLine("action node " + doName + " no such DO defined");
continue;
}
// 获取DO实例
var targetDo = doMap[doName];
if (targetDo == null)
{
// 如果DO不存在则读取下一个Action节点
sbReason.AppendLine("action node " + doName + " no such DO defined");
continue;
}
if (actionNode.HasAttribute("tip"))
tip = actionNode.GetAttribute("tip");
if (actionNode.HasAttribute("tip.zh-CN"))
dicTips["zh-CN"] = actionNode.GetAttribute("tip.zh-CN");
if (actionNode.HasAttribute("tip.en-US"))
dicTips["en-US"] = actionNode.GetAttribute("tip.en-US");
var ignoreReverse_All = false;//单个Action的全局ignore为True时所有Limit条件不反向控制
if (actionNode.HasAttribute("ignoreReverse"))
{
var strIgnoreReverse = actionNode.GetAttribute("ignoreReverse");
if (!bool.TryParse(strIgnoreReverse, out ignoreReverse_All))
LOG.Error($"Unable to convert attribute 'ignoreReverse' of action '{doName}' to bool.");
}
var moduleName = GetModuleFromIo(targetDo.Name);
if (!_dicActionsPerModule.ContainsKey(moduleName))
_dicActionsPerModule[moduleName] = new List<TAction>();
// 创建InterlockAction对象首先判断该动作是否已经存在若存在则报错
var action = (TAction)Activator.CreateInstance(typeof(TAction), moduleName.ToString(),
targetDo, doValue, tip, dicTips);
if (_lstActions.Exists(x => x.IsSame(action.ActionName, doValue)))
{
var err = $"Interlock Action {action.ActionName}={doValue} duplicated in {configFile}";
LOG.Error(err);
Debug.Assert(false, err);
}
// 遍历Action下的Limit节点并创建InterlockLimit对象。
foreach (XmlNode limitNode in actionNode.ChildNodes)
{
if (!CheckIsXmlElement(limitNode, out var node))
continue;
if (node.Name.ToLower() == "or")
{
var groupOr = CreateOrLimitGroup(node, out var err0);
action.AddLogicOrGroup(groupOr);
}
else
{
// 获取InterlockLimit对象。
var limit = CreateInterlockLimit(node, out var err);
if (limit != null)
action.AddLimit(limit);
else
sbReason.AppendLine(err);
}
}
_dicActionsPerModule[moduleName].Add(action);
_lstActions.Add(action);
// 如果当前Action设置了忽略翻转则不要注册Limit到当前Action的映射避免Limit触发导致Action翻转。
if (!ignoreReverse_All)
{
foreach (var limit in action.Limits)
{
if (!limit.IgnoreReverse)//如果单个limit也忽略反转不要注册Limit到当前Action的映射避免Limit触发导致Action翻转。
{
// 创建以Limit分组的Action字典
if (!_dicLimitToActionMap.ContainsKey(limit))
_dicLimitToActionMap[limit] = new List<IInterlockAction>();
_dicLimitToActionMap[limit].Add(action);
}
}
}
}
Debug.Assert(!_dicActionsPerModule.ContainsKey(ModuleName.UnDefined),
$"InterlockManager {nameof(_dicActionsPerModule)} contains key {ModuleName.UnDefined}");
Debug.Assert(!_dicLimitsPerModule.ContainsKey(ModuleName.UnDefined),
$"InterlockManager {nameof(_dicLimitsPerModule)} contains key {ModuleName.UnDefined}");
}
catch (Exception ex)
{
sbReason.AppendLine(ex.Message);
}
finally
{
if (sbReason.Length > 0)
reason = sbReason.ToString().TrimEnd('\n', '\r');
}
return sbReason.Length == 0;
}
/// <summary>
/// 背景扫描线程执行的任务。
/// </summary>
public abstract void Monitor();
/// <summary>
/// 通过XML配置创建互锁限制条件对象。
/// </summary>
/// <param name="limitNode">包含Limit定义的Xml节点。</param>
/// <param name="reason">创建失败并返回null的原因。</param>
/// <returns></returns>
private IInterlockLimit CreateInterlockLimit(XmlElement limitNode, out string reason)
{
reason = "";
// 检查节点名称是否为Limit
if (limitNode.Name != "Limit") // 节点名称不是Limit
{
reason = "the name of xml node is not 'Limit'";
if (limitNode.NodeType != XmlNodeType.Comment)
{
reason = "interlock config file contains no comments content, " + limitNode.InnerXml;
LOG.Write(reason);
}
return null;
}
string limitValue;
if (limitNode.HasAttribute("value"))
limitValue = limitNode.GetAttribute("value");
else
{
reason = "limit node lack of value attribute";
return null;
}
var tip = string.Empty;
var dicLimitTips = new Dictionary<string, string>();
if (limitNode.HasAttribute("tip"))
tip = limitNode.GetAttribute("tip");
if (limitNode.HasAttribute("tip.zh-CN"))
{
dicLimitTips["zh-CN"] = limitNode.GetAttribute("tip.zh-CN");
if (string.IsNullOrEmpty(tip))
tip = dicLimitTips["zh-CN"];
}
if (limitNode.HasAttribute("tip.en-US"))
{
dicLimitTips["en-US"] = limitNode.GetAttribute("tip.en-US");
if (string.IsNullOrEmpty(tip))
tip = dicLimitTips["en-US"];
}
var ignoreReverse = false;
if (limitNode.HasAttribute("ignoreReverse"))
{
var strIgnoreReverse = limitNode.GetAttribute("ignoreReverse");
if (!bool.TryParse(strIgnoreReverse, out ignoreReverse))
LOG.Error($"Unable to convert attribute 'ignoreReverse' of Limit to bool.");
}
// 节点不包含didoaiao或者不包含value属性
IInterlockLimit limit;
if (limitNode.HasAttribute("di"))
{
var limitName = limitNode.GetAttribute("di");
var io = GetIoByIoType(IOType.DI, limitName);
var provider = new DiValueProvider((DIAccessor)io);
limit = new DiLimit(provider, limitValue, tip, dicLimitTips, ignoreReverse);
}
else if (limitNode.HasAttribute("do"))
{
var limitName = limitNode.GetAttribute("do");
var io = GetIoByIoType(IOType.DO, limitName);
var provider = new DoValueProvider((DOAccessor)io);
limit = new DoLimit(provider, limitValue, tip, dicLimitTips, ignoreReverse);
}
else if (limitNode.HasAttribute("ai"))
{
var limitName = limitNode.GetAttribute("ai");
var io = GetIoByIoType(IOType.AI, limitName);
var provider = new AiValueProvider((AIAccessor)io);
limit = new AiLimit(provider, limitValue, tip, dicLimitTips, ignoreReverse);
}
else if (limitNode.HasAttribute("ao"))
{
var limitName = limitNode.GetAttribute("ao");
var io = GetIoByIoType(IOType.AO, limitName);
var provider = new AoValueProvider((AOAccessor)io);
limit = new AoLimit(provider, limitValue, tip, dicLimitTips, ignoreReverse);
}
else if (limitNode.HasAttribute("polldouble"))
{
var limitName = limitNode.GetAttribute("polldouble");
var provider = new DoubleDataPollProvider(limitName);
limit = new DoubleDataPollLimit(provider, limitValue, tip, dicLimitTips, ignoreReverse);
}
else if (limitNode.HasAttribute("pollbool"))
{
var limitName = limitNode.GetAttribute("pollbool");
var provider = new BoolDataPollProvider(limitName);
limit = new BoolDataPollLimit(provider, limitValue, tip, dicLimitTips, ignoreReverse);
}
else
{
reason = "limit node lack of di/do/ai/ao/io/pollbool/polldouble attribute";
return null;
}
// 从_dicLimitsPerModule字典中查找当前创建的Limit是否已经存在
// 如果存在直接返回已存在的Limit实例
// 否则将当前创建的Limit放到字典中然后返回实例
var moduleName = GetModuleFromIo(limit.Name);
if (_dicLimitsPerModule.TryGetValue(moduleName, out var list))
{
var existLimit = list.FirstOrDefault(x => x.UniqueId == limit.UniqueId);
if(existLimit != null)
limit = existLimit; // 返回字典中的Limit实例
else
list.Add(limit); // 将当前Limit加入字典
}
else
{
// 在字典中创建Module并将Limit实例放入字典
_dicLimitsPerModule[moduleName] = new List<IInterlockLimit>(new[] { limit });
}
return limit;
}
/// <summary>
/// 创建“OR”定义的Limit组。
/// </summary>
/// <param name="xmlNodeOr"></param>
/// <param name="reason"></param>
/// <returns></returns>
private List<IInterlockLimit> CreateOrLimitGroup(XmlNode xmlNodeOr, out string reason)
{
reason = "";
var limitList = new List<IInterlockLimit>();
foreach (XmlNode childNode in xmlNodeOr.ChildNodes)
{
if (!CheckIsXmlElement(childNode, out var node))
continue;
var limit = CreateInterlockLimit(node, out reason);
if (limit != null)
limitList.Add(limit);
else
return null;
}
return limitList;
}
/// <summary>
/// 根据给定的IO类型和IO名称从IO列表中获取IO对象实例。
/// </summary>
/// <param name="type">IO类型请参考<see cref="IOType"/>。</param>
/// <param name="ioName">IO名称。</param>
/// <returns></returns>
/// <exception cref="InvalidIoTypeExeption">无效的IO类型。</exception>
/// <exception cref="IoNotFoundException">未找到IO。</exception>
private static IIOAccessor GetIoByIoType(IOType type, string ioName)
{
List<IIOAccessor> dictMap;
switch (type)
{
case IOType.DI:
dictMap = _diMap;
break;
case IOType.DO:
dictMap = _doMap;
break;
case IOType.AI:
dictMap = _aiMap;
break;
case IOType.AO:
dictMap = _aoMap;
break;
default:
throw new InvalidIoTypeExeption();
}
var io = dictMap.FirstOrDefault(x => x.Name == ioName);
if (io != null)
return io;
throw new IoNotFoundException(type, ioName);
}
/// <summary>
/// 从指定的IO中解析该IO所属的模组。
/// </summary>
/// <param name="ioName"></param>
/// <returns></returns>
protected static ModuleName GetModuleFromIo(string ioName)
{
Debug.Assert(!string.IsNullOrEmpty(ioName), "the IO name can not be empty.");
if (string.IsNullOrEmpty(ioName))
{
LOG.Error($"InterlockManager GetModuleFromIo failed, the parameter ioName is empty");
return ModuleName.UnDefined;
}
// Simulator中TM的Io表配置中没有"TM."前缀;
// 为兼容此问题如果没有找到Module前缀则用System代替
var posDot = ioName.IndexOf('.');
if (posDot <= 0)
return ModuleName.System;
var moduleName = ioName.Substring(0, posDot);
return ModuleHelper.Converter(moduleName);
}
/// <summary>
/// 检查制定的XmlNode是否为XmlElement.
/// </summary>
/// <param name="node"></param>
/// <param name="element"></param>
/// <returns></returns>
private static bool CheckIsXmlElement(XmlNode node, out XmlElement element)
{
element = null;
if (node.NodeType != XmlNodeType.Comment && node is XmlElement)
{
element = (XmlElement)node;
return true;
}
return false;
}
#endregion
}

View File

@ -0,0 +1,51 @@
using System.Collections.Generic;
using Aitex.Core.RT.IOCore.Interlock.DataProvider;
using Aitex.Core.RT.IOCore.Interlock.Utils;
namespace Aitex.Core.RT.IOCore.Interlock.Limits;
/// <summary>
/// 基于AI判断的互锁限制条件。
/// </summary>
internal class AiLimit : InterlockLimitBase<AiValueProvider, double>
{
#region Constructors
public AiLimit(AiValueProvider dataProvider, string value, string tip,
Dictionary<string, string> cultureTip,bool ignoreReverse)
: base(dataProvider, value, tip, cultureTip, ignoreReverse)
{
LimitRange = new InterlockLimitRangeDouble(value);
}
#endregion
#region Properties
private InterlockLimitRangeDouble LimitRange { get; }
public override double CurrentValue => (double)DataProvider.GetValue();
public override string LimitReason =>
$"{DataProvider.Name} = {CurrentValue:F1}, out of range [{LimitRange}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
public override string DaemonReason =>
$"{DataProvider.Name} = {CurrentValue:F1}, in the range [{LimitRange}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
#endregion
#region Methods
protected override bool CheckInRange()
{
return LimitRange.CheckIsInRange(CurrentValue);
}
public override string ToString()
{
return $"{Name}, Limit={LimitRange}";
}
#endregion
}

View File

@ -0,0 +1,52 @@
using System.Collections.Generic;
using Aitex.Core.RT.IOCore.Interlock.DataProvider;
using Aitex.Core.RT.IOCore.Interlock.Utils;
namespace Aitex.Core.RT.IOCore.Interlock.Limits;
/// <summary>
/// 基于AO判断的互锁限制条件。
/// </summary>
internal class AoLimit : InterlockLimitBase<AoValueProvider, double>
{
#region Constructors
public AoLimit(AoValueProvider dataProvider, string value, string tip, Dictionary<string, string> cultureTip, bool ignoreReverse)
: base(dataProvider, value, tip, cultureTip, ignoreReverse)
{
LimitRange = new InterlockLimitRangeDouble(value);
}
#endregion
#region Properties
private InterlockLimitRangeDouble LimitRange { get; }
public override double CurrentValue => (double)DataProvider.GetValue();
public override string LimitReason =>
$"{DataProvider.Name} = {CurrentValue:F1}, out of range [{LimitRange}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
public override string DaemonReason =>
$"{DataProvider.Name} = {CurrentValue:F1}, in the range [{LimitRange}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
#endregion
#region Methods
protected override bool CheckInRange()
{
return LimitRange.CheckIsInRange(CurrentValue);
}
public override string ToString()
{
return $"{Name}, Limit={LimitRange}";
}
#endregion
}

View File

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using Aitex.Core.RT.IOCore.Interlock.DataProvider;
using DocumentFormat.OpenXml.Math;
namespace Aitex.Core.RT.IOCore.Interlock.Limits;
public class BoolDataPollLimit : InterlockLimitBase<BoolDataPollProvider, bool>
{
#region Constructors
public BoolDataPollLimit(IInterlockLimitDataProvider dataProvider, string value, string tip,
Dictionary<string, string> cultureTip,bool ignoreReverse) : base(dataProvider, value, tip, cultureTip, ignoreReverse)
{
if (bool.TryParse(value, out var limitValue))
LimitValue = limitValue;
else
throw new InvalidCastException($"unable to convert {value} to boolean.");
}
#endregion
#region Properties
/// <summary>
/// <inheritdoc cref="InterlockLimitBase{TAccessor,TValue}.CurrentValue"/>
/// </summary>
public override bool CurrentValue
{
get
{
if (DataProvider.GetValue() is bool value)
return value;
// 如果Poll不到值始终返回LimitValue相反的值避免当前Limit被命中
return !LimitValue;
}
}
/// <summary>
/// <inheritdoc cref="InterlockLimitBase{TAccessor,TValue}.LimitReason"/>
/// </summary>
public override string LimitReason =>
$"{DataProvider.Name} = [{(CurrentValue ? "ON" : "OFF")}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
/// <summary>
/// <inheritdoc cref="InterlockLimitBase{TAccessor,TValue}.DaemonReason"/>
/// </summary>
public override string DaemonReason =>
$"{DataProvider.Name} = [{(LimitValue ? "ON" : "OFF")}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
#endregion
/// <summary>
/// <inheritdoc cref="InterlockLimitBase{TAccessor,TValue}.CheckInRange"/>
/// </summary>
/// <returns></returns>
protected override bool CheckInRange()
{
return CurrentValue == LimitValue;
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using Aitex.Core.RT.IOCore.Interlock.DataProvider;
namespace Aitex.Core.RT.IOCore.Interlock.Limits;
/// <summary>
/// 基于DI判断的互锁限制条件。
/// </summary>
internal class DiLimit : InterlockLimitBase<DiValueProvider, bool>
{
public override bool CurrentValue => (bool)DataProvider.GetValue();
public override string LimitReason =>
$"{DataProvider.Name} = [{(CurrentValue ? "ON" : "OFF")}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
public override string DaemonReason =>
$"{DataProvider.Name} = [{(LimitValue ? "ON" : "OFF")}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
public DiLimit(DiValueProvider dataProvider, string value, string tip, Dictionary<string, string> cultureTip,bool ignoreReverse)
: base(dataProvider, value, tip, cultureTip,ignoreReverse)
{
if (bool.TryParse(value, out var limitValue))
LimitValue = limitValue;
else
throw new InvalidCastException($"unable to convert {value} to boolean.");
}
protected override bool CheckInRange()
{
return CurrentValue == LimitValue;
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using Aitex.Core.RT.IOCore.Interlock.DataProvider;
namespace Aitex.Core.RT.IOCore.Interlock.Limits;
/// <summary>
/// 基于AO判断的互锁限制条件。
/// </summary>
internal class DoLimit : InterlockLimitBase<DoValueProvider, bool>
{
public override bool CurrentValue => (bool)DataProvider.GetValue();
public override string LimitReason =>
$"{DataProvider.Name} = [{(CurrentValue ? "ON" : "OFF")}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
public override string DaemonReason =>
$"{DataProvider.Name} = [{(LimitValue ? "ON" : "OFF")}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
public DoLimit(DoValueProvider dataProvider, string value, string tip, Dictionary<string, string> cultureTip, bool ignoreReverse)
: base(dataProvider, value, tip, cultureTip, ignoreReverse)
{
if (bool.TryParse(value, out var limitValue))
LimitValue = limitValue;
else
throw new InvalidCastException($"unable to convert {value} to boolean.");
}
protected override bool CheckInRange()
{
return CurrentValue == LimitValue;
}
}

View File

@ -0,0 +1,74 @@
using System.Collections.Generic;
using Aitex.Core.RT.IOCore.Interlock.DataProvider;
using Aitex.Core.RT.IOCore.Interlock.Utils;
namespace Aitex.Core.RT.IOCore.Interlock.Limits;
public class DoubleDataPollLimit : InterlockLimitBase<DoubleDataPollProvider, double>
{
#region Constructors
public DoubleDataPollLimit(IInterlockLimitDataProvider dataProvider, string value, string tip,
Dictionary<string, string> cultureTip,bool ignoreReverse) : base(dataProvider, value, tip, cultureTip, ignoreReverse)
{
LimitRange = new InterlockLimitRangeDouble(value);
}
#endregion
#region Properties
private InterlockLimitRangeDouble LimitRange { get; }
/// <summary>
/// <inheritdoc cref="InterlockLimitBase{TAccessor,TValue}.CurrentValue"/>
/// </summary>
public override double CurrentValue
{
get
{
if (DataProvider.GetValue() is double value)
return value;
// 如果Poll不到值始终返回LimitValue相反的值避免当前Limit被命中
return double.NaN;
}
}
/// <summary>
/// <inheritdoc cref="InterlockLimitBase{TAccessor,TValue}.LimitReason"/>
/// </summary>
public override string LimitReason =>
$"{DataProvider.Name} = {CurrentValue:F1}, in the range [{LimitRange}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
/// <summary>
/// <inheritdoc cref="InterlockLimitBase{TAccessor,TValue}.DaemonReason"/>
/// </summary>
public override string DaemonReason =>
$"{DataProvider.Name} = {CurrentValue:F1}, in the range [{LimitRange}]{(string.IsNullOrEmpty(Tip) ? "" : $",{Tip}")}";
#endregion
#region Methods
/// <summary>
/// <inheritdoc cref="InterlockLimitBase{TAccessor,TValue}.CheckInRange"/>
/// </summary>
/// <returns></returns>
protected override bool CheckInRange()
{
return LimitRange.CheckIsInRange(CurrentValue);
}
/// <summary>
/// <inheritdoc cref="InterlockLimitBase{TAccessor,TValue}.ToString"/>
/// </summary>
/// <returns></returns>
public override string ToString()
{
return $"{Name}, Limit={LimitRange}";
}
#endregion
}

View File

@ -0,0 +1,187 @@
using System.Collections.Generic;
using System.Diagnostics;
using Aitex.Core.Util;
namespace Aitex.Core.RT.IOCore.Interlock.Limits;
/// <summary>
/// 互锁限制条件。
/// </summary>
public abstract class InterlockLimitBase<TAccessor, TValue> : IInterlockLimit
where TAccessor : IInterlockLimitDataProvider
where TValue : struct
{
#region Variables
private Dictionary<string, string> _cultureTip;
private readonly R_TRIG _trigger;
#endregion
#region Constructor
/// <summary>
/// 互锁限制条件的构造函数。
/// </summary>
/// <param name="dataProvider">数据供应器对象实例</param>
/// <param name="value">当前限制条件中的IO状态。</param>
/// <param name="tip">默认语言提示信息。</param>
/// <param name="cultureTip">多国语言提示信息。</param>
/// <param name="ignoreReverse">是否触发Action反转</param>
/// <exception cref="InvalidIoNameException">IO名称错误前缀不是“DI_”、”DO_“、”AI_“或"AO_".</exception>
protected InterlockLimitBase(IInterlockLimitDataProvider dataProvider, string value, string tip,
Dictionary<string, string> cultureTip,bool ignoreReverse = false)
{
Debug.Assert(dataProvider != null, "The data provider can not be null.");
_trigger = new R_TRIG();
DataProvider = dataProvider;
Tip = tip;
_cultureTip = cultureTip;
UniqueId = $"{dataProvider.Name}.{value}";
IgnoreReverse = ignoreReverse;
}
#endregion
#region Properties
/// <summary>
/// 放回当前Limit所对应的IO对象。
/// </summary>
protected IInterlockLimitDataProvider DataProvider { get; }
/// <summary>
/// 返回互锁限制条件的唯一识别码。
/// <remarks>
/// 该唯一识别码用于创建字典时作为字典的Key值使用。
/// <br/>
/// 该值由Name+LimitValue组成。
/// </remarks>
/// </summary>
public string UniqueId { get; }
/// <summary>
/// <inheritdoc cref="IInterlockLimit.Name"/>
/// </summary>
public string Name => DataProvider?.Name ?? "";
/// <summary>
/// <inheritdoc cref="IInterlockLimit.Description"/>
/// </summary>
public string Description => DataProvider?.Description ?? "";
/// <summary>
/// 返回当前Limit对应的数据值。
/// </summary>
public abstract TValue CurrentValue { get; }
/// <summary>
/// 返回当前互锁限制条件触发的原因。
/// </summary>
public abstract string LimitReason { get; }
/// <summary>
/// 返回守护条件满足时输出的信息。
/// </summary>
public abstract string DaemonReason { get; }
/// <summary>
/// 返回当前互锁条件的信号约束值。
/// </summary>
public TValue LimitValue { get; protected set; }
/// <summary>
/// 返回当前互锁条件提示信息。
/// </summary>
public string Tip { get; }
/// <summary>
/// 返回该Limit是否对Action进行反转
/// </summary>
public bool IgnoreReverse { get; }
#endregion
#region Methods
/*
/// <summary>
/// 判断两个互锁限制条件是否相等。
/// </summary>
/// <param name="interlockLimit">待比较的互锁限制条件。</param>
/// <returns>
/// <para>True: 相同False不同。</para>
/// </returns>
public bool IsSame(object interlockLimit)
{
if (interlockLimit is not InterlockLimit<TAccessor, TValue> li)
return false;
return Name == li.Name && li.LimitValue == LimitValue;
}*/
/// <summary>
/// 返回互锁限制监测的信号当前值和期望值不相等的条件是否触发。
/// <remarks>
/// 捕获当前值和期望值不相等信号的上升沿当上升沿到达时触发输出Q。
/// </remarks>
/// </summary>
/// <returns></returns>
public bool IsTriggered()
{
_trigger.CLK = !CheckInRange();
return _trigger.Q;
}
/// <summary>
/// 根据互锁条件判断是否允许DO输出。
/// </summary>
/// <param name="reason">如果禁止DO输出返回互锁原因。</param>
/// <returns></returns>
public bool CanDo(out string reason)
{
reason = string.Empty;
if (CheckInRange())
return true;
reason = LimitReason;
return false;
}
/// <summary>
/// 获取指定互锁限制条件中限制条件的内容。
/// </summary>
/// <returns></returns>
public virtual string GetLimitValue()
{
return LimitValue.ToString();
}
/// <summary>
/// 获取指定互锁限制条件中当前IO状态。
/// </summary>
/// <returns></returns>
public virtual string GetCurrentValue()
{
return CurrentValue.ToString();
}
/// <summary>
/// 检查当前值是否在指定范围内。
/// </summary>
/// <returns>True: 在范围内; False超出范围。</returns>
protected abstract bool CheckInRange();
/// <summary>
/// <inheritdoc />
/// </summary>
/// <returns></returns>
public override string ToString()
{
return $"{Name}, Limit={LimitValue}";
}
#endregion
}

View File

@ -1,6 +1,7 @@
using System;
using SciChart.Core.Extensions;
namespace Aitex.Core.RT.IOCore;
namespace Aitex.Core.RT.IOCore.Interlock.Utils;
public class InterlockLimitRangeDouble : IAnalogInterlockLimitRange<double>
{
@ -40,6 +41,9 @@ public class InterlockLimitRangeDouble : IAnalogInterlockLimitRange<double>
public virtual bool CheckIsInRange(double currentValue)
{
if (double.IsNaN(currentValue))
return false;
return currentValue >= Min && currentValue <= Max;
}

View File

@ -1,6 +1,6 @@
using System;
namespace Aitex.Core.RT.IOCore;
namespace Aitex.Core.RT.IOCore.Interlock.Utils;
public class InterlockLimitRangeInt: IAnalogInterlockLimitRange<int>
{

View File

@ -1,6 +1,6 @@
using System;
namespace Aitex.Core.RT.IOCore;
namespace Aitex.Core.RT.IOCore.Interlock.Utils;
public class InterlockLimitRangeShort : IAnalogInterlockLimitRange<short>
{

View File

@ -48,9 +48,6 @@
<RootNamespace>MECF.Framework.Common</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="CommandLine">
<HintPath>..\Dependencies\CommandLine.dll</HintPath>
</Reference>
<Reference Include="DocumentFormat.OpenXml, Version=2.10.1.0, Culture=neutral, PublicKeyToken=8fb06cb64d019a17, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\Dependencies\DocumentFormat.OpenXml.dll</HintPath>
@ -340,18 +337,34 @@
<Compile Include="Aitex\Core\RT\IOCore\GetValue.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Index.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interfaces\IAnalogInterlockLimitRange.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interfaces\IInterlockAction.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interfaces\IInterlockLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interfaces\IInterlockLimitDataProvider.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interfaces\IIOAccessor.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\AiLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\AoLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\DiLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\DoLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\InterlockAction.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\InterlockLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\InterlockLimitRangeDouble.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\InterlockLimitRangeInt.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\InterlockLimitRangeShort.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Actions\InterlockAction.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Actions\InterlockActionBase.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Actions\InterlockDaemonAction.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\DataProvider\AiValueProvider.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\DataProvider\AoValueProvider.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\DataProvider\BoolDataPollProvider.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\DataProvider\DataPollProviderBase.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\DataProvider\DiValueProvider.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\DataProvider\DoubleDataPollProvider.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\DataProvider\DoValueProvider.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\DataProvider\IoValueProviderBase.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\InterlockDaemonManager.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\InterlockManager.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\InterlockManagerBase.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Limits\AiLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Limits\AoLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Limits\BoolDataPollLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Limits\DiLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Limits\DoLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Limits\DoubleDataPollLimit.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Limits\InterlockLimitBase.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Utils\InterlockLimitRangeDouble.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Utils\InterlockLimitRangeInt.cs" />
<Compile Include="Aitex\Core\RT\IOCore\Interlock\Utils\InterlockLimitRangeShort.cs" />
<Compile Include="Aitex\Core\RT\IOCore\IO.cs" />
<Compile Include="Aitex\Core\RT\IOCore\IOAccessor.cs" />
<Compile Include="Aitex\Core\RT\IOCore\IOType.cs" />

View File

@ -164,16 +164,16 @@ namespace MECF.Framework.Common.Equipment
}
/// <summary>
/// 将指定的字符串转换为模组名称枚举。
/// 将指定的字符串转换为模组名称枚举。
/// </summary>
/// <remarks>
/// 关于有效的模组名称,请参考<see cref="ModuleName"/>枚举。
/// 关于有效的模组名称,请参考<see cref="ModuleName"/>枚举。
/// </remarks>
/// <param name="module">模组名称。</param>
/// <param name="module">模组名称。</param>
/// <returns>
/// <see cref="ModuleName"/>。
/// <see cref="ModuleName"/>。
/// <br/>
/// 如果指定的名称不存在,则返回<see cref="ModuleName.UnDefined"/>。
/// 如果指定的名称不存在,则返回<see cref="ModuleName.UnDefined"/>。
/// </returns>
public static ModuleName Converter(string module)
{

View File

@ -5,6 +5,7 @@ using System.Xml;
using Aitex.Core.RT.DataCenter;
using Aitex.Core.RT.Event;
using Aitex.Core.RT.IOCore;
using Aitex.Core.RT.IOCore.Interlock;
using Aitex.Core.RT.Log;
using Aitex.Core.RT.OperationCenter;
using Aitex.Core.RT.SCCore;
@ -51,21 +52,28 @@ namespace MECF.Framework.Common.IOCore
private PeriodicJob _monitorThread;
public void Initialize(string interlockConfigFile)
public void Initialize(string interlockConfigFile, string daemonConfigFile = "")
{
string reason = string.Empty;
if (!Singleton<InterlockManager>.Instance.Initialize(interlockConfigFile, _doMap, _diMap, _aiMap, _aoMap, out reason))
{
throw new Exception($"interlock define file found error: \r\n {reason}");
}
_monitorThread = new PeriodicJob(200, OnTimer, "IO Monitor Thread", isStartNow: true);
}
if (!Singleton<InterlockManager>.Instance.Initialize(interlockConfigFile, _doMap, _diMap, _aiMap, _aoMap,
out var reason1))
throw new Exception($"init {nameof(InterlockManager)} error: \r\n{reason1}");
/*if (!Singleton<InterlockDaemonManager>.Instance.Initialize(daemonConfigFile, _doMap, _diMap, _aiMap, _aoMap,
out var reason2))
{
//TODO 暂时允许不定义Daemon配置文件
// throw new Exception($"init {nameof(InterlockDaemonManager)} error: \r\n {reason2}");
LOG.Error($"init {nameof(InterlockDaemonManager)} error: \r\n{reason2}");
}*/
}
private bool OnTimer()
{
try
{
Singleton<InterlockManager>.Instance.Monitor();
// Singleton<InterlockDaemonManager>.Instance.Monitor();
}
catch (Exception ex)
{
@ -113,6 +121,8 @@ namespace MECF.Framework.Common.IOCore
OP.Subscribe("System.SetAoValueWithPrivoder", InvokeSetAoWithPrivoder);
OP.Subscribe("System.SetAiBuffer", InvokeSetAiBuffer);
OP.Subscribe("System.SetDiBuffer", InvokeSetDiBuffer);
_monitorThread = new PeriodicJob(200, OnTimer, "IO Monitor Thread", isStartNow: true);
}
private bool InvokeSetDo(string arg1, object[] args)

View File

@ -341,7 +341,7 @@ namespace Aitex.Core.RT.Device.Devices
_scUpLatchTimeout = ParseScNode("ChamberMoveBodyUpLatchTimeOut", node, "PM", "PM.LidMotionTimeout");
_scForwardTimeout = ParseScNode("ChamberMoveBodyForwardTimeOut", node, "PM", "PM.LidMotionTimeout");
_scBackwardTimeout = ParseScNode("ChamberMoveBodyBackwardTimeOut", node, "PM", "PM.LidMotionTimeout");
_scByPassInterLock= ParseScNode("BypassInterlock", node, "PM", "System.BypassInterlock");
_scByPassInterLock= ParseScNode("BypassInterlock", node, "PM", $"PM.{module}.BypassInterlock");
}
public bool Initialize()

View File

@ -13,6 +13,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using MECF.Framework.Common.MECF.Framework.Common.Utilities;
namespace Aitex.Core.RT.Device.Devices
{
@ -138,6 +139,11 @@ namespace Aitex.Core.RT.Device.Devices
}
}
/// <summary>
/// 返回隔热罩在位判断结果,是否在高位。
/// </summary>
public bool RingInpUp => DoubleUtil.IsEqual(RingCurPos, RingUpPos);
public float RingDownPos
{
get
@ -149,6 +155,11 @@ namespace Aitex.Core.RT.Device.Devices
}
}
/// <summary>
/// 返回隔热罩在位判断结果,是否在低位。
/// </summary>
public bool RingInpDown => DoubleUtil.IsEqual(RingCurPos, RingDownPos);
private DeviceTimer _timer = new DeviceTimer();
private SCConfigItem _scUpPos;
@ -201,8 +212,12 @@ namespace Aitex.Core.RT.Device.Devices
DATA.Subscribe($"{Module}.{Name}.RingUpPos", () => RingUpPos);
DATA.Subscribe($"{Module}.{Name}.RingDownPos", () => RingDownPos);
DATA.Subscribe($"{Module}.{Name}.RingInpUp", () => RingInpUp);
DATA.Subscribe($"{Module}.{Name}.RingInpDown", () => RingInpDown);
DATA.Subscribe($"{Module}.{Name}.RingUpSensor", () => RingUpSensor);
DATA.Subscribe($"{Module}.{Name}.RingDownSensor", () => RingDownSensor);
DATA.Subscribe($"{Module}.{Name}.RingDone", () => RingDone);
DATA.Subscribe($"{Module}.{Name}.RingIsServoOn", () => RingServoOn);

View File

@ -168,21 +168,21 @@ namespace Aitex.Core.RT.Device.Devices
No related alarm according to the interlock table
<Limit ai="PM1.AI_ActualSpeed" value="0" tip="" tip.zh-CN="" tip.en-US="AI-118" /> ChamMoveBodyUpDownEnableCanDo函数里
*/
[Subscription("PSU1.AllHeatEnable")]
public bool AllHeatEnable { get; set; }
//[Subscription("PSU1.AllHeatEnable")]
//public bool AllHeatEnable { get; set; }
[Subscription("IsService")]
public bool IsService { get; set; }
/// <summary>
/// DO-100
/// </summary>
public bool ChamMoveBodyUpDownEnable
{
get
{
return this.ChamMoveBodyUpDownEnableCanDo();
}
}
//[Subscription("IsService")]
//public bool IsService { get; set; }
///// <summary>
///// DO-100
///// </summary>
//public bool ChamMoveBodyUpDownEnable
//{
// get
// {
// return this.ChamMoveBodyUpDownEnableCanDo();
// }
//}
#endregion
@ -583,7 +583,7 @@ namespace Aitex.Core.RT.Device.Devices
public bool Initialize()
{
DATA.Subscribe($"{Module}.{Name}.ChamMoveBodyUpDownEnable", () => ChamMoveBodyUpDownEnable);
//DATA.Subscribe($"{Module}.{Name}.ChamMoveBodyUpDownEnable", () => ChamMoveBodyUpDownEnable);
return true;
@ -820,9 +820,7 @@ namespace Aitex.Core.RT.Device.Devices
}
public bool DoReactorATMTransferReady
{
get
@ -1209,18 +1207,29 @@ namespace Aitex.Core.RT.Device.Devices
}
/// <summary>
/// ChamMoveBodyUpDownEnableCanDo
/// </summary>
/// <returns></returns>
public bool ChamMoveBodyUpDownEnableCanDo()
{
//PSU disable
///// <summary>
///// ChamMoveBodyUpDownEnableCanDo
///// </summary>
///// <returns></returns>
//public bool ChamMoveBodyUpDownEnableCanDo()
//{
// //PSU disable
//Service mode
// //Service mode
// var v39 = (bool)DATA.Poll(Module, "V39.Status");
// var v40 = (bool)DATA.Poll(Module, "V40.Status");
// var v41 = (bool)DATA.Poll(Module, "V41.Status");
// var v53 = (bool)DATA.Poll(Module, "V53.Status");
// var v54 = (bool)DATA.Poll(Module, "V54.Status");
// var v55 = (bool)DATA.Poll(Module, "V55.Status");
// var v59 = (bool)DATA.Poll(Module, "V59.Status");
return (!AllHeatEnable) && IsService && (_aiActualSpeed.Value == 0);
}
// var isService = (bool)DATA.Poll(Module, "IsService");
// var heaterEnabled = (bool)DATA.Poll(Module, "PSU1.AllHeatEnable");
// return (!AllHeatEnable) && IsService && (_aiActualSpeed.Value == 0);
//}
}
}

View File

@ -9,6 +9,7 @@ using Aitex.Core.Util;
using Aitex.Core.Utilities;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;
using System.Xml;
@ -584,7 +585,12 @@ namespace Aitex.Core.RT.Device.Devices
if (isEnable)
{
if (!_doTVValveEnable.SetValue(isEnable, out reason))
{
EV.PostWarningLog(Module, $"Turn {Display} On failed for Interlock!");
EV.PostMessage(Module, EventEnum.SwInterlock, Module, string.Format("Valve {0} was {1}Reason{2}", Display, "Closed", reason));
return false;
}
Thread.Sleep(50);

View File

@ -19,6 +19,8 @@ namespace Aitex.Core.RT.Device.Devices
public class IoValve : BaseDevice, IDevice, IValve
{
#region Variables
private readonly DeviceTimer _timMfcRampTimeout = new DeviceTimer();
private const int MFC_RAMP_DURATION_SEC = 5;
private const int MFC_RAMP_TIMEOUT_SEC = 10;
@ -26,6 +28,7 @@ namespace Aitex.Core.RT.Device.Devices
private IoMFC _mappedMfc;
private bool _isValveOnAfterMfcRamp;
#endregion
public string GVName { get { return Name; } }
@ -151,13 +154,18 @@ namespace Aitex.Core.RT.Device.Devices
_doClose = ParseDoNode("doClose", node, ioModule);
_uniqueName = $"{Module}.{Name}";
_scBypassEnableTable = ParseScNode("BypassEnableTable", node, module, "System.BypassEnableTable");
if(module.StartsWith("PM"))
_scBypassEnableTable = ParseScNode("scBypassEnableTable", node, module, $"PM.{module}.BypassEnableTable");
else
_scBypassEnableTable = ParseScNode("scBypassEnableTable", node, module, $"{module}.BypassEnableTable");
}
public bool Initialize()
{
DATA.Subscribe($"Device.{Module}.{GVName}", () => DeviceData);
DATA.Subscribe($"{_uniqueName}.DeviceData", () => DeviceData);
DATA.Subscribe($"{_uniqueName}.Status", () => Status);
OP.Subscribe($"{_uniqueName}.{AITValveOperation.GVTurnValve}", InvokeOpenCloseValve);
@ -227,7 +235,7 @@ namespace Aitex.Core.RT.Device.Devices
return true;
}
public void Monitor()
protected override void HandleMonitor()
{
try
{
@ -274,7 +282,7 @@ namespace Aitex.Core.RT.Device.Devices
}
}
else if(ActionAfterOnOff != null)
else if (ActionAfterOnOff != null)
{
ActionAfterOnOff(Status);
}
@ -306,7 +314,7 @@ namespace Aitex.Core.RT.Device.Devices
}
//达到一定条件,强制打开阀门
if(FuncForceOpen!= null && !_scBypassEnableTable.BoolValue)
if (FuncForceOpen != null && !_scBypassEnableTable.BoolValue)
{
forceOpenTrigger.CLK = FuncForceOpen(Status);
if (forceOpenTrigger.Q)
@ -319,9 +327,7 @@ namespace Aitex.Core.RT.Device.Devices
catch (Exception ex)
{
LOG.Write(ex);
}
}
@ -349,7 +355,8 @@ namespace Aitex.Core.RT.Device.Devices
{
if (!FuncCheckInterLock(isOn))
{
EV.PostWarningLog(Module, $"Turn {Display} {ValveStateToString(isOn)} failed for check condition!");
reason = "Interlock check failed!";
//EV.PostWarningLog(Module, $"Turn {Display} {ValveStateToString(isOn)} failed for check condition!");
return false;
}
}
@ -448,7 +455,7 @@ namespace Aitex.Core.RT.Device.Devices
{
EV.PostInfoLog(Module, $"Start to ramp {_mfcName} to 0.0{_mappedMfc.Unit}");
_isValveOnAfterMfcRamp = isOn;
_mappedMfc.Ramp(0, 5*1000);
_mappedMfc.Ramp(0, 5 * 1000);
_timMfcRampTimeout.Start(10 * 1000);
}

View File

@ -45,6 +45,10 @@ namespace MECF.Framework.RT.EquipmentLibrary.HardwareUnits.PMs
public abstract bool IsService { get; }
public bool IsBypassInterlock => SC.GetValue<bool>($"PM.{Module}.BypassInterlock");
public bool IsBypassEnableTable => SC.GetValue<bool>($"PM.{Module}.BypassEnableTable");
#endregion
#region Methods
@ -60,20 +64,14 @@ namespace MECF.Framework.RT.EquipmentLibrary.HardwareUnits.PMs
/// 检查是否Bypass了Interlock。
/// </summary>
/// <returns></returns>
protected void DoIfInterlockEnabled(Action action)
protected void DoIfInterlockNotBypassed(Action action)
{
// 读取系统配置
var byPassInterlock = SC.GetValue<bool>("System.BypassInterlock");
var byPassEnableTable = SC.GetValue<bool>("System.BypassEnableTable");
if (byPassInterlock)
EV.PostWarningLog(Module, "System.BypassInterlock is True");
else if (byPassEnableTable)
EV.PostWarningLog(Module, "System.BypassEnableTable is True");
if (IsBypassInterlock)
EV.PostWarningLog(Module, $"Interlock is bypassed");
else if (IsBypassEnableTable)
EV.PostWarningLog(Module, $"Enable table is bypassed");
else
{
action.Invoke();
}
}
#endregion

View File

@ -12,6 +12,7 @@ using Caliburn.Micro.Core;
using System.Windows.Media;
using System.Windows.Threading;
using System.Data;
using System.Diagnostics;
using System.Threading.Tasks;
using Aitex.Core.Common.DeviceData.IoDevice;
using OpenSEMI.ClientBase;
@ -61,7 +62,6 @@ namespace MECF.Framework.UI.Client.CenterViews.Modules.PM
public PMProcessViewModel()
{
_recipeGasFlowCalculator = IoC.Get<IRecipeGasFlowCalculator>();
Subscribe("System.IsEngMode");
IsBusyGasFlowSum = false;
_busyIndicatorContentExport = new Progress<ProgressUpdatingEventArgs>(e =>
{
@ -94,9 +94,7 @@ namespace MECF.Framework.UI.Client.CenterViews.Modules.PM
NotifyOfPropertyChange();
}
}
public bool IsEngMode { get; private set; }
[Subscription("IsBusy")]
public bool IsBusy { get; set; }
@ -398,9 +396,6 @@ namespace MECF.Framework.UI.Client.CenterViews.Modules.PM
{
try
{
if (data.TryGetValue("System.IsEngMode", out var isEngMode) && isEngMode is bool b)
IsEngMode = b;
if (_needLoadRecipe)
_needLoadRecipe = false;
@ -829,9 +824,25 @@ namespace MECF.Framework.UI.Client.CenterViews.Modules.PM
public void StartProcess()
{
if (IsEngMode)
var pollBypassInterlock= $"{SystemName}.IsBypassInterlock";
var pollBypassEnableTable = $"{SystemName}.IsBypassEnableTable";
var pollIsService = $"{SystemName}.IsService";
var data = QueryDataClient.Instance.Service.PollData(new[]
{ pollBypassInterlock, pollBypassEnableTable, pollIsService });
Debug.Assert(data != null, "Unable to poll data from RT.");
Debug.Assert(data.Count == 3, "The count of polled data is incorrect.");
if ((bool)data[pollBypassInterlock] || (bool)data[pollBypassEnableTable])
{
DialogBox.ShowError("Interlock is bypassed, can not start process.");
DialogBox.ShowError("Interlock or EnableTable is bypassed, can not start process.");
return;
}
if ((bool)data[pollIsService])
{
DialogBox.ShowError($"{SystemName} is in Service Mode, can not start process.");
return;
}

View File

@ -0,0 +1,64 @@
<UserControl x:Class="MECF.Framework.UI.Client.Ctrlib.Controls.ModuleStatusIndicator"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MECF.Framework.UI.Client.Ctrlib.Controls"
xmlns:converters="clr-namespace:MECF.Framework.UI.Core.Converters;assembly=MECF.Framework.UI.Core"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<converters:BoolReverseConverter x:Key="BoolReverseConverter"/>
<converters:BoolVisibilityConverter x:Key="BoolVisibilityConverter"/>
<local:ModuleIsOnlineToBgColorConverter x:Key="IsOnlineToBdrColor"/>
<local:ModuleStatusToBackgroundColorConverter x:Key="StatusToBgColor"/>
</UserControl.Resources>
<DockPanel LastChildFill="True" DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Mode=OneTime}">
<Label
DockPanel.Dock="Left"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Width="{Binding CaptionWidth}"
BorderBrush="{Binding IsOnline, Converter={StaticResource IsOnlineToBdrColor}}"
Content="{Binding Caption}"
ToolTip="{Binding ModuleDescription}"
Style="{DynamicResource TopLable_LeftTop}" HorizontalAlignment="Left">
<Label.ContextMenu>
<ContextMenu>
<MenuItem
x:Name="CmSetOnline"
Header="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu}, Path=PlacementTarget.Content}"
HeaderStringFormat="Set {0} Online"
IsEnabled="{Binding IsOnline, Converter={StaticResource BoolReverseConverter}}"
Click="CmSetOnline_OnClick"/>
<MenuItem
x:Name="CmSetOffline"
Header="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu}, Path=PlacementTarget.Content}"
HeaderStringFormat="Set {0} Offline"
IsEnabled="{Binding IsOnline}"
Click="CmSetOffline_OnClick"/>
</ContextMenu>
</Label.ContextMenu>
</Label>
<Grid>
<Image
Source="pack://application:,,,/MECF.Framework.UI.Core;component/Resources/SystemLog/Warning.png"
HorizontalAlignment="Right"
Margin="0 0 10 0"
Height="16"
Width="16"
Visibility="{Binding HasWarning, Converter={StaticResource BoolVisibilityConverter}, ConverterParameter=True}"
ToolTip="{Binding WarningTip}"
Panel.ZIndex="999"/>
<TextBox
VerticalContentAlignment="Center"
Background="{Binding Status, Converter={StaticResource StatusToBgColor}}"
Style="{StaticResource TextBox_Top}"
Text="{Binding Status, Mode=OneWay}"
IsReadOnly="True"
TextWrapping="Wrap" Margin="0,2,2,2" />
</Grid>
</DockPanel>
</UserControl>

View File

@ -0,0 +1,259 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
namespace MECF.Framework.UI.Client.Ctrlib.Controls
{
/// <summary>
/// Interaction logic for ModuleStatusIndicator.xaml
/// </summary>
public partial class ModuleStatusIndicator : UserControl
{
public ModuleStatusIndicator()
{
InitializeComponent();
}
#region Dependency Properties
#region DP - Caption
public static readonly DependencyProperty CaptionProperty = DependencyProperty.Register(
nameof(Caption), typeof(string), typeof(ModuleStatusIndicator), new PropertyMetadata(default(string)));
/// <summary>
/// 设置或返回标题
/// </summary>
public string Caption
{
get => (string)GetValue(CaptionProperty);
set => SetValue(CaptionProperty, value);
}
#endregion
#region DP - Caption Width
public static readonly DependencyProperty CaptionWidthProperty = DependencyProperty.Register(
nameof(CaptionWidth), typeof(double), typeof(ModuleStatusIndicator), new PropertyMetadata(100.0d));
public double CaptionWidth
{
get => (double)GetValue(CaptionWidthProperty);
set => SetValue(CaptionWidthProperty, value);
}
#endregion
#region DP - Status
public static readonly DependencyProperty StatusProperty = DependencyProperty.Register(
nameof(Status), typeof(string), typeof(ModuleStatusIndicator), new PropertyMetadata(default(string)));
public string Status
{
get => (string)GetValue(StatusProperty);
set => SetValue(StatusProperty, value);
}
#endregion
#region DP - IsOnline
public static readonly DependencyProperty IsOnlineProperty = DependencyProperty.Register(
nameof(IsOnline), typeof(bool), typeof(ModuleStatusIndicator), new PropertyMetadata(default(bool)));
public bool IsOnline
{
get => (bool)GetValue(IsOnlineProperty);
set => SetValue(IsOnlineProperty, value);
}
#endregion
#region DP - ModuleName
public static readonly DependencyProperty ModuleNameProperty = DependencyProperty.Register(
nameof(ModuleName), typeof(string), typeof(ModuleStatusIndicator), new PropertyMetadata(default(string)));
public string ModuleName
{
get => (string)GetValue(ModuleNameProperty);
set => SetValue(ModuleNameProperty, value);
}
#endregion
#region DP - Module Description
public static readonly DependencyProperty ModuleDescriptionProperty = DependencyProperty.Register(
nameof(ModuleDescription), typeof(string), typeof(ModuleStatusIndicator), new PropertyMetadata(default(string)));
public string ModuleDescription
{
get => (string)GetValue(ModuleDescriptionProperty);
set => SetValue(ModuleDescriptionProperty, value);
}
#endregion
#region DP - HasWarning
public static readonly DependencyProperty HasWarningProperty = DependencyProperty.Register(
nameof(HasWarning), typeof(bool), typeof(ModuleStatusIndicator), new PropertyMetadata(default(bool)));
public bool HasWarning
{
get => (bool)GetValue(HasWarningProperty);
set => SetValue(HasWarningProperty, value);
}
#endregion
#region DP - WarningTips
public static readonly DependencyProperty WarningTipProperty = DependencyProperty.Register(
nameof(WarningTip), typeof(string), typeof(ModuleStatusIndicator), new PropertyMetadata(default(string)));
public string WarningTip
{
get => (string)GetValue(WarningTipProperty);
set => SetValue(WarningTipProperty, value);
}
#endregion
#region DP - SetOnline Command
public static readonly DependencyProperty SetOnlineCommandProperty = DependencyProperty.Register(
nameof(SetOnlineCommand), typeof(ICommand), typeof(ModuleStatusIndicator), new PropertyMetadata(default(ICommand)));
public ICommand SetOnlineCommand
{
get => (ICommand)GetValue(SetOnlineCommandProperty);
set => SetValue(SetOnlineCommandProperty, value);
}
#endregion
#region DP - SetOffline Command
public static readonly DependencyProperty SetOfflineCommandProperty = DependencyProperty.Register(
nameof(SetOfflineCommand), typeof(ICommand), typeof(ModuleStatusIndicator), new PropertyMetadata(default(ICommand)));
public ICommand SetOfflineCommand
{
get => (ICommand)GetValue(SetOfflineCommandProperty);
set => SetValue(SetOfflineCommandProperty, value);
}
#endregion
#endregion
#region Event Handler
private void CmSetOnline_OnClick(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(SetOnlineEvent));
}
private void CmSetOffline_OnClick(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(SetOfflineEvent));
}
#endregion
#region Routed Events
public static readonly RoutedEvent SetOnlineEvent = EventManager.RegisterRoutedEvent(nameof(SetOnline), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(UserControl));
public event RoutedEventHandler SetOnline
{
add => AddHandler(SetOnlineEvent, value);
remove => RemoveHandler(SetOnlineEvent, value);
}
public static readonly RoutedEvent SetOfflineEvent = EventManager.RegisterRoutedEvent(nameof(SetOffline), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(UserControl));
public event RoutedEventHandler SetOffline
{
add => AddHandler(SetOfflineEvent, value);
remove => RemoveHandler(SetOfflineEvent, value);
}
#endregion
}
internal class ModuleIsOnlineToBgColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool isOnline)
{
if (isOnline)
return new SolidColorBrush(Colors.LawnGreen);
else
{
return new SolidColorBrush(Colors.Gray);
}
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
internal class ModuleStatusToBackgroundColorConverter : IValueConverter
{
public static Color GetStatusBackground(string status)
{
if (status != null)
{
status = status.Trim().ToLower();
}
switch (status)
{
case "error":
return Colors.OrangeRed;
case "processidle":
case "vacidle":
case "idle":
case "manual":
return Colors.White;
case "notconnect":
case "init":
return Colors.Yellow;
case "offline":
case "notinstall":
return Colors.Gray;
default:
return Colors.LawnGreen;
}
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string status)
{
return new SolidColorBrush(GetStatusBackground(status));
}
return Colors.BlanchedAlmond;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -257,6 +257,9 @@
<Compile Include="Ctrlib\Controls\HeaterResPresenter.xaml.cs">
<DependentUpon>HeaterResPresenter.xaml</DependentUpon>
</Compile>
<Compile Include="Ctrlib\Controls\ModuleStatusIndicator.xaml.cs">
<DependentUpon>ModuleStatusIndicator.xaml</DependentUpon>
</Compile>
<Compile Include="Ctrlib\Controls\PanelLocker.xaml.cs">
<DependentUpon>PanelLocker.xaml</DependentUpon>
</Compile>
@ -913,6 +916,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Ctrlib\Controls\ModuleStatusIndicator.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="RecipeEditorLib\DGExtension\DataGridRecipe.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>

View File

@ -1,28 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace MECF.Framework.UI.Core.Converters
{
/// <summary>
/// EngMode转换为主界面边框颜色
/// </summary>
public class EngModeToBdColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool isEngMode)
{
return isEngMode ? new SolidColorBrush(Colors.OrangeRed) : new SolidColorBrush(Colors.LightCyan);
}
return new SolidColorBrush(Colors.Red);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,31 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace MECF.Framework.UI.Core.Converters
{
/// <summary>
/// EngMode转换为主界面边框宽度
/// </summary>
public class EngModeToBdThicknessConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
/*
if (value is bool isEngMode)
{
return isEngMode ? new Thickness(5) : new Thickness(2);
}
*/
return new Thickness(2);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,28 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace MECF.Framework.UI.Core.Converters
{
/// <summary>
/// EngMode转换为主界面背景水印显示状态
/// </summary>
public class EngModeToBgWatermarkVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool isEngMode)
{
return isEngMode ? Visibility.Visible : Visibility.Hidden;
}
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -291,9 +291,6 @@
<Compile Include="Converters\CenterBorderGapMaskConverter.cs" />
<Compile Include="Converters\ColorConverter_IsTestOK.cs" />
<Compile Include="Converters\ColorToBrushConverter.cs" />
<Compile Include="Converters\EngModeToBdColorConverter.cs" />
<Compile Include="Converters\EngModeToBdThicknessConverter.cs" />
<Compile Include="Converters\EngModeToBgWatermarkVisibilityConverter.cs" />
<Compile Include="Converters\GeneralConverter.cs" />
<Compile Include="Converters\IntPermissionToIsEnabledConverter.cs" />
<Compile Include="Converters\MarkdownToHtmlConverter.cs" />

View File

@ -1,10 +1,4 @@
using Xunit;
using Aitex.Core.RT.IOCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Aitex.Core.RT.SCCore;
using MECF.Framework.Common.IOCore;
using MECF.Framework.Common.SCCore;
@ -19,6 +13,7 @@ namespace Aitex.Core.RT.IOCore.Tests
private const string FN_IO_TABLE = "SupportFiles\\_ioDefinePM1.xml";
private const string FN_DEVICE_MODEL = "SupportFiles\\DeviceModelPM1.xml";
private const string FN_INTERLOCK = "SupportFiles\\interlockPM1.xml";
private const string FN_DAEMON = "SupportFiles\\interlockDaemonPM1.xml";
[Fact()]
public void InitializeTest()

View File

@ -62,6 +62,9 @@
<Content Include="SupportFiles\DeviceModelPM1.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="SupportFiles\InterlockDaemonPM1.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="SupportFiles\interlockPM1.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View File

@ -556,6 +556,7 @@
diPSU2Status="DI_PSU2Status" diPSU3Status="DI_PSU3Status" diSCR1Status="DI_SCR1Status" diSCR2Status="DI_SCR2Status" diSCR3Status="DI_SCR3Status"
doPSU1Enable="DO_PSU1Enable" doPSU2Enable="DO_PSU2Enable" doPSU3Enable="DO_PSU3Enable" doSCR1Enable="DO_SCR1Enable"
doSCR2Enable="DO_SCR2Enable" doSCR3Enable="DO_SCR3Enable" doTCSSupply="DO_TCSFluidInfusion" aiTempCtrl1="AI_PSUTC"
doReactorATMTransferReady="DO_ReactorATMTransferReady" doReactorVACTransferReady="DO_ReactorVACTransferReady" diPMATMSW="DI_PMATMSW"
aiActualSpeed="AI_ActualSpeed" diPSUEnable="DI_PSUEnableFB" diHeaterTempBelow900CSW="DI_HeaterTempLowLimitSW"
doPMASlitDoorClosed="DO_SlitVlvClosed" diChamLidClosed="DI_ChamLidClosed" diConfinementRingDown="DI_ConfinementRingDown"

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<Daemon>
<Action do="PM1.DO_ReactorATMTransferReady" value="true" tip="" tip.zh-CN="" tip.en-US="DO-170">
<OR>
<Limit di="PM1.DI_PMATMSW" value="true" tip="" tip.zh-CN="" tip.en-US="DI-9"/>
<Limit di="PM1.DI_ConfinementRingDown" value="true" tip="" tip.zh-CN="" tip.en-US="DI-50"/>
</OR>
<Limit do="PM1.DO_HeaterEnable" value="false" tip="" tip.zh-CN="" tip.en-US="DO-56"/>
<Limit ai="PM1.AI_ActualSpeed" value="-0.001:0.001" tip="" tip.zh-CN="" tip.en-US="AI-118"/>
<Limit pollbool="PM1.ConfinementRing.RingInpDown" value="true" tip="" tip.zh-CN="" tip.en-US=""/>
</Action>
<Action do="PM1.DO_ReactorVACTransferReady" value="true" tip="" tip.zh-CN="" tip.en-US="DO-171">
<Limit di="PM1.DI_PMATMSW" value="true" tip="" tip.zh-CN="" tip.en-US="DI-9"/>
<Limit di="PM1.DI_ConfinementRingDown" value="true" tip="" tip.zh-CN="" tip.en-US="DI-50"/>
<Limit di="PM1.DI_HeaterTempLowLimitSW" value="true" tip="" tip.zh-CN="" tip.en-US="DI-11"/>
<Limit ai="PM1.AI_ActualSpeed" value="-0.001:0.001" tip="" tip.zh-CN="" tip.en-US="AI-118"/>
<Limit pollbool="PM1.ConfinementRing.RingInpDown" value="true" tip="" tip.zh-CN="" tip.en-US=""/>
</Action>
</Daemon>