完善STBlinkPattern对象的Parse方法。
SeeBlue的TreeView样式模板中支持VirtualizingStackPanel作为ItemsPanel。
完善IoSignalTower对Blink模式的支持。

[UnitTest]
新增MECF.Framework.RT.EquipmentLibrary.Test工程。
新增针对STBlinkPattern对象的单元测试。
This commit is contained in:
DESKTOP-1N1NK8A\auvkk 2023-04-28 09:17:28 +08:00
parent e495181fad
commit 10704cd5f1
9 changed files with 347 additions and 97 deletions

View File

@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using Aitex.Core.RT.Log;
using BlinkDataType = System.Collections.Generic.KeyValuePair<MECF.Framework.Common.Device.Bases.TowerLightStatus, uint>;
namespace MECF.Framework.Common.Device.Bases;
@ -14,6 +15,15 @@ namespace MECF.Framework.Common.Device.Bases;
[DataContract]
public class STBlinkPattern
{
#region Variables
/// <summary>
/// 模式字符串中每个字符对应的延时时长,单位毫秒。
/// </summary>
private const uint DELAY_MS_PER_CHAR = 100;
#endregion
#region Constructors
/// <summary>
@ -68,12 +78,12 @@ public class STBlinkPattern
#region Static Methods
public bool Parse(out List<uint> blinkData, out string reason)
public bool Parse(out List<BlinkDataType> blinkData, out string reason)
{
// var regex = new Regex(@"(([-\.])(\2*))");
reason = "";
blinkData = null;
blinkData = new List<BlinkDataType>();
// 校验Pattern字串的正则Pattern仅允许有字符-‘和’.’组成。
var regPatternFormat = new Regex(@"([^\-\.])+");
@ -89,7 +99,35 @@ public class STBlinkPattern
return false;
}
var m = regGroup.Matches(Pattern);
var matches = regGroup.Matches(Pattern);
foreach (Match match in matches)
{
if (match.Success && match.Groups.Count == 2)
{
var leadChar = match.Groups[1].Value; // 编组字符
var length = (uint)match.Length;
// 根据每组的字符判断动作。
TowerLightStatus action;
switch (leadChar)
{
case "-":
action = TowerLightStatus.On;
break;
case ".":
action = TowerLightStatus.Off;
break;
default:
LOG.Error($"Undefined character {leadChar} in pattern");
continue;
}
blinkData.Add(new(action, DELAY_MS_PER_CHAR * length));
}
}
return true;
}

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{9808C3D5-3EC7-47EF-8FF7-5670BEB9DEED}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MECF.Framework.RT.EquipmentLibrary.Test</RootNamespace>
<AssemblyName>MECF.Framework.RT.EquipmentLibrary.Test</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TestProjectType>UnitTest</TestProjectType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="xunit.core">
<HintPath>..\packages\xunit.extensibility.core.2.4.2\lib\net452\xunit.core.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.extensibility.core">
<Version>2.4.2</Version>
</PackageReference>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
<Compile Include="MECF\Framework\Common\Device\Bases\STBlinkPatternTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MECF.Framework.Common\MECF.Framework.Common.csproj">
<Project>{EFAD063F-FA97-42B7-87F8-8279EBA30D34}</Project>
<Name>MECF.Framework.Common</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@ -0,0 +1,47 @@
using Xunit;
namespace MECF.Framework.Common.Device.Bases.Tests
{
public class STBlinkPatternTests
{
[Fact()]
public void ParseTest1()
{
var pattern = new STBlinkPattern(5, "-----.....");
var ret = pattern.Parse(out var blinkData, out var reason);
Assert.True(ret);
Assert.True(blinkData.Count == 2);
Assert.True(blinkData[0].Key == TowerLightStatus.On);
Assert.True(blinkData[0].Value == 500);
Assert.True(blinkData[1].Key == TowerLightStatus.Off);
Assert.True(blinkData[1].Value == 500);
}
[Fact()]
public void ParseTest2()
{
var pattern = new STBlinkPattern(5, "..---...----");
var ret = pattern.Parse(out var blinkData, out var reason);
Assert.True(ret);
Assert.True(blinkData.Count == 4);
Assert.True(blinkData[0].Key == TowerLightStatus.Off);
Assert.True(blinkData[0].Value == 200);
Assert.True(blinkData[1].Key == TowerLightStatus.On);
Assert.True(blinkData[1].Value == 300);
Assert.True(blinkData[2].Key == TowerLightStatus.Off);
Assert.True(blinkData[2].Value == 300);
Assert.True(blinkData[3].Key == TowerLightStatus.On);
Assert.True(blinkData[3].Value == 400);
}
[Fact()]
public void ParseIllegalCharTest()
{
var pattern = new STBlinkPattern(5, "..---...1----");
var ret = pattern.Parse(out var blinkData, out var reason);
Assert.False(ret);
Assert.True(reason == "pattern contains illegal characters");
}
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MECF.Framework.RT.EquipmentLibrary.Test")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MECF.Framework.RT.EquipmentLibrary.Test")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("9808c3d5-3ec7-47ef-8ff7-5670beb9deed")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -5,6 +5,7 @@ using Aitex.Core.RT.IOCore;
using Aitex.Core.RT.Log;
using Aitex.Core.Util;
using MECF.Framework.Common.Device.Bases;
using BlinkDataType = System.Collections.Generic.KeyValuePair<MECF.Framework.Common.Device.Bases.TowerLightStatus, uint>;
namespace MECF.Framework.RT.EquipmentLibrary.Devices
{
@ -26,6 +27,7 @@ namespace MECF.Framework.RT.EquipmentLibrary.Devices
On,
PrepareOff,
Off,
ActionDone,
CycleFinished,
Halt
}
@ -33,19 +35,22 @@ namespace MECF.Framework.RT.EquipmentLibrary.Devices
private DOAccessor _doLight;
private readonly LightType _lightType;
private TowerLightStatus _action;
/// <summary>
/// 闪烁次数计数器。
/// <remarks>当该计数器归零时,结束闪烁。</remarks>
/// </summary>
private uint _blinkCycleDownCounter;
private List<uint> _blinkData;
private Queue<uint> _qBlinkData;
private List<BlinkDataType> _blinkData;
private Queue<BlinkDataType> _qBlinkData;
private BlinkDataType _nextBlinkData;
private FsmStateBlink _blinkStage;
private readonly DeviceTimer _timBlinkOn;
private readonly DeviceTimer _timBlinkOff;
#endregion
#region Constructors
@ -63,7 +68,7 @@ namespace MECF.Framework.RT.EquipmentLibrary.Devices
_timBlinkOn = new DeviceTimer();
_timBlinkOff = new DeviceTimer();
}
#endregion
#region Properties
@ -74,7 +79,119 @@ namespace MECF.Framework.RT.EquipmentLibrary.Devices
public LightType Type => _lightType;
#endregion
#region Monitor Methods
public void Monitor()
{
/*
* switch-case的建议状态机Blink ON-OFF切换
* 使线Blink模式线
*/
// 如果时Blinking模式实现Blinking逻辑
MonitorBlink();
}
/// <summary>
/// 执行Blink逻辑。
/// </summary>
private void MonitorBlink()
{
if (_action == TowerLightStatus.Blinking && _blinkCycleDownCounter > 0)
{
switch (_blinkStage)
{
case FsmStateBlink.Halt:
// 清楚所有状态
// 注意该状态不应反复进入因为_action和_blinkCycleDownCounter在首次进入Halt
// 状态时会被清除从而进入Switch-Case状态机的条件无法满足
Reset();
break;
case FsmStateBlink.Init:
// 如果闪烁数据列队为空,可能:
// 1、第一次进入状态机
// 2、上个Cycle已经结束需要执行下个Cycle
if (_qBlinkData.Count <= 0)
_qBlinkData = new Queue<BlinkDataType>(_blinkData);
_nextBlinkData = _qBlinkData.Dequeue();
if (_nextBlinkData.Key == TowerLightStatus.On)
_blinkStage = FsmStateBlink.PrepareOn;
else if (_nextBlinkData.Key == TowerLightStatus.Off)
_blinkStage = FsmStateBlink.PrepareOff;
else
{
LOG.Error($"Incorrect status {_nextBlinkData.Key} in blink pattern");
_blinkStage = FsmStateBlink.Halt;
}
_doLight.Value = false;
_timBlinkOn.Stop();
_timBlinkOff.Stop();
break;
case FsmStateBlink.PrepareOn:
_doLight.Value = true;
_timBlinkOn.Start(_nextBlinkData.Value);
_timBlinkOff.Stop();
_blinkStage = FsmStateBlink.On;
break;
case FsmStateBlink.On:
// 等待输出ON状态结束
if (_timBlinkOn.IsTimeout())
_blinkStage = FsmStateBlink.ActionDone;
break;
case FsmStateBlink.PrepareOff:
_doLight.Value = false;
_timBlinkOn.Stop();
_timBlinkOff.Start(_nextBlinkData.Value);
_blinkStage = FsmStateBlink.Off;
break;
case FsmStateBlink.Off:
// 等待输出OFF状态结束
if (_timBlinkOff.IsTimeout())
_blinkStage = FsmStateBlink.ActionDone;
break;
case FsmStateBlink.ActionDone:
// 上个输出完成后,判断动作列队是否为空。
// 如果不为空,则继续执行;
// 否则表示一个循环已完成跳转到CycleFinished状态
if (_qBlinkData.Count <= 0)
_blinkStage = FsmStateBlink.CycleFinished;
else
_blinkStage = FsmStateBlink.Init;
break;
case FsmStateBlink.CycleFinished:
_blinkCycleDownCounter--;
if (_blinkCycleDownCounter <= 0)
{
// Blink循环结束关闭输出
_doLight.Value = false;
_action = TowerLightStatus.Off;
_blinkStage = FsmStateBlink.Halt;
}
else
{
// 进入下一个循环。
_blinkStage = FsmStateBlink.Init;
}
break;
}
}
}
#endregion
#region Methods
/// <summary>
/// 初始化信号塔元件。
/// </summary>
@ -116,10 +233,8 @@ namespace MECF.Framework.RT.EquipmentLibrary.Devices
switch (_action)
{
case TowerLightStatus.On:
{
_blinkCycleDownCounter = 0;
_doLight.Value = true;
}
break;
case TowerLightStatus.Off:
@ -130,7 +245,7 @@ namespace MECF.Framework.RT.EquipmentLibrary.Devices
case TowerLightStatus.Blinking:
// 当工作在闪烁状态时,如果没有指定闪烁模式,则创建一个默认模式。
blinkPattern ??= new STBlinkPattern();
// 当循环次数小于等于0时无限循环。
_blinkCycleDownCounter = blinkPattern.TotalCycles <= 0 ? int.MaxValue : blinkPattern.TotalCycles;
@ -140,13 +255,13 @@ namespace MECF.Framework.RT.EquipmentLibrary.Devices
LOG.Error($"Unable to set {Type} to {action}, {reason}");
_action = TowerLightStatus.Off;
}
// 使能状态机
//! 注意:此步需放在最后,因为状态机异步使能,相关数据可能还未准备好
_blinkStage = FsmStateBlink.Init;
break;
case TowerLightStatus.Unknown:
LOG.Error($"{Type} Undefined output status");
_action = TowerLightStatus.Off;
@ -157,88 +272,6 @@ namespace MECF.Framework.RT.EquipmentLibrary.Devices
}
}
public void Monitor()
{
/*
* switch-case的建议状态机Blink ON-OFF切换
* 使线Blink模式线
*/
// 如果时Blinking模式实现Blinking逻辑
if (_action == TowerLightStatus.Blinking)
{
// 如果闪烁次数归零,则停止闪烁。
if (_blinkCycleDownCounter > 0)
{
switch (_blinkStage)
{
case FsmStateBlink.Init:
_qBlinkData = new Queue<uint>(_blinkData);
_blinkStage = FsmStateBlink.PrepareOn;
_doLight.Value = false;
_timBlinkOn.Stop();
_timBlinkOff.Stop();
break;
case FsmStateBlink.PrepareOn:
if (_qBlinkData.Count <= 0)
{
// 闪烁数据列队空,结束
_blinkStage = FsmStateBlink.CycleFinished;
break;
}
_doLight.Value = true;
_timBlinkOn.Start(_qBlinkData.Dequeue());
_timBlinkOff.Stop();
_blinkStage = FsmStateBlink.On;
break;
case FsmStateBlink.On:
if (_timBlinkOn.IsTimeout()) // 如果Blink-ON阶段还未结束啥也不干
_blinkStage = FsmStateBlink.PrepareOff;
break;
case FsmStateBlink.PrepareOff:
if (_qBlinkData.Count <= 0)
{
// 闪烁数据列队空,结束
_blinkStage = FsmStateBlink.CycleFinished;
break;
}
_doLight.Value = false;
_timBlinkOn.Stop();
_timBlinkOff.Start(_qBlinkData.Dequeue());
_blinkStage = FsmStateBlink.Off;
break;
case FsmStateBlink.Off:
if (_timBlinkOff.IsTimeout()) // 如果Blink-OFF阶段还未结束啥也不干
_blinkStage = FsmStateBlink.CycleFinished;
break;
case FsmStateBlink.CycleFinished:
_blinkCycleDownCounter--;
if (_blinkCycleDownCounter <= 0)
{
// Blink循环结束关闭输出
_doLight.Value = false;
_action = TowerLightStatus.Off;
_blinkStage = FsmStateBlink.Halt;
}
else
{
// 进入下一个循环。
_blinkStage = FsmStateBlink.Init;
}
break;
}
}
}
}
/// <summary>
/// 复位信号塔元件。
/// </summary>
@ -248,6 +281,7 @@ namespace MECF.Framework.RT.EquipmentLibrary.Devices
_action = TowerLightStatus.Off;
_doLight.Value = false;
_qBlinkData.Clear();
_blinkStage = FsmStateBlink.Halt;
_timBlinkOn.Stop();
_timBlinkOff.Stop();
@ -262,5 +296,7 @@ namespace MECF.Framework.RT.EquipmentLibrary.Devices
_timBlinkOn.Stop();
_timBlinkOff.Stop();
}
#endregion
}
}

View File

@ -788,7 +788,6 @@
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="Caliburn.Micro\Caliburn.snk" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>

View File

@ -177,12 +177,23 @@
Padding="4"
CanContentScroll="False"
Focusable="False">
<ItemsPresenter />
<ItemsPresenter/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="VirtualizingStackPanel.IsVirtualizing" Value="true">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="TreeViewItemExtend" TargetType="{x:Type TreeViewItem}">

View File

@ -26,6 +26,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SicUI", "UIDebug\SicUI\SicU
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MECF.Framework.Common.Test", "UnitTest\MECF.Framework.Common.Test\MECF.Framework.Common.Test.csproj", "{25E5D56D-461A-43A0-BEA8-CF5057356C80}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MECF.Framework.RT.EquipmentLibrary.Test", "MECF.Framework.RT.EquipmentLibrary.Test\MECF.Framework.RT.EquipmentLibrary.Test.csproj", "{9808C3D5-3EC7-47EF-8FF7-5670BEB9DEED}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -114,6 +116,14 @@ Global
{25E5D56D-461A-43A0-BEA8-CF5057356C80}.DebugWithoutCopyFiles|Any CPU.Build.0 = Debug|Any CPU
{25E5D56D-461A-43A0-BEA8-CF5057356C80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25E5D56D-461A-43A0-BEA8-CF5057356C80}.Release|Any CPU.Build.0 = Release|Any CPU
{9808C3D5-3EC7-47EF-8FF7-5670BEB9DEED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9808C3D5-3EC7-47EF-8FF7-5670BEB9DEED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9808C3D5-3EC7-47EF-8FF7-5670BEB9DEED}.DebugWithoutCopy|Any CPU.ActiveCfg = Debug|Any CPU
{9808C3D5-3EC7-47EF-8FF7-5670BEB9DEED}.DebugWithoutCopy|Any CPU.Build.0 = Debug|Any CPU
{9808C3D5-3EC7-47EF-8FF7-5670BEB9DEED}.DebugWithoutCopyFiles|Any CPU.ActiveCfg = Debug|Any CPU
{9808C3D5-3EC7-47EF-8FF7-5670BEB9DEED}.DebugWithoutCopyFiles|Any CPU.Build.0 = Debug|Any CPU
{9808C3D5-3EC7-47EF-8FF7-5670BEB9DEED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9808C3D5-3EC7-47EF-8FF7-5670BEB9DEED}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -122,6 +132,7 @@ Global
{F619C5AD-D0D9-4758-A85E-D747156709E6} = {A9774FAC-A372-4CFE-9E84-8CEF067A0442}
{290FE38F-45F9-408C-B25B-C899B467E3F8} = {137A5FA1-1B36-4635-81EA-91A74853B86D}
{25E5D56D-461A-43A0-BEA8-CF5057356C80} = {A9774FAC-A372-4CFE-9E84-8CEF067A0442}
{9808C3D5-3EC7-47EF-8FF7-5670BEB9DEED} = {A9774FAC-A372-4CFE-9E84-8CEF067A0442}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BF012408-B30F-40C6-B6F6-B78688F4E4ED}