alielie
2026-04-16 1afe548ac40cc7f975917ef9bf2e156595cf81f3
优化;
77个文件已修改
40个文件已添加
4个文件已删除
7528 ■■■■■ 已修改文件
CellStepSender/CellStepSender.csproj 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CellStepSender/Properties/AssemblyInfo.cs 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CellStepSender/StepSender.cs 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CellStepSender/TcpClientTool3.cs 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/AlarmInfoManage.xaml.cs 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/HMI.AlarmInfoManage.csproj 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.AlermInfoManage/app.config 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Entity/ParameterData.cs 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/FontSizeConverter.cs 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Manager/ProductManager.cs 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Properties/AssemblyInfo.cs 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Setting/ParamSetting.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/HexUtil.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/PathHelper.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Core/Util/Sqlite3Helper.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.DataViewer/DataViewer.xaml.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/LogViewer.xaml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.LogViewer/LogViewer.xaml.cs 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/Model/XmlModel.cs 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/ParameterManage.xaml.cs 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/ParameterModal.xaml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/ParameterModal.xaml.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/SettingModal.xaml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.ParameterManage/SettingModal.xaml.cs 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.StdControl/HMI.StdControl.csproj 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.StdControl/StdSettingCheckBox.xaml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.StdControl/StdSettingCheckBox.xaml.cs 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/DateStrConverter.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/RadioEnumConverter.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Converter/ResultConverter.cs 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/HMI.UI.csproj 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/MaskControl.xaml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/RequirePasswordByUser.xaml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/RequirePasswordByUser.xaml.cs 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/RequirePasswordWindow.xaml 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/MsgBoxStyle.xaml 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UI/Style/PanuonResource.xaml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/App.xaml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/HMI.UnitTest.csproj 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/MainWindow.xaml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.UnitTest/MainWindow.xaml.cs 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/Alarm/AlarmUtil.cs 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/ExcelHelper.cs 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/HMI.Util.csproj 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/IniFileHelper.cs 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HMI.Util/packages.config 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
OPCClient/OPCClient.csproj 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
OPCClient/app.config 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/BurnInOven.xaml.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.CurrentSensorControl/BurnInSettingModal.xaml.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/Plugin.DOControl.csproj 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DOControl/app.config 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataViewer/AssemblyInfo.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataViewer/CustomSortColumn.cs 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataViewer/DataViewerControl.xaml 480 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataViewer/DataViewerControl.xaml.cs 800 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.DataViewer/Plugin.DataViewer.csproj 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/DataTable.xaml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/DataTable.xaml.cs 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/InfoWindow.xaml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Lixun/SNValidator.cs 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ManualDebugWindow.xaml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ManualDebugWindow.xaml.cs 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Model/TcpServer.cs 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Plugin.TestResult.csproj 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Scanner.xaml.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/SingleCell.xaml 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/SingleCell.xaml.cs 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/TestResultView.xaml.cs 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/Util/PNUtil.cs 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ViewControl.xaml 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/ViewControl.xaml.cs 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/app.config 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TestResult/packages.config 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Plugin.TsExecution.csproj 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/Properties/AssemblyInfo.cs 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenGrid.xaml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenGrid.xaml.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenTouch.xaml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenTouch.xaml.cs 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenTouchWin.xaml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/ScreenTouchWin.xaml.cs 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/SingleControl.xaml 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/SingleControl.xaml.cs 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsExecution/TsExecution.xaml.cs 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsSupport/ITestExecutionWrapper.cs 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsSupport/Plugin.TsSupport.csproj 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsSupport/TeststandWrapper.cs 228 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.TsSupport/TsExecutionWrapper.cs 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.UICollection/AssemblyInfo.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.UICollection/Plugin.UICollection.csproj 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.UICollection/TcpDebugger.xaml 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Plugin.UICollection/TcpDebugger.xaml.cs 393 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/Parameter.cs 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/ParameterUtil.cs 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/ProductUtil.cs 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SimpleUtil/HMI.Simple.Util/Properties/AssemblyInfo.cs 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/AssemblyInfo.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/ManualDebug.xaml 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/ManualDebug.xaml.cs 467 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/ManualDebugStyle.xaml 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/StatusBtn.xaml 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/StatusBtn.xaml.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Util.ManualDebug/Util.ManualDebug.csproj 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/App.config 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/App.xaml.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/MainTabControl.xaml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Controls/MainTabControl.xaml.cs 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/TestResult.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/en-US/app.json 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/TestResult.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Lang/zh-CN/app.json 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/PanuonWindow.xaml 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/PanuonWindow.xaml.cs 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Properties/AssemblyInfo.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/ResourceListener.cs 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/Settings.ini 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/UI/ResourceUI.xaml 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/UI/ResourceUI.xaml.cs 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/WPFProject.csproj 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WPFProject/WPFProject.sln 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CellStepSender/CellStepSender.csproj
@@ -43,6 +43,7 @@
  <ItemGroup>
    <Compile Include="StepSender.cs" />
    <Compile Include="TcpClientTool.cs" />
    <Compile Include="TcpClientTool3.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
CellStepSender/Properties/AssemblyInfo.cs
@@ -29,5 +29,5 @@
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyVersion("1.0.0.1")]
[assembly: AssemblyFileVersion("1.0.0.1")]
CellStepSender/StepSender.cs
@@ -28,10 +28,10 @@
        {
            errorMsg = null;
            TcpClientTool client = null;
            TcpClientTool3 client = null;
            try
            {
                client = new TcpClientTool(ip, port);
                client = new TcpClientTool3(ip, port);
                string res = null;
                client.ExecuteMessageChanged += (sender, data) =>
                {
CellStepSender/TcpClientTool3.cs
New file
@@ -0,0 +1,81 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace CellStepSender
{
    public class TcpClientTool3
    {
        private string _ip;
        private int _port;
        private bool _isConnected = false; // 是否连接
        private Socket _clientSocket; // 套接字
        public event EventHandler<string> ExecuteMessageChanged;
        public TcpClientTool3(string ip, int port)
        {
            this._ip = ip;
            this._port = port;
        }
        public void Connect()
        {
            try
            {
                _clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPAddress ipaddress = IPAddress.Parse(this._ip);
                IPEndPoint endpoint = new IPEndPoint(ipaddress, _port);
                _clientSocket.Connect(endpoint);
                _isConnected = true;
            }
            catch (Exception ex)
            {
                _clientSocket.Dispose();
                _isConnected = false;
                throw ex;
            }
        }
        public void Disconnect()
        {
            _clientSocket.Dispose();
        }
        public void Send(string data)
        {
            byte[] arrClientSendMsg = Encoding.UTF8.GetBytes(data);
            lock (this)
            {
                _clientSocket.Send(arrClientSendMsg);
            }
        }
        public bool SendStep(
            string ip, int port,
            int ovenIndex, int cellIndex, string step, string value, string limitUp,
            string limitDown, string unit, string result, string updateTime, out string errorMsg)
        {
            errorMsg = null;
            try
            {
                if (!_isConnected)
                {
                    errorMsg = "Not Connected";
                    return false;
                }
                string msg = $"{ovenIndex};{cellIndex};{step},{value},{limitUp},{limitDown},{unit},{result},{updateTime}\n";
                Send(msg);
                return true;
            }
            catch (Exception ex)
            {
                errorMsg = ex.Message;
                return false;
            }
        }
    }
}
HMI.AlermInfoManage/AlarmInfoManage.xaml.cs
@@ -315,7 +315,14 @@
        {
            try
            {
                AlarmTexts = XmlHelper.DeserializeFromXml<ObservableCollection<AlarmText>>(alarmTextPath);
                if (string.IsNullOrEmpty(alarmTextPath) || !File.Exists(alarmTextPath))
                {
                    AlarmTexts = new ObservableCollection<AlarmText>();
                }
                else
                {
                    AlarmTexts = XmlHelper.DeserializeFromXml<ObservableCollection<AlarmText>>(alarmTextPath);
                }
            }
            catch (Exception ex)
            {
@@ -323,8 +330,6 @@
                AlarmTexts = new ObservableCollection<AlarmText>();
            }
        }
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
@@ -339,6 +344,11 @@
            Loaded -= UserControl_Loaded;
        }
        public override void DoClose(object sender, EventArgs e)
        {
            // AlarmUtil.OnAlarm -= AlarmUtil_OnAlarm;
        }
        //private void MonitorTimer_Elapsed_bak(object sender, ElapsedEventArgs e)
        //{
        //    try
HMI.AlermInfoManage/HMI.AlarmInfoManage.csproj
@@ -150,7 +150,6 @@
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
    </EmbeddedResource>
    <None Include="app.config" />
    <None Include="packages.config" />
    <None Include="Properties\Settings.settings">
      <Generator>SettingsSingleFileGenerator</Generator>
HMI.AlermInfoManage/app.config
File was deleted
HMI.Core/Entity/ParameterData.cs
@@ -9,6 +9,13 @@
        public string Name { get; set; }
        [ExcelColumn("设置项类型")]
        public string Type { get; set; }
        [ExcelColumn("是否为数组")]
        public bool IsArray { get; set; } = false;
        [ExcelColumn("数组长度")]
        public int Length { get; set; } = 1;
        [ExcelColumn("值")]
        public string Value { get; set; }
        [ExcelColumn("单位")]
HMI.Core/FontSizeConverter.cs
@@ -8,6 +8,7 @@
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            /// "{size},{基准值},{最小值},{最大值}"
            if (parameter == null) return 15;
            string[] ps = parameter.ToString().Split(',');
HMI.Core/Manager/ProductManager.cs
@@ -1,4 +1,5 @@
using HMI.Core.Entity;
using HMI.Core.Setting;
using HMI.Core.Util;
using System.Collections.Generic;
using System.IO;
@@ -11,13 +12,32 @@
    /// </summary>
    public class ProductManager
    {
        public static string GetFolder()
        {
            if (ParamSetting.ShareConfig != null)
            {
                return Path.Combine(ParamSetting.ShareConfig, "Product");
            }
            return Path.Combine(PathHelper.ConfigFolder, "Product");
        }
        public static string GetDataFilePath()
        {
            return Path.Combine(GetFolder(), "Product.data");
        }
        public static string GetParamFolderByProductName(string productName)
        {
            return Path.Combine(GetFolder(), "Parameter", $"Parameter_{productName}.data");
        }
        /// <summary>
        /// 获取保存的产品
        /// </summary>
        /// <returns></returns>
        public static T GetProducts<T>() where T : IList<ProductData>
        {
            string path = PathHelper.ProductDataFile;
            string path = GetDataFilePath();
            if (!File.Exists(path)) return default(T);
            T datas = XmlHelper.DeserializeFromXml<T>(path);
            return datas;
@@ -29,7 +49,7 @@
        /// <returns></returns>
        public static T GetParametersByProductName<T>(string productName) where T : IList<ParameterData>
        {
            string path = Path.Combine(PathHelper.ProductParameterFolder, $"Parameter_{productName}.data");
            string path = GetParamFolderByProductName(productName);
            if (!File.Exists(path)) return default(T);
            T datas = XmlHelper.DeserializeFromXml<T>(path);
            return datas;
HMI.Core/Properties/AssemblyInfo.cs
@@ -32,5 +32,5 @@
//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
//通过使用 "*",如下所示:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyVersion("1.0.0.1")]
[assembly: AssemblyFileVersion("1.0.0.1")]
HMI.Core/Setting/ParamSetting.cs
@@ -34,6 +34,9 @@
        /// 启用扫码换型
        /// </summary>
        public static bool EnableAutoModel { get; private set; } = false;
        public static string ShareConfig { get; private set; } = null;
        public static bool ShowBurnInAbort { get; private set; } = true;
        public static string SocketNames { get; private set; } = string.Empty;
        static ParamSetting()
        {
@@ -148,6 +151,33 @@
            {
            }
            try
            {
                ShareConfig = ParameterManager.GetCommonParameterByName<string>("ShareConfig", null);
            }
            catch
            {
            }
            try
            {
                ShowBurnInAbort = ParameterManager.GetCommonParameterByName<bool>("ShowBurnInAbort", true);
            }
            catch
            {
            }
            try
            {
                SocketNames = ParameterManager.GetCommonParameterByName<string>("SocketNames", string.Empty);
            }
            catch
            {
            }
        }
    }
}
HMI.Core/Util/HexUtil.cs
@@ -2,15 +2,15 @@
namespace HMI.Core.Util
{
    public class HexUtil
    {
        public static byte[] ToBytesFromHexString(string hexString)
        {
            //以 ' ' 分割字符串,并去掉空字符
            string[] chars = hexString.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            byte[] returnBytes;
            returnBytes = BitConverter.GetBytes(Convert.ToInt32(chars[0]));
            return returnBytes;
        }
    }
    //public class HexUtil
    //{
    //    public static byte[] ToBytesFromHexString(string hexString)
    //    {
    //        //以 ' ' 分割字符串,并去掉空字符
    //        string[] chars = hexString.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
    //        byte[] returnBytes;
    //        returnBytes = BitConverter.GetBytes(Convert.ToInt32(chars[0]));
    //        return returnBytes;
    //    }
    //}
}
HMI.Core/Util/PathHelper.cs
@@ -50,16 +50,8 @@
        /// </summary>
        public static string ParameterDataPath { get; private set; }
        /// <summary>
        /// 产品数据目录
        /// </summary>
        public static string ProductDataFolder { get; private set; }
        public static string ProductParameterFolder { get; private set; }
        /// <summary>
        /// 产品数据源文件
        /// </summary>
        public static string ProductDataFile { get; private set; }
        /// <summary>
        /// 测试项数据目录
@@ -94,11 +86,11 @@
            ParameterFolder = InitFolder(ConfigFolder + "\\Parameter");
            ParameterDataPath = ParameterFolder + "\\CommonParameter.data";
            ProductDataFolder = InitFolder(ConfigFolder + "\\Product");
            ProductParameterFolder = InitFolder(ProductDataFolder + "\\Parameter");
            ProductDataFile = ProductDataFolder + "\\Product.data";
            //ProductDataFolder = InitFolder(ConfigFolder + "\\Product");
            //ProductParameterFolder = InitFolder(ProductDataFolder + "\\Parameter");
            //ProductDataFile = ProductDataFolder + "\\Product.data";
            TestItemDataFolder = InitFolder(ProductDataFolder + "\\TestItem");
            // TestItemDataFolder = InitFolder(ProductDataFolder + "\\TestItem");
            TestItemDataFile = TestItemDataFolder + "\\TestItem.data";
            LogViewerDir = InitFolder(ConfigFolder + "\\LogViewer");
HMI.Core/Util/Sqlite3Helper.cs
@@ -52,6 +52,46 @@
            }
        }
        public long InsertWithId(string Sql)
        {
            try
            {
                if (Sql.EndsWith(";"))
                {
                    Sql += "SELECT last_insert_rowid();";
                }
                else
                {
                    Sql += ";SELECT last_insert_rowid();";
                }
                using (var cmd = new SQLiteCommand(Sql, conn))
                {
                    return (long)cmd.ExecuteScalar();
                }
            }
            catch (Exception ex)
            {
                string message = "ExecuteSql Error:" + ex.Message;
                throw new Exception(message);
            }
        }
        public object SelectOne(string Sql)
        {
            try
            {
                using (var cmd = new SQLiteCommand(Sql, conn))
                {
                    return cmd.ExecuteScalar();
                }
            }
            catch (Exception ex)
            {
                string message = "ExecuteSql Error:" + ex.Message;
                throw new Exception(message);
            }
        }
        public int ExecuteBatch(IEnumerable<string> sqls)
        {
            SQLiteTransaction tr = null;
HMI.DataViewer/DataViewer.xaml.cs
@@ -1,6 +1,7 @@
using HMI.Base;
using HMI.Core.Util;
using HMI.DataViewer.Entity;
using HMI.UI.Converter;
using HMI.UI.Util;
using NPOI.SS.Formula.Functions;
using NPOI.SS.UserModel;
@@ -19,22 +20,7 @@
namespace HMI.DataViewer
{
    public class DefaultValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is DateTime date)
            {
                return date.ToString("yyyy-MM-dd HH:mm:ss");
            }
            return value.ToString();
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value;
        }
    }
    /// <summary>
    /// UserControl1.xaml 的交互逻辑
@@ -65,8 +51,6 @@
        }
        private Dictionary<string, DataBase_Setting> Map = new Dictionary<string, DataBase_Setting>();
        private IValueConverter defaultConverter = new DefaultValueConverter();
        private void Button_Click(object sender, RoutedEventArgs e)
        {
@@ -285,7 +269,7 @@
                Header = header,
                Binding = new System.Windows.Data.Binding(bindingPath)
                {
                    Converter = defaultConverter
                    Converter = DateStrConverter.Instance
                },
                Width = new DataGridLength(1, DataGridLengthUnitType.Star)
            };
HMI.LogViewer/LogViewer.xaml
@@ -71,7 +71,7 @@
                <StackPanel DockPanel.Dock="Right" Style="{StaticResource ButtonGroup}">
                    <Button Style="{StaticResource QueryBtn}"
                            x:Name="queryBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource RfhBtn}"
                    <Button Style="{StaticResource ResetBtn}"
                            x:Name="resetBtn" Click="Button_Click"/>
                    <!--<Button x:Name="exportBtn"
                            Style="{StaticResource ExportBtn}" Click="Button_Click"/>-->
@@ -88,14 +88,16 @@
                        <ComboBoxItem Content="Error"/>
                    </ComboBox>
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="14" Text="{DynamicResource App.StartTime}"/>
                    <pu:DateTimePicker Style="{StaticResource TimePicker}" x:Name="startTimeInput"/>
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="14" Text="{DynamicResource App.EndTime}"/>
                    <pu:DateTimePicker Style="{StaticResource TimePicker}" x:Name="endTimeInput"/>
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="14" Text="{DynamicResource log.source}"/>
                    <TextBox Style="{StaticResource EditorTextBoxStyle}" x:Name="sourceInput" Width="180"/>
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="14" Text="{DynamicResource App.StartTime}"/>
                    <pu:DateTimePicker Style="{StaticResource TimePicker}" IsEnabled="{Binding AutoRfh,Converter={StaticResource reverseBool}}"
                                       x:Name="startTimeInput"/>
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="14" Text="{DynamicResource App.EndTime}"/>
                    <pu:DateTimePicker Style="{StaticResource TimePicker}" IsEnabled="{Binding AutoRfh,Converter={StaticResource reverseBool}}"
                                       x:Name="endTimeInput"/>
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="14" Text="{DynamicResource log.autoRefresh}"/>
                    <CheckBox x:Name="autoRfhInput" HorizontalAlignment="Center" VerticalAlignment="Center"
@@ -159,7 +161,7 @@
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <Border Style="{StaticResource CellBorder}">
                                        <TextBlock VerticalAlignment="Center" Text="{Binding time}"/>
                                        <TextBlock VerticalAlignment="Center" Text="{Binding time,Converter={x:Static uic:DateStrConverter.Instance},ConverterParameter='yyyy-MM-dd HH:mm:ss.fff'}"/>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
@@ -177,6 +179,7 @@
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Width="1420">
                            <GridViewColumn.Header>
                                <TextBlock Text="{DynamicResource log.text}"/>
@@ -194,6 +197,12 @@
                </ListView.View>
            </ListView>
            <StackPanel Grid.Row="3" HorizontalAlignment="Right">
                <pu:Pagination Style="{StaticResource DefaultPage}"
                               CurrentPage="{Binding CurrentPage}"
                               MaxPage="{Binding MaxPage}"/>
            </StackPanel>
            <!--<DataGrid Grid.Row="2" x:Name="table" 
                      CellStyle="{StaticResource sbCell}"
                  RowHeight="{StaticResource nanD}" Style="{StaticResource DefaultDataGrid}">
HMI.LogViewer/LogViewer.xaml.cs
@@ -33,23 +33,26 @@
        private Timer autoRfhTimer;
        private int currentPage = 1;
        public int CurrentPage
        private long pageSize = 50;
        private long currentPage = 1;
        public long CurrentPage
        {
            get => currentPage;
            set
            {
                Set(ref currentPage, value);
                Filtrate(value, pageSize);
            }
        }
        private int maxPage = 0;
        public int MaxPage
        private long maxPage = 1;
        public long MaxPage
        {
            get => maxPage;
            set
            {
                Set(ref currentPage, value);
                Set(ref maxPage, value);
            }
        }
@@ -65,6 +68,7 @@
            };
            autoRfhTimer.Elapsed += AutoRfhTimer_Elapsed;
            InitializeComponent();
            QueryReset();
        }
        public override void InitFinish()
@@ -93,7 +97,6 @@
            //}
        }
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            Loaded -= UserControl_Loaded;
@@ -103,6 +106,10 @@
        {
            Refresh100items();
        }
        //private void RefreshTable() {
        //    if(CurrentPage == )
        //}
        private void Button_Click(object sender, RoutedEventArgs e)
        {
@@ -114,6 +121,7 @@
                    break;
                case "resetBtn":
                    QueryReset();
                    CurrentPage = 1;
                    break;
                case "exportBtn":
                    DoExport();
@@ -144,9 +152,10 @@
                    return;
                }
                queryBtn.IsEnabled = false;
                autoRfhInput.IsEnabled = false;
                Filtrate();
                AutoRfh = false;
                //queryBtn.IsEnabled = false;
                //autoRfhInput.IsEnabled = false;
                CurrentPage = 1;
            }
            catch (Exception ex)
            {
@@ -162,31 +171,60 @@
        private void QueryReset()
        {
            var today = DateTime.Now;
            var start = DateTime.Parse(today.ToString("yyyy-MM-dd 00:00:00")).AddDays(-7);
            var end = DateTime.Parse(today.ToString("yyyy-MM-dd 00:00:00")).AddDays(1);
            levelInput.Text = null;
            startTimeInput.SelectedDateTime = null;
            endTimeInput.SelectedDateTime = null;
            startTimeInput.Text = null;
            table.ItemsSource = null;
            startTimeInput.SelectedDateTime = start;
            endTimeInput.SelectedDateTime = end;
        }
        private void Filtrate()
        private void Filtrate(long page, long size)
        {
            string str = LoadSql();
            DataTable dt = new DataTable();
            var files = GetOldDbFiles(PathHelper.LogViewerDir, new DateTime?[] { startTimeInput.SelectedDateTime,
            try
            {
                var files = GetOldDbFiles(PathHelper.LogViewerDir, new DateTime?[] { startTimeInput.SelectedDateTime,
                endTimeInput.SelectedDateTime });
            foreach (string file in files)
            {
                using (SqliteHelper sql = new SqliteHelper(file))
                if (files.Length <= 0)
                {
                    dt.Merge(sql.GetDataTable(str));
                    table.ItemsSource = new DataTable().DefaultView;
                    return;
                }
                using (Sqlite3Helper helper = new Sqlite3Helper(files[0]))
                {
                    for (int i = 0; i < files.Length; i++)
                    {
                        var attach = $"ATTACH DATABASE '{files[i]}' AS 'db{i}'";
                        helper.Execute(attach);
                    }
                    string sql = string.Join(" UNION ALL ", GetSqls(files));
                    string sortName = "time", sortDirection = "DESC";
                    string baseSql = $"SELECT level,time,source,text FROM ({sql}) as _temp";
                    string countSql = $"SELECT COUNT(1) FROM ({sql}) as __temp";
                    long total = (long)helper.SelectOne(countSql);
                    long pages = (total/pageSize)+ ((total % pageSize) > 0 ? 1 : 0);
                    sql = $"{baseSql} ORDER BY {sortName} {sortDirection} LIMIT {size} OFFSET {(page - 1) * size} ";
                    DataTable dt = helper.GetDataTable(sql);
                    table.ItemsSource = dt.DefaultView;
                    MaxPage = pages;
                }
            }
            if (dt.Rows.Count > 0)
            catch (Exception ex)
            {
                dt.DefaultView.Sort = "time Desc";
                log.Error("Select Log Error.", ex);
                MsgBoxHelper2.Error($"查询失败: {ex.Message}");
            }
            table.ItemsSource = dt.DefaultView;
        }
        public static string[] GetOldDbFiles(string folderPath, DateTime?[] range)
@@ -217,35 +255,40 @@
        }
        /// <summary>
        /// 加载界面判断数据库语句
        ///
        /// </summary>
        /// <returns> str数据库的语句 </returns>
        private string LoadSql()
        /// <param name="files"></param>
        /// <returns></returns>
        private string[] GetSqls(string[] files)
        {
            string sql = " select level,time,source,text from log";
            List<string> datas = new List<string>();
            if (!string.IsNullOrEmpty(levelInput.Text))
            string[] sqls = new string[files.Length];
            for (int i = 0; i < files.Length; i++)
            {
                datas.Add($"level='{levelInput.Text}'");
            }
            if (!string.IsNullOrEmpty(startTimeInput.Text))
            {
                datas.Add($"time>='{startTimeInput.Text}'");
            }
            if (!string.IsNullOrEmpty(endTimeInput.Text))
            {
                datas.Add($"time<='{endTimeInput.Text}'");
            }
            if (!string.IsNullOrEmpty(sourceInput.Text))
            {
                datas.Add($"source='{sourceInput.Text}'");
            }
                string sql = $"SELECT level,time,source,text FROM db{i}.log";
                List<string> datas = new List<string>();
                if (!string.IsNullOrEmpty(levelInput.Text))
                {
                    datas.Add($"level='{levelInput.Text}'");
                }
                if (!string.IsNullOrEmpty(startTimeInput.Text))
                {
                    datas.Add($"time>='{startTimeInput.Text}'");
                }
                if (!string.IsNullOrEmpty(endTimeInput.Text))
                {
                    datas.Add($"time<'{endTimeInput.Text}'");
                }
                if (!string.IsNullOrEmpty(sourceInput.Text))
                {
                    datas.Add($"source='{sourceInput.Text}'");
                }
            string condition = string.Join(" AND ", datas);
            condition = string.IsNullOrEmpty(condition) ? string.Empty : $" WHERE {condition}";
            sql = $"{sql}{condition} order by time desc";
            return sql;
                string condition = string.Join(" AND ", datas);
                condition = string.IsNullOrEmpty(condition) ? string.Empty : $" WHERE {condition}";
                sql = $"{sql}{condition}";
                sqls[i] = sql;
            }
            return sqls;
        }
        /// <summary>
@@ -267,6 +310,7 @@
                        this.Dispatcher.BeginInvoke((Action)delegate ()
                        {
                            table.ItemsSource = dt.DefaultView;
                            MaxPage = 1;
                        });
                    }
                }
HMI.ParameterManage/Model/XmlModel.cs
@@ -191,7 +191,7 @@
        /// <param name="datas"></param>
        private void SaveProduct(IList<ProductData> datas)
        {
            XmlHelper.SerializeToXml(PathHelper.ProductDataFile, datas);
            XmlHelper.SerializeToXml(ProductManager.GetDataFilePath(), datas);
        }
        #endregion
@@ -257,13 +257,13 @@
        /// <param name="datas"></param>
        private void SaveParameter(IList<ParameterData> datas)
        {
            string fileName = Path.Combine(PathHelper.ProductDataFolder, "Parameter", $"Parameter_{SelectedProduct.Name}.data");
            string fileName = ProductManager.GetParamFolderByProductName(SelectedProduct.Name);
            XmlHelper.SerializeToXml(fileName, datas);
        }
        private void SaveParameter(string product, IList<ParameterData> datas)
        {
            string fileName = Path.Combine(PathHelper.ProductDataFolder, "Parameter", $"Parameter_{product}.data");
            string fileName = ProductManager.GetParamFolderByProductName(product);
            XmlHelper.SerializeToXml(fileName, datas);
        }
        #endregion
HMI.ParameterManage/ParameterManage.xaml.cs
@@ -1,11 +1,16 @@
using HMI.Base;
using HMI.Core.Entity;
using HMI.Core.Manager;
using HMI.Core.Util;
using HMI.ParameterManage.Model;
using HMI.UI.Util;
using NPOI.POIFS.NIO;
using NPOI.SS.Formula.Functions;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
namespace HMI.ParameterManage
{
@@ -97,13 +102,34 @@
                ToastHelper.Warning(Window.GetWindow(this), this.lang("Pm.PleaseSelectProduct"));
                return;
            }
            var win = new ParameterModal(Model)
            //var win = new ParameterModal(Model)
            //{
            //    Title = data == null ? this.lang("Pm.Parameter.Add") : this.lang("Pm.Parameter.Edit"),
            //    Owner = Window.GetWindow(this),
            //};
            //if (win.Open(Model.SelectedProduct.Name, data ?? new ParameterData { ProductName = Model.SelectedProduct.Name }, Model.groups) == true)
            //{
            //    Model.LoadParameters();
            //}
            SettingModal modal = new SettingModal()
            {
                Title = data == null ? this.lang("Pm.Parameter.Add") : this.lang("Pm.Parameter.Edit"),
                Owner = Window.GetWindow(this),
                Owner = Window.GetWindow(this)
            };
            if (win.Open(Model.SelectedProduct.Name, data ?? new ParameterData { ProductName = Model.SelectedProduct.Name }, Model.groups) == true)
            var target = data ?? new ParameterData();
            if (modal.Open(target, Model.Parameters, Model.groups, Model.SelectedProduct.Name) == true)
            {
                //if (index == -1)
                //{
                //    Datasource.Add(modal.Data);
                //    ParameterManager.SaveCommonParameter(Datasource);
                //}
                //else
                //{
                //    ParameterManager.SaveCommonParameter(Datasource);
                //}
                Model.SaveOrUpdateParameter(target);
                Model.LoadParameters();
            }
        }
HMI.ParameterManage/ParameterModal.xaml
@@ -46,6 +46,10 @@
                </ComboBox>
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgIsArray" Header="IsArray" dp:FormGroupHelper.IsRequired="True" GroupName="MyFormGroup">
                <CheckBox x:Name="CbIsArray" />
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgValue" Style="{StaticResource RequiredInputFormGroupStyle}" 
                          Header="{DynamicResource Pm.Parameter.Value}" 
dp:FormGroupHelper.IsRequired="True" GroupName="MyFormGroup">
HMI.ParameterManage/ParameterModal.xaml.cs
@@ -81,6 +81,7 @@
            {
                Data.Name = TbName.Text;
                Data.Type = CbType.Text;
                Data.IsArray = CbIsArray.IsChecked == true;
                Data.Value = GetTargetValue();
                Data.Unit = TbUnit.Text;
                Data.Description = TbDescription.Text;
@@ -95,6 +96,7 @@
            Data = data;
            TbName.Text = Data.Name;
            CbType.SelectedValue = Data.Type;
            CbIsArray.IsChecked = Data.IsArray;
            CbGroup.ItemsSource = groups;
            UpdateWindow(Data.Type);
            UpdateForm(Data);
HMI.ParameterManage/SettingModal.xaml
@@ -19,6 +19,13 @@
    <Grid Margin="10">
        <StackPanel x:Name="formPanel">
            <pu:FormGroup x:Name="FgProductName"  Visibility="Collapsed"
              Header="{DynamicResource Pm.Product.Name}"
              dp:FormGroupHelper.IsRequired="True"
              GroupName="MyFormGroup">
                <TextBox x:Name="TbProductName" IsReadOnly="True" Style="{StaticResource EditorTextBoxStyle}" />
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgName"
                          dp:FormGroupHelper.ValidateMethod="ValidateName"
                          Header="{DynamicResource Pm.Parameter.Name}" 
@@ -37,6 +44,14 @@
                    <ComboBoxItem>Bool</ComboBoxItem>
                    <ComboBoxItem>String</ComboBoxItem>
                </ComboBox>
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgIsArray" Header="IsArray" dp:FormGroupHelper.IsRequired="True" GroupName="MyFormGroup">
                <CheckBox x:Name="CbIsArray" Checked="CbIsArray_Checked" Unchecked="CbIsArray_Checked"/>
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgLength" Visibility="Collapsed" Header="Length" dp:FormGroupHelper.IsRequired="True" GroupName="MyFormGroup">
                <ui:NumberInput x:Name="NiLength" TextChanged="NiLength_TextChanged" Style="{StaticResource EditorTextBoxStyle}"/>
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgValue" Style="{StaticResource RequiredInputFormGroupStyle}" 
@@ -58,6 +73,8 @@
                        <TextBox x:Name="TbStringValue" Style="{StaticResource EditorTextBoxStyle}"/>
                        <Button Grid.Column="1" Click="Button_Click" Style="{StaticResource SearchButton}"/>
                    </Grid>
                    <DataGrid Visibility="Collapsed"
                              x:Name="arrayInput" MinHeight="100" MaxHeight="200" CanUserAddRows="False" CanUserDeleteRows="False"></DataGrid>
                </Grid>
            </pu:FormGroup>
HMI.ParameterManage/SettingModal.xaml.cs
@@ -3,17 +3,62 @@
using HMI.UI.DpUtil;
using HMI.UI.Util;
using Microsoft.Win32;
using NPOI.SS.Formula.Functions;
using NPOI.SS.Formula.PTG;
using Panuon.WPF.UI;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Controls;
namespace HMI.ParameterManage
{
    public class BoolData
    {
        public int Index { get; }
        public bool Value { get; set; }
        public BoolData(int index)
        {
            this.Index = index;
        }
    }
    public class IntData
    {
        public int Index { get; }
        public int Value { get; set; }
        public IntData(int index)
        {
            this.Index = index;
        }
    }
    public class DoubleData
    {
        public int Index { get; }
        public double Value { get; set; }
        public DoubleData(int index)
        {
            this.Index = index;
        }
    }
    public class StringData
    {
        public int Index { get; }
        public string Value { get; set; } = string.Empty;
        public StringData(int index)
        {
            this.Index = index;
        }
    }
    /// <summary>
    /// ModalWindow.xaml 的交互逻辑
    /// </summary>
@@ -93,6 +138,9 @@
            {
                Data.Name = TbName.Text;
                Data.Type = CbType.Text;
                Data.IsArray = CbIsArray.IsChecked == true;
                Data.Length = int.Parse(NiLength.Text);
                Data.Value = GetTargetValue();
                Data.Unit = TbUnit.Text;
                Data.Description = TbDescription.Text;
@@ -102,14 +150,23 @@
            return flag;
        }
        public bool? Open(ParameterData data, ObservableCollection<ParameterData> datasource, List<string> groups)
        public bool? Open(ParameterData data, ObservableCollection<ParameterData> datasource, List<string> groups,string productName = null)
        {
            if (!string.IsNullOrEmpty(productName))
            {
                FgProductName.Visibility = System.Windows.Visibility.Visible;
                TbProductName.Text = productName;
            }
            Datasource = datasource;
            Data = data;
            TbGroup.ItemsSource = groups;
            TbName.Text = Data.Name;
            CbType.SelectedValue = Data.Type;
            CbIsArray.IsChecked = Data.IsArray;
            NiLength.Text = Data.Length.ToString();
            UpdateWindow(Data.Type);
            UpdateForm(Data);
@@ -123,6 +180,12 @@
        private string GetTargetValue()
        {
            var type = CbType.Text;
            if (CbIsArray.IsChecked == true)
            {
                var arr = (object[])arrayInput.ItemsSource;
                return string.Join(",", arr.Select(e => e.GetType().GetProperty("Value").GetValue(e).ToString()).ToArray());
            }
            if (type == "Int" || type == "Double")
            {
                return NiNumberValue.Text;
@@ -139,17 +202,25 @@
        private void UpdateForm(ParameterData data)
        {
            if (Data.Type == "Int" || Data.Type == "Int")
            if (data.IsArray)
            {
                NiNumberValue.Text = Data.Value;
            }
            else if (Data.Type == "Bool")
            {
                CbBoolValue.SelectedValue = Data.Value;
                string[] arr = data.Value.Split(',');
                arrayInput.ItemsSource = RefreshArray(arr, Data.Type);
            }
            else
            {
                TbStringValue.Text = Data.Value;
                if (Data.Type == "Int" || Data.Type == "Int")
                {
                    NiNumberValue.Text = Data.Value;
                }
                else if (Data.Type == "Bool")
                {
                    CbBoolValue.SelectedValue = Data.Value;
                }
                else
                {
                    TbStringValue.Text = Data.Value;
                }
            }
        }
@@ -183,6 +254,10 @@
                val = ((ComboBoxItem)e.AddedItems[0]).Content.ToString();
            }
            UpdateWindow(val);
            if (CbIsArray.IsChecked == true)
            {
                arrayInput.ItemsSource = RefreshArray(GetLength(), val);
            }
        }
        private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
@@ -193,5 +268,111 @@
                TbStringValue.Text = ofd.FileName;
            }
        }
        private void CbIsArray_Checked(object sender, System.Windows.RoutedEventArgs e)
        {
            bool isChecked = ((CheckBox)sender).IsChecked == true;
            if (isChecked)
            {
                FgLength.Visibility = System.Windows.Visibility.Visible;
                arrayInput.Visibility = System.Windows.Visibility.Visible;
                arrayInput.ItemsSource = RefreshArray(GetLength(), CbType.Text);
            }
            else
            {
                FgLength.Visibility = System.Windows.Visibility.Collapsed;
                arrayInput.Visibility = System.Windows.Visibility.Collapsed;
            }
        }
        private object[] RefreshArray(int length, string type)
        {
            var arr = new object[length];
            for (int i = 0; i < length; i++)
            {
                if (type == "Int")
                {
                    arr[i] = new IntData(i);
                }
                else if (type == "Double")
                {
                    arr[i] = new DoubleData(i);
                }
                else if (type == "Bool")
                {
                    arr[i] = new BoolData(i);
                }
                else
                {
                    arr[i] = new StringData(i);
                }
            }
            return arr;
        }
        private object[] RefreshArray(string[] source, string type)
        {
            var arr = new object[source.Length];
            type = string.IsNullOrEmpty(type) ? "String" : type;
            for (int i = 0; i < arr.Length; i++)
            {
                if (type == "Int")
                {
                    arr[i] = new IntData(i)
                    {
                        Value = int.Parse(source[i])
                    };
                }
                else if (type == "Double")
                {
                    arr[i] = new DoubleData(i)
                    {
                        Value = double.Parse(source[i])
                    };
                }
                else if (type == "Bool")
                {
                    arr[i] = new BoolData(i)
                    {
                        Value = bool.Parse(source[i])
                    };
                }
                else
                {
                    arr[i] = new StringData(i)
                    {
                        Value = source[i]
                    };
                }
            }
            return arr;
        }
        private int GetLength()
        {
            try
            {
                string text = NiLength.Text;
                if (text.Contains("."))
                {
                    text = text.Split('.')[0];
                }
                int length = int.Parse(text);
                return length > 0 ? length : 1;
            }
            catch
            {
                return 1;
            }
        }
        private void NiLength_TextChanged(object sender, TextChangedEventArgs e)
        {
            if (CbIsArray.IsChecked != true)
            {
                return;
            }
            arrayInput.ItemsSource = RefreshArray(GetLength(), CbType.Text);
        }
    }
}
HMI.StdControl/HMI.StdControl.csproj
@@ -46,12 +46,19 @@
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Properties\AssemblyInfo.cs" />
    <Compile Include="StdSettingCheckBox.xaml.cs">
      <DependentUpon>StdSettingCheckBox.xaml</DependentUpon>
    </Compile>
    <Compile Include="StdEnableMes.xaml.cs">
      <DependentUpon>StdEnableMes.xaml</DependentUpon>
    </Compile>
    <Compile Include="StdUIEventArg.cs" />
  </ItemGroup>
  <ItemGroup>
    <Page Include="StdSettingCheckBox.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Page Include="StdEnableMes.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
HMI.StdControl/StdSettingCheckBox.xaml
New file
@@ -0,0 +1,14 @@
<UserControl x:Class="HMI.StdControl.StdSettingCheckBox"
             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:HMI.StdControl"
             mc:Ignorable="d"
             x:Name="self"
             d:DesignHeight="450" d:DesignWidth="800">
    <CheckBox x:Name="CbEnableMes" IsChecked="{Binding IsChecked,ElementName=self,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
              VerticalContentAlignment="Center"
              IsEnabled="{Binding CbEnabled,ElementName=self}"
              Content="{Binding Text,ElementName=self}" />
</UserControl>
HMI.StdControl/StdSettingCheckBox.xaml.cs
New file
@@ -0,0 +1,155 @@
using HMI.Core.Manager;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
namespace HMI.StdControl
{
    /// <summary>
    /// StdEnableMes.xaml 的交互逻辑
    /// </summary>
    public partial class StdSettingCheckBox : UserControl, INotifyPropertyChanged
    {
        #region ...
        public virtual event PropertyChangedEventHandler PropertyChanged;
        protected virtual void Set([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
        protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null)
        {
            old = @new;
            if (propertyName != null)
            {
                Set(propertyName);
            }
        }
        #endregion
        private event EventHandler<StdUIEventArg> valueChanged;
        public event EventHandler<StdUIEventArg> ValueChanged
        {
            add
            {
                valueChanged += value;
            }
            remove
            {
                valueChanged -= value;
            }
        }
        public static DependencyProperty IsCheckedProperty = DependencyProperty.Register(
                            "IsChecked",
                            typeof(bool),
                            typeof(StdSettingCheckBox),
                            new FrameworkPropertyMetadata(false));
        public static DependencyProperty ValueProperty = DependencyProperty.Register(
                            "Value",
                            typeof(string),
                            typeof(StdSettingCheckBox),
                            new FrameworkPropertyMetadata(null));
        public static DependencyProperty CbEnabledProperty = DependencyProperty.Register(
                            "CbEnabled",
                            typeof(bool),
                            typeof(StdSettingCheckBox),
                            new FrameworkPropertyMetadata(true));
        public static DependencyProperty TextProperty = DependencyProperty.Register(
                            "Text",
                            typeof(string),
                            typeof(StdSettingCheckBox),
                            new FrameworkPropertyMetadata(null));
        public bool CbEnabled
        {
            get => (bool)GetValue(CbEnabledProperty);
            set
            {
                SetValue(CbEnabledProperty, value);
            }
        }
        public string Value
        {
            get => (string)GetValue(ValueProperty);
            set
            {
                SetValue(ValueProperty, value);
            }
        }
        public string Text
        {
            get => (string)GetValue(TextProperty);
            set
            {
                SetValue(TextProperty, value);
            }
        }
        public bool IsChecked
        {
            get => (bool)GetValue(IsCheckedProperty);
            set
            {
                if (WriteConfig(Value, value))
                {
                    SetValue(IsCheckedProperty, value);
                }
            }
        }
        public StdSettingCheckBox()
        {
            InitializeComponent();
            this.Loaded += StdSettingCheckBox_Loaded;
        }
        private void StdSettingCheckBox_Loaded(object sender, RoutedEventArgs e)
        {
            try
            {
                var value = ParameterManager.GetCommonParameterByName(Value);
                if (value == null)
                {
                    this.Visibility = System.Windows.Visibility.Collapsed;
                }
                else
                {
                    IsChecked = bool.Parse(value.Value);
                }
            }
            catch
            {
                IsChecked = false;
            }
        }
        private bool WriteConfig(string name, bool value)
        {
            try
            {
                valueChanged?.Invoke(this, new StdUIEventArg()
                {
                    StringValue = name,
                    BoolValue = value
                });
                return ParameterManager.UpdateCommonParameter(name, value, out string errMsg);
            }
            catch (Exception ex)
            {
                return false;
            }
        }
    }
}
HMI.UI/Converter/DateStrConverter.cs
New file
@@ -0,0 +1,29 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace HMI.UI.Converter
{
    public class DateStrConverter : IValueConverter
    {
        public static DateStrConverter Instance { get; } = new DateStrConverter();
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is DateTime date)
            {
                if (parameter != null)
                {
                    return date.ToString(parameter.ToString());
                }
                return date.ToString("yyyy-MM-dd HH:mm:ss");
            }
            return value.ToString();
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return DateTime.Parse(value.ToString());
        }
    }
}
HMI.UI/Converter/RadioEnumConverter.cs
New file
@@ -0,0 +1,19 @@
using System;
using System.Windows.Data;
namespace HMI.UI.Converter
{
    public class RadioEnumConverter : IValueConverter
    {
        public static RadioEnumConverter Instance { get; } = new RadioEnumConverter();
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value?.Equals(parameter);
        }
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value?.Equals(true) == true ? parameter : Binding.DoNothing;
        }
    }
}
HMI.UI/Converter/ResultConverter.cs
New file
@@ -0,0 +1,31 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace HMI.UI.Converter
{
    public class ResultConverter : IValueConverter
    {
        public static ResultConverter Instance = new ResultConverter();
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null) return value;
            var str = value.ToString();
            if (str.Equals("ok", StringComparison.OrdinalIgnoreCase) || str.Equals("pass", StringComparison.OrdinalIgnoreCase))
            {
                return "PASS";
            }
            if (str.Equals("ng", StringComparison.OrdinalIgnoreCase) || str.Equals("fail", StringComparison.OrdinalIgnoreCase))
            {
                return "FAIL";
            }
            return value;
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value;
        }
    }
}
HMI.UI/HMI.UI.csproj
@@ -98,6 +98,9 @@
    <Compile Include="Controls\treegrid\TreeItemConverter.cs" />
    <Compile Include="Controls\treegrid\TreeNodeExtend.cs" />
    <Compile Include="Controls\VisualHost.cs" />
    <Compile Include="Converter\DateStrConverter.cs" />
    <Compile Include="Converter\RadioEnumConverter.cs" />
    <Compile Include="Converter\ResultConverter.cs" />
    <Compile Include="Converter\ReverseBoolConverter.cs" />
    <Compile Include="Converter\IndentConverter.cs" />
    <Compile Include="Converter\CollectionHasItem.cs" />
@@ -135,6 +138,9 @@
    <Compile Include="MDI\Workspace.xaml.cs">
      <DependentUpon>Workspace.xaml</DependentUpon>
    </Compile>
    <Compile Include="RequirePasswordByUser.xaml.cs">
      <DependentUpon>RequirePasswordByUser.xaml</DependentUpon>
    </Compile>
    <Compile Include="RequirePasswordWindow.xaml.cs">
      <DependentUpon>RequirePasswordWindow.xaml</DependentUpon>
    </Compile>
@@ -152,6 +158,10 @@
    <Compile Include="Util\MsgBoxHelper.cs" />
    <Compile Include="Util\MsgBoxHelper2.cs" />
    <Compile Include="Util\ToastHelper.cs" />
    <Page Include="RequirePasswordByUser.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Page Include="RequirePasswordWindow.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
HMI.UI/MaskControl.xaml
@@ -21,7 +21,7 @@
                    </Grid.ColumnDefinitions>
                    <Border Padding="10,5,0,5" Background="{DynamicResource PrimaryColor}"
                   CornerRadius="5,5,0,0">
                        <Label Padding="2" BorderThickness="0" Margin="0" Foreground="#FFF" x:Name="titleLabel" Content="请稍后"/>
                        <Label Padding="2" Background="Transparent" BorderThickness="0" Margin="0" Foreground="#FFF" x:Name="titleLabel" Content="请稍后"/>
                    </Border>
                    <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center"
                                VerticalAlignment="Center"
@@ -48,7 +48,7 @@
                                </EventTrigger>
                            </TextBlock.Triggers>
                        </TextBlock>
                        <TextBlock Grid.Row="1" Margin="20,0,0,0" Grid.Column="1" x:Name="contentLabel"
                        <TextBlock Grid.Row="1" Margin="20,0,0,0" Grid.Column="1" Background="Transparent" x:Name="contentLabel"
                                   VerticalAlignment="Center" TextWrapping="Wrap"/>
                    </StackPanel>                    
                </Grid>
HMI.UI/RequirePasswordByUser.xaml
New file
@@ -0,0 +1,33 @@
<pu:WindowX x:Class="HMI.Base.RequirePasswordByUser"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HMI.Base"
        mc:Ignorable="d"
        Style="{StaticResource ModalWindow}"
        xmlns:pu="https://opensource.panuon.com/wpf-ui"
        xmlns:dp="clr-namespace:HMI.UI.DpUtil"
        Loaded="WindowX_Loaded"
        FontSize="14"
        WindowStartupLocation="CenterOwner"
        Height="200"
        pu:WindowXModalDialog.OKButtonContent="确定"
        Title="请输入密码" Width="500">
    <Grid dp:GridHelper.Space="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <pu:FormGroup Header="当前用户" Orientation="Horizontal" Style="{StaticResource InputFormGroupStyle}">
            <TextBlock Text="{Binding Username}"/>
        </pu:FormGroup>
        <pu:FormGroup Grid.Row="1" Header="密码" Orientation="Horizontal"
                      Style="{StaticResource RequiredInputFormGroupStyle}">
            <PasswordBox x:Name="PwdPassword" VerticalContentAlignment="Center"
             Style="{StaticResource SignInPasswordBoxStyle}"
             pu:PasswordBoxHelper.Watermark="{DynamicResource App.Password}"/>
        </pu:FormGroup>
    </Grid>
</pu:WindowX>
HMI.UI/RequirePasswordByUser.xaml.cs
New file
@@ -0,0 +1,62 @@
using HMI.Core.Entity;
using HMI.Core.Util;
using HMI.UI.Util;
using Panuon.WPF.UI;
using System.ComponentModel;
namespace HMI.Base
{
    /// <summary>
    /// RequirePasswordWindow.xaml 的交互逻辑
    /// </summary>
    public partial class RequirePasswordByUser : WindowX
    {
        public RequirePasswordByUser(string username)
        {
            this.Username = username;
            this.DataContext = this;
            InitializeComponent();
        }
        private string username;
        public string Username
        {
            get => username;
            set
            {
                Set(ref username, value);
            }
        }
        protected override void OnClosing(CancelEventArgs e)
        {
            //Press Continue Button
            if (DialogResult == true)
            {
                try
                {
                    using (var helper = new Sqlite3Helper(PathHelper.DatabasePath))
                    {
                        if (!helper.HasData($"SELECT 1 FROM user WHERE username = '{Username}' AND password = '{PwdPassword.Password}';"))
                        {
                            ToastHelper.Error(this, "密码错误");
                            e.Cancel = true;
                        }
                    }
                }
                catch (System.Exception ex)
                {
                    ToastHelper.Error(this, $"验证用户信息失败: {ex.Message}");
                    e.Cancel = true;
                }
            }
            base.OnClosing(e);
        }
        private void WindowX_Loaded(object sender, System.Windows.RoutedEventArgs e)
        {
            Username = UserInfo.User.Username;
            PwdPassword.Password = string.Empty;
        }
    }
}
HMI.UI/RequirePasswordWindow.xaml
@@ -11,6 +11,7 @@
        Loaded="WindowX_Loaded"
        FontSize="14"
        Height="200"
        WindowStartupLocation="CenterOwner"
        pu:WindowXModalDialog.OKButtonContent="确定"
        Title="请输入密码" Width="500">
    <Grid dp:GridHelper.Space="10">
HMI.UI/Style/MsgBoxStyle.xaml
@@ -3,11 +3,10 @@
                        xmlns:pu="https://opensource.panuon.com/wpf-ui"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:puconfig="clr-namespace:Panuon.WPF.UI.Configurations;assembly=Panuon.WPF.UI">
    <pu:MessageBoxXSettings x:Key="MsgBoxWarn"
                                 InteropOwnersMask="False"
                                 InverseButtonsSequence="False">
        <pu:MessageBoxXSettings.WindowXStyle>
    <pu:MessageBoxXSetting x:Key="MsgBoxWarn"
         InteropOwnersMask="False"
         InverseButtonsSequence="False">
        <pu:MessageBoxXSetting.WindowXStyle>
            <Style TargetType="pu:WindowX" BasedOn="{StaticResource {x:Static pu:MessageBoxX.WindowXStyleKey}}">
                <Setter Property="pu:WindowXCaption.Background" Value="{DynamicResource WarningColor}" />
                <Setter Property="pu:WindowXCaption.Foreground" Value="{DynamicResource WarningColorContract}" />
@@ -15,9 +14,9 @@
                <Setter Property="Height" Value="200" />
                <Setter Property="SizeToContent" Value="Manual" />
            </Style>
        </pu:MessageBoxXSettings.WindowXStyle>
        </pu:MessageBoxXSetting.WindowXStyle>
        <pu:MessageBoxXSettings.ButtonStyle>
        <pu:MessageBoxXSetting.ButtonStyle>
            <Style TargetType="Button" BasedOn="{StaticResource {x:Static pu:MessageBoxX.ButtonStyleKey}}">
                <Style.Triggers>
@@ -33,20 +32,20 @@
                        <Setter Property="ContentTemplate">
                            <Setter.Value>
                                <DataTemplate>
                                    <TextBlock Text="{Binding}" TextDecorations="Underline" />
                                    <TextBlock Text="{DynamicResource App.Cancel}" TextDecorations="Underline" />
                                </DataTemplate>
                            </Setter.Value>
                        </Setter>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </pu:MessageBoxXSettings.ButtonStyle>
    </pu:MessageBoxXSettings>
        </pu:MessageBoxXSetting.ButtonStyle>
    </pu:MessageBoxXSetting>
    <pu:MessageBoxXSettings x:Key="MsgBoxError"
                             InteropOwnersMask="False"
                             InverseButtonsSequence="False">
        <pu:MessageBoxXSettings.WindowXStyle>
    <pu:MessageBoxXSetting x:Key="MsgBoxError"
     InteropOwnersMask="False"
     InverseButtonsSequence="False">
        <pu:MessageBoxXSetting.WindowXStyle>
            <Style TargetType="pu:WindowX" BasedOn="{StaticResource {x:Static pu:MessageBoxX.WindowXStyleKey}}">
                <Setter Property="pu:WindowXCaption.Background" Value="{DynamicResource DangerColor}" />
                <Setter Property="pu:WindowXCaption.Foreground" Value="{DynamicResource DangerColorContrast}" />
@@ -54,9 +53,9 @@
                <Setter Property="Height" Value="200" />
                <Setter Property="SizeToContent" Value="Manual" />
            </Style>
        </pu:MessageBoxXSettings.WindowXStyle>
        </pu:MessageBoxXSetting.WindowXStyle>
        <pu:MessageBoxXSettings.ButtonStyle>
        <pu:MessageBoxXSetting.ButtonStyle>
            <Style TargetType="Button" BasedOn="{StaticResource {x:Static pu:MessageBoxX.ButtonStyleKey}}">
                <Style.Triggers>
@@ -72,20 +71,20 @@
                        <Setter Property="ContentTemplate">
                            <Setter.Value>
                                <DataTemplate>
                                    <TextBlock Text="{Binding}" TextDecorations="Underline" />
                                    <TextBlock Text="{DynamicResource App.Cancel}" TextDecorations="Underline" />
                                </DataTemplate>
                            </Setter.Value>
                        </Setter>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </pu:MessageBoxXSettings.ButtonStyle>
    </pu:MessageBoxXSettings>
        </pu:MessageBoxXSetting.ButtonStyle>
    </pu:MessageBoxXSetting>
    <pu:MessageBoxXSettings x:Key="MsgBoxInfo"
                             InteropOwnersMask="False"
                             InverseButtonsSequence="False">
        <pu:MessageBoxXSettings.WindowXStyle>
    <pu:MessageBoxXSetting x:Key="MsgBoxInfo"
     InteropOwnersMask="False"
     InverseButtonsSequence="False">
        <pu:MessageBoxXSetting.WindowXStyle>
            <Style TargetType="pu:WindowX" BasedOn="{StaticResource {x:Static pu:MessageBoxX.WindowXStyleKey}}">
                <Setter Property="pu:WindowXCaption.Background" Value="{DynamicResource PrimaryColor}" />
                <Setter Property="pu:WindowXCaption.Foreground" Value="{DynamicResource PrimaryColorContract}" />
@@ -93,9 +92,9 @@
                <Setter Property="Height" Value="200" />
                <Setter Property="SizeToContent" Value="Manual" />
            </Style>
        </pu:MessageBoxXSettings.WindowXStyle>
        </pu:MessageBoxXSetting.WindowXStyle>
        <pu:MessageBoxXSettings.ButtonStyle>
        <pu:MessageBoxXSetting.ButtonStyle>
            <Style TargetType="Button" BasedOn="{StaticResource {x:Static pu:MessageBoxX.ButtonStyleKey}}">
                <Style.Triggers>
@@ -111,15 +110,15 @@
                        <Setter Property="ContentTemplate">
                            <Setter.Value>
                                <DataTemplate>
                                    <TextBlock Text="{Binding}" TextDecorations="Underline" />
                                    <TextBlock Text="{DynamicResource App.Cancel}" TextDecorations="Underline" />
                                </DataTemplate>
                            </Setter.Value>
                        </Setter>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </pu:MessageBoxXSettings.ButtonStyle>
    </pu:MessageBoxXSettings>
        </pu:MessageBoxXSetting.ButtonStyle>
    </pu:MessageBoxXSetting>
    <pu:ToastSetting x:Key="toastSuccess" Spacing="25">
        <pu:ToastSetting.LabelStyle>
HMI.UI/Style/PanuonResource.xaml
@@ -225,6 +225,7 @@
        <Setter Property="pu:WindowXModalDialog.OKButtonContent" 
                Value="{DynamicResource App.Save}"/>
        <Setter Property="pu:WindowXModalDialog.ButtonPanelMargin" Value="0,30,10,10"/>
        <Setter Property="InteropOwnersMask" Value="False"/>
    </Style>
    <Style x:Key="DefaultListBox" TargetType="ListBox" BasedOn="{StaticResource {x:Static pu:StyleKeys.ListBoxStyle}}">
@@ -319,4 +320,22 @@
            </Setter.Value>
        </Setter>
    </Style>
    <Style x:Key="DefaultPageItem" TargetType="pu:PaginationItem">
        <Setter Property="Cursor" Value="Hand" />
        <Setter Property="MinWidth" Value="30" />
        <Setter Property="Width" Value="Auto" />
        <Setter Property="Width" Value="Auto" />
    </Style>
    <Style x:Key="DefaultPage" TargetType="pu:Pagination">
        <Setter Property="Height" Value="30"/>
        <Setter Property="HorizontalAlignment" Value="Right"/>
        <Setter Property="Foreground" Value="Black" />
        <Setter Property="ItemsBackground" Value="Transparent"/>
        <Setter Property="ItemsCornerRadius" Value="12"/>
        <Setter Property="ItemsSelectedBackground" Value="{DynamicResource SecondPrimaryColor}"/>
        <Setter Property="ItemContainerStyle" Value="{StaticResource DefaultPageItem}"/>
    </Style>
</ResourceDictionary>
HMI.UnitTest/App.xaml
@@ -16,7 +16,7 @@
                <ResourceDictionary Source="/HMI.UI;component/Style/PanuonResource.xaml"/>
                <ResourceDictionary Source="/HMI.UI;component/Style/MsgBoxStyle.xaml"/>
            </ResourceDictionary.MergedDictionaries>
            <Style TargetType="{x:Type pu:FormGroup}" BasedOn="{StaticResource InputFormGroupStyle}"></Style>
            <!--<Style TargetType="{x:Type pu:FormGroup}" BasedOn="{StaticResource InputFormGroupStyle}"></Style>-->
            <uic:PathImageConverter x:Key="path2Image"/>
            <uic:DataGridRowNumberConverter x:Key="rowNumber"/>
            <uic:VisibilityBoolConverter x:Key="vbConverter"/>
HMI.UnitTest/HMI.UnitTest.csproj
@@ -131,6 +131,10 @@
    <None Include="App.config" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\..\UILib\UILib.csproj">
      <Project>{27325fe0-3ce2-4cd5-8eb5-7e45cc441479}</Project>
      <Name>UILib</Name>
    </ProjectReference>
    <ProjectReference Include="..\ConnectionUtil\ConnectionUtil.csproj">
      <Project>{d0806fa6-3f6d-4304-81b8-ead9da7cb5b2}</Project>
      <Name>ConnectionUtil</Name>
@@ -167,10 +171,18 @@
      <Project>{202d9a7b-96b3-4ad4-9977-fc4263207a02}</Project>
      <Name>Plugin.TsExecution</Name>
    </ProjectReference>
    <ProjectReference Include="..\Plugin.UICollection\Plugin.UICollection.csproj">
      <Project>{22dc8d91-50b2-47ca-86a8-29a6da48f388}</Project>
      <Name>Plugin.UICollection</Name>
    </ProjectReference>
    <ProjectReference Include="..\SimpleUtil\HMI.Simple.Util\HMI.Simple.Util.csproj">
      <Project>{cf5bebe9-2803-49c3-b773-73d082e0984a}</Project>
      <Name>HMI.Simple.Util</Name>
    </ProjectReference>
    <ProjectReference Include="..\Util.ManualDebug\Util.ManualDebug.csproj">
      <Project>{d0944e62-ee52-4abe-832a-a09d98814db1}</Project>
      <Name>Util.ManualDebug</Name>
    </ProjectReference>
  </ItemGroup>
  <ItemGroup>
    <Folder Include="Entity\" />
HMI.UnitTest/MainWindow.xaml
@@ -13,19 +13,15 @@
    <Grid x:Name="page">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
            <RowDefinition />
        </Grid.RowDefinitions>
        <Button Click="Button_Click">测试</Button>
        <!--<Button Click="Button_Click2" Grid.Row="1">发送</Button>
        <Button Click="Button_Click3" Grid.Row="2">发送TCP</Button>
        <Button Click="Button_Click4" Grid.Row="4">发送Data</Button>-->
        <TextBox x:Name="input" Height="0" Grid.Row="3"></TextBox>
        <!--<TextBox x:Name="input" Height="0" Grid.Row="3"></TextBox>
        <TextBlock Grid.Row="5" Height="0" x:Name="console"></TextBlock>
        <Border Grid.Row="6" x:Name="border"></Border>
        <Border Grid.Row="6" x:Name="border"></Border>-->
        <Border Grid.Row="1" x:Name="border2"></Border>
    </Grid>
</pu:WindowX>
HMI.UnitTest/MainWindow.xaml.cs
@@ -3,16 +3,15 @@
using HMI.Core.Util;
using HMI.Simple.Util;
using log4net;
using ModBusTcp;
using Panuon.WPF.UI;
using Plugin.DataBoard;
using Plugin.TestResult_WPF;
using Plugin.TsExecution;
using Plugin.UICollection;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Util.ManualDebug;
namespace UnitTest
{
    /// <summary>
@@ -27,22 +26,9 @@
            LangUtil.Load();
            InitializeComponent();
            //RebuildDb();
            border2.Child = new TcpDebugger();
            ModBusTcpController controller = new ModBusTcpController("127.0.0.1", 502);
            if (controller.Start(out msg)) {
                bool res;
                int? target;
                controller.WriteBool("1", true, out msg);
                controller.ReadDOBool("1", out res, out msg);
                Console.WriteLine(res);
                controller.ReadDOInt("1", out target, out msg);
                Console.WriteLine(target);
                controller.WriteInt("1", 1, out msg);
                controller.ReadDOInt("1", out target, out msg);
                Console.WriteLine(target);
                Console.WriteLine(target);
            }
            //page.Children.Add(new UserControl1());
@@ -142,6 +128,12 @@
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var win = new ManualDebug()
            {
                seqPath = "C:\\Users\\chrry\\Desktop\\手动调试示例.seq"
            };
            win.Show();
            //Task.Run(() =>
            //{
            //    for (int j = 0; j < 1000; j++)
@@ -198,7 +190,7 @@
            //});
            //tsExecution.UpdatePositionStatus(0, "Passed");
            tsExecution.UpdatePositionStatus(0, "Failed");
            //tsExecution.UpdatePositionStatus(0, "Failed");
            //tsExecution.UpdatePositionProcessValue(0, 12);
            //tsExecution.PausePosition(0);
            //tsExecution.SetPositionBarcode(0, "123456789");
@@ -231,73 +223,6 @@
            Console.WriteLine($"{e.Data}");
        }
        private void Client_OnData(object sender, PipeDataEventArgs e)
        {
            Dispatcher.Invoke(() =>
            {
                console.Text += e.Data;
            });
        }
        private void Button_Click2(object sender, RoutedEventArgs e)
        {
            var allStart = DateTime.Now;
            for (int i = 0; i < 100; i++)
            {
                var start = DateTime.Now;
                if (client.SendMessage("ping", out string res))
                {
                    var interval = (DateTime.Now - start).TotalMilliseconds;
                    Console.WriteLine($"{interval}: {res}");
                }
            }
            Console.WriteLine($"Total: {(DateTime.Now - allStart).TotalMilliseconds}");
        }
        private void Button_Click3(object sender, RoutedEventArgs e)
        {
            try
            {
                var allStart = DateTime.Now;
                for (int i = 0; i < 500; i++)
                {
                    var start = DateTime.Now;
                    string data = input.Text;
                    string str = $"{{\"command\":5,\"value\":{data},\"id\":\"DITag0\"}}\r\n";
                    string res = tcpClient.SendMessage(str);
                    var interval = (DateTime.Now - start).TotalMilliseconds;
                    Console.WriteLine($"{interval}: {res}");
                }
                Console.WriteLine($"Total: {(DateTime.Now - allStart).TotalMilliseconds}");
            }
            catch (Exception ex)
            {
            }
        }
        private void Button_Click4(object sender, RoutedEventArgs e)
        {
            try
            {
                var allStart = DateTime.Now;
                for (int i = 0; i < 500; i++)
                {
                    var start = DateTime.Now;
                    string data = input.Text;
                    string str = $"{{\"command\":5,\"value\":{data},\"id\":\"DITag0\"}}";
                    client.SendMessage(str, out string res);
                    var interval = (DateTime.Now - start).TotalMilliseconds;
                    Console.WriteLine($"{interval}: {res}");
                }
                Console.WriteLine($"Total: {(DateTime.Now - allStart).TotalMilliseconds}");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.StackTrace);
            }
        }
        private void HandlePlugin(object obj)
        {
@@ -318,10 +243,10 @@
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            var plugin = new TsExecution();
            HandlePlugin(plugin);
            border.Child = plugin;
            tsExecution = plugin;
            //var plugin = new TsExecution();
            //HandlePlugin(plugin);
            //border.Child = plugin;
            //tsExecution = plugin;
            this.Loaded -= Window_Loaded;
        }
    }
HMI.Util/Alarm/AlarmUtil.cs
@@ -2,6 +2,7 @@
using HMI.Core.Entity;
using HMI.Core.Setting;
using HMI.Core.Util;
using log4net;
using S7Driver;
using System;
using System.Collections.Generic;
@@ -18,8 +19,11 @@
        public event EventHandler<LoadMsg> LoadMessageAppend;
        private static ILog log = LogManager.GetLogger("AlarmHelper");
        internal AlarmHelper()
        {
        }
        public ObservableCollection<AlarmSetting> BlockSettings { get; private set; }
@@ -178,7 +182,7 @@
            }
            catch (Exception ex)
            {
                //log.Error("Read OPCServer Data Error.", ex);
                log.Error("Read PLC Alarm Data Error.", ex);
            }
            finally
            {
@@ -279,7 +283,7 @@
            }
            catch (Exception ex)
            {
                //log.Error("PcTimer Execute Failed", ex);
                log.Error("Read MSMQ Alarm Data Failed", ex);
            }
            finally
            {
HMI.Util/ExcelHelper.cs
@@ -1,10 +1,12 @@
using HMI.Core;
using HMI.Core.Util;
using NPOI.HSSF.UserModel;
using NPOI.SS.Formula.Functions;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -55,6 +57,53 @@
            }
        }
        public static void ExportExcel<T>(string targetPath, DataTable table)
        {
            Type type = typeof(T);
            var properties = PropertyHelper.GetAtterProperty<T, ExcelColumnAttribute>(out var attrs);
            if (properties == null || properties.Count <= 0)
            {
                throw new Exception("No Attribute");
            }
            IWorkbook workbook = null;
            if (targetPath.EndsWith(".xlsx"))
            {
                workbook = new XSSFWorkbook();
            }
            else if (targetPath.EndsWith(".xls"))
            {
                workbook = new HSSFWorkbook();
            }
            workbook.CreateSheet("sheet1");////新建一个工作表
            ISheet SheetOne = workbook.GetSheet("Sheet1");
            IRow row1 = SheetOne.CreateRow(0);
            for (int i = 0; i < properties.Count; i++)
            {
                row1.CreateCell(i).SetCellValue(LangUtil.Get((attrs[i]).Name));
            }
            for (int i = 0; i < table.Rows.Count; i++)
            {
                IRow rowTemp = SheetOne.CreateRow(i + 1);
                for (int j = 0; j < properties.Count; j++)
                {
                    var attr = attrs[j];
                    object obj = attr.IgnoreExport ? "" : table.Rows[i][properties[j].Name];
                    string value = obj?.ToString();
                    if (obj is DateTime time)
                    {
                        value = time.ToString("yyyy-MM-dd HH:mm:ss");
                    }
                    rowTemp.CreateCell(j).SetCellValue(value);
                }
            }
            using (FileStream fs = new FileStream(targetPath, FileMode.OpenOrCreate))
            {
                workbook.Write(fs);
            }
        }
        public static List<T> ReadFromExcel<T>(string sourcePath)
        {
HMI.Util/HMI.Util.csproj
@@ -52,6 +52,9 @@
    <Reference Include="ICSharpCode.SharpZipLib, Version=1.0.0.999, Culture=neutral, PublicKeyToken=1b03e6acf1164f73, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\SharpZipLib.1.0.0\lib\net45\ICSharpCode.SharpZipLib.dll</HintPath>
    </Reference>
    <Reference Include="log4net, Version=3.0.4.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\log4net.3.0.4\lib\net462\log4net.dll</HintPath>
    </Reference>
    <Reference Include="NPOI, Version=2.4.1.0, Culture=neutral, PublicKeyToken=0df73ec7942b34e1, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\NPOI.2.4.1\lib\net45\NPOI.dll</HintPath>
    </Reference>
@@ -65,7 +68,9 @@
      <HintPath>..\WPFProject\packages\NPOI.2.4.1\lib\net45\NPOI.OpenXmlFormats.dll</HintPath>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Configuration" />
    <Reference Include="System.Core" />
    <Reference Include="System.Web" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
HMI.Util/IniFileHelper.cs
@@ -26,6 +26,9 @@
        private static extern uint GetPrivateProfileStringA(string section, string key,
            string def, Byte[] retVal, int size, string filePath);
        [DllImport("kernel32")]
        private static extern int GetPrivateProfileSection(string section, byte[] retVal, int size, string filePath);
        #endregion
        public static List<string> ReadSections()
        {
@@ -108,5 +111,34 @@
                return true;
        }
        #endregion
        public static Dictionary<string, string> GetKeyValuesBySection(string iniFile, string section)
        {
            int size = 65536;
            byte[] buffer = new byte[size];
            Dictionary<string, string> result = new Dictionary<string, string>();
            int bytesRead = GetPrivateProfileSection(section, buffer, size, iniFile);
            if (bytesRead > 0)
            {
                string sectionData = Encoding.Default.GetString(buffer, 0, bytesRead - 1);
                string[] keyValuePairs = sectionData.Split('\0');
                foreach (string pair in keyValuePairs)
                {
                    if (!string.IsNullOrEmpty(pair))
                    {
                        int equalSignIndex = pair.IndexOf('=');
                        if (equalSignIndex > 0)
                        {
                            string key = pair.Substring(0, equalSignIndex).Trim();
                            string value = pair.Substring(equalSignIndex + 1).Trim();
                            result[key] = value;
                        }
                    }
                }
            }
            return result;
        }
    }
}
HMI.Util/packages.config
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="log4net" version="3.0.4" targetFramework="net472" />
  <package id="NPOI" version="2.4.1" targetFramework="net472" />
  <package id="SharpZipLib" version="1.0.0" targetFramework="net472" />
</packages>
OPCClient/OPCClient.csproj
@@ -73,7 +73,6 @@
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <ItemGroup>
    <None Include="app.config" />
    <None Include="packages.config" />
  </ItemGroup>
  <ItemGroup>
OPCClient/app.config
File was deleted
Plugin.CurrentSensorControl/BurnInOven.xaml.cs
@@ -14,7 +14,7 @@
    {
        private BaseConfig config;
        private static readonly string[] datas = new string[] { "_CoolerStatus", "_LEDLight", "_Req", "_SetTemp", "_SaveTemp", "_TempDown", "_TempUp", "_AvgTemp" };
        private static readonly string[] datas = new string[] { "_LEDLight", "_Req", "_SetTemp", "_SaveTemp", "_TempDown", "_TempUp", "_AvgTemp" };
        [ObservableProperty]
        private string header;
Plugin.CurrentSensorControl/BurnInSettingModal.xaml.cs
@@ -125,6 +125,14 @@
            }
        }
        private void TryDeleteData(PLCTagData data)
        {
            if (MsgBoxHelper2.WarnChoose($"确定删除数据: {data.Name} 吗?") == System.Windows.MessageBoxResult.OK)
            {
                Datasource.Remove(data);
            }
        }
        private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            var btn = sender as Button;
@@ -135,10 +143,12 @@
                case "importBtn":
                    DoImport();
                    break;
                case "delBtn":
                    TryDeleteData((PLCTagData)btn.DataContext);
                    break;
                default:
                    break;
            }
        }
    }
}
Plugin.DOControl/Plugin.DOControl.csproj
@@ -118,7 +118,6 @@
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
    </EmbeddedResource>
    <None Include="app.config" />
    <None Include="packages.config" />
    <None Include="Properties\Settings.settings">
      <Generator>SettingsSingleFileGenerator</Generator>
Plugin.DOControl/app.config
File was deleted
Plugin.DataViewer/AssemblyInfo.cs
New file
@@ -0,0 +1,12 @@
using HMI.Core.Annotation;
using System.Windows;
[assembly:ThemeInfo(
    ResourceDictionaryLocation.None,            //where theme specific resource dictionaries are located
                                                //(used if a resource is not found in the page,
                                                // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly   //where the generic resource dictionary is located
                                                //(used if a resource is not found in the page,
                                                // app, or any theme specific resource dictionaries)
)]
[assembly: PluginClassName("Plugin.DataViewer.DataViewerControl")]
Plugin.DataViewer/CustomSortColumn.cs
New file
@@ -0,0 +1,108 @@
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace Plugin.DataViewer
{
    public class CustomSortColumnConverter : IValueConverter
    {
        public static CustomSortColumnConverter Instance = new CustomSortColumnConverter();
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value as ListSortDirection? == ListSortDirection.Ascending)
            {
                return true;
            }
            if (value as ListSortDirection? == ListSortDirection.Descending)
            {
                return false;
            }
            return null;
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value;
        }
    }
    public interface ICustomSortColumn
    {
        public bool? IsAsc { get; set; }
        void UpdateOther(bool? value);
        DataGrid GetOwner();
    }
    public class CustomSortTemplateColumn : DataGridTemplateColumn, ICustomSortColumn
    {
        public static DependencyProperty IsAscProperty = DependencyProperty.Register(
                            "IsAsc",
                            typeof(bool?),
                            typeof(DataGridTemplateColumn),
                            new FrameworkPropertyMetadata(null));
        public bool? IsAsc
        {
            get => (bool?)GetValue(IsAscProperty);
            set
            {
                SetValue(IsAscProperty, value);
            }
        }
        public void UpdateOther(bool? value)
        {
            foreach (var col in DataGridOwner.Columns)
            {
                if (col == this) continue;
                if (col is ICustomSortColumn csc)
                {
                    csc.IsAsc = value;
                }
            }
        }
        public DataGrid GetOwner()
        {
            return this.DataGridOwner;
        }
    }
    public class CustomSortTextColumn : DataGridTextColumn, ICustomSortColumn
    {
        public static DependencyProperty IsAscProperty = DependencyProperty.Register(
                            "IsAsc",
                            typeof(bool?),
                            typeof(DataGridTextColumn),
                            new FrameworkPropertyMetadata(null));
        public bool? IsAsc
        {
            get => (bool?)GetValue(IsAscProperty);
            set
            {
                SetValue(IsAscProperty, value);
            }
        }
        public void UpdateOther(bool? value)
        {
            foreach (var col in DataGridOwner.Columns)
            {
                if (col == this) continue;
                if (col is ICustomSortColumn csc)
                {
                    csc.IsAsc = value;
                }
            }
        }
        public DataGrid GetOwner()
        {
            return this.DataGridOwner;
        }
    }
}
Plugin.DataViewer/DataViewerControl.xaml
New file
@@ -0,0 +1,480 @@
<hb:SimpleMaskControl x:Class="Plugin.DataViewer.DataViewerControl"
             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:Plugin.DataViewer"
             mc:Ignorable="d"
             xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
             xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
             xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI"
             xmlns:pu="https://opensource.panuon.com/wpf-ui"
             xmlns:sc="clr-namespace:System.ComponentModel;assembly=System"
             xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <pu:StyleDictionary Includes="Label" />
            </ResourceDictionary.MergedDictionaries>
            <Style x:Key="SB" TargetType="DataGridColumnHeader">
                <Setter Property="Foreground" Value="{DynamicResource PrimaryColorContract}"/>
                <!--<Setter Property="Background" Value="{DynamicResource PrimaryColor}"/>
            <Setter Property="Foreground" Value="{DynamicResource PrimaryColorContract}"/>
            <Setter Property="BorderBrush" Value="{DynamicResource PrimaryColorContract}"/>
            <Setter Property="BorderThickness" Value="0,0,1,1"/>-->
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="DataGridColumnHeader">
                            <Border BorderBrush="#FFF" Background="{DynamicResource PrimaryColor}"
                                MouseLeftButtonDown="Grid_MouseLeftButtonUp"
                                Tag="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=DataGridColumnHeader}}"
                                BorderThickness="0,0,1,1">
                                <Grid Margin="{TemplateBinding Padding}">
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="*" />
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="Auto" />
                                    </Grid.ColumnDefinitions>
                                    <Thumb x:Name="PART_LeftHeaderGripper" Grid.Column="0" HorizontalAlignment="Left"
Width="1" BorderThickness="0" BorderBrush="Transparent"
Cursor="SizeWE"/>
                                    <ContentPresenter Grid.Column="0"
                                    VerticalAlignment="Center"
                                    Content="{Binding Column.Header,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=DataGridColumnHeader}}"/>
                                    <TextBlock VerticalAlignment="Center" Grid.Column="2" Text="&#xe603;" x:Name="sorter"> <!---->
                                        <TextBlock.Style>
                                            <Style TargetType="TextBlock">
                                                <Setter Property="FontFamily" Value="{StaticResource iconfont}"/>
                                                <Setter Property="Visibility" Value="Collapsed"/>
                                                <Style.Triggers>
                                                    <!--<DataTrigger Binding="{Binding Column.CustomSort,
                                                    RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=DataGridColumnHeader}}" Value="{x:Null}">
                                                    <Setter  Property="Visibility" Value="Collapsed"/>
                                                </DataTrigger>-->
                                                    <DataTrigger Binding="{Binding Column.IsAsc,
                                                    RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=DataGridColumnHeader}}" Value="false">
                                                        <Setter Property="Visibility" Value="Visible"/>
                                                    </DataTrigger>
                                                    <DataTrigger Binding="{Binding Column.IsAsc,
                                                    RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=DataGridColumnHeader}}" Value="True">
                                                        <Setter Property="Visibility" Value="Visible"/>
                                                        <Setter Property="LayoutTransform">
                                                            <Setter.Value>
                                                                <RotateTransform Angle="180"/>
                                                            </Setter.Value>
                                                        </Setter>
                                                    </DataTrigger>
                                                </Style.Triggers>
                                            </Style>
                                        </TextBlock.Style>
                                    </TextBlock>
                                    <Thumb x:Name="PART_RightHeaderGripper" Grid.Column="3" HorizontalAlignment="Right" Width="1" BorderThickness="0"
                                       BorderBrush="Transparent" Cursor="SizeWE"/>
                                </Grid>
                            </Border>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
            <Style x:Key="ThisDataGridRow" TargetType="DataGridRow">
                <Setter Property="Background" Value="White"/>
                <Style.Triggers>
                    <Trigger Property="AlternationIndex" Value="1">
                        <Setter Property="Background" Value="#eeeeee"/>
                    </Trigger>
                    <DataTrigger Binding="{Binding result,Converter={x:Static uic:ResultConverter.Instance}}" Value="FAIL">
                        <Setter Property="Background" Value="{DynamicResource DangerColor}"/>
                        <Setter Property="Foreground" Value="{DynamicResource SecondPrimaryColorContract}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding status,Converter={x:Static uic:ResultConverter.Instance}}" Value="FAIL">
                        <Setter Property="Background" Value="{DynamicResource DangerColor}"/>
                        <Setter Property="Foreground" Value="{DynamicResource SecondPrimaryColorContract}"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
            <Style x:Key="OutLabel" TargetType="Label">
                <Setter Property="Height" Value="25"/>
                <Setter Property="Padding" Value="0"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
                <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                <Setter Property="VerticalContentAlignment" Value="Stretch"/>
                <Setter Property="Background" Value="#ffffff"/>
                <Setter Property="BorderThickness" Value="2"/>
                <Setter Property="FontSize" Value="10"/>
                <Setter Property="Foreground" Value="#000000"/>
            </Style>
            <Style x:Key="InLabel" TargetType="Label">
                <Setter Property="VerticalContentAlignment" Value="Center"/>
                <Setter Property="BorderThickness" Value="0"/>
                <Setter Property="pu:LabelHelper.CornerRadius" Value="2,0,0,2"/>
            </Style>
        </ResourceDictionary>
    </UserControl.Resources>
    <Border Style="{StaticResource CardBoarder}">
        <Grid>
            <Grid dp:GridHelper.Space="10" dp:GridHelper.SpaceIgnore="ALL">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="4*"/>
                    <ColumnDefinition Width="6*"/>
                </Grid.ColumnDefinitions>
                <DockPanel Grid.ColumnSpan="2">
                    <!--<StackPanel DockPanel.Dock="Right" Style="{StaticResource ButtonGroup}">
                    <Button Style="{StaticResource AddBtn}" x:Name="addBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource EditBtn}" x:Name="editBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource RfhBtn}" x:Name="rfhBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource DelBtn}" x:Name="delBtn" Click="Button_Click"/>
                </StackPanel>-->
                    <TextBlock Style="{StaticResource TitleBox}" Text="{DynamicResource Dv.Title}"/>
                </DockPanel>
                <!--<Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
            </Grid>-->
                <StackPanel Grid.Row="1" Grid.ColumnSpan="2" HorizontalAlignment="Left" Style="{StaticResource ButtonGroup}">
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="{StaticResource ContentFontSize2}"
       Text="SN"/>
                    <pu:SearchBox x:Name="snInput" Tag="sn" SearchTextChanged="SearchTextChanged" MinWidth="180" />
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="{StaticResource ContentFontSize2}"
                           Text="穴位号"/>
                    <pu:SearchBox x:Name="positionInput" Tag="position" SearchTextChanged="SearchTextChanged" MinWidth="180" />
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="{StaticResource ContentFontSize2}" Text="型号"/>
                    <pu:SearchBox x:Name="modelInput" Tag="model" SearchTextChanged="SearchTextChanged" MinWidth="180" />
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="{StaticResource ContentFontSize2}" Text="结果"/>
                    <pu:SearchBox x:Name="resultInput" Tag="result" SearchTextChanged="SearchTextChanged" MinWidth="180" />
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="{StaticResource ContentFontSize2}"
                           Text="{DynamicResource App.StartTime}"/>
                    <pu:DateTimePicker Style="{StaticResource TimePicker}"
                                   SelectedDateTime="{x:Null}" x:Name="startTimeInput"/>
                    <TextBlock Style="{StaticResource TitleBox}" FontSize="{StaticResource ContentFontSize2}"
                           Text="{DynamicResource App.EndTime}"/>
                    <pu:DateTimePicker Style="{StaticResource TimePicker}"
                                   SelectedDateTime="{x:Null}" x:Name="endTimeInput"/>
                    <Button Style="{StaticResource QueryBtn}"
                        x:Name="srhBtn" Click="Button_Click"/>
                    <Button Style="{StaticResource ResetBtn}"
                        x:Name="resetBtn" Click="Button_Click"/>
                </StackPanel>
                <Grid  Grid.Row="2" Grid.Column="0" dp:GridHelper.Space="5" dp:GridHelper.SpaceIgnore="ALL">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <DockPanel>
                        <Button DockPanel.Dock="Right" Style="{StaticResource ExportBtn}"
x:Name="exportBtn" Click="Button_Click"/>
                        <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10">
                        <Label
Height="25"
Padding="0"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
pu:LabelHelper.CornerRadius="2"
Background="#ffffff"
BorderBrush="{DynamicResource PrimaryColor}"
BorderThickness="2"
FontSize="10"
FontWeight="Bold"
Foreground="#000000">
                            <DockPanel Margin="-1" MinWidth="80">
                                <Label VerticalContentAlignment="Center" BorderThickness="0" Content="ALL" Foreground="{DynamicResource PrimaryColorContract}"
                          pu:LabelHelper.CornerRadius="2,0,0,2" DockPanel.Dock="Left" Background="{DynamicResource PrimaryColor}" />
                                <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="14" Foreground="Black">
    <Run Text="{Binding TotalCount}"/>
                                </TextBlock>
                            </DockPanel>
                        </Label>
                        <Label
Height="25"
Padding="0"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
pu:LabelHelper.CornerRadius="2"
Background="#ffffff"
BorderBrush="{DynamicResource SuccessColor}"
BorderThickness="2"
FontSize="10"
FontWeight="Bold"
Foreground="#000000">
                            <DockPanel Margin="-1" MinWidth="80">
                                <Label VerticalContentAlignment="Center" BorderThickness="0" Content="PASS" Foreground="{DynamicResource SuccessColorContrast}"
                          pu:LabelHelper.CornerRadius="2,0,0,2" DockPanel.Dock="Left" Background="{DynamicResource SuccessColor}"/>
                                <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="14" Foreground="Black">
<Run Text="{Binding PassCount}"/>
                                </TextBlock>
                            </DockPanel>
                        </Label>
                        <Label
Height="25"
Padding="0"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
pu:LabelHelper.CornerRadius="2"
Background="#ffffff"
BorderBrush="{DynamicResource DangerColor}"
BorderThickness="2"
FontSize="10"
                        FontWeight="Bold"
Foreground="#000000">
                            <DockPanel Margin="-1" MinWidth="80">
                                <Label VerticalContentAlignment="Center" BorderThickness="0" Content="FAIL" Foreground="{DynamicResource DangerColorContrast}"
                          pu:LabelHelper.CornerRadius="2,0,0,2" DockPanel.Dock="Left" Background="{DynamicResource DangerColor}" />
                                <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="14" Foreground="Black">
<Run Text="{Binding FailCount}"/>
                                </TextBlock>
                            </DockPanel>
                        </Label>
                        </StackPanel>
                    </DockPanel>
                    <DataGrid Grid.Row="1" CanUserSortColumns="False"
                      AutoGenerateColumns="False" x:Name="resultTable"
                      RowStyle="{StaticResource ThisDataGridRow}"
                      Style="{StaticResource DefaultDataGrid}"
                      ColumnHeaderStyle="{StaticResource SB}"
                      VirtualizingStackPanel.CacheLength="10"
                      SelectedItem="{Binding SelectedResult}"
                      MouseDoubleClick="resultTable_MouseDoubleClick"
                      ColumnHeaderHeight="30">
                        <DataGrid.Columns>
                            <DataGridTemplateColumn Header="#" IsReadOnly="True" Width="Auto">
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <TextBlock Text="{Binding Converter={StaticResource rowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"/>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                            <local:CustomSortTextColumn IsReadOnly="True" Width="*" Binding="{Binding sn}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="SN"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                            <local:CustomSortTextColumn IsReadOnly="True"  Width="80" Binding="{Binding position}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="穴位号"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                            <local:CustomSortTextColumn IsReadOnly="True"  Width="80" Binding="{Binding model}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="型号"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                            <local:CustomSortTextColumn IsReadOnly="True"  Width="80" Binding="{Binding result}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="测试结果"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                            <local:CustomSortTextColumn IsReadOnly="True"  Width="160" Binding="{Binding time,Converter={x:Static uic:DateStrConverter.Instance}}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="测试时间"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                        </DataGrid.Columns>
                    </DataGrid>
                    <pu:Pagination Grid.Row="2" Height="30" Margin="10" HorizontalAlignment="Right" Foreground="Black"
ItemsBackground="Transparent"
ItemsCornerRadius="12"
ItemsSelectedBackground="{DynamicResource SecondPrimaryColor}"
                            x:Name="page" CurrentPageChanged="page_CurrentPageChanged"
                            CurrentPage="{Binding CurrentPage}"
                                        MaxPage="{Binding TotalPage}">
                        <pu:Pagination.ItemContainerStyle>
                            <Style TargetType="pu:PaginationItem">
                                <Setter Property="MinWidth" Value="30" />
                                <Setter Property="Width" Value="Auto" />
                                <Setter Property="Width" Value="Auto" />
                            </Style>
                        </pu:Pagination.ItemContainerStyle>
                    </pu:Pagination>
                </Grid>
                <Grid Grid.Row="2" Grid.Column="1"  dp:GridHelper.Space="5" dp:GridHelper.SpaceIgnore="ALL">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <DockPanel>
                        <Button DockPanel.Dock="Right" Style="{StaticResource ExportBtn}"
x:Name="exportItemBtn" Click="Button_Click"/>
                        <Button DockPanel.Dock="Right" Margin="0,0,10,0" Style="{StaticResource QueryBtn}"
x:Name="srhItemBtn" Click="Button_Click"/>
                        <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10">
                            <Label>
                                <DockPanel Margin="-1" MinWidth="80" >
                                    <Label VerticalContentAlignment="Center" BorderThickness="0" Content="测试结果" Foreground="{DynamicResource SuccessColorContrast}"
                          pu:LabelHelper.CornerRadius="2,0,0,2" DockPanel.Dock="Left" Background="Transparent"/>
                                    <TextBlock VerticalAlignment="Center" Padding="0,0,5,0"
                                               HorizontalAlignment="Center" FontSize="14" Foreground="{DynamicResource SuccessColorContrast}">
<Run Text="{Binding SelectedStatus,Converter={x:Static uic:ResultConverter.Instance}}"/>
                                    </TextBlock>
                                </DockPanel>
                                <Label.Style>
                                    <Style TargetType="Label" BasedOn="{StaticResource {x:Static pu:StyleKeys.LabelStyle}}">
                                        <Setter Property="pu:LabelHelper.CornerRadius" Value="2"/>
                                        <Setter Property="Background" Value="{DynamicResource SuccessColor}"/>
                                        <Setter Property="BorderBrush" Value="{DynamicResource SuccessColor}"/>
                                        <Setter Property="Height" Value="25"/>
                                        <Setter Property="Padding" Value="0"/>
                                        <Setter Property="HorizontalContentAlignment" Value="Center"/>
                                        <Setter Property="VerticalContentAlignment" Value="Center"/>
                                        <Setter Property="BorderThickness" Value="2"/>
                                        <Setter Property="FontSize" Value="10"/>
                                        <Setter Property="FontWeight" Value="Bold"/>
                                        <Setter Property="Foreground" Value="Black"/>
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding SelectedStatus}" Value="{x:Null}">
                                                <Setter Property="Visibility" Value="Collapsed"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding SelectedStatus,Converter={x:Static uic:ResultConverter.Instance}}" Value="FAIL">
                                                <Setter Property="Background" Value="{DynamicResource DangerColor}"/>
                                                <Setter Property="BorderBrush" Value="{DynamicResource DangerColor}"/>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </Label.Style>
                            </Label>
                            <TextBlock Style="{StaticResource TitleBox}" FontSize="{StaticResource ContentFontSize2}" Text="测试项"/>
                            <pu:SearchBox x:Name="stepInput" Tag="step" SearchTextChanged="SearchTextChanged" MinWidth="180" />
                            <TextBlock Style="{StaticResource TitleBox}" FontSize="{StaticResource ContentFontSize2}" Text="结果"/>
                            <pu:SearchBox x:Name="statusInput" Tag="status" SearchTextChanged="SearchTextChanged" MinWidth="180" />
                        </StackPanel>
                    </DockPanel>
                    <DataGrid Grid.Row="1" x:Name="testItemTable" ColumnHeaderHeight="30"
                          ColumnHeaderStyle="{StaticResource SB}"
                      RowStyle="{StaticResource ThisDataGridRow}"
                          Style="{StaticResource DefaultDataGrid}">
                        <DataGrid.Columns>
                            <DataGridTemplateColumn Header="#" IsReadOnly="True" CanUserResize="False">
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <TextBlock Text="{Binding Converter={StaticResource rowNumber},
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow},AncestorLevel=1}}"/>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                            <local:CustomSortTextColumn IsReadOnly="True" Width="Auto" Binding="{Binding sn}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="SN"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                            <local:CustomSortTextColumn IsReadOnly="True"  Width="*" Binding="{Binding step}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="测试项"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                            <local:CustomSortTextColumn IsReadOnly="True"  Width="*" Binding="{Binding value}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="测试值"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                            <local:CustomSortTextColumn IsReadOnly="True"  Width="80" Binding="{Binding limitDown}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="下限"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                            <local:CustomSortTextColumn IsReadOnly="True"  Width="80" Binding="{Binding limitUp}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="上限"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                            <local:CustomSortTextColumn IsReadOnly="True"  Width="80" Binding="{Binding unit}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="单位"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                            <local:CustomSortTextColumn IsReadOnly="True"  Width="80" Binding="{Binding status}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="结果"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                            <local:CustomSortTextColumn IsReadOnly="True"  Width="160" Binding="{Binding time,Converter={x:Static uic:DateStrConverter.Instance}}">
                                <local:CustomSortTextColumn.Header>
                                    <TextBlock Text="测试时间"/>
                                </local:CustomSortTextColumn.Header>
                            </local:CustomSortTextColumn>
                        </DataGrid.Columns>
                    </DataGrid>
                    <pu:Pagination Grid.Row="2" Height="30" HorizontalAlignment="Right" Foreground="Black" ItemsBackground="Transparent"
ItemsCornerRadius="12"
ItemsSelectedBackground="{DynamicResource SecondPrimaryColor}"
                            x:Name="itemPage" CurrentPageChanged="page_CurrentPageChanged"
                            CurrentPage="{Binding TestItemPage}"
                                        MaxPage="{Binding TestItemTotalPage}">
                        <pu:Pagination.ItemContainerStyle>
                            <Style TargetType="pu:PaginationItem">
                                <Setter Property="MinWidth" Value="30" />
                                <Setter Property="Cursor" Value="Hand" />
                                <Setter Property="Width" Value="Auto" />
                            </Style>
                        </pu:Pagination.ItemContainerStyle>
                    </pu:Pagination>
                </Grid>
                <ui:MaskControl Grid.Row="2" Grid.Column="1" x:Name="mask2"/>
            </Grid>
            <ui:MaskControl x:Name="topMask"/>
        </Grid>
    </Border>
</hb:SimpleMaskControl>
Plugin.DataViewer/DataViewerControl.xaml.cs
New file
@@ -0,0 +1,800 @@
using CommunityToolkit.Mvvm.ComponentModel;
using HMI.Base;
using HMI.Core;
using HMI.Core.Setting;
using HMI.Core.Util;
using HMI.UI.Util;
using HMI.Util;
using Microsoft.Win32;
using Panuon.WPF.UI;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.IO;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace Plugin.DataViewer
{
    public class TestResult
    {
        [ExcelColumn("SN")]
        public string sn { get; set; }
        [ExcelColumn("穴位号")]
        public string position { get; set; }
        [ExcelColumn("型号")]
        public string model { get; set; }
        [ExcelColumn("测试结果")]
        public string result { get; set; }
        [ExcelColumn("测试时间")]
        public string time { get; set; }
    }
    public class SerialData
    {
        [ExcelColumn("SN")]
        public string sn { get; set; }
        [ExcelColumn("穴位号")]
        public string position { get; set; }
        [ExcelColumn("型号")]
        public string model { get; set; }
        [ExcelColumn("测试项")]
        public string step { get; set; }
        [ExcelColumn("测试值")]
        public string value { get; set; }
        [ExcelColumn("下限")]
        public string limitDown { get; set; }
        [ExcelColumn("上限")]
        public string limitUp { get; set; }
        [ExcelColumn("结果")]
        public string status { get; set; }
        [ExcelColumn("单位")]
        public string unit { get; set; }
        [ExcelColumn("测试时间")]
        public string time { get; set; }
    }
    public class SortParam
    {
        public string name { get; set; }
        public ListSortDirection direction { get; set; }
        public string sort { get; set; }
    }
    public class ResultData
    {
        public string sn { get; set; }
        public string position { get; set; }
    }
    /// <summary>
    /// DataViewerControl.xaml 的交互逻辑
    /// </summary>
    [ObservableObject]
    public partial class DataViewerControl : SimpleMaskControl
    {
        [ObservableProperty]
        private DataView resultView;
        [ObservableProperty]
        private ObservableCollection<ResultData> resultData;
        [ObservableProperty]
        private long currentPage = 1;
        [ObservableProperty]
        private long totalPage = 0;
        private int pageSize = 30;
        [ObservableProperty]
        private long totalCount;
        [ObservableProperty]
        private long passCount;
        [ObservableProperty]
        private long failCount;
        [ObservableProperty]
        private long testItemPage = 1;
        [ObservableProperty]
        private long testItemTotalPage = 0;
        [ObservableProperty]
        private object selectedResult;
        [ObservableProperty]
        private string selectedStatus;
        private SortParam sorter;
        private SortParam itemSorter;
        public DataViewerControl()
        {
            DataContext = this;
            InitializeComponent();
            Reset();
        }
        private string[] GetFiles(DateTime? startTime, DateTime? endTime)
        {
            string dir = ParamSetting.DataLogFolder;
            string startName = $"Data_{startTime?.ToString("yyyyMMdd")}";
            string endName = $"Data_{endTime?.ToString("yyyyMMdd")}";
            var oldFiles = Directory.GetFiles(dir, "Data_*.db");
            oldFiles = oldFiles.Where(file =>
            {
                var fileName = Path.GetFileNameWithoutExtension(file);
                bool flag = true;
                flag &= (string.Compare(fileName, startName) == 1 || fileName == startName);
                flag &= (string.Compare(endName, fileName) == 1 || fileName == endName);
                return flag;
            }).ToArray();
            return oldFiles;
        }
        private void Reset()
        {
            var today = DateTime.Now;
            var startTime = today.AddDays(-7);
            var end = today.AddDays(1);
            snInput.Text = string.Empty;
            positionInput.Text = string.Empty;
            modelInput.Text = string.Empty;
            resultInput.Text = string.Empty;
            startTimeInput.SelectedDateTime = DateTime.Parse(startTime.ToString("yyyy-MM-dd 00:00:00"));
            endTimeInput.SelectedDateTime = DateTime.Parse(end.ToString("yyyy-MM-dd 00:00:00"));
        }
        private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            var btn = sender as Button;
            if (btn.Name == "resetBtn")
            {
                Reset();
                if (CurrentPage == 1)
                {
                    SelectPage(1, this.pageSize);
                }
                else
                {
                    CurrentPage = 1;
                }
                return;
            }
            if (btn.Name == "srhBtn")
            {
                if (CurrentPage == 1)
                {
                    SelectPage(1, this.pageSize);
                }
                else
                {
                    CurrentPage = 1;
                }
                return;
            }
            if (btn.Name == "srhItemBtn")
            {
                if (TestItemPage == 1)
                {
                    RefreshTestItem(this.selectResultId, 1, this.pageSize);
                }
                else
                {
                    TestItemPage = 1;
                }
                return;
            }
            if (btn.Name == "exportBtn")
            {
                TryExport();
            }
            if (btn.Name == "exportItemBtn")
            {
                TryExportItem();
            }
        }
        private void TryExport()
        {
            SaveFileDialog sfd = new SaveFileDialog
            {
                Filter = "Excel 文件 (*.xls)|*.xls"
            };
            if (sfd.ShowDialog() != true)
            {
                return;
            }
            string filePath = sfd.FileName;
            DateTime? startTime = startTimeInput.SelectedDateTime, endTime = endTimeInput.SelectedDateTime;
            topMask.ShowMask(this, () =>
            {
                var table = SelectResult(startTime, endTime);
                ExcelHelper.ExportExcel<TestResult>(filePath, table);
            }, (ex) =>
            {
                log.Error("Export Result Failed.", ex);
                Dispatcher.Invoke(() =>
                {
                    MsgBoxHelper2.Error($"导出失败!{ex.Message}");
                });
            }, "导出中,请稍后...");
        }
        private void TryExportItem()
        {
            SaveFileDialog sfd = new SaveFileDialog
            {
                Filter = "Excel 文件 (*.xls)|*.xls"
            };
            if (sfd.ShowDialog() != true)
            {
                return;
            }
            string filePath = sfd.FileName;
            DateTime? startTime = startTimeInput.SelectedDateTime, endTime = endTimeInput.SelectedDateTime;
            topMask.ShowMask(this, () =>
            {
                var table = SelectItem(startTime, endTime);
                ExcelHelper.ExportExcel<SerialData>(filePath, table);
            }, (ex) =>
            {
                log.Error("Export Result Failed.", ex);
                Dispatcher.Invoke(() =>
                {
                    MsgBoxHelper2.Error($"导出失败!{ex.Message}");
                });
            }, "导出中,请稍后...");
        }
        private DataTable SelectResult(DateTime? startTime, DateTime? endTime)
        {
            var files = GetFiles(startTime, endTime);
            if (files == null || files.Length <= 0)
            {
                return new DataTable();
            }
            using (var helper = new Sqlite3Helper(files[0]))
            {
                for (int i = 0; i < files.Length; i++)
                {
                    var attach = $"ATTACH DATABASE '{files[i]}' AS 'db{i}'";
                    helper.Execute(attach);
                }
                for (int i = 0; i < files.Length; i++)
                {
                    if (!helper.HasData($"SELECT 1 FROM pragma_table_info('testResult','db{i}') WHERE name = 'resultId';"))
                    {
                        helper.Execute($"ALTER TABLE db{i}.testResult ADD resultId TEXT;");
                    }
                }
                string sql = string.Join(" UNION ALL ", GetResultSqlsByFiles(files, startTime, endTime));
                string sortName = "time", sortDirection = "DESC";
                string baseSql = $"SELECT id,sn,model,station,position,result,time,resultId FROM ({sql}) as _temp";
                sql = $"{baseSql} ORDER BY {sortName} {sortDirection}";
                return helper.GetDataTable(sql);
                //for (int i = 0; i < files.Length; i++)
                //{
                //    helper.Execute($"DETACH DATABASE db{i}");
                //}
            }
        }
        private DataTable SelectItem(DateTime? startTime, DateTime? endTime)
        {
            if (string.IsNullOrEmpty(this.selectResultId))
            {
                return new DataTable();
            }
            var files = GetFiles(startTime, endTime);
            if (files == null || files.Length <= 0)
            {
                return new DataTable();
            }
            using (var helper = new Sqlite3Helper(files[0]))
            {
                for (int i = 0; i < files.Length; i++)
                {
                    var attach = $"ATTACH DATABASE '{files[i]}' AS 'db{i}'";
                    helper.Execute(attach);
                }
                for (int i = 0; i < files.Length; i++)
                {
                    if (!helper.HasData($"SELECT 1 FROM pragma_table_info('serialData','db{i}') WHERE name = 'resultId';"))
                    {
                        helper.Execute($"ALTER TABLE db{i}.serialData ADD resultId TEXT;");
                    }
                }
                string sql = string.Join(" UNION ALL ", GetTestItemSqlsByFiles(this.selectResultId, files));
                string sortName = "time", sortDirection = "DESC";
                string baseSql = $"SELECT sn, position, model,step, limitDown, limitUp,unit, value,status,time FROM ({sql}) as _temp";
                sql = $"{baseSql} ORDER BY {sortName} {sortDirection}";
                return helper.GetDataTable(sql);
                //for (int i = 0; i < files.Length; i++)
                //{
                //    helper.Execute($"DETACH DATABASE db{i}");
                //}
            }
        }
        private void SelectPage(long page, long pageSize)
        {
            try
            {
                var files = GetFiles(startTimeInput.SelectedDateTime, endTimeInput.SelectedDateTime);
                if (files == null || files.Length <= 0)
                {
                    return;
                }
                using (var helper = new Sqlite3Helper(files[0]))
                {
                    for (int i = 0; i < files.Length; i++)
                    {
                        var attach = $"ATTACH DATABASE '{files[i]}' AS 'db{i}'";
                        helper.Execute(attach);
                    }
                    for (int i = 0; i < files.Length; i++)
                    {
                        if (!helper.HasData($"SELECT 1 FROM pragma_table_info('testResult','db{i}') WHERE name = 'resultId';"))
                        {
                            helper.Execute($"ALTER TABLE db{i}.testResult ADD resultId TEXT;");
                        }
                    }
                    string sql = string.Join(" UNION ALL ", GetResultSqlsByFiles(files, startTimeInput.SelectedDateTime, endTimeInput.SelectedDateTime));
                    string sortName = "time", sortDirection = "DESC";
                    if (sorter != null)
                    {
                        sortName = sorter.name;
                        sortDirection = sorter.sort;
                    }
                    string countSql = $"SELECT COUNT(1) FROM ({sql}) as _temp";
                    long total = (long)helper.SelectOne(countSql);
                    string baseSql = $"SELECT id,sn,model,station,position,result,time,resultId FROM ({sql}) as _temp";
                    sql = $"{baseSql} ORDER BY {sortName} {sortDirection} LIMIT {pageSize} OFFSET {(page - 1) * pageSize}";
                    var dt = helper.GetDataTable(sql);
                    TotalPage = total / pageSize + (total % pageSize > 0 ? 1 : 0);
                    resultTable.ItemsSource = dt.DefaultView;
                    sql = $"SELECT result,COUNT(1) as num FROM ({baseSql}) GROUP BY result";
                    var nums = helper.GetDataTable(sql);
                    long all = 0, pass = 0, fail = 0;
                    foreach (DataRow row in nums.Rows)
                    {
                        if ("ok".Equals(row["result"].ToString(), StringComparison.OrdinalIgnoreCase)
                            || "pass".Equals(row["result"].ToString(), StringComparison.OrdinalIgnoreCase))
                        {
                            pass += (long)row["num"];
                            all += (long)row["num"];
                        }
                        if ("ng".Equals(row["result"].ToString(), StringComparison.OrdinalIgnoreCase)
                            || "fail".Equals(row["result"].ToString(), StringComparison.OrdinalIgnoreCase))
                        {
                            fail += (long)row["num"];
                            all += (long)row["num"];
                        }
                    }
                    TotalCount = all;
                    PassCount = pass;
                    FailCount = fail;
                    //for (int i = 0; i < files.Length; i++)
                    //{
                    //    helper.Execute($"DETACH DATABASE db{i}");
                    //}
                }
            }
            catch (Exception ex)
            {
                log.Error("Select TestResult Error.", ex);
            }
        }
        private void RefreshTestItem(string resultId, long page, int pageSize)
        {
            try
            {
                if (string.IsNullOrEmpty(resultId))
                {
                    testItemTable.ItemsSource = new DataTable().DefaultView;
                    return;
                }
                var files = GetFiles(startTimeInput.SelectedDateTime, endTimeInput.SelectedDateTime);
                if (files == null || files.Length <= 0)
                {
                    return;
                }
                string sql = string.Join(" UNION ALL ", GetTestItemSqlsByFiles(resultId, files));
                string sortName = "time", sortDirection = "DESC";
                if (itemSorter != null)
                {
                    sortName = itemSorter.name;
                    sortDirection = itemSorter.sort;
                }
                mask2.ShowMask(null, () =>
                {
                    using (var helper = new Sqlite3Helper(files[0]))
                    {
                        for (int i = 0; i < files.Length; i++)
                        {
                            var attach = $"ATTACH DATABASE '{files[i]}' AS 'db{i}'";
                            helper.Execute(attach);
                        }
                        for (int i = 0; i < files.Length; i++)
                        {
                            if (!helper.HasData($"SELECT 1 FROM pragma_table_info('serialData','db{i}') WHERE name = 'resultId';"))
                            {
                                helper.Execute($"ALTER TABLE db{i}.serialData ADD resultId TEXT;");
                            }
                        }
                        string countSql = $"SELECT COUNT(1) FROM ({sql}) as _temp";
                        long total = (long)helper.SelectOne(countSql);
                        string baseSql = $"SELECT sn, model,step, limitDown, limitUp,unit, value,status,time FROM ({sql}) as _temp";
                        sql = $"{baseSql} ORDER BY {sortName} {sortDirection} LIMIT {pageSize} OFFSET {(page - 1) * pageSize}";
                        var dt = helper.GetDataTable(sql);
                        //for (int i = 0; i < files.Length; i++)
                        //{
                        //    helper.Execute($"DETACH DATABASE db{i}");
                        //}
                        Dispatcher.BeginInvoke(() =>
                        {
                            TestItemTotalPage = total / pageSize + (total % pageSize > 0 ? 1 : 0);
                            testItemTable.ItemsSource = dt.DefaultView;
                        }, System.Windows.Threading.DispatcherPriority.Normal);
                    }
                }, (ex) =>
                {
                    log.Error("Select TestItem Error.", ex);
                },
                "查询中...");
            }
            catch (Exception ex)
            {
                log.Error("Try Select TestItem Error.", ex);
            }
        }
        public string[] GetResultSqlsByFiles(string[] files, DateTime? startTime, DateTime? endTime)
        {
            string sn = string.Empty, position = string.Empty, model = string.Empty, result = string.Empty;
            Dispatcher.Invoke(() =>
            {
                sn = snInput.Text;
                position = positionInput.Text;
                model = modelInput.Text;
                result = resultInput.Text;
            });
            string[] sqls = new string[files.Length];
            List<string> querys = new List<string>();
            if (!string.IsNullOrEmpty(sn))
            {
                querys.Add($"sn = '{sn}'");
            }
            if (!string.IsNullOrEmpty(position))
            {
                querys.Add($"position = '{position}'");
            }
            if (!string.IsNullOrEmpty(model))
            {
                querys.Add($"model = '{model}'");
            }
            if (!string.IsNullOrEmpty(result))
            {
                querys.Add($"result = '{result}'");
            }
            if (startTime != null)
            {
                querys.Add($"time >= '{startTime.Value.ToString("yyyy-MM-dd HH:mm:ss")}'");
            }
            if (endTime != null)
            {
                querys.Add($"time < '{endTime.Value.ToString("yyyy-MM-dd HH:mm:ss")}'");
            }
            string segment = "";
            if (querys.Count > 0)
            {
                segment = $" WHERE {string.Join(" AND ", querys)}";
            }
            for (int i = 0; i < files.Length; i++)
            {
                sqls[i] = $"SELECT id,sn,model,station,position,result,time,resultId FROM db{i}.testResult {segment}";
            }
            return sqls;
        }
        private string[] GetTestItemSqlsByFiles(string resultId, string[] files)
        {
            List<string> querys = new List<string>() {
                $"resultId = '{resultId}'"
            };
            string step = string.Empty, status = string.Empty;
            Dispatcher.Invoke(() =>
            {
                step = stepInput.Text;
                status = statusInput.Text;
            });
            if (!string.IsNullOrEmpty(step))
            {
                querys.Add($"step = '{step}'");
            }
            if (!string.IsNullOrEmpty(status))
            {
                querys.Add($"status = '{status}'");
            }
            string segment = string.Join(" AND ", querys); ;
            string[] sqls = new string[files.Length];
            for (int i = 0; i < files.Length; i++)
            {
                sqls[i] = $"SELECT sn, position, model,step, limitDown, limitUp,unit, value,status,time FROM db{i}.serialData WHERE {segment}";
            }
            return sqls;
        }
        public string[] GetSource(string[] files, string name, string tableName, string value)
        {
            try
            {
                if (files == null || files.Length <= 0)
                {
                    return new string[0];
                }
                string[] GetModelSqls()
                {
                    string[] sqls = new string[files.Length];
                    for (int i = 0; i < files.Length; i++)
                    {
                        sqls[i] = $"SELECT DISTINCT {name} FROM db{i}.{tableName} WHERE {name} LIKE '%{value}%'";
                    }
                    return sqls;
                }
                using (var helper = new Sqlite3Helper(files[0]))
                {
                    for (int i = 0; i < files.Length; i++)
                    {
                        var attach = $"ATTACH DATABASE '{files[i]}' AS 'db{i}'";
                        helper.Execute(attach);
                    }
                    string sql = string.Join(" UNION ", GetModelSqls());
                    sql = $"SELECT DISTINCT {name} FROM ({sql}) LIMIT 10";
                    var dt = helper.GetDataTable(sql);
                    return dt.Select().Select(row => row[name].ToString()).ToArray();
                }
                return new string[0];
            }
            catch (Exception ex)
            {
                return new string[0];
            }
        }
        private void SearchTextChanged(object sender, Panuon.WPF.SearchTextChangedRoutedEventArgs e)
        {
            SearchBox sb = sender as SearchBox;
            string tag = sb.Tag.ToString(), value = sb.Text;
            var files = GetFiles(startTimeInput.SelectedDateTime, endTimeInput.SelectedDateTime);
            Task.Run(() =>
            {
                string[] items = new string[0];
                if (tag == "step" || tag == "status")
                {
                    items = GetSource(files, tag, "serialData", value);
                }
                else
                {
                    items = GetSource(files, tag, "testResult", value);
                }
                Dispatcher.BeginInvoke(new Action(() =>
                {
                    sb.ItemsSource = items;
                }), System.Windows.Threading.DispatcherPriority.Normal);
            });
        }
        private void Grid_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            var border = sender as Border;
            var header = border.Tag as DataGridColumnHeader;
            var col = header.Column as ICustomSortColumn;
            if (col == null) return;
            col.UpdateOther(null);
            var dataGrid = col.GetOwner();
            if (col.IsAsc == null)
            {
                col.IsAsc = true;
                if (dataGrid == resultTable)
                {
                    sorter = new SortParam { name = header.Column.SortMemberPath, direction = ListSortDirection.Ascending, sort = "ASC" };
                }
                else {
                    itemSorter = new SortParam { name = header.Column.SortMemberPath, direction = ListSortDirection.Ascending, sort = "ASC" };
                }
            }
            else if (col.IsAsc == true)
            {
                col.IsAsc = false;
                if (dataGrid == resultTable)
                {
                    sorter = new SortParam { name = header.Column.SortMemberPath, direction = ListSortDirection.Descending, sort = "DESC" };
                }
                else
                {
                    itemSorter = new SortParam { name = header.Column.SortMemberPath, direction = ListSortDirection.Descending, sort = "DESC" };
                }
            }
            else
            {
                col.IsAsc = null;
                if (dataGrid == resultTable)
                {
                    sorter = null;
                }
                else
                {
                    itemSorter = null;
                }
            }
            if (dataGrid == resultTable)
            {
                if (CurrentPage == 1)
                {
                    SelectPage(1, this.pageSize);
                }
                else
                {
                    CurrentPage = 1;
                }
            }
            else
            {
                if (TestItemPage == 1)
                {
                    RefreshTestItem(this.selectResultId, 1, this.pageSize);
                }
                else
                {
                    TestItemPage = 1;
                }
            }
        }
        private void page_CurrentPageChanged(object sender, Panuon.WPF.SelectedValueChangedRoutedEventArgs<int> e)
        {
            if (sender == page)
            {
                SelectPage(e.NewValue, this.pageSize);
            }
            else
            {
                RefreshTestItem(this.selectResultId, e.NewValue, this.pageSize);
            }
        }
        private string selectResultId;
        partial void OnSelectedResultChanged(object value)
        {
            if (value != null)
            {
                return;
            }
            var selectedRow = value as DataRowView;
            var resultId = selectedRow?["resultId"]?.ToString();
            this.SelectedStatus = selectedRow?["result"]?.ToString();
            this.selectResultId = resultId;
            if (TestItemPage == 1)
            {
                RefreshTestItem(resultId, 1, this.pageSize);
            }
            else
            {
                TestItemPage = 1;
            }
        }
        private void resultTable_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            var selectedRow = this.SelectedResult as DataRowView;
            var resultId = selectedRow?["resultId"]?.ToString();
            this.SelectedStatus = selectedRow?["result"]?.ToString();
            this.selectResultId = resultId;
            if (TestItemPage == 1)
            {
                RefreshTestItem(resultId, 1, this.pageSize);
            }
            else
            {
                TestItemPage = 1;
            }
        }
    }
}
Plugin.DataViewer/Plugin.DataViewer.csproj
New file
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Library</OutputType>
    <TargetFramework>net472</TargetFramework>
    <Nullable>enable</Nullable>
    <LangVersion>10.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
    <PackageReference Include="log4net" Version="3.0.4" />
    <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
    <PackageReference Include="Panuon.WPF.UI" Version="1.3.0.2" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\HMI.Base\HMI.Base.csproj" />
    <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" />
    <ProjectReference Include="..\HMI.UI\HMI.UI.csproj" />
    <ProjectReference Include="..\HMI.Util\HMI.Util.csproj" />
  </ItemGroup>
</Project>
Plugin.TestResult/DataTable.xaml
@@ -35,12 +35,12 @@
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <TextBlock FontSize="{Binding ActualWidth,RelativeSource={RelativeSource AncestorType=UserControl}, 
Converter={StaticResource FontSizeConverter}, ConverterParameter='20,940'}"
                   FontWeight="Bold"
@@ -75,7 +75,7 @@
        <!--<local:Chart Grid.Row="2" x:Name="chart" BorderThickness="0,1,0,1" BorderBrush="Black" 
                      MouseDoubleClick="chart_MouseDoubleClick"/>-->
        <StatusBar FontSize="18" Grid.Row="3">
        <StatusBar FontSize="14" Grid.Row="3">
            <StatusBar.ItemsPanel>
                <ItemsPanelTemplate>
                    <DockPanel/>
@@ -123,23 +123,29 @@
            </StatusBarItem>
            <StatusBarItem >
                <TextBox IsReadOnly="True" Background="Transparent" BorderThickness="0" Text="{Binding statusText}"/>
                <TextBox IsReadOnly="True" FontSize="16" Background="Transparent" BorderThickness="0" Text="{Binding statusText}"/>
            </StatusBarItem>
        </StatusBar>
        <Grid Grid.Row="4">
            <Grid.ColumnDefinitions>
        <UniformGrid Grid.Row="4" Rows="1">
            <!--<UniformGrid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            </UniformGrid.ColumnDefinitions>-->
            <Button Click="ScanWindow_Click" Style="{StaticResource PrimaryButton}"
        Content="{DynamicResource Tr.Scan}"/>
            <Button Grid.Column="1" Click="ClearAllSnr_Click" Style="{StaticResource DangerButton}"
                    IsEnabled="{Binding IsTesting,Converter={StaticResource reverseBool}}"
Content="{DynamicResource Tr.ClearAllSNR}"/>
            <!--<Button Grid.Column="2" Click="SetStationGlobal_Click" Style="{StaticResource SuccessButton}"
        Content="{DynamicResource App.Start}"/>-->
            <Button Grid.Column="2" Click="StartSingleClick">
        Content="{DynamicResource App.Start}"/> Background="#1296db"-->
            <Button Grid.Column="2" Click="Abort_Click" x:Name="abortBtn"
        Style="{DynamicResource DangerButton}" Background="#1296db" Content="Abort"/>
            <Button Grid.Column="3" Click="StartSingleClick">
                <Button.Style>
                    <Style TargetType="Button" BasedOn="{StaticResource SuccessButton}">
                        <Setter Property="Content" Value="{DynamicResource App.Start}"/>
@@ -152,7 +158,6 @@
                    </Style>
                </Button.Style>
            </Button>
        </Grid>
        </UniformGrid>
    </Grid>
</UserControl>
Plugin.TestResult/DataTable.xaml.cs
@@ -1,10 +1,13 @@
using HMI.Core.Setting;
using HMI.Base;
using HMI.Core.Setting;
using HMI.Core.Util;
using HMI.LogUtil;
using HMI.UI.Util;
using HMI.Util;
using Plugin.TestResult;
using Plugin.TestResult.Util;
using Plugin.TestResult_WPF.Model;
using Plugin.TsSupport;
using SuperSocket.SocketBase.Config;
using SuperSocket.SocketBase.Protocol;
using System;
@@ -15,6 +18,7 @@
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
namespace Plugin.TestResult_WPF
{
@@ -90,7 +94,7 @@
        public int index { get; private set; }
        private readonly int cellCount;
        private readonly string chartTitle;
        private readonly TestResultView parent;
        public readonly TestResultView parent;
        private BaseConfig config;
        private LiveChart chart;
@@ -137,7 +141,7 @@
            {
                var cellRow = (i / config.PositionColumn) + 1;
                var cellCol = (i % config.PositionColumn) + 1;
                var cellTitle = $"{title}-{cellRow}-{cellCol}";
                var cellTitle = $"{title}-{cellRow}-{cellCol}";
                var cell = new SingleCell(this, cellTitle, i, config.CellListMaxSize, countIniPath);
                mainGrid.Children.Add(cell);
@@ -152,6 +156,7 @@
            testTimeTimer.AutoReset = true;
            testTimeTimer.Elapsed += TestTimeTimer_Elapsed;
            if (!ParamSetting.ShowCoolerSetting) coolerSetting.Visibility = Visibility.Collapsed;
            if (!ParamSetting.ShowBurnInAbort) abortBtn.Visibility = Visibility.Collapsed;
        }
        private void TestTimeTimer_Elapsed(object sender, ElapsedEventArgs e)
@@ -306,13 +311,42 @@
            {
                if (MsgBoxHelper2.WarnChoose(this.lang("Tr.MakeSureStopOvenTest", title)) == MessageBoxResult.OK)
                {
                    LogHelper.Info("UI", $"老化炉[{title}]停止按钮按下", out _);
                    ViewControl.Logger.Info($"老化炉[{title}]停止按钮按下");
                    parent.SetStartFlag(this.index, false);
                }
            }
            else
            {
                ClearChartData();
                parent.SetStartFlag(this.index, true);
                /// 检查是否关门
                if (parent.executionWrapper?.GetStationGlobalValue<bool>($"StationState.DoorClose[{this.index}]", 0) != true)
                {
                    /// 没关门不允许启动
                    MsgBoxHelper2.Warn("请先关闭舱门!");
                    return;
                }
                /// 检查是否有条码
                bool flag = false;
                foreach (var cell in cells)
                {
                    if (!string.IsNullOrEmpty(cell.sn))
                    {
                        flag = true;
                        break;
                    }
                }
                if (flag)
                {
                    LogHelper.Info("UI", $"老化炉[{title}]启动按钮按下", out _);
                    ViewControl.Logger.Info($"老化炉[{title}]启动按钮按下");
                    ClearChartData();
                    parent.SetStartFlag(this.index, true);
                }
                else
                {
                    MsgBoxHelper2.Warn("请先完成扫码!");
                }
            }
        }
@@ -346,8 +380,9 @@
        private void Scanner_ComplateClick(object sender, RoutedEventArgs e)
        {
            string paramName = $"StationState.Scanner[{index}]";
            parent.engine.Globals.SetValBoolean(paramName, 1, true);
            //parent.engine.CommitGlobalsToDisk(promptOnSaveConflicts: false);
            parent.executionWrapper.SetStationGlobalValue<bool>(paramName, 1, true);
            //parent.engine.Globals.SetValBoolean(paramName, 1, true);
        }
        private void Scanner_Closed(object sender, EventArgs e)
@@ -357,7 +392,8 @@
        private void Scanner_FinishBarcode(object sender, BarcodeEventArg e)
        {
            if (parent.SequenceContext == null) return;
            if (parent.executionWrapper == null) return;
            //if (parent.SequenceContext == null) return;
            int rows = (config.CellCount / config.PositionColumn) + 1;
            CellInfo cellInfo = ScannerParser.GetCellInfoByCode(CbProduct.SelectedItem?.ToString(), mainGrid.Columns, rows, e.positionCode);
@@ -381,9 +417,11 @@
                    throw new Exception(this.lang("Tr.PositionCantClearSNR", oldCellInfo.OriginCode));
                }
                // 移除条码
                parent.engine.Globals.SetValString(oldCellInfo.LookUpStr, 1, "");
                parent.engine.CommitGlobalsToDisk(promptOnSaveConflicts: false);
                parent.executionWrapper.SetStationGlobalValue<string>(oldCellInfo.LookUpStr, 1, "");
                parent.executionWrapper.CommitToDisk(false);
                //parent.engine.Globals.SetValString(oldCellInfo.LookUpStr, 1, "");
                //parent.engine.CommitGlobalsToDisk(promptOnSaveConflicts: false);
                oldCell.SetStatus("waiting");
                oldCell.ClearSN();
                return;
@@ -397,8 +435,11 @@
            }
            // 写入条码
            parent.engine.Globals.SetValString(LookupStr, 1, snrCode);
            parent.engine.CommitGlobalsToDisk(promptOnSaveConflicts: false);
            parent.executionWrapper.SetStationGlobalValue<string>(LookupStr, 1, snrCode);
            parent.executionWrapper.CommitToDisk(false);
            //parent.engine.Globals.SetValString(LookupStr, 1, snrCode);
            //parent.engine.CommitGlobalsToDisk(promptOnSaveConflicts: false);
            TestResultView.nameTables[cellInfo.OvenName].GetSingleCell(cellInfo.Index).SetStatus("hadCode");
            TestResultView.nameTables[cellInfo.OvenName].GetSingleCell(cellInfo.Index).sn = snrCode;
@@ -532,8 +573,6 @@
            string strs = datas_10015[2];
            string[] values = strs.Split(',');
            //string step = datas_10015[2];
            //string result = datas_10015[3];
            GetSingleCell(cellIndex).SetInfos(new Info
            {
                step = values[0],
@@ -747,10 +786,10 @@
        private void ClearAllSnr_Click(object sender, RoutedEventArgs e)
        {
            var str = $"StationState.DoorClose[{index}]";
            if (parent.engine.Globals.GetValBoolean(str, 0) == true)
            if (parent.executionWrapper?.GetStationGlobalValue<bool>($"StationState.DoorClose[{this.index}]", 0) == true)
            {
                MsgBoxHelper.Error(Window.GetWindow(this), this.lang("Tr.CantClearSNR"));
                /// 关门了就不允许清条码
                MsgBoxHelper2.Warn(this.lang("Tr.CantClearSNR"));
                return;
            }
@@ -779,8 +818,7 @@
        public void TryClearCellSN(int cellIndex)
        {
            var str = $"StationState.DoorClose[{this.index}]";
            if (parent.engine.Globals.GetValBoolean(str, 0) == true)
            if (parent.executionWrapper?.GetStationGlobalValue<bool>($"StationState.DoorClose[{this.index}]", 0) == true)
            {
                MsgBoxHelper.Error(Window.GetWindow(this), this.lang("Tr.CantClearSNR"));
                return;
@@ -795,18 +833,50 @@
            string LookupStr = $"StationState.SN[{dutNo}]";
            var lastSN = cell.sn;
            cell.SetStatus("waiting");
            parent.engine.Globals.SetValString(LookupStr, 1, "");
            parent.engine.CommitGlobalsToDisk(promptOnSaveConflicts: false);
            parent.executionWrapper.SetStationGlobalValue<string>(LookupStr, 1, "");
            parent.executionWrapper.CommitToDisk(false);
            //parent.engine.Globals.SetValString(LookupStr, 1, "");
            //parent.engine.CommitGlobalsToDisk(promptOnSaveConflicts: false);
            if (!string.IsNullOrEmpty(lastSN)) Scanner.RemoveSNR(lastSN);
            cell.ClearSN();
        }
        private void Button_Click_2(object sender, RoutedEventArgs e)
        private void Abort_Click(object sender, RoutedEventArgs e)
        {
            if (MsgBoxHelper.WarnMessage(Window.GetWindow(this), this.lang("Tr.MakeSureStopOvenTest", title)) == MessageBoxResult.OK)
            if (!parent.IsRunning)
            {
                parent.SetStartFlag(this.index, false);
                MsgBoxHelper.WarnMessage(this, this.lang("Tr.StartSeqFirst"), null, MessageBoxButton.OK);
                return;
            }
            string key = $"StationState.Start[{this.index}]";
            //if (parent.executionWrapper?.GetStationGlobalValue<bool>(key, 0) == true)
            //{
            //    MsgBoxHelper2.Warn("测试启动中,不允许Abort");
            //    return;
            //}
            var win = new RequirePasswordByUser("admin")
            {
                Owner = Window.GetWindow(this)
            };
            if (win.ShowDialog() != true)
            {
                return;
            }
            if (MsgBoxHelper2.WarnChoose("请确认老化仓在安全温度,水冷机吹水完成,单击OK按钮仓位回到初始化状态") != MessageBoxResult.OK)
            {
                return;
            }
            LogHelper.Info("UI", "Abort按钮按下", out _);
            ViewControl.Logger.Info("Abort按钮按下");
            key = $"StationState.Abort[{this.index}]";
            parent.executionWrapper?.SetStationGlobalValue(key, 0, true);
        }
    }
}
Plugin.TestResult/InfoWindow.xaml
@@ -33,7 +33,9 @@
              CanUserDeleteRows="False"
              IsReadOnly="True"
              ColumnWidth="*"
              VirtualizingStackPanel.VirtualizationMode="Standard"
              VirtualizingPanel.IsContainerVirtualizable="True"
              VirtualizingPanel.IsVirtualizing="True"
              VirtualizingStackPanel.VirtualizationMode="Recycling"
              FontSize="16">
            <DataGrid.Resources>
                <Style TargetType="ScrollBar" BasedOn="{StaticResource ScrollStyle_Mobile}"/>
Plugin.TestResult/Lixun/SNValidator.cs
New file
@@ -0,0 +1,91 @@
using HMI.Core.Manager;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Plugin.TestResult.Lixun
{
    public class SNValidator
    {
        public static bool ValidateSN(string model, string sn, out string msg)
        {
            msg = string.Empty;
            string validate = string.Empty;
            try
            {
                validate = ProductManager.GetParameterByProductAndName(model, "SNRValidate")?.Value;
                if (string.IsNullOrEmpty(validate))
                {
                    msg = $"当前型号[{model}]未配置SNRValidate参数,无法校验SN合法性";
                    return true;
                }
                string[] arr = validate.Split(';');
                if (arr.Length == 1)
                {
                    if (!sn.Contains(arr[0]))
                    {
                        msg = $"条码不包含固定码[{arr[0]}]";
                        return false;
                    }
                    return true;
                }
                if (arr.Length == 2)
                {
                    int length = int.Parse(arr[1]);
                    if (!sn.Contains(arr[0]))
                    {
                        msg = $"条码不包含固定码[{arr[0]}]";
                        return false;
                    }
                    if (sn.Length != length)
                    {
                        msg = $"条码长度应为[{length}]";
                        return false;
                    }
                    return true;
                }
                if (arr.Length >= 3)
                {
                    int length = int.Parse(arr[1]);
                    int startIndex = int.Parse(arr[2]);
                    if (sn.Length != length)
                    {
                        msg = $"条码长度应为[{length}]";
                        return false;
                    }
                    if (startIndex >= sn.Length)
                    {
                        msg = $"条码长读小于起始下标[{validate}]";
                        return false;
                    }
                    if (arr[0].Length > sn.Length)
                    {
                        msg = $"条码长读小于固定码长度[{validate}]";
                        return false;
                    }
                    if (sn.Substring(startIndex, arr[0].Length) != arr[0])
                    {
                        msg = $"固定码检查失败: [{validate}]";
                        return false;
                    }
                    return true;
                }
                msg = $"当前型号[{model}]配置参数[{validate}]解析失败";
                return false;
            }
            catch (Exception ex)
            {
                msg = $"当前型号[{model}]配置参数[{validate}]解析失败";
                return false;
            }
        }
    }
}
Plugin.TestResult/ManualDebugWindow.xaml
New file
@@ -0,0 +1,21 @@
<pu:WindowX x:Class="Plugin.TestResult.ManualDebugWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        xmlns:sb="clr-namespace:Util.SB;assembly=Util.ManualDebug"
        Title="ManualDebugWindow"
        xmlns:pu="https://opensource.panuon.com/wpf-ui"
        Style="{StaticResource ModalWindow}"
        Closing="Window_Closing"
        WindowStartupLocation="CenterScreen"
        pu:WindowXModalDialog.Buttons="{x:Null}"
        Height="1000" Width="900">
    <Grid x:Name="sb">
        <sb:ManualDebug OvenName="{Binding OvenName,Mode=OneWay}"
    CellName="{Binding CellName,Mode=OneWay}"
    OvenIndex="{Binding OvenIndex,Mode=OneWay}"
    CellIndex="{Binding CellIndex,Mode=OneWay}"/>
    </Grid>
</pu:WindowX>
Plugin.TestResult/ManualDebugWindow.xaml.cs
New file
@@ -0,0 +1,95 @@
using HMI.Util;
using Panuon.WPF.UI;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Windows;
using Util.SB;
namespace Plugin.TestResult
{
    /// <summary>
    /// ManualDebugWindow.xaml 的交互逻辑
    /// </summary>
    public partial class ManualDebugWindow : WindowX
    {
        private ManualDebug debug;
        public ManualDebugWindow(string seqPath, string modelPath)
        {
            DataContext = this;
            InitializeComponent();
            debug = (ManualDebug)sb.Children[0];
            ReadBtns();
            //List<BtnConfig> settings = new List<BtnConfig> { new BtnConfig { Title = "上电激活", Name = "Active" } };
            debug.seqPath = seqPath;
            debug.modelPath = modelPath;
        }
        private string ovenName;
        public string OvenName
        {
            get => ovenName;
            set
            {
                Set(ref ovenName, value);
            }
        }
        private string cellName;
        public string CellName
        {
            get => cellName;
            set
            {
                Set(ref cellName, value);
            }
        }
        private int ovenIndex;
        public int OvenIndex
        {
            get => ovenIndex;
            set
            {
                Set(ref ovenIndex, value);
            }
        }
        private int cellIndex;
        public int CellIndex
        {
            get => cellIndex;
            set
            {
                Set(ref cellIndex, value);
            }
        }
        private void ReadBtns()
        {
            string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Settings.ini");
            var map = IniFileHelper.GetKeyValuesBySection(path, "Btns");
            List<BtnConfig> settings = new List<BtnConfig>();
            foreach (var kv in map)
            {
                settings.Add(new BtnConfig { Title = kv.Key, Name = kv.Value });
            }
            debug.RefreshButtons(settings);
        }
        private void Window_Closing(object sender, CancelEventArgs e)
        {
            if (MessageBox.Show("确定要关闭吗?", "提示", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
            {
                debug.Stop();
            }
            else
            {
                e.Cancel = true;
            }
        }
    }
}
Plugin.TestResult/Model/TcpServer.cs
@@ -1,8 +1,6 @@
using ScottPlot.Colormaps;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase.Protocol;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
@@ -13,7 +11,6 @@
        public override StringRequestInfo Filter(byte[] readBuffer, int offset, int length, bool toBeCopied, out int rest)
        {
            rest = 0;
            StringBuilder sb = new StringBuilder();
            int i = offset;
            List<byte> bytes = new List<byte>();
            while (i < readBuffer.Length)
@@ -23,17 +20,33 @@
                {
                    break;
                }
                sb.Append((char)b);
                bytes.Add(b);
            }
            byte[] arr = bytes.ToArray();
            return new StringRequestInfo(DateTime.Now + "", Encoding.UTF8.GetString(arr), null);//sb.ToString()
            return new StringRequestInfo(DateTime.Now.ToString("yyyyMMddHHmmssfff"), Encoding.UTF8.GetString(arr), null);//sb.ToString()
        }
    }
    public class StringParser : IRequestInfoParser<StringRequestInfo>
    {
        public StringRequestInfo ParseRequestInfo(string source)
        {
            return new StringRequestInfo(DateTime.Now.ToString("yyyyMMddHHmmssfff"), source, null);
        }
    }
    //public class TcpServer : AppServer<TcpSession>
    //{
    //    public TcpServer() : base(new DefaultReceiveFilterFactory<DTReceiveFilter, StringRequestInfo>())
    //    {
    //    }
    //}
    public class TcpServer : AppServer<TcpSession>
    {
        public TcpServer() : base(new DefaultReceiveFilterFactory<DTReceiveFilter, StringRequestInfo>())
        public TcpServer() : base(new TerminatorReceiveFilterFactory("\n", Encoding.UTF8, new StringParser()))
        {
        }
Plugin.TestResult/Plugin.TestResult.csproj
@@ -103,6 +103,9 @@
      <SpecificVersion>False</SpecificVersion>
      <HintPath>..\Libs\NationalInstruments.TestStand.Interop.UI.AxControls.dll</HintPath>
    </Reference>
    <Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
    </Reference>
    <Reference Include="OpenTK, Version=3.3.1.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
      <HintPath>..\WPFProject\packages\OpenTK.3.3.1\lib\net20\OpenTK.dll</HintPath>
    </Reference>
@@ -206,6 +209,10 @@
      <DependentUpon>CoolerStatus.xaml</DependentUpon>
    </Compile>
    <Compile Include="CSVPrinter.cs" />
    <Compile Include="Lixun\SNValidator.cs" />
    <Compile Include="ManualDebugWindow.xaml.cs">
      <DependentUpon>ManualDebugWindow.xaml</DependentUpon>
    </Compile>
    <Compile Include="Model\BaseConfig.cs" />
    <Compile Include="Model\CellInfo.cs" />
    <Compile Include="Model\Info.cs" />
@@ -257,6 +264,10 @@
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Page Include="InfoWindow.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Page Include="ManualDebugWindow.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
@@ -321,7 +332,6 @@
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
    </EmbeddedResource>
    <None Include="app.config" />
    <None Include="Config\log4net.config" />
    <None Include="Config\log4net.unix.config" />
    <None Include="packages.config" />
@@ -382,6 +392,14 @@
      <Project>{3068124e-f12b-473e-99a0-2da5a0385d05}</Project>
      <Name>Plugin.TsSupport</Name>
    </ProjectReference>
    <ProjectReference Include="..\SimpleUtil\HMI.LogUtil\HMI.LogUtil.csproj">
      <Project>{6d0aab08-0f3e-48ce-b755-9d29bc8f1225}</Project>
      <Name>HMI.LogUtil</Name>
    </ProjectReference>
    <ProjectReference Include="..\Util.ManualDebug\Util.ManualDebug.csproj">
      <Project>{d0944e62-ee52-4abe-832a-a09d98814db1}</Project>
      <Name>Util.ManualDebug</Name>
    </ProjectReference>
  </ItemGroup>
  <ItemGroup>
    <Resource Include="Resource\abort.png" />
Plugin.TestResult/Scanner.xaml.cs
@@ -4,6 +4,7 @@
using HMI.UI.Util;
using HMI.Util;
using Panuon.WPF.UI;
using Plugin.TestResult.Lixun;
using Plugin.TestResult.Model;
using Plugin.TestResult.Util;
using Plugin.TestResult_WPF.Model;
@@ -228,10 +229,34 @@
                    var EnableMes = ParameterManager.GetCommonParameterByName<bool>("EnableMes", false);
                    if (ParamSetting.CustomerCode == "LIXUN" && EnableMes)
                    {
                        if (PNUtil.GetPN(ParamSetting.MesAddress, snr, out string pn, out string msg))
                        var table = TestResultView.tables[this.ovenIndex];
                        string currentPn = table.CbProduct.Text;
                        /// 检查格式
                        if (!SNValidator.ValidateSN(currentPn, snr, out string msg))
                        {
                            var table = TestResultView.tables[this.ovenIndex];
                            string currentPn = table.CbProduct.Text;
                            TestResultView.tables[this.ovenIndex].GetSingleCell(targetCellInfo.Index).LightRed(true, out _);
                            MsgBoxHelper2.Error($"检查产品条码异常:{msg}");
                            snrText.Text = string.Empty;
                            snrText.Focus();
                            return;
                        }
                        /// 获取站名
                        string stationName = ProductManager.GetParameterByProductAndName(currentPn, "MES站位")?.Value;
                        if (string.IsNullOrEmpty(stationName))
                        {
                            TestResultView.tables[this.ovenIndex].GetSingleCell(targetCellInfo.Index).LightRed(true, out _);
                            MsgBoxHelper2.Error($"检查产品条码异常,当前产品[{currentPn}]未设置参数MES站位");
                            snrText.Text = string.Empty;
                            snrText.Focus();
                            return;
                        }
                        /// 检查PN
                        if (PNUtil.GetPN(ParamSetting.MesAddress, stationName, snr, out string pn, out msg))
                        {
                            if (pn != currentPn)
                            {
                                TestResultView.tables[this.ovenIndex].GetSingleCell(targetCellInfo.Index).LightRed(true, out _);
@@ -249,6 +274,27 @@
                            snrText.Focus();
                            return;
                        }
                        /// 检查工序
                        if (PNUtil.CheckIn(ViewControl.Logger, ParamSetting.MesAddress, stationName, snr, out bool needCheckIn, out msg))
                        {
                            if (needCheckIn != true)
                            {
                                TestResultView.tables[this.ovenIndex].GetSingleCell(targetCellInfo.Index).LightRed(true, out _);
                                MsgBoxHelper2.Error($"条码[{snr}]工序不在本站:{msg}");
                                snrText.Text = string.Empty;
                                snrText.Focus();
                                return;
                            }
                        }
                        else
                        {
                            TestResultView.tables[this.ovenIndex].GetSingleCell(targetCellInfo.Index).LightRed(true, out _);
                            MsgBoxHelper2.Error($"条码[{snr}]CheckIn失败:{msg}");
                            snrText.Text = string.Empty;
                            snrText.Focus();
                            return;
                        }
                    }
                    //var _ = ScannerParser.GetCellInfoByCode(this.model, this.columns, this.rows, snr);
                    //ToastHelper.Warning(this, "产品码格式异常");
Plugin.TestResult/SingleCell.xaml
@@ -9,6 +9,7 @@
xmlns:pu="https://opensource.panuon.com/wpf-ui"
xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI"
             x:Name="uc"
             PreviewMouseRightButtonDown="OnPreviewMouseRightButtonDown"
             MouseDoubleClick="UserControl_MouseDoubleClick"
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
@@ -19,13 +20,19 @@
        <ToolTip Padding="5" BorderThickness="1" BorderBrush="Black" Background="Yellow" Content="{Binding tooltipContent}"/>
    </UserControl.ToolTip>-->
    <UserControl.ContextMenu>
    <!--<UserControl.ContextMenu>
        <ContextMenu>
            <!--<MenuItem IsCheckable="True" IsChecked="{Binding CellIsDisabled,Mode=TwoWay}"  Header="禁用"/>-->
            <MenuItem IsCheckable="False" x:Name="clearSNRItem" Click="MenuItem_Click" Header="{DynamicResource Tr.ClearSNR}"/>
            <MenuItem IsCheckable="False" DataContext="{Binding table}" IsEnabled="{Binding IsTesting,Converter={StaticResource reverseBool}}" x:Name="clearSNRItem" Click="MenuItem_Click" Header="{DynamicResource Tr.ClearSNR}"/>
            <MenuItem IsCheckable="False" x:Name="exportCSVItem" Click="MenuItem_Click" Header="{DynamicResource App.ExportCSV}"/>
            <MenuItem IsCheckable="False" x:Name="manualDebugItem" Click="MenuItem_Click" Header="手动调试">
                <MenuItem.Style>
                    <Style TargetType="MenuItem" BasedOn="{StaticResource {x:Static pu:StyleKeys.MenuItemStyle}}">
                    </Style>
                </MenuItem.Style>
            </MenuItem>
        </ContextMenu>
    </UserControl.ContextMenu>
    </UserControl.ContextMenu>-->
    <UserControl.Style>
        <Style TargetType="UserControl">
@@ -65,32 +72,14 @@
    </UserControl.Style>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="18"/>
            <RowDefinition />
            <RowDefinition Height="14"/>
            <RowDefinition Height="14"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <StackPanel Orientation="Horizontal" Background="LightGray" Grid.Row="1" Grid.Column="1" VerticalAlignment="Top"  HorizontalAlignment="Right">
            <TextBlock Foreground="Red" Text="{DynamicResource Ts.ProbeAlarm}">
                <TextBlock.Style>
                    <Style TargetType="TextBlock">
                        <Setter Property="Visibility" Value="Collapsed"/>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding probeAlarm}" Value="True">
                                <Setter Property="Visibility" Value="Visible"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </TextBlock.Style>
            </TextBlock>
            <TextBlock Margin="0">
                <Run Text="{Binding ProbeCount}"/>
                <Run Text="/"/>
                <Run Text="{Binding ProbeWarn}"/>
            </TextBlock>
        </StackPanel>
        <Label Grid.Row="0" Grid.Column="0" HorizontalContentAlignment="Left" VerticalContentAlignment="Top" Padding="0" Margin="5,5,0,0" 
      FontSize="{Binding ActualWidth,RelativeSource={RelativeSource AncestorType=UserControl}, 
   Converter={StaticResource FontSizeConverter}, ConverterParameter='8,103'}" 
@@ -106,9 +95,10 @@
                </Style>
            </Label.Style>
        </Label>
        <ui:ScrollLabel Grid.Row="1" Grid.Column="0" Margin="3,0,0,5" Padding="0" Value="{Binding sn}"/>
        <Image Grid.Row="0" Grid.Column="1" Width="30" HorizontalAlignment="Right" Height="30" Margin="0,5,5,0">
        <Image Grid.Row="0" Grid.Column="1" Width="{Binding ActualWidth,RelativeSource={RelativeSource AncestorType=UserControl},
   Converter={StaticResource FontSizeConverter}, ConverterParameter='10,20'}" HorizontalAlignment="Right"
               Margin="0,5,5,0">
            <Image.Style>
                <Style TargetType="Image">
                    <Setter Property="HorizontalAlignment" Value="Center"/>
@@ -148,5 +138,27 @@
                </Style>
            </Image.Style>
        </Image>
        <ui:ScrollLabel Grid.Row="1" Grid.Column="0" FontSize="10" Grid.ColumnSpan="2" Margin="3,0,0,0" Padding="0" Value="{Binding sn}"/>
        <StackPanel Orientation="Horizontal" Background="LightGray" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" VerticalAlignment="Top" HorizontalAlignment="Right">
            <TextBlock Foreground="Red" Text="{DynamicResource Ts.ProbeAlarm}" FontSize="10">
                <TextBlock.Style>
                    <Style TargetType="TextBlock">
                        <Setter Property="Visibility" Value="Collapsed"/>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding probeAlarm}" Value="True">
                                <Setter Property="Visibility" Value="Visible"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </TextBlock.Style>
            </TextBlock>
            <TextBlock Margin="0" FontSize="10">
        <Run Text="{Binding ProbeCount}"/>
        <Run Text="/"/>
        <Run Text="{Binding ProbeWarn}"/>
            </TextBlock>
        </StackPanel>
    </Grid>
</UserControl>
Plugin.TestResult/SingleCell.xaml.cs
@@ -3,8 +3,10 @@
using HMI.Core.Manager;
using HMI.Core.Setting;
using HMI.Core.Util;
using HMI.UI.Util;
using HMI.Util;
using PLCHelper;
using Plugin.TestResult;
using Plugin.TestResult.Util;
using Plugin.TestResult_WPF.Constant;
using Plugin.TestResult_WPF.Model;
@@ -49,7 +51,7 @@
        private InfoWindow window;
        private int maxSize;
        private DataTable table;
        public DataTable table { get; private set; }
        public SingleCell(DataTable table, string title, int index, int maxSize, string iniFile)
        {
@@ -296,47 +298,66 @@
        public void SetStatus(string status)
        {
            var oldStatus = this.status;
            this.status = status;
            if (oldStatus != "testing" && status == "testing")
            Dispatcher.Invoke(() =>
            {
                UpdateProbeCount(ProbeCount + 1);
                var now = DateTime.Now;
                string dateStr = now.ToString("yyyyMMdd");
                visualTestId = $"{title}_{now.ToString("yyyyMMddHHmmssfff")}";
                helper = CheckSqlHelper(dateStr);
            }
                var oldStatus = this.status;
                this.status = status;
                if (oldStatus != "testing" && status == "testing")
                {
                    UpdateProbeCount(ProbeCount + 1);
                    var now = DateTime.Now;
                    string dateStr = now.ToString("yyyyMMdd");
                    visualTestId = $"{title}_{now.ToString("yyyyMMddHHmmssfff")}";
                    helper = CheckSqlHelper(dateStr);
                    InsertARecourd();
                }
            if (oldStatus != "ok" && status == "ok")
            {
                SaveInfoToDB();
            }
                if (oldStatus != "ok" && status == "ok")
                {
                    SaveInfoToDB();
                }
            if (oldStatus != "ng" && status == "ng")
                if (oldStatus != "ng" && status == "ng")
                {
                    SaveInfoToDB();
                }
            });
        }
        private void InsertARecourd()
        {
            try
            {
                SaveInfoToDB();
                string model = table?.CbProduct?.SelectedItem?.ToString();
                var timestr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                var sql = $"INSERT INTO testResult (sn,model,station,position,result,time,resultId) VALUES ('{sn}','{model}','','{title}','{this.status}','{timestr}','{visualTestId}');";
                helper.Execute(sql);
            }
            catch (Exception ex)
            {
                ViewControl.Logger.Error($"Cell[{title}] SaveToDB Error.", ex);
            }
        }
        public void WriteCSV(Info info)
        {
            try
            {
                if (ParamSetting.CustomerCode != "LIXUN")
                {
                    return;
                }
                string fileName = this.sn?.Replace("/", "#")?.Replace("\\", "#") + ".csv";
                string dir = Path.Combine(ParamSetting.DataLogFolder, "csv", this.title);
                string title = $"{this.lang("Test.Name")},{this.lang("Test.Value")},{this.lang("Test.LimitDown")},{this.lang("Test.LimitUp")},{this.lang("Test.Unit")},{this.lang("Test.Result")},{this.lang("Test.TestTime")}";
                string line = $"{info.step},{info.value},{info.limitDown},{info.limitUp},{info.unit},{info.result},{info.updateTime}";
            //try
            //{
            //    if (ParamSetting.CustomerCode != "LIXUN")
            //    {
            //        return;
            //    }
            //    string fileName = this.sn?.Replace("/", "#")?.Replace("\\", "#") + ".csv";
            //    string dir = Path.Combine(ParamSetting.DataLogFolder, "csv", this.title);
            //    string title = $"{this.lang("Test.Name")},{this.lang("Test.Value")},{this.lang("Test.LimitDown")},{this.lang("Test.LimitUp")},{this.lang("Test.Unit")},{this.lang("Test.Result")},{this.lang("Test.TestTime")}";
            //    string line = $"{info.step},{info.value},{info.limitDown},{info.limitUp},{info.unit},{info.result},{info.updateTime}";
                CSVPrinter.Print(dir, fileName, title, line);
                // ReverseCSV(Path.Combine(dir, fileName));
            }
            catch (Exception ex)
            {
            }
            //    CSVPrinter.Print(dir, fileName, title, line);
            //    // ReverseCSV(Path.Combine(dir, fileName));
            //}
            //catch (Exception ex)
            //{
            //}
        }
        public void SendToChart(Info info)
@@ -378,30 +399,47 @@
                allInfos.Add(info);
                SendToChart(info);
                WriteCSV(info);
                if (queue.Count >= maxSize)
                /// 目前先不限制数量,后续如果数据过多导致性能问题再考虑限制数量
                //if (queue.Count >= maxSize)
                //{
                //    queue.TryDequeue(out var first);
                //    queue.Enqueue(info);
                //    this.infos = new ObservableCollection<Info>(queue);
                //    if (window != null) window.UpdateSource(this.infos);
                //}
                //else
                //{
                //    queue.Enqueue(info);
                //    if (window != null)
                //    {
                //        window.Dispatcher.Invoke(() =>
                //        {
                //            this.infos.Add(info);
                //        });
                //    }
                //    else
                //    {
                //        Dispatcher.Invoke(() =>
                //        {
                //            this.infos.Add(info);
                //        });
                //    }
                //}
                if (window != null)
                {
                    queue.TryDequeue(out var first);
                    queue.Enqueue(info);
                    this.infos = new ObservableCollection<Info>(queue);
                    if (window != null) window.UpdateSource(this.infos);
                    window.Dispatcher.Invoke(() =>
                    {
                        this.infos.Add(info);
                    });
                }
                else
                {
                    queue.Enqueue(info);
                    if (window != null)
                    Dispatcher.Invoke(() =>
                    {
                        window.Dispatcher.Invoke(() =>
                        {
                            this.infos.Add(info);
                        });
                    }
                    else
                    {
                        Dispatcher.Invoke(() =>
                        {
                            this.infos.Add(info);
                        });
                    }
                        this.infos.Add(info);
                    });
                }
                Dispatcher.Invoke(() =>
@@ -813,8 +851,10 @@
            {
                try
                {
                    string model = table?.CbProduct?.SelectedItem?.ToString();
                    var timestr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                    var sql = $"INSERT INTO testResult (sn,station,position,result,time,resultId) VALUES ('{sn}','','{title}','{this.status}','{timestr}','{visualTestId}');";
                    //var sql = $"INSERT INTO testResult (sn,model,station,position,result,time,resultId) VALUES ('{sn}','{model}','','{title}','{this.status}','{timestr}','{visualTestId}');";
                    var sql = $"UPDATE testResult SET time = '{timestr}', result = '{this.status}' WHERE resultId = '{visualTestId}'";
                    helper.Execute(sql);
                    helper.Dispose();
                    helper = null;
@@ -850,11 +890,42 @@
                case "exportCSVItem":
                    TryExportCSV();
                    break;
                case "manualDebugItem":
                    OpenManualDebugWindow();
                    break;
                default:
                    break;
            }
        }
        /// <summary>
        /// 打开手动调试窗口
        /// </summary>
        private void OpenManualDebugWindow()
        {
            string manualDebugSeq = ParameterManager.GetCommonParameterByName("ManualDebugSeqPath")?.Value;
            if (string.IsNullOrEmpty(manualDebugSeq))
            {
                MsgBoxHelper2.Error("请先设置手动调试序列路径[ManualDebugSeqPath]!");
                return;
            }
            string modelPath = table.parent.testModelFile;
            var win = new ManualDebugWindow(manualDebugSeq, modelPath)
            {
                Title = $"{this.title}手动调试",
                Owner = Window.GetWindow(this),
                OvenIndex = this.table.index,
                OvenName = this.table.title,
                CellIndex = this.index,
                CellName = this.title
            };
            win.ShowDialog();
        }
        /// <summary>
        /// 尝试导出CSV
        /// </summary>
        private void TryExportCSV()
        {
            System.Windows.Forms.SaveFileDialog sfd = new System.Windows.Forms.SaveFileDialog
@@ -885,5 +956,60 @@
            return S7Context.WriteData(RunTimeVariablePool.DEFAULT_PLC_NAME,
                $"Oven{table.index + 1}_Cell{index + 1}_Light_NG", on, out msg);
        }
        private void OnPreviewMouseRightButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            ContextMenu contextMenu = new ContextMenu();
            contextMenu.Closed += ContextMenu_Closed;
            //<  />
            //<
            //< MenuItem IsCheckable = "False" x: Name = "manualDebugItem" Click = "MenuItem_Click" Header = "手动调试" >
            //    < MenuItem.Style >
            //        < Style TargetType = "MenuItem" BasedOn = "{StaticResource {x:Static pu:StyleKeys.MenuItemStyle}}" >
            //        </ Style >
            //    </ MenuItem.Style >
            //</ MenuItem >
            var item = new MenuItem
            {
                IsCheckable = false,
                IsEnabled = !table.IsTesting,
                Name = "clearSNRItem",
                Header = this.lang("Tr.ClearSNR"),
            };
            item.Click += MenuItem_Click;
            contextMenu.Items.Add(item);
            item = new MenuItem
            {
                IsCheckable = false,
                Name = "exportCSVItem",
                Header = this.lang("App.ExportCSV"),
            };
            item.Click += MenuItem_Click;
            contextMenu.Items.Add(item);
            if (table.parent.inManualDebugMode)
            {
                item = new MenuItem
                {
                    IsCheckable = false,
                    Name = "manualDebugItem",
                    Header = this.lang("手动调试"),
                };
                item.Click += MenuItem_Click;
                contextMenu.Items.Add(item);
            }
            this.ContextMenu = contextMenu;
        }
        private void ContextMenu_Closed(object sender, RoutedEventArgs e)
        {
            this.ContextMenu = null;
        }
    }
}
Plugin.TestResult/TestResultView.xaml.cs
@@ -8,6 +8,7 @@
using Plugin.TsSupport;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
@@ -26,23 +27,31 @@
        public static Dictionary<string, DataTable> nameTables { get; private set; } = new Dictionary<string, DataTable>();
        private readonly BaseConfig config;
        public AxApplicationMgr testApp;
        public string testModelFile;
        public string testSeqFile;
        /// <summary>
        /// 测试序列
        /// </summary>
        private SequenceFile testSeq = null;
        public Engine engine;
        private Execution execution = null;
        /// <summary>
        /// 测试序列操作句柄
        /// </summary>
        public SequenceContext SequenceContext { get; private set; } = null;
        /// <summary>
        /// 测试模型序列
        /// </summary>
        private SequenceFile modeSeq;
        public bool inManualDebugMode = false;
        #region TS相关
        //public AxApplicationMgr testApp;
        ///// <summary>
        ///// 测试序列
        ///// </summary>
        //private SequenceFile testSeq = null;
        //public Engine engine;
        //private Execution execution = null;
        ///// <summary>
        ///// 测试序列操作句柄
        ///// </summary>
        //public SequenceContext SequenceContext { get; private set; } = null;
        ///// <summary>
        ///// 测试模型序列
        ///// </summary>
        //private SequenceFile modeSeq;
        public ITestExecutionWrapper executionWrapper;
        #endregion
        public TestResultView() : this(new BaseConfig { testing = false })
        {
@@ -53,7 +62,7 @@
        public void RefreshCooler()
        {
        }
        private void RefreshNumberConfig(BaseConfig config)
@@ -133,18 +142,27 @@
            InitializeComponent();
            testApp = TestAppManager.GetAxApplicationMgr();
            testApp.ExitApplication += this.axApplicationMgr_ExitApplication;
            testApp.UIMessageEvent += this.axApplicationMgr_UIMessageEvent;
            testApp.ExecutionClosed += this.axApplicationMgr_ExecutionClosed;
            executionWrapper = new TsExecutionWrapper(); //new TsExecutionWrapper();
            if (executionWrapper is TsExecutionWrapper tsWrapper)
            {
                tsWrapper.AxApplicationMgr.ExitApplication += this.axApplicationMgr_ExitApplication;
                tsWrapper.AxApplicationMgr.UIMessageEvent += this.axApplicationMgr_UIMessageEvent;
                tsWrapper.AxApplicationMgr.ExecutionClosed += this.axApplicationMgr_ExecutionClosed;
                ((System.ComponentModel.ISupportInitialize)(tsWrapper.AxApplicationMgr)).BeginInit();//myClient为第三方控件名
                host.Child = tsWrapper.AxApplicationMgr;
                ((System.ComponentModel.ISupportInitialize)(tsWrapper.AxApplicationMgr)).EndInit();
            }
            ((System.ComponentModel.ISupportInitialize)(this.testApp)).BeginInit();//myClient为第三方控件名
            host.Child = testApp;
            ((System.ComponentModel.ISupportInitialize)(this.testApp)).EndInit();
            //InitTestStandEngine();
            //StartTestSequence();
            //testApp = TestAppManager.GetAxApplicationMgr();
            //testApp.ExitApplication += this.axApplicationMgr_ExitApplication;
            //testApp.UIMessageEvent += this.axApplicationMgr_UIMessageEvent;
            //testApp.ExecutionClosed += this.axApplicationMgr_ExecutionClosed;
            //((System.ComponentModel.ISupportInitialize)(this.testApp)).BeginInit();//myClient为第三方控件名
            //host.Child = testApp;
            //((System.ComponentModel.ISupportInitialize)(this.testApp)).EndInit();
        }
        private void Refresh()
@@ -203,12 +221,13 @@
        {
            try
            {
                this.engine = this.testApp.GetEngine();
                this.engine.PersistBreakpoints = false;
                this.testApp.IsEditor = true;
                this.testApp.LoginOnStart = false;
                this.testApp.Start();
                this.modeSeq = this.engine.GetSequenceFileEx(testModelFile);
                //this.engine = this.testApp.GetEngine();
                //this.engine.PersistBreakpoints = false;
                //this.testApp.IsEditor = true;
                //this.testApp.LoginOnStart = false;
                //this.testApp.Start();
                //this.modeSeq = this.engine.GetSequenceFileEx(testModelFile);
                executionWrapper.Init();
            }
            catch (Exception ex)
            {
@@ -222,10 +241,10 @@
            try
            {
                if (string.IsNullOrEmpty(testSeqFile)) return;
                this.modeSeq = this.engine.GetSequenceFileEx(@"C:\Program Files\National Instruments\TestStand 2020\Components\Models\TestStandModels\ParallelModel.seq");
                this.testSeq = this.engine.GetSequenceFileEx(this.testSeqFile, 0, TypeConflictHandlerTypes.ConflictHandler_UseGlobalType);
                this.execution = this.engine.NewExecution(this.testSeq, "Test UUTs", this.modeSeq, false, 0, null, null, null);
                this.engine.Globals.SetValBoolean("StationState.StopTest", 0, false);
                executionWrapper?.StartSequence(this.testSeqFile,
                    @"C:\Program Files\National Instruments\TestStand 2020\Components\Models\TestStandModels\ParallelModel.seq");
                executionWrapper.SetStationGlobalValue<bool>("StationState.StopTest", 0, false);
            }
            catch (Exception ex)
            {
@@ -238,13 +257,24 @@
        {
            try
            {
                if (this.SequenceContext == null)
                    return;
                bool val = this.SequenceContext.StationGlobals.Exists("StationState.StopTest", 0);
                if (val)
                executionWrapper?.SetStationGlobalValue<bool>("StationState.StopTest", 0, true, true);
                try
                {
                    this.SequenceContext.StationGlobals.SetValBoolean("StationState.StopTest", 0, true);
                    bool isStop = this.execution.WaitForEndEx(2000, true, null, null);
                    executionWrapper?.WaitForExit();
                    executionWrapper?.ReleaseSeqFile();
                }
                catch (Exception ex)
                {
                    ViewControl.Logger.Error("Wait For Exit error.", ex);
                    //if (ex is NullReferenceException ne)
                    //{
                    //}
                    //else
                    //{
                    //    throw ex;
                    //}
                }
            }
            catch (Exception ex)
@@ -256,7 +286,7 @@
        private void axApplicationMgr_ExitApplication(object sender, EventArgs e)
        {
            Environment.ExitCode = testApp.ExitCode;
            //Environment.ExitCode = testApp.ExitCode;
            //TSHelper.DoSynchronousGCForCOMObjectDestruction();
        }
@@ -286,7 +316,11 @@
                if (MethodCode == 0)
                {
                    /// 设置句柄
                    this.SequenceContext = (SequenceContext)e.uiMsg.ActiveXData;
                    //this.SequenceContext = (SequenceContext)e.uiMsg.ActiveXData;
                    if (executionWrapper is TsExecutionWrapper tsWrapper)
                    {
                        tsWrapper.context = (SequenceContext)e.uiMsg.ActiveXData;
                    }
                    return;
                }
@@ -425,6 +459,7 @@
                    int ovenIndex = int.Parse(datas_10015[0]);
                    int cellIndex = int.Parse(datas_10015[1]);
                    tables[ovenIndex].GetSingleCell(cellIndex).ClearInfo();
                    //GC.Collect();
                    return;
                }
                if (MethodCode == 12)
@@ -550,11 +585,13 @@
                    int index = int.Parse(datas[0]);
                    if (MsgBoxHelper2.WarnChoose(datas[1], "提示") == MessageBoxResult.OK)
                    {
                        SequenceContext.StationGlobals.SetValNumber($"StationState.Pipe[{index}]", 1, 1);
                        executionWrapper.SetStationGlobalValue<int>($"StationState.Pipe[{index}]", 1, 1);
                        //SequenceContext.StationGlobals.SetValNumber($"StationState.Pipe[{index}]", 1, 1);
                    }
                    else
                    {
                        SequenceContext.StationGlobals.SetValNumber($"StationState.Pipe[{index}]", 1, 0);
                        executionWrapper.SetStationGlobalValue<int>($"StationState.Pipe[{index}]", 1, 0);
                        //SequenceContext.StationGlobals.SetValNumber($"StationState.Pipe[{index}]", 1, 0);
                    }
                    return;
                }
@@ -567,20 +604,34 @@
        public void SetStartFlag(int index, bool data)
        {
            if (this.SequenceContext == null) return;
            string name = tables[index].title;
            ViewControl.Logger?.Info($"将老化炉[{name}] 的Start变量设置为 {data};");
            string key = $"StationState.Start[{index}]";
            this.engine.Globals.SetValBoolean(key, 1, data);
            //bool val = this.engine.Globals.GetValBoolean(key, 0);
            //this.SequenceContext.StationGlobals.SetValBoolean(key, 0, data);
            //bool val = this.SequenceContext.StationGlobals.GetValBoolean(key, 0);
            executionWrapper?.SetStationGlobalValue<bool>(key, 1, data);
            if (data == false)
            {
                key = $"StationState.Scanner[{index}]";
                executionWrapper?.SetStationGlobalValue<bool>(key, 1, false);
            }
        }
        public bool? GetStartFlag(int index)
        {
            string key = $"StationState.Start[{index}]";
            return executionWrapper.GetStationGlobalValue<bool>(key, 0);
        }
        public void ReleaseResource()
        {
            testApp.ExitApplication -= this.axApplicationMgr_ExitApplication;
            testApp.UIMessageEvent -= this.axApplicationMgr_UIMessageEvent;
            testApp.ExecutionClosed -= this.axApplicationMgr_ExecutionClosed;
            if (executionWrapper is TsExecutionWrapper tsWrapper)
            {
                tsWrapper.AxApplicationMgr.ExitApplication -= this.axApplicationMgr_ExitApplication;
                tsWrapper.AxApplicationMgr.UIMessageEvent -= this.axApplicationMgr_UIMessageEvent;
                tsWrapper.AxApplicationMgr.ExecutionClosed -= this.axApplicationMgr_ExecutionClosed;
            }
            foreach (var table in tables)
            {
                table.RelaseResource();
@@ -611,9 +662,10 @@
            for (int i = 0; i < tables.Length; i++)
            {
                var table = tables[i];
                // table.SetCurrentProduct(currentProduct);
                string LookupStr = $"StationState.Model[{i}]";
                string tableModel = engine?.Globals?.GetValString(LookupStr, 1);
                string tableModel = executionWrapper?.GetStationGlobalValue<string>(LookupStr, 1);
                //engine?.Globals?.GetValString(LookupStr, 1);
                table.SetCurrentProduct(tableModel);
            }
        }
@@ -621,8 +673,12 @@
        public void SetOvenProduct(int index, string product)
        {
            string LookupStr = $"StationState.Model[{index}]";
            engine?.Globals?.SetValString(LookupStr, 1, product);
            engine?.CommitGlobalsToDisk(promptOnSaveConflicts: false);
            executionWrapper?.SetStationGlobalValue<string>(LookupStr, 1, product);
            executionWrapper?.CommitToDisk(false);
        }
        public void RefreshProducts()
        {
        }
    }
}
Plugin.TestResult/Util/PNUtil.cs
@@ -1,4 +1,7 @@
using HMI.Core.Util;
using log4net;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Plugin.TestResult_WPF;
using System;
using System.Net.Http;
@@ -15,13 +18,14 @@
    public class PNUtil
    {
        public static bool GetPN(string mesAddress, string sn, out string pn, out string msg)
        public static bool GetPN(string mesAddress, string stationName, string sn, out string pn, out string msg)
        {
            pn = null;
            msg = string.Empty;
            try
            {
                string json = $"{{\"csn\":\"{sn}\",\"station_name\":\"OP170Burn-In老化\"}}";
                //OP170Burn-In老化
                string json = $"{{\"csn\":\"{sn}\",\"station_name\":\"{stationName}\"}}";
                HttpClient val = new HttpClient();
                ViewControl.Logger.Debug($"Get PN Send: [{json}]");
                try
@@ -52,5 +56,53 @@
                return false;
            }
        }
        public static bool CheckIn(ILog log, string mesAddress, string stationName, string sn, out bool needCheckIn, out string msg)
        {
            JObject data = new JObject();
            data.Add("cmd", "QUERY_RECORD");
            data.Add("p", "UOP");
            data.Add("sn", sn);
            data.Add("station_name", stationName);
            data.Add("op", "A0000001");
            needCheckIn = false;
            msg = string.Empty;
            string sendData = string.Empty;
            string resData = null;
            try
            {
                sendData = data.ToString(Formatting.None);
                using (HttpClient val = new HttpClient())
                {
                    StringContent val2 = new StringContent(sendData, Encoding.UTF8, "application/json");
                    HttpResponseMessage result = val.PostAsync("http://" + mesAddress + "/mes-module-system-2.1.1/lixun/api/checkIn", (HttpContent)(object)val2).Result;
                    result.EnsureSuccessStatusCode();
                    resData = result.Content.ReadAsStringAsync().Result;
                    JObject httpResult = JObject.Parse(resData);
                    log.Info("CheckIn Send: " + httpResult.Value<string>("sendStr"));
                    log.Info("CheckIn Receive: " + httpResult.Value<string>("receiveStr"));
                    log.Info("CheckIn Get: " + resData);
                    msg = httpResult.Value<string>("msg");
                    if (httpResult.Value<int>("code") == 200)
                    {
                        needCheckIn = httpResult.Value<bool>("needCheckIn");
                        return true;
                    }
                    log.Error("CheckIn Error: " + msg);
                    return false;
                }
            }
            catch (Exception ex)
            {
                msg = ex.Message + " in " + ex.StackTrace + ".SendStr: " + sendData + ",ResultStr: " + resData;
                log.Error("CheckIn Unknow Error: " + msg);
                return false;
            }
        }
    }
}
Plugin.TestResult/ViewControl.xaml
@@ -41,11 +41,24 @@
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>-->
        <local:TestResultView x:Name="testResultView" Grid.Row="1" Grid.Column="0" Loaded="testResultView_Loaded" />
        <DockPanel Margin="10,0" Grid.Row="0" Grid.Column="1" Background="#FFF" VerticalAlignment="Center">
        <DockPanel Margin="10,0" Grid.Row="0" Grid.Column="1" LastChildFill="False" Background="#FFF" VerticalAlignment="Center">
            <pu:FormGroup DockPanel.Dock="Right">
                <StackPanel Orientation="Horizontal" VerticalAlignment="Center" dp:StackPanelHelper.ChildSpace="20">
                    <Button x:Name="btnStart" Click="Start_Click" Style="{StaticResource SuccessButton}"
                       Width="100" FontSize="14" Padding="0" Height="30" Content="{DynamicResource App.Start}"/>
                    <Button x:Name="btnStart" Click="Start_Click"
                       Width="100" FontSize="14" Padding="0" Height="30" Content="{DynamicResource App.Start}">
                        <Button.Style>
                            <Style TargetType="Button" BasedOn="{StaticResource SuccessButton}">
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding EnableManualDebug}" Value="True">
                                        <Setter Property="IsEnabled" Value="False"/>
                                    </DataTrigger>
                                    <DataTrigger Binding="{Binding IsRunning}" Value="True">
                                        <Setter Property="IsEnabled" Value="False"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Button.Style>
                    </Button>
                    <Button x:Name="btnStop" Click="Stop_Click" Style="{StaticResource DangerButton}"
                            Width="100" FontSize="14" Padding="0" Height="30"
@@ -83,6 +96,13 @@
                                  ValueChanged="StdEnableMes_ValueChanged"/>
            </pu:FormGroup>
            <pu:FormGroup x:Name="FgEnableManualDebug" Visibility="Collapsed">
                <CheckBox FontSize="20"
                          IsChecked="{Binding EnableManualDebug,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
          VerticalContentAlignment="Center"
          Content="手动调试" />
            </pu:FormGroup>
            <!--<pu:FormGroup Header="{DynamicResource Tr.Status}" FontSize="16">
                <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10">
Plugin.TestResult/ViewControl.xaml.cs
@@ -4,6 +4,7 @@
using HMI.Core.Manager;
using HMI.Core.Setting;
using HMI.Core.Util;
using HMI.LogUtil;
using HMI.StdControl;
using HMI.UI.Util;
using log4net;
@@ -11,6 +12,7 @@
using NationalInstruments.TestStand.Interop.UI.Ax;
using PLCHelper;
using Plugin.TestResult.Util;
using Plugin.TsSupport;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -25,10 +27,6 @@
    /// </summary>
    public partial class ViewControl : PluginMaskControl, IAlarmHandler
    {
        /// <summary>
        /// 测试序列操作句柄
        /// </summary>
        private SequenceContext SequenceContext = null;
        private System.Timers.Timer timer;
@@ -59,6 +57,22 @@
            set => Set(ref _fileName, value);
        }
        private bool enableManualDebug;
        public bool EnableManualDebug
        {
            get => enableManualDebug;
            set
            {
                if (IsRunning)
                {
                    MsgBoxHelper2.Warn("请先结束自动测试");
                    return;
                }
                testResultView.inManualDebugMode = value;
                Set(ref enableManualDebug, value);
            }
        }
        public override void SetLogger(ILog logger)
        {
            base.SetLogger(logger);
@@ -74,13 +88,20 @@
                Set(ref isRunning, value);
                testResultView.IsRunning = value;
                this.modelSelect.IsEnabled = !value;
                this.btnStart.IsEnabled = !value;
                this.btnStop.IsEnabled = value;
                stdEnableMes.CbEnabled = !value;
            }
        }
        private bool AutoExecuteSeq = true;
        #region TS相关
        /// <summary>
        /// 测试序列操作句柄
        /// </summary>
        private SequenceContext SequenceContext = null;
        #endregion
        public ViewControl()
        {
@@ -90,9 +111,18 @@
            timer.Interval = 1000;
            timer.Elapsed += Timer_Elapsed;
            InitializeComponent();
            testResultView.testApp.ExitApplication += new EventHandler(this.axApplicationMgr_ExitApplication);
            testResultView.testApp.UIMessageEvent += new _ApplicationMgrEvents_UIMessageEventEventHandler(this.axApplicationMgr_UIMessageEvent);
            testResultView.testApp.ExecutionClosed += new _ApplicationMgrEvents_ExecutionClosedEventHandler(this.axApplicationMgr_ExecutionClosed);
            if (testResultView.executionWrapper is TsExecutionWrapper tsWrapper)
            {
                tsWrapper.AxApplicationMgr.ExitApplication += new EventHandler(this.axApplicationMgr_ExitApplication);
                tsWrapper.AxApplicationMgr.UIMessageEvent += new _ApplicationMgrEvents_UIMessageEventEventHandler(this.axApplicationMgr_UIMessageEvent);
                tsWrapper.AxApplicationMgr.ExecutionClosed += new _ApplicationMgrEvents_ExecutionClosedEventHandler(this.axApplicationMgr_ExecutionClosed);
            }
            //testResultView.testApp.ExitApplication += new EventHandler(this.axApplicationMgr_ExitApplication);
            //testResultView.testApp.UIMessageEvent += new _ApplicationMgrEvents_UIMessageEventEventHandler(this.axApplicationMgr_UIMessageEvent);
            //testResultView.testApp.ExecutionClosed += new _ApplicationMgrEvents_ExecutionClosedEventHandler(this.axApplicationMgr_ExecutionClosed);
        }
        private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
@@ -144,26 +174,35 @@
                testResultView.testSeqFile = string.Empty;
                FileName = string.Empty;
            }
            RefreshUIConfig();
        }
        public override void DoClose(object sender, EventArgs e)
        /// <summary>
        /// 刷新UI配置
        /// </summary>
        private void RefreshUIConfig()
        {
            try
            {
                timer?.Stop();
                testResultView.ReleaseResource();
                /// 运行中就不管
                if (IsRunning)
                {
                    return;
                }
                bool enableManualDebug = ParameterManager.GetCommonParameterByName("EnableManualDebug", false);
                if (enableManualDebug)
                {
                    FgEnableManualDebug.Visibility = Visibility.Visible;
                }
                else
                {
                    FgEnableManualDebug.Visibility = Visibility.Collapsed;
                    EnableManualDebug = false;
                }
            }
            catch
            catch (Exception ex)
            {
            }
        }
        public override void OnClose(object sender, CancelEventArgs e)
        {
            if (IsRunning)
            {
                MsgBoxHelper2.Warn("请先停止老化测试!");
                e.Cancel = true;
                log.Error("RefreshUIConfig Error.", ex);
            }
        }
@@ -265,18 +304,20 @@
                productSource.Add(prd.Name);
            }
            testResultView.SetProductSource(productSource);
            testResultView.SetCurrentProduct(null);
            string model = testResultView.engine.Globals.GetValString("ProductModel", 1);
            if (!string.IsNullOrEmpty(model))
            {
                this.modelSelect.SelectedItem = model;
                testResultView.SetCurrentProduct(model);
            }
            else if (products.Count > 0)
            {
                this.modelSelect.SelectedIndex = 0;
                testResultView.SetCurrentProduct(products[0].Name);
            }
            //string model = testResultView.executionWrapper.GetStationGlobalValue<string>("ProductModel", 1);
            ////testResultView.engine.Globals.GetValString("ProductModel", 1);
            //if (!string.IsNullOrEmpty(model))
            //{
            //    this.modelSelect.SelectedItem = model;
            //    testResultView.SetCurrentProduct(model);
            //}
            //else if (products.Count > 0)
            //{
            //    this.modelSelect.SelectedIndex = 0;
            //    testResultView.SetCurrentProduct(products[0].Name);
            //}
        }
        private void modelSelect_SelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -285,8 +326,12 @@
            {
                string currentModel = this.modelSelect.SelectedItem.ToString();
                List<ParameterData> parameters = ProductManager.GetParametersByProductName(currentModel);
                testResultView.engine.Globals.SetValString("ProductModel", 0, currentModel);
                testResultView.engine.CommitGlobalsToDisk(promptOnSaveConflicts: false);
                testResultView.executionWrapper.SetStationGlobalValue<string>("ProductModel", 0, currentModel);
                testResultView.executionWrapper.CommitToDisk(false);
                //testResultView.engine.Globals.SetValString("ProductModel", 0, currentModel);
                //testResultView.engine.CommitGlobalsToDisk(promptOnSaveConflicts: false);
            }
            catch (Exception ex)
            {
@@ -304,6 +349,8 @@
                    IsRunning = false;
                    return;
                }
                LogHelper.Info("UI", "总启动按钮按下", out _);
                log.Info("总启动按钮按下");
                ShowMask(() =>
                {
                    testResultView.StartTestSequence();
@@ -332,7 +379,30 @@
        {
            try
            {
                bool breakStop = false;
                foreach (var oven in TestResultView.tables)
                {
                    if (oven.IsTesting)
                    {
                        breakStop = true;
                        break;
                    }
                }
                if (breakStop)
                {
                    MsgBoxHelper2.Warn(this.lang("Tr.OvenIsTesting"));
                    return;
                }
                if (MsgBoxHelper2.WarnChoose(this.lang("Tr.MakeSureStopAllTest")) != MessageBoxResult.OK)
                {
                    return;
                }
                LogHelper.Info("UI", "总停止按钮按下", out _);
                log.Info("总停止按钮按下");
                testResultView.StopTest();
                /// 已经加了WaitForExit,这里可以改为false
                IsRunning = false;
            }
            catch (Exception ex)
@@ -368,13 +438,15 @@
        private void StdEnableMes_ValueChanged(object sender, StdUIEventArg e)
        {
            testResultView.SequenceContext?.StationGlobals.SetValBoolean($"SystemSetting.{e.StringValue}", 1, e.BoolValue);
            testResultView.executionWrapper.SetStationGlobalValue<bool>($"SystemSetting.{e.StringValue}", 1, e.BoolValue);
            //testResultView.SequenceContext?.StationGlobals.SetValBoolean($"SystemSetting.{e.StringValue}", 1, e.BoolValue);
        }
        public void HandleAlarm(AlarmText alarm)
        {
            if (alarm.Level >= 4 && TestResultView.tables != null)
            {
                log.Info($"4级报警触发: [{alarm.AlarmInfo}],将停止所有炉体测试");
                foreach (var oven in TestResultView.tables)
                {
                    testResultView.SetStartFlag(oven.index, false);
@@ -398,8 +470,37 @@
            if (alarm.Level >= 3)
            {
                /// 3级以上的报警会停止测试
                log.Info($"3级报警触发: [{alarm.AlarmInfo}],将停止老化炉[{targetOven.title}]测试");
                testResultView.SetStartFlag(targetOven.index, false);
            }
        }
        public override void Active()
        {
            base.Active();
            LoadProduct();
            RefreshUIConfig();
        }
        public override void OnClose(object sender, CancelEventArgs e)
        {
            if (IsRunning)
            {
                MsgBoxHelper2.Warn("请先停止老化测试!");
                e.Cancel = true;
            }
        }
        public override void DoClose(object sender, EventArgs e)
        {
            try
            {
                timer?.Stop();
                testResultView.ReleaseResource();
            }
            catch
            {
            }
        }
    }
}
Plugin.TestResult/app.config
File was deleted
Plugin.TestResult/packages.config
@@ -9,6 +9,7 @@
  <package id="LiveChartsCore.SkiaSharpView.WPF" version="2.0.0-rc5.4" targetFramework="net472" />
  <package id="log4net" version="3.0.4" targetFramework="net472" />
  <package id="Microsoft.Bcl.AsyncInterfaces" version="8.0.0" targetFramework="net472" />
  <package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" />
  <package id="OpenTK" version="3.3.1" targetFramework="net472" />
  <package id="OpenTK.GLWpfControl" version="3.3.0" targetFramework="net472" />
  <package id="Panuon.WPF" version="1.1.3" targetFramework="net472" />
Plugin.TsExecution/Plugin.TsExecution.csproj
@@ -156,6 +156,18 @@
    <Reference Include="WindowsFormsIntegration" />
  </ItemGroup>
  <ItemGroup>
    <Page Include="ScreenGrid.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Page Include="ScreenTouch.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Page Include="ScreenTouchWin.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Page Include="WorkOrderWin.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
@@ -173,6 +185,15 @@
      <SubType>Designer</SubType>
    </Page>
    <Compile Include="Entity\TestStep.cs" />
    <Compile Include="ScreenGrid.xaml.cs">
      <DependentUpon>ScreenGrid.xaml</DependentUpon>
    </Compile>
    <Compile Include="ScreenTouch.xaml.cs">
      <DependentUpon>ScreenTouch.xaml</DependentUpon>
    </Compile>
    <Compile Include="ScreenTouchWin.xaml.cs">
      <DependentUpon>ScreenTouchWin.xaml</DependentUpon>
    </Compile>
    <Compile Include="WorkOrderWin.xaml.cs">
      <DependentUpon>WorkOrderWin.xaml</DependentUpon>
    </Compile>
Plugin.TsExecution/Properties/AssemblyInfo.cs
@@ -49,6 +49,6 @@
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.2")]
[assembly: AssemblyFileVersion("1.0.0.2")]
[assembly: AssemblyVersion("1.0.0.3")]
[assembly: AssemblyFileVersion("1.0.0.3")]
[assembly: PluginClassName("Plugin.TsExecution.TsExecution")]
Plugin.TsExecution/ScreenGrid.xaml
New file
@@ -0,0 +1,15 @@
<hb:NotifyControl x:Class="Plugin.TsExecution.ScreenGrid"
             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:Plugin.TsExecution"
             mc:Ignorable="d"
             xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
             xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
             Background="LightGray" Padding="0" Margin="0"
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid x:Name="grid" dp:GridHelper.Space="2" Margin="0">
    </Grid>
</hb:NotifyControl>
Plugin.TsExecution/ScreenGrid.xaml.cs
New file
@@ -0,0 +1,45 @@
using HMI.Base;
using HMI.UI.DpUtil;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace Plugin.TsExecution
{
    public class ScreenGridConfig
    {
        public int rows { get; set; }
        public int columns { get; set; }
        public ObservableCollection<bool> values { get; set; }
    }
    /// <summary>
    /// ScreenTouch.xaml 的交互逻辑
    /// </summary>
    public partial class ScreenGrid : NotifyControl
    {
        public ScreenGrid(int rows, int columns)
        {
            InitializeComponent();
            for (int i = 0; i < rows; i++)
            {
                grid.RowDefinitions.Add(new RowDefinition());
            }
            for (int i = 0; i < columns; i++)
            {
                grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto});
            }
        }
        public void AddScreen(ScreenTouch screen, int row, int col)
        {
            grid.Children.Add(screen);
            Grid.SetRow(screen, row);
            Grid.SetColumn(screen, col);
            GridHelper.RefreshGridSpace(grid);
        }
    }
}
Plugin.TsExecution/ScreenTouch.xaml
New file
@@ -0,0 +1,14 @@
<hb:NotifyControl x:Class="Plugin.TsExecution.ScreenTouch"
             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:Plugin.TsExecution"
             mc:Ignorable="d"
             xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
             xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
             d:DesignHeight="450" d:DesignWidth="800">
    <UniformGrid x:Name="uniform" dp:UniformGridHelper.InnerBorder="0.3" Background="LightGray"
                 dp:UniformGridHelper.InnerBorderBrush="Transparent">
    </UniformGrid>
</hb:NotifyControl>
Plugin.TsExecution/ScreenTouch.xaml.cs
New file
@@ -0,0 +1,156 @@
using HMI.Base;
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace Plugin.TsExecution
{
    public class ColorConverter : IMultiValueConverter
    {
        public static ColorConverter Instance = new ColorConverter();
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values[0] is ObservableCollection<string> list && values[1] is string tag)
            {
                if (list?.Contains(tag) == true) return Brushes.Green;
                return Brushes.White;
            }
            return values;
        }
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            return null;
        }
    }
    /// <summary>
    /// ScreenTouch.xaml 的交互逻辑
    /// </summary>
    public partial class ScreenTouch : NotifyControl
    {
        public static readonly DependencyProperty ValuesProperty = DependencyProperty.Register(
            nameof(Values),
            typeof(ObservableCollection<bool>),
            typeof(ScreenTouch),
            new PropertyMetadata(null, OnValuesChanged));
        private static void OnValuesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is ScreenTouch control)
            {
                // 如果有旧的集合,取消订阅
                if (e.OldValue is ObservableCollection<bool> oldCollection)
                {
                    oldCollection.CollectionChanged -= control.Value_CollectionChanged;
                }
                // 如果有新的集合,订阅变更事件
                if (e.NewValue is ObservableCollection<bool> newCollection)
                {
                    newCollection.CollectionChanged += control.Value_CollectionChanged;
                }
                // 刷新所有单元格
                control.PaintPoints();
            }
        }
        public ScreenTouch(int rows, int columns)
        {
            this.rows = rows;
            this.columns = columns;
            InitializeComponent();
            Refresh();
            Values = new ObservableCollection<bool>(new bool[rows * columns]);
        }
        public ObservableCollection<bool> Values
        {
            get => (ObservableCollection<bool>)GetValue(ValuesProperty);
            set
            {
                SetValue(ValuesProperty, value);
            }
        }
        private void Value_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            PaintPoints();
        }
        public int rows { get; private set; }
        public int columns { get; private set; }
        private Border[] children;
        public void Refresh()
        {
            uniform.Children.Clear();
            uniform.Rows = rows;
            uniform.Columns = columns;
            children = new Border[rows * columns];
            for (int i = 0; i < children.Length; i++)
            {
                var border = new Border
                {
                    BorderThickness = new Thickness(0),
                    Background = Brushes.White
                };
                children[i] = border;
                uniform.Children.Add(border);
            }
        }
        public void Refresh(int rows, int columns)
        {
            this.rows = rows;
            this.columns = columns;
            Refresh();
        }
        public void Clear()
        {
            Values = new ObservableCollection<bool>(new bool[rows * columns]);
        }
        public void DrawPoint(int x, int y)
        {
            Values[y * columns + x] = true;
        }
        public void DrawPoints(int[][] points)
        {
            var temp = new ObservableCollection<bool>(new bool[rows * columns]);
            foreach (int[] point in points)
            {
                //children[point[0]][point[1]].Background = Brushes.Green;
                temp[point[1] * columns + point[0]] = true;
            }
            Values = temp;
        }
        private void PaintPoints()
        {
            for (int i = 0; i < Values.Count; i++)
            {
                if (Values[i])
                {
                    children[i].Background = Brushes.Green;
                }
                else
                {
                    children[i].Background = Brushes.White;
                }
            }
        }
    }
}
Plugin.TsExecution/ScreenTouchWin.xaml
New file
@@ -0,0 +1,20 @@
<pu:WindowX x:Class="Plugin.TsExecution.ScreenTouchWin"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Plugin.TsExecution"
        mc:Ignorable="d"
        xmlns:pw="https://opensource.panuon.com/wpf"
        xmlns:pu="https://opensource.panuon.com/wpf-ui"
        SizeChanged="Window_SizeChanged"
        Style="{StaticResource ModalWindow}"
        pu:WindowXModalDialog.Buttons="{x:Null}"
        WindowStartupLocation="CenterScreen"
        WindowState="Maximized"
        Background="LightGray"
        Title="Screens" Height="450" Width="800">
    <Grid x:Name="grid" HorizontalAlignment="Center">
    </Grid>
</pu:WindowX>
Plugin.TsExecution/ScreenTouchWin.xaml.cs
New file
@@ -0,0 +1,67 @@
using Panuon.WPF.UI;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Plugin.TsExecution
{
    /// <summary>
    /// ScreenTouchWin.xaml 的交互逻辑
    /// </summary>
    public partial class ScreenTouchWin : WindowX
    {
        private ScreenGrid screenGrid;
        private ScreenTouch[] screens;
        public ScreenTouchWin(ScreenTouch[] configs)
        {
            screenGrid = new ScreenGrid(1, configs.Length);
            screens = new ScreenTouch[configs.Length];
            InitializeComponent();
            int space = 2 * (screens.Length + 1);
            for (int i = 0; i < screens.Length; i++)
            {
                var screen = new ScreenTouch(configs[i].rows, configs[i].columns)
                {
                    Width = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length), 0),
                    Height = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length / 16 * 9), 0)
                };
                screen.SetBinding(ScreenTouch.ValuesProperty, new Binding(nameof(ScreenTouch.Values)) { Source = configs[i], Mode = BindingMode.OneWay });
                screens[i] = screen;
                screenGrid.AddScreen(screen, 0, i);
            }
            grid.Children.Add(screenGrid);
        }
        private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (screenGrid == null)
            {
                return;
            }
            int space = 2 * (screens.Length + 1);
            for (int i = 0; i < screens.Length; i++)
            {
                var screen = screens[i];
                screen.Width = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length), 0);
                screen.Height = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length / 16 * 9), 0);
            }
        }
    }
}
Plugin.TsExecution/SingleControl.xaml
@@ -15,6 +15,7 @@
             FontSize="{Binding ActualHeight,RelativeSource={RelativeSource AncestorType=UserControl}, 
Converter={StaticResource hfc}, ConverterParameter='12,816'}"
             x:Name="control"
             SizeChanged="control_SizeChanged"
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <ResourceDictionary>
@@ -51,21 +52,20 @@
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid dp:GridHelper.InnerBorder="1" dp:GridHelper.Space="2"
        <Grid dp:GridHelper.InnerBorder="1" dp:GridHelper.Space="2" x:Name="tableHeader"
              dp:GridHelper.InnerBorderBrush="{DynamicResource CommonBoarder}">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
                <ColumnDefinition />
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <StackPanel Grid.RowSpan="3">
                <StackPanel.Style>
                    <Style TargetType="StackPanel">
                        <Setter Property="Background" Value="DeepSkyBlue"/>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding Status}" Value="Passed">
                                <Setter Property="Background" Value="{StaticResource passedColor}"/>
@@ -89,6 +89,7 @@
                <Label>
                    <Label.Style>
                        <Style TargetType="Label">
                            <Setter Property="VerticalAlignment" Value="Center"/>
                            <Setter Property="Padding" Value="10,10,10,0"/>
                            <Setter Property="Margin" Value="0"/>
                            <Setter Property="FontFamily" Value="{StaticResource iconfont}"/>
@@ -123,7 +124,7 @@
                    </Label.Style>
                </Label>
                <Label Content="{Binding Status}" VerticalContentAlignment="Center"
                <Label Content="{Binding Status}" VerticalAlignment="Center" VerticalContentAlignment="Center"
                       Padding="0"
                       Margin="0"
                       HorizontalContentAlignment="Center"
@@ -135,7 +136,7 @@
            <!--<Label HorizontalContentAlignment="Left" Grid.Column="1" Content="{Binding Status}"/>-->
            <Label Style="{StaticResource baseLabel}" Background="#FFF" Grid.Row="1" Grid.Column="1">
            <Label Style="{StaticResource baseLabel}" Background="#FFF" Grid.Row="0" Grid.Column="1">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Style="{StaticResource baseText}" Text="{DynamicResource Ts.Total}"/>
                    <TextBlock Style="{StaticResource baseText}" Margin="2,0,18,0" Text="{Binding TotalNum}"/>
@@ -150,7 +151,7 @@
                </StackPanel>
            </Label>
            <Label Style="{StaticResource baseLabel}" Background="#FFF" Grid.Row="2" Grid.Column="1">
            <Label Style="{StaticResource baseLabel}" Background="#FFF" Grid.Row="1" Grid.Column="1">
                <StackPanel Orientation="Horizontal">
                    <Label Style="{StaticResource baseLabel}" Content="{DynamicResource Ts.CurrentSnr}"/>
                    <Label Style="{StaticResource baseLabel}" Margin="2,0,18,0" Content="{Binding Barcode}"/>
@@ -158,6 +159,9 @@
                    <Label Style="{StaticResource baseLabel}" Margin="2,0,18,0" Content="{Binding ElapseTime}"/>
                </StackPanel>
            </Label>
            <Border x:Name="screenContainer" BorderThickness="0" Grid.Column="2" Grid.RowSpan="2" MouseLeftButtonDown="screenContainer_MouseLeftButtonDown">
            </Border>
        </Grid>
        <Grid Grid.Row="2" x:Name="simplePanel" dp:GridHelper.Space="2" dp:GridHelper.SpaceIgnore="TOP">
@@ -337,7 +341,12 @@
                </ListView.View>
            </ListView>-->
            <DataGrid Grid.Row="0" Style="{StaticResource DefaultDataGrid}" Margin="2,0,2,2"
            <Canvas Grid.Row="0" x:Name="canvas" Background="Transparent">
                <DataGrid Canvas.Left="0" Canvas.Top="0" Height="{Binding ActualHeight,ElementName=canvas}"
                          Width="{Binding ActualWidth,ElementName=canvas}"
                          Style="{StaticResource DefaultDataGrid}" Margin="2,0,2,2"
                  FontSize="{DynamicResource tableFontSize}"
                  dp:DataGridHelper.ShowRowNumber="True"
                  VirtualizingPanel.IsContainerVirtualizable="True"
@@ -346,74 +355,75 @@
                  RowHeight="{StaticResource nanD}"
                  ColumnHeaderHeight="{Binding ActualWidth,ElementName=control,
Converter={StaticResource fc}, ConverterParameter='32,1093,30,45'}"
                      HorizontalScrollBarVisibility="Disabled"
                  HorizontalScrollBarVisibility="Disabled"
                  ItemsSource="{Binding Datasource}" x:Name="table">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="#"
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="#"
                            ElementStyle="{StaticResource TableTextBlock}"
            Binding="{Binding Header, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}, Mode=FindAncestor}}" 
                        Width="0.4*"/>
                    <DataGridTextColumn Width="2.5*" Binding="{Binding Name}"
                        <DataGridTextColumn Width="2.5*" Binding="{Binding Name}"
                            ElementStyle="{StaticResource TableTextBlock}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource Test.Name}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTextColumn Width="0.8*" Binding="{Binding Unit}"
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource Test.Name}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="0.8*" Binding="{Binding Unit}"
                            ElementStyle="{StaticResource TableTextBlock}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource Test.Unit}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTextColumn Width="2*" Binding="{Binding Value}"
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource Test.Unit}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="2*" Binding="{Binding Value}"
                            ElementStyle="{StaticResource TableTextBlock}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource Test.Value}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTextColumn Width="1.5*" Binding="{Binding LimitDown}"
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource Test.Value}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="1.5*" Binding="{Binding LimitDown}"
                            ElementStyle="{StaticResource TableTextBlock}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource Test.LimitDown}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTextColumn Width="1.5*" Binding="{Binding LimitUp}"
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource Test.LimitDown}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="1.5*" Binding="{Binding LimitUp}"
                            ElementStyle="{StaticResource TableTextBlock}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource Test.LimitUp}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTextColumn Width="0.6*" Binding="{Binding ConsumeTime}"
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource Test.LimitUp}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTextColumn Width="0.6*" Binding="{Binding ConsumeTime}"
        ElementStyle="{StaticResource TableTextBlock}">
                        <DataGridTextColumn.Header>
                            <TextBlock Text="{DynamicResource Test.ConsumeTime}"/>
                        </DataGridTextColumn.Header>
                    </DataGridTextColumn>
                    <DataGridTemplateColumn Width="1*">
                        <DataGridTemplateColumn.Header>
                            <TextBlock Text="{DynamicResource Test.Result}"/>
                        </DataGridTemplateColumn.Header>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Grid x:Name="border" Margin="0">
                                    <TextBlock x:Name="text" Margin="-1" Padding="0" VerticalAlignment="Center" Text="{Binding Result}"/>
                                </Grid>
                                <DataTemplate.Triggers>
                                    <DataTrigger Binding="{Binding Result}" Value="Passed">
                                        <Setter TargetName="border" Property="Background" Value="{StaticResource passedColor}"/>
                                        <Setter TargetName="text" Property="Foreground" Value="#000"/>
                                    </DataTrigger>
                                    <DataTrigger Binding="{Binding Result}" Value="Failed">
                                        <Setter TargetName="border" Property="Background" Value="{DynamicResource DangerColor}"/>
                                        <Setter TargetName="text" Property="Foreground" Value="#000"/>
                                    </DataTrigger>
                                </DataTemplate.Triggers>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>
                            <DataGridTextColumn.Header>
                                <TextBlock Text="{DynamicResource Test.ConsumeTime}"/>
                            </DataGridTextColumn.Header>
                        </DataGridTextColumn>
                        <DataGridTemplateColumn Width="1*">
                            <DataGridTemplateColumn.Header>
                                <TextBlock Text="{DynamicResource Test.Result}"/>
                            </DataGridTemplateColumn.Header>
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <Grid x:Name="border" Margin="0">
                                        <TextBlock x:Name="text" Margin="-1" Padding="0" VerticalAlignment="Center" Text="{Binding Result}"/>
                                    </Grid>
                                    <DataTemplate.Triggers>
                                        <DataTrigger Binding="{Binding Result}" Value="Passed">
                                            <Setter TargetName="border" Property="Background" Value="{StaticResource passedColor}"/>
                                            <Setter TargetName="text" Property="Foreground" Value="#000"/>
                                        </DataTrigger>
                                        <DataTrigger Binding="{Binding Result}" Value="Failed">
                                            <Setter TargetName="border" Property="Background" Value="{DynamicResource DangerColor}"/>
                                            <Setter TargetName="text" Property="Foreground" Value="#000"/>
                                        </DataTrigger>
                                    </DataTemplate.Triggers>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                    </DataGrid.Columns>
                </DataGrid>
            </Canvas>
            <Grid Grid.Row="1" Height="20" Margin="0">
                <ProgressBar Grid.Row="3" x:Name="testProcess" Value="{Binding ProcessValue}"/>
@@ -424,7 +434,8 @@
            <Grid Background="#f1eded" Grid.Row="2" Height="25">
                <DockPanel>
                    <Label Padding="0" FontSize="12" VerticalContentAlignment="Center" Content="{Binding CurrentStep}"/>
                    <Label Margin="10,0,0,0" Padding="0" FontSize="16" VerticalContentAlignment="Center" Content="{Binding SocketName}"/>
                    <Label Margin="20,0,0,0" Padding="0" FontSize="14" VerticalContentAlignment="Center" Content="{Binding CurrentStep}"/>
                    <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Margin="0,0,5,0">
                        <!--<Button FontSize="12" x:Name="scannerSetting" 
Plugin.TsExecution/SingleControl.xaml.cs
@@ -1,4 +1,5 @@
using HMI.Core.Manager;
using HMI.Core.Entity;
using HMI.Core.Manager;
using HMI.Core.Util;
using HMI.Util;
using NationalInstruments.TestStand.Interop.API;
@@ -7,6 +8,7 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Timers;
using System.Windows;
@@ -36,11 +38,13 @@
            }
        }
        #endregion
        private bool isLoadOK = false;
        private int index;
        private string CountIniPath;
        private string status = "Ready";
        private ScreenTouch[] screens;
        private ScreenGrid screenGrid;
        public string Status
        {
@@ -127,6 +131,16 @@
                        Set("TestInfo");
                    }
                }
            }
        }
        private string socketName = string.Empty;
        public string SocketName
        {
            get { return socketName; }
            set
            {
                Set(ref socketName, value);
            }
        }
@@ -550,5 +564,117 @@
                //Canvas.SetTop(modal, (scannerPanel.ActualHeight - scannerVisualModal.ActualHeight) / 2);
            }
        }
        public void RefreshScreen(int[][] configs)
        {
            //foreach (UIElement item in canvas.Children)
            //{
            //    if (item is ScreenTouch)
            //    {
            //        canvas.Children.Remove(item);
            //    }
            //}
            screenContainer.Child = null;
            screenGrid = new ScreenGrid(1, configs.Length);
            screens = new ScreenTouch[configs.Length];
            int space = 2 * (screens.Length + 1);
            for (int i = 0; i < configs.Length; i++)
            {
                var screen = new ScreenTouch(configs[i][0], configs[i][1])
                {
                    Width = Math.Max((tableHeader.ActualHeight / 9 * 16 - space) / screens.Length, 0),
                    Height = Math.Max((tableHeader.ActualHeight / 9 * 16 - space) / screens.Length / 16 * 9, 0)
                };
                screens[i] = screen;
                screenGrid.AddScreen(screen, 0, i);
            }
            screenContainer.Child = screenGrid;
            //canvas.Children.Add(screens[0]);
            //Canvas.SetBottom(screens[0], 0);
            //Canvas.SetRight(screens[0], 0);
            //Panel.SetZIndex(screens[0], 999);
        }
        public void DrawScreenPoint(int index, int x, int y)
        {
            screens[index].DrawPoint(x, y);
        }
        public void DrawScreenPoints(int index, int[][] points)
        {
            screens[index].DrawPoints(points);
        }
        public void ClearScreenPoint(int index)
        {
            screens[index].Clear();
        }
        public void RefreshProduct(ProductData product)
        {
            if (product == null)
            {
                screenContainer.Child = null;
                return;
            }
            try
            {
                string screenSize = ProductManager.GetParameterByProductAndName(product.Name, "ScreenTouch")?.Value;
                if (string.IsNullOrEmpty(screenSize))
                {
                    screenContainer.Child = null;
                    return;
                }
                string[] arr = screenSize.Split(';');
                int[][] configs = new int[arr.Length][];
                for (int i = 0; i < arr.Length; i++)
                {
                    string[] size = arr[i].Split(',');
                    configs[i] = new int[2] { int.Parse(size[0]), int.Parse(size[1]) };
                }
                RefreshScreen(configs);
            }
            catch (Exception ex)
            {
                TsExecution.logger.Error("RefreshProduct Failed.", ex);
                screenContainer.Child = null;
            }
        }
        private void control_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (screenGrid == null)
            {
                return;
            }
            //screenGrid.Height = tableHeader.ActualHeight - 2;
            //screenGrid.Width = tableHeader.ActualHeight / 9 * 16 - 2;
            int space = 2 * (screens.Length + 1);
            for (int i = 0; i < screens.Length; i++)
            {
                var screen = screens[i];
                screen.Width = Math.Max(Math.Floor((tableHeader.ActualHeight / 9 * 16 - space) / screens.Length), 0);
                screen.Height = Math.Max(Math.Floor(screen.Width / 16 * 9), 0);
            }
        }
        private void screenContainer_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            if (e.ClickCount == 2)
            {
                new ScreenTouchWin(screens)
                {
                    Owner = Window.GetWindow(this)
                }.ShowDialog();
            }
        }
    }
}
Plugin.TsExecution/TsExecution.xaml.cs
@@ -20,6 +20,7 @@
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
@@ -50,6 +51,8 @@
        private SequenceFile modeSeq = null;
        public AxApplicationMgr AxApplicationMgr;
        private Execution execution = null;
        private SequenceContext sequenceContext = null;
        private Chart chart;
        #endregion
@@ -101,6 +104,13 @@
            {
                Set(ref selectedProduct, value);
                CurrentProductModel = value?.Name;
                if (tsSockets != null)
                {
                    foreach (SingleControl socket in tsSockets)
                    {
                        socket.RefreshProduct(value);
                    }
                }
                SetTestSeqFile(value);
            }
        }
@@ -446,6 +456,13 @@
                SelectedProduct = tempProduct;
            }
            RefershUI();
            if (tsSockets != null)
            {
                foreach (SingleControl socket in tsSockets)
                {
                    socket.RefreshProduct(SelectedProduct);
                }
            }
            SampleStatus = false;
        }
@@ -471,24 +488,31 @@
        private void InitTsEngine()
        {
            AxApplicationMgr = TestAppManager.GetAxApplicationMgr();
            //AxApplicationMgr. += AxApplicationMgr_EndExecution;
            AxApplicationMgr.EndExecution += AxApplicationMgr_EndExecution;
            AxApplicationMgr.ExitApplication += new EventHandler(this.AxApplicationMgr_ExitApplication);
            AxApplicationMgr.UIMessageEvent +=
                new _ApplicationMgrEvents_UIMessageEventEventHandler(this.AxApplicationMgr_UIMessageEvent);
            AxApplicationMgr.ExecutionClosed +=
                new _ApplicationMgrEvents_ExecutionClosedEventHandler(this.AxApplicationMgr_ExecutionClosed);
            AxApplicationMgr.ExitApplication += this.AxApplicationMgr_ExitApplication;
            AxApplicationMgr.UIMessageEvent += this.AxApplicationMgr_UIMessageEvent;
            AxApplicationMgr.ExecutionClosed += this.AxApplicationMgr_ExecutionClosed;
            ((System.ComponentModel.ISupportInitialize)(AxApplicationMgr)).BeginInit();
            host.Child = AxApplicationMgr;
            ((System.ComponentModel.ISupportInitialize)(AxApplicationMgr)).EndInit();
        }
        private volatile bool canSwitchModel = false;
        private volatile Action switchAction = null;
        private void AxApplicationMgr_EndExecution(object sender, _ApplicationMgrEvents_EndExecutionEvent e)
        {
            log.Info("AxApplicationMgr_EndExecution");
            if (ReleaseSequenceFile())
            {
                IsTesting = false;
                log.Info("Release SeqFile Success!");
                if (canSwitchModel) switchAction?.Invoke();
            }
            else
            {
@@ -594,10 +618,21 @@
        private void RefershUI()
        {
            panel.Children.Clear();
            string[] names = ParamSetting.SocketNames.Split(',');
            //重新生成子面板
            for (int i = 0; i < this.socketNum; i++)
            {
                SingleControl control = new SingleControl(i, CountIniPath, this.probeWarn);
                string name = string.Empty;
                if (i < names.Length)
                {
                    name = names[i];
                }
                SingleControl control = new SingleControl(i, CountIniPath, this.probeWarn)
                {
                    SocketName = name
                };
                panel.Children.Add(control);
                tsSockets.Add(control);
@@ -1027,7 +1062,7 @@
        private void StartSequence()
        {
            if (!isLoadOK) return;
            engine?.Globals.SetValBoolean("StationState.StopTest", 0, false);
            if (ParamSetting.EnableAutoModel)
            {
                try
@@ -1085,35 +1120,12 @@
        public void StopSequenceClick()
        {
            canSwitchModel = false;
            switchAction = null;
            ShowMask(() =>
            {
                bool stoped = StopSequence();
                if (stoped)
                {
                    IsTesting = false;
                }
            }, null, null, LangUtil.Get("App.SequenceIsStopping"));
            //new MaskWindow((WindowX)Window.GetWindow(this),
            //        (win) =>
            //        {
            //            try
            //            {
            //                bool stoped = StopSequence();
            //                if (stoped)
            //                {
            //                    IsTesting = false;
            //                }
            //            }
            //            finally
            //            {
            //                Dispatcher.Invoke(() =>
            //                {
            //                    win.Close();
            //                });
            //            }
            //        }
            //        ).Info(LangUtil.Get("App.SequenceIsStopping"));
        }
        /// <summary>
@@ -1128,18 +1140,15 @@
                    for (int i = 0; i < tsSockets.Count; i++)
                    {
                        var context = this.tsSockets[i].SequenceFileContext;
                        //bool val = context.StationGlobals.Exists("StationState.StopTest", 0);
                        //if (val)
                        //{
                        //    context.StationGlobals.SetValBoolean("StationState.StopTest", 0, true);
                        //    bool isStop = this.execution.WaitForEndEx(2000, true, null, null);
                        //}
                        context?.StationGlobals.SetValBoolean("StationState.StopTest", 1, true);
                    }
                }
                if (this.execution == null) return true;
                bool res = this.execution.WaitForEndEx(2000, true, null, null);
                //bool res = this.execution.WaitForEndEx(-1, true, null, null);
                bool res = this.execution?.WaitForEndEx(-1, true, Type.Missing, null) == true;
                Marshal.ReleaseComObject(this.execution);
                this.execution = null;
                return res;
            }
            catch (Exception ex)
@@ -1174,6 +1183,7 @@
        private void StartClick()
        {
            canSwitchModel = true;
            ShowMask(() =>
            {
                StartSequence();
@@ -1358,7 +1368,7 @@
                    this.tsSockets[index].ElapseTime = strData;
                    break;
                case (int)UIMessageCodes.UIMsg_EndExecution://执行结束
                    IsTesting = false;
                    //IsTesting = false;
                    //this.ReleaseSequenceFile();
                    break;
                case 10030://显示测试进度
@@ -1405,11 +1415,75 @@
                case 10034:
                    CurrentProductModel = strData;
                    break;
                case 10035:
                    string[] arr_10035 = strData.Split(';');
                    int[][] points = new int[arr_10035.Length - 1][];
                    for (int i = 1; i < arr_10035.Length; i++)
                    {
                        string[] strs = arr_10035[i].Split(',');
                        points[i - 1] = new int[2] { int.Parse(strs[0]), int.Parse(strs[1]) };
                    }
                    this.tsSockets[index].DrawScreenPoints(int.Parse(arr_10035[0]), points);
                    break;
                case 10036:
                    this.tsSockets[index].ClearScreenPoint(int.Parse(strData));
                    break;
                case 10099:
                    string[] arr = strData.Split(',');
                    if (canSwitchModel) SwitchSequenceByModel(index, arr[0], arr[1]);
                    break;
                default:
                    break;
            }
        }
        private void SwitchSequenceByModel(int index, string model, string snr)
        {
            Dispatcher.Invoke(() =>
            {
                var target = Products.FirstOrDefault(e => e.Name == model);
                ShowMask(() =>
                {
                    bool stoped = StopSequence();
                    //if (stoped)
                    //{
                    //    IsTesting = false;
                    //}
                }, null, null, "换型中,请稍后...");
                if (target == null)
                {
                    MsgBoxHelper2.Warn($"请先在产品管理页面配置型号 [{model}] 的产品信息!");
                    return;
                }
                switchAction = () =>
                {
                    switchAction = null;
                    log?.Info($"尝试自动换型: {switchAction}");
                    if (canSwitchModel)
                    {
                        Dispatcher.Invoke(() =>
                        {
                            try
                            {
                                log?.Info("开始自动换型");
                                SelectedProduct = target;
                                engine?.Globals.SetValString($"CacheSNR[{index}]", 1, snr);
                                engine?.CommitGlobalsToDisk(false);
                                StartClick();
                            }
                            catch (Exception ex)
                            {
                                log?.Error("自动换型失败", ex);
                            }
                        });
                    }
                };
            });
        }
        private void ShowAlarm(AlarmText alarm)
@@ -1433,7 +1507,7 @@
                try
                {
                    log.Info($"Event: {eventCode}, NumericData: {index}, StringData: {strData}");
                    log.Debug($"Event: {eventCode}, NumericData: {index}, StringData: {strData}");
                }
                catch
                {
@@ -1441,6 +1515,8 @@
                }
                if (eventCode == 10001)
                {
                    sequenceContext = (SequenceContext)e.uiMsg.ActiveXData;
                    tsSockets[index].SequenceFileContext = (SequenceContext)e.uiMsg.ActiveXData;
                    return;
                }
@@ -1522,6 +1598,10 @@
            {
                lock (this)
                {
                    if (this.testSeq == null)
                    {
                        return true;
                    }
                    //execution.Abort();
                    int count = 10;
                    while (count > 0)
@@ -1531,15 +1611,18 @@
                            if (this.testSeq == null)
                                return true;
                            if (this.engine.ReleaseSequenceFileEx(this.testSeq, 4))
                            if (this.engine?.ReleaseSequenceFileEx(this.testSeq, 4) == true)
                            {
                                Marshal.ReleaseComObject(this.testSeq);
                                this.testSeq = null;
                                // GC.Collect();
                            }
                            if (this.engine.ReleaseSequenceFileEx(this.modeSeq, 4))
                            if (this.engine?.ReleaseSequenceFileEx(this.modeSeq, 4) == true)
                            {
                                Marshal.ReleaseComObject(this.modeSeq);
                                this.modeSeq = null;
                            }
                            this.engine.UnloadAllModules();
                            this.engine?.UnloadAllModules();
                            return this.testSeq == null && this.modeSeq == null;
                        }
                        catch (Exception ex)
Plugin.TsSupport/ITestExecutionWrapper.cs
New file
@@ -0,0 +1,47 @@
using NationalInstruments.TestStand.Interop.API;
using System;
namespace Plugin.TsSupport
{
    public class ExecutionEndEvent
    {
        public object Origin { get; set; }
        public object Data { get; set; }
    }
    public class UIMessageEvent
    {
        public object Origin { get; set; }
        public object Data { get; set; }
        public int EventCode { get; set; }
        public double NumberData { get; set; }
        public bool BoolData { get; set; }
        public string StrData { get; set; }
    }
    public interface ITestExecutionWrapper
    {
        event EventHandler<UIMessageEvent> UIMessageEvent;
        event EventHandler<ExecutionEndEvent> EndExecutionEvent;
        object Init();
        object StartSequence(string filePath, string modelPath);
        T GetStationGlobalValue<T>(string key, int option);
        T GetFileGlobalValue<T>(string key, int option);
        void SetStationGlobalValue<T>(string key, int option, T value, bool inContext = false);
        void SetFileGlobalValue<T>(string key, int option, T value);
        void CommitToDisk(bool option);
        bool WaitForExit();
        bool ReleaseSeqFile();
        void SetContext(object context);
    }
}
Plugin.TsSupport/Plugin.TsSupport.csproj
@@ -87,8 +87,11 @@
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="ITestExecutionWrapper.cs" />
    <Compile Include="TestAppManager.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
    <Compile Include="TeststandWrapper.cs" />
    <Compile Include="TsExecutionWrapper.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
Plugin.TsSupport/TeststandWrapper.cs
New file
@@ -0,0 +1,228 @@
using NationalInstruments.TestStand.Interop.API;
using NationalInstruments.TestStand.Interop.UI.Ax;
using System;
using System.Diagnostics;
namespace Plugin.TsSupport
{
    public class TeststandWrapper : ITestExecutionWrapper
    {
        #region TestStand事件
        /// <summary>
        /// TS 引擎
        /// </summary>
        private Engine engine = null;
        /// <summary>
        /// 测试序列路径
        /// </summary>
        private string testSeqFile = string.Empty;
        /// <summary>
        /// 测试序列
        /// </summary>
        private SequenceFile testSeq = null;
        /// <summary>
        /// 测试模型序列
        /// </summary>
        private SequenceFile modeSeq = null;
        /// <summary>
        /// 运行实例
        /// </summary>
        private Execution execution = null;
        public SequenceContext context { get; set; } = null;
        /// <summary>
        /// 事件管理器
        /// </summary>
        public AxApplicationMgr AxApplicationMgr { get; private set; }
        #endregion
        public event EventHandler<UIMessageEvent> UIMessageEvent;
        public event EventHandler<ExecutionEndEvent> EndExecutionEvent;
        public TeststandWrapper()
        {
            AxApplicationMgr = TestAppManager.GetAxApplicationMgr();
            AxApplicationMgr.EndExecution += AxApplicationMgr_EndExecution;
            AxApplicationMgr.UIMessageEvent += AxApplicationMgr_UIMessageEvent;
            //AxApplicationMgr.ExitApplication += new EventHandler(this.AxApplicationMgr_ExitApplication);
            //AxApplicationMgr.ExecutionClosed +=
            //    new _ApplicationMgrEvents_ExecutionClosedEventHandler(this.AxApplicationMgr_ExecutionClosed);
        }
        private void AxApplicationMgr_UIMessageEvent(object sender, _ApplicationMgrEvents_UIMessageEventEvent e)
        {
            UIMessageEvent?.Invoke(this, new UIMessageEvent()
            {
                Origin = e,
                EventCode = Convert.ToInt32(e.uiMsg.Event),
                NumberData = Convert.ToInt16(e.uiMsg.NumericData),
                Data = e.uiMsg.ActiveXData,
                StrData = e.uiMsg.StringData
            });
        }
        private void AxApplicationMgr_EndExecution(object sender, _ApplicationMgrEvents_EndExecutionEvent e)
        {
            EndExecutionEvent?.Invoke(this, new ExecutionEndEvent() { Origin = e });
        }
        public object Init()
        {
            this.engine = this.AxApplicationMgr.GetEngine();
            this.engine.PersistBreakpoints = false;
            this.AxApplicationMgr.IsEditor = true;
            this.AxApplicationMgr.LoginOnStart = false;
            this.AxApplicationMgr.Start();
            return this.AxApplicationMgr;
        }
        public object StartSequence(string filePath, string modelPath)
        {
            this.testSeqFile = filePath;
            this.modeSeq = this.engine.GetSequenceFileEx(modelPath);
            this.testSeq = this.engine.GetSequenceFileEx(this.testSeqFile, 2, TypeConflictHandlerTypes.ConflictHandler_UseGlobalType);
            this.execution = this.engine.NewExecution(this.testSeq, "Test UUTs", this.modeSeq, false, 0, null, null, null);
            return execution;
        }
        public T GetFileGlobalValue<T>(string key, int option)
        {
            if (typeof(T) == typeof(bool))
            {
                return (T)(object)context?.FileGlobals?.GetValBoolean(key, option);
            }
            else if (typeof(T) == typeof(string))
            {
                return (T)(object)context?.FileGlobals?.GetValString(key, option);
            }
            else if (typeof(T) == typeof(int))
            {
                return (T)(object)context?.FileGlobals?.GetValNumber(key, option);
            }
            else if (typeof(T) == typeof(double))
            {
                return (T)(object)context?.FileGlobals?.GetValNumber(key, option);
            }
            else if (typeof(T) == typeof(float))
            {
                return (T)(object)context?.FileGlobals?.GetValNumber(key, option);
            }
            return default(T);
        }
        public T GetStationGlobalValue<T>(string key, int option)
        {
            if (typeof(T) == typeof(bool))
            {
                return (T)(object)engine?.Globals?.GetValBoolean(key, option);
            }
            else if (typeof(T) == typeof(string))
            {
                return (T)(object)engine?.Globals?.GetValString(key, option);
            }
            else if (typeof(T) == typeof(int))
            {
                return (T)(object)engine?.Globals?.GetValNumber(key, option);
            }
            else if (typeof(T) == typeof(double))
            {
                return (T)(object)engine?.Globals?.GetValNumber(key, option);
            }
            else if (typeof(T) == typeof(float))
            {
                return (T)(object)engine?.Globals?.GetValNumber(key, option);
            }
            return default(T);
        }
        public void SetFileGlobalValue<T>(string key, int option, T value)
        {
            if (value is bool bVal)
            {
                context?.FileGlobals?.SetValBoolean(key, option, bVal);
            }
            else if (value is string sVal)
            {
                context?.FileGlobals?.SetValString(key, option, sVal);
            }
            else if (value is int iVal)
            {
                context?.FileGlobals?.SetValNumber(key, option, iVal);
            }
            else if (value is double dVal)
            {
                context?.FileGlobals?.SetValNumber(key, option, dVal);
            }
            else if (value is float fVal)
            {
                context?.FileGlobals?.SetValNumber(key, option, fVal);
            }
        }
        public void SetStationGlobalValue<T>(string key, int option, T value, bool inContext = false)
        {
            PropertyObject obj = null;
            if (inContext)
            {
                obj = context?.StationGlobals;
            }
            else
            {
                obj = engine?.Globals;
            }
            if (value is bool bVal)
            {
                obj?.SetValBoolean(key, option, bVal);
            }
            else if (value is string sVal)
            {
                obj?.SetValString(key, option, sVal);
            }
            else if (value is int iVal)
            {
                obj?.SetValNumber(key, option, iVal);
            }
            else if (value is double dVal)
            {
                obj?.SetValNumber(key, option, dVal);
            }
            else if (value is float fVal)
            {
                obj?.SetValNumber(key, option, fVal);
            }
            //engine?.CommitGlobalsToDisk(promptOnSaveConflicts: false);
        }
        public void CommitToDisk(bool option)
        {
            engine?.CommitGlobalsToDisk(option);
        }
        public bool WaitForExit()
        {
            return this.execution?.WaitForEndEx(-1, true, Type.Missing, null) == true;
        }
        public bool ReleaseSeqFile()
        {
            if (testSeq != null)
            {
                return engine?.ReleaseSequenceFileEx(testSeq, 4) == true;
            }
            return true;
        }
        public void SetContext(object context)
        {
            this.context = (SequenceContext)context;
        }
    }
}
Plugin.TsSupport/TsExecutionWrapper.cs
New file
@@ -0,0 +1,214 @@
using NationalInstruments.TestStand.Interop.API;
using NationalInstruments.TestStand.Interop.UI.Ax;
using System;
namespace Plugin.TsSupport
{
    public class TsExecutionWrapper : ITestExecutionWrapper
    {
        #region TestStand事件
        /// <summary>
        /// TS 引擎
        /// </summary>
        private Engine engine = null;
        /// <summary>
        /// 测试序列路径
        /// </summary>
        private string testSeqFile = string.Empty;
        /// <summary>
        /// 测试序列
        /// </summary>
        private SequenceFile testSeq = null;
        /// <summary>
        /// 测试模型序列
        /// </summary>
        private SequenceFile modeSeq = null;
        /// <summary>
        /// 运行实例
        /// </summary>
        private Execution execution = null;
        public SequenceContext context { get; set; } = null;
        /// <summary>
        /// 事件管理器
        /// </summary>
        public AxApplicationMgr AxApplicationMgr { get; private set; }
        public event _ApplicationMgrEvents_EndExecutionEventHandler OnEndExecution;
        public event EventHandler<UIMessageEvent> UIMessageEvent;
        public event EventHandler<ExecutionEndEvent> EndExecutionEvent;
        public TsExecutionWrapper()
        {
            AxApplicationMgr = TestAppManager.GetAxApplicationMgr();
            //AxApplicationMgr.EndExecution += AxApplicationMgr_EndExecution;
            //AxApplicationMgr.ExitApplication += new EventHandler(this.AxApplicationMgr_ExitApplication);
            //AxApplicationMgr.UIMessageEvent +=
            //    new _ApplicationMgrEvents_UIMessageEventEventHandler(this.AxApplicationMgr_UIMessageEvent);
            //AxApplicationMgr.ExecutionClosed +=
            //    new _ApplicationMgrEvents_ExecutionClosedEventHandler(this.AxApplicationMgr_ExecutionClosed);
        }
        #endregion
        public object Init()
        {
            this.engine = this.AxApplicationMgr.GetEngine();
            this.engine.PersistBreakpoints = false;
            this.AxApplicationMgr.IsEditor = true;
            this.AxApplicationMgr.LoginOnStart = false;
            this.AxApplicationMgr.Start();
            return this.AxApplicationMgr;
        }
        public object StartSequence(string filePath, string modelPath)
        {
            this.testSeqFile = filePath;
            this.modeSeq = this.engine.GetSequenceFileEx(modelPath);
            this.testSeq = this.engine.GetSequenceFileEx(this.testSeqFile, 2, TypeConflictHandlerTypes.ConflictHandler_UseGlobalType);
            this.execution = this.engine.NewExecution(this.testSeq, "Test UUTs", this.modeSeq, false, 0, null, null, null);
            return execution;
        }
        public T GetFileGlobalValue<T>(string key, int option)
        {
            if (typeof(T) == typeof(bool))
            {
                return (T)(object)context?.FileGlobals?.GetValBoolean(key, option);
            }
            else if (typeof(T) == typeof(string))
            {
                return (T)(object)context?.FileGlobals?.GetValString(key, option);
            }
            else if (typeof(T) == typeof(int))
            {
                return (T)(object)context?.FileGlobals?.GetValNumber(key, option);
            }
            else if (typeof(T) == typeof(double))
            {
                return (T)(object)context?.FileGlobals?.GetValNumber(key, option);
            }
            else if (typeof(T) == typeof(float))
            {
                return (T)(object)context?.FileGlobals?.GetValNumber(key, option);
            }
            return default(T);
        }
        public T GetStationGlobalValue<T>(string key, int option)
        {
            if (typeof(T) == typeof(bool))
            {
                return (T)(object)engine?.Globals?.GetValBoolean(key, option);
            }
            else if (typeof(T) == typeof(string))
            {
                return (T)(object)engine?.Globals?.GetValString(key, option);
            }
            else if (typeof(T) == typeof(int))
            {
                return (T)(object)engine?.Globals?.GetValNumber(key, option);
            }
            else if (typeof(T) == typeof(double))
            {
                return (T)(object)engine?.Globals?.GetValNumber(key, option);
            }
            else if (typeof(T) == typeof(float))
            {
                return (T)(object)engine?.Globals?.GetValNumber(key, option);
            }
            return default(T);
        }
        public void SetFileGlobalValue<T>(string key, int option, T value)
        {
            if (value is bool bVal)
            {
                context?.FileGlobals?.SetValBoolean(key, option, bVal);
            }
            else if (value is string sVal)
            {
                context?.FileGlobals?.SetValString(key, option, sVal);
            }
            else if (value is int iVal)
            {
                context?.FileGlobals?.SetValNumber(key, option, iVal);
            }
            else if (value is double dVal)
            {
                context?.FileGlobals?.SetValNumber(key, option, dVal);
            }
            else if (value is float fVal)
            {
                context?.FileGlobals?.SetValNumber(key, option, fVal);
            }
        }
        public void SetStationGlobalValue<T>(string key, int option, T value, bool inContext = false)
        {
            PropertyObject obj = null;
            if (inContext)
            {
                obj = context?.StationGlobals;
            }
            else
            {
                obj = engine?.Globals;
            }
            if (value is bool bVal)
            {
                obj?.SetValBoolean(key, option, bVal);
            }
            else if (value is string sVal)
            {
                obj?.SetValString(key, option, sVal);
            }
            else if (value is int iVal)
            {
                obj?.SetValNumber(key, option, iVal);
            }
            else if (value is double dVal)
            {
                obj?.SetValNumber(key, option, dVal);
            }
            else if (value is float fVal)
            {
                obj?.SetValNumber(key, option, fVal);
            }
            //engine?.CommitGlobalsToDisk(promptOnSaveConflicts: false);
        }
        public void CommitToDisk(bool option)
        {
            engine?.CommitGlobalsToDisk(option);
        }
        public bool WaitForExit()
        {
            return this.execution?.WaitForEndEx(-1, true, Type.Missing, null) == true;
        }
        public bool ReleaseSeqFile()
        {
            if (testSeq != null)
            {
                return engine?.ReleaseSequenceFileEx(testSeq, 4) == true;
            }
            return true;
        }
        public void SetContext(object context)
        {
        }
    }
}
Plugin.UICollection/AssemblyInfo.cs
New file
@@ -0,0 +1,10 @@
using System.Windows;
[assembly:ThemeInfo(
    ResourceDictionaryLocation.None,            //where theme specific resource dictionaries are located
                                                //(used if a resource is not found in the page,
                                                // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly   //where the generic resource dictionary is located
                                                //(used if a resource is not found in the page,
                                                // app, or any theme specific resource dictionaries)
)]
Plugin.UICollection/Plugin.UICollection.csproj
New file
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Library</OutputType>
    <TargetFramework>net472</TargetFramework>
    <Nullable>enable</Nullable>
    <LangVersion>10.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
    <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
    <PackageReference Include="Panuon.WPF.UI" Version="1.3.0.2" />
    <PackageReference Include="TouchSocket" Version="4.2.0" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\..\UILib\UILib.csproj" />
    <ProjectReference Include="..\..\UtilLib\UtilLib.csproj" />
    <ProjectReference Include="..\HMI.UI\HMI.UI.csproj" />
  </ItemGroup>
</Project>
Plugin.UICollection/TcpDebugger.xaml
New file
@@ -0,0 +1,176 @@
<UserControl x:Class="Plugin.UICollection.TcpDebugger"
             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:Plugin.UICollection"
             mc:Ignorable="d"
             xmlns:pu="https://opensource.panuon.com/wpf-ui"
             xmlns:dp="clr-namespace:UILib.DpUtil;assembly=UILib"
             xmlns:uic="clr-namespace:UILib.Converter;assembly=UILib"
             FontSize="16"
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <ResourceDictionary>
            <!--<ResourceDictionary.MergedDictionaries>
                <pu:StyleDictionary Excludes="Button" />
            </ResourceDictionary.MergedDictionaries>-->
            <Style TargetType="ComboBox"/>
            <Style TargetType="ComboBoxItem"/>
            <Style TargetType="Button">
            </Style>
            <Style TargetType="GroupBox">
            </Style>
            <Style TargetType="TextBox">
                <Setter Property="VerticalContentAlignment" Value="Center"/>
                <Setter Property="MinWidth" Value="120"/>
                <Setter Property="Height" Value="Auto"/>
            </Style>
        </ResourceDictionary>
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <StackPanel dp:StackPanelHelper.ChildSpace="10" Margin="0,10,0,10">
            <pu:FormGroup Header="IP" GroupName="client">
                <TextBox Text="{Binding Ip}"/>
            </pu:FormGroup>
            <pu:FormGroup Header="Port" GroupName="client">
                <TextBox Text="{Binding Port}"/>
            </pu:FormGroup>
            <pu:FormGroup Header="客户端端口" GroupName="client">
                <TextBox Text="{Binding ClientPort}"/>
            </pu:FormGroup>
            <Label>
                <Label.Style>
                    <Style TargetType="Label">
                        <Setter Property="Foreground" Value="White"/>
                        <Setter Property="Background" Value="Red"/>
                        <Setter Property="VerticalContentAlignment" Value="Center"/>
                        <Setter Property="HorizontalContentAlignment" Value="Center"/>
                        <Setter Property="Content" Value="未连接"/>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding IsConnected}" Value="True">
                                <Setter Property="Background" Value="Green"/>
                                <Setter Property="Content" Value="已连接"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </Label.Style>
            </Label>
            <Button Tag="Connect" Content="连接" Click="Button_Click"/>
            <GroupBox Header="接收设置">
                <StackPanel dp:StackPanelHelper.ChildSpace="10" Margin="0,10">
                    <StackPanel Orientation="Horizontal">
                        <RadioButton Content="字符串" IsChecked="{Binding ReceiveDataType,Converter={x:Static uic:RadioEnumConverter.Instance},ConverterParameter={x:Static local:DataType.String}}" GroupName="ReceiveType"/>
                        <RadioButton Content="Hex"  IsChecked="{Binding ReceiveDataType,Converter={x:Static uic:RadioEnumConverter.Instance},ConverterParameter={x:Static local:DataType.HEX}}" GroupName="ReceiveType"/>
                    </StackPanel>
                    <pu:FormGroup Header="编码格式">
                        <ComboBox x:Name="receiveEncodeInput" SelectedValue="{Binding ReceiveEncode}"/>
                        <pu:FormGroup.Style>
                            <Style TargetType="pu:FormGroup">
                                <Setter Property="IsEnabled" Value="False"/>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding ReceiveDataType}" Value="{x:Static local:DataType.String}">
                                        <Setter Property="IsEnabled" Value="True"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </pu:FormGroup.Style>
                    </pu:FormGroup>
                </StackPanel>
            </GroupBox>
        </StackPanel>
        <StackPanel Grid.Row="1" Grid.Column="0">
            <GroupBox Header="发送设置">
                <StackPanel dp:StackPanelHelper.ChildSpace="10" Margin="0,10">
                    <StackPanel Orientation="Horizontal">
                        <RadioButton Content="字符串" IsChecked="{Binding SendDataType,Converter={x:Static uic:RadioEnumConverter.Instance},ConverterParameter={x:Static local:DataType.String}}" GroupName="SendType"/>
                        <RadioButton Content="Hex"  IsChecked="{Binding SendDataType,Converter={x:Static uic:RadioEnumConverter.Instance},ConverterParameter={x:Static local:DataType.HEX}}" GroupName="SendType"/>
                    </StackPanel>
                    <pu:FormGroup Header="编码格式">
                        <ComboBox x:Name="sendEncodeInput" SelectedValue="{Binding SendEncode}"/>
                        <pu:FormGroup.Style>
                            <Style TargetType="pu:FormGroup">
                                <Setter Property="IsEnabled" Value="False"/>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding SendDataType}" Value="{x:Static local:DataType.String}">
                                        <Setter Property="IsEnabled" Value="True"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </pu:FormGroup.Style>
                    </pu:FormGroup>
                    <pu:FormGroup Header="分隔符">
                        <ComboBox ItemsSource="{Binding EndMarkSource}" SelectedValue="{Binding EndMark}" DisplayMemberPath="Name" SelectedValuePath="Type"/>
                        <pu:FormGroup.Style>
                            <Style TargetType="pu:FormGroup">
                                <Setter Property="IsEnabled" Value="False"/>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding SendDataType}" Value="{x:Static local:DataType.String}">
                                        <Setter Property="IsEnabled" Value="True"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </pu:FormGroup.Style>
                    </pu:FormGroup>
                    <pu:FormGroup Header="自定义分隔符">
                        <TextBox Text="{Binding CustomEndMark}"/>
                        <pu:FormGroup.Style>
                            <Style TargetType="pu:FormGroup">
                                <Setter Property="IsEnabled" Value="False"/>
                                <Style.Triggers>
                                    <MultiDataTrigger>
                                        <MultiDataTrigger.Conditions>
                                            <Condition Binding="{Binding SendDataType}" Value="{x:Static local:DataType.String}"/>
                                            <Condition Binding="{Binding EndMark}" Value="2"/>
                                        </MultiDataTrigger.Conditions>
                                        <Setter Property="IsEnabled" Value="True"/>
                                    </MultiDataTrigger>
                                </Style.Triggers>
                            </Style>
                        </pu:FormGroup.Style>
                    </pu:FormGroup>
                </StackPanel>
            </GroupBox>
        </StackPanel>
        <Grid Grid.Row="1" Grid.Column="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <GroupBox Header="数据发送">
                <TextBox x:Name="sendBox" TextWrapping="Wrap" VerticalContentAlignment="Top"
                         PreviewTextInput="TextBox_PreviewTextInput"/>
            </GroupBox>
            <Button Grid.Column="1" Padding="20" Content="发送" Tag="Send" Click="Button_Click"/>
        </Grid>
        <Grid Grid.Row="0" Grid.Column="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <GroupBox Header="数据接收">
                <RichTextBox x:Name="console" IsReadOnly="True"/>
            </GroupBox>
        </Grid>
    </Grid>
</UserControl>
Plugin.UICollection/TcpDebugger.xaml.cs
New file
@@ -0,0 +1,393 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
using TouchSocket.Core;
using TouchSocket.Sockets;
using UtilLib;
namespace Plugin.UICollection
{
    public enum DataType
    {
        String = 0,
        HEX = 1
    }
    public enum SupportEncode
    {
        GBK = 0,
        ASCII = 1,
        UTF8 = 2
    }
    public class EndMarkSource
    {
        public string Name { get; set; }
        public int Type { get; set; }
    }
    public class ByteStringConverter : IValueConverter
    {
        private TcpDebugger control { get; set; }
        public ByteStringConverter(TcpDebugger control)
        {
            this.control = control;
        }
        public static string GroupByTwo(string str)
        {
            if (string.IsNullOrEmpty(str))
                return string.Empty;
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < str.Length; i += 2)
            {
                if (i > 0)
                    result.Append(' ');
                int length = Math.Min(2, str.Length - i);
                result.Append(str.Substring(i, length));
            }
            return result.ToString();
        }
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            DataType sendType = control.SendDataType;
            if (sendType != DataType.HEX)
            {
                return value;
            }
            var str = value.ToString();
            return GroupByTwo(str);
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            DataType sendType = control.SendDataType;
            if (sendType != DataType.HEX)
            {
                return value;
            }
            return value.ToString().Replace(" ", "");
        }
    }
    /// <summary>
    /// TcpDebugger.xaml 的交互逻辑
    /// </summary>
    [ObservableObject]
    public partial class TcpDebugger : UserControl
    {
        [ObservableProperty]
        private string ip = "127.0.0.1";
        [ObservableProperty]
        private int port;
        [ObservableProperty]
        private int clientPort;
        [ObservableProperty]
        private DataType receiveDataType = DataType.String;
        [ObservableProperty]
        private DataType sendDataType = DataType.String;
        [ObservableProperty]
        private bool isConnected = false;
        [ObservableProperty]
        private string sendText;
        [ObservableProperty]
        private SupportEncode receiveEncode;
        [ObservableProperty]
        private SupportEncode sendEncode;
        [ObservableProperty]
        private ObservableCollection<EndMarkSource> endMarkSource = new ObservableCollection<EndMarkSource> {
            new EndMarkSource{ Name="无",Type=-1},
            new EndMarkSource{ Name="\\r",Type=0},
            new EndMarkSource{ Name="\\r\\n",Type=1},
            new EndMarkSource{ Name="自定义",Type=2},
        };
        [ObservableProperty]
        private int endMark;
        [ObservableProperty]
        private string customEndMark;
        private ByteStringConverter bsConverter;
        private TcpClient client;
        public TcpDebugger()
        {
            DataContext = this;
            InitializeComponent();
            sendEncodeInput.ItemsSource = Enum.GetValues(typeof(SupportEncode)).Cast<SupportEncode>();
            receiveEncodeInput.ItemsSource = Enum.GetValues(typeof(SupportEncode)).Cast<SupportEncode>();
            DataObject.AddPastingHandler(sendBox, OnPasting);
            bsConverter = new ByteStringConverter(this);
            BindingOperations.SetBinding(sendBox, TextBox.TextProperty, new Binding(nameof(SendText))
            {
                Source = this,
                Mode = BindingMode.TwoWay,
                UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
                Converter = bsConverter
            });
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var btn = (Button)sender;
            if (btn.Tag.ToString() == "Connect")
            {
                TryConnect();
                return;
            }
            if (btn.Tag.ToString() == "Send")
            {
                TrySend();
                return;
            }
        }
        private void TryConnect()
        {
            try
            {
                if (IsConnected) return;
                var config = new TouchSocketConfig().SetRemoteIPHost($"tcp://{Ip}:{Port}");
                client = new TcpClient();
                client.Received = TcpReceived;
                client.Connected = (sender, e) =>
                {
                    Dispatcher.Invoke(() =>
                    {
                        Add("连接", "连接成功", Brushes.Green);
                        IsConnected = true;
                    });
                    return EasyTask.CompletedTask;
                };
                client.Closed = (sender, e) =>
                {
                    Dispatcher.Invoke(() =>
                    {
                        Add("连接", "连接断开", Brushes.Red);
                        IsConnected = false;
                    });
                    return EasyTask.CompletedTask;
                };
                client.SetupAsync(config);
                client.ConnectAsync();
            }
            catch (Exception ex)
            {
                IsConnected = false;
                // throw ex;
            }
        }
        private Task TcpReceived(object sender, ReceivedDataEventArgs e)
        {
            try
            {
                var datas = e.Memory.Span.ToArray();
                string msg = null;
                if (ReceiveDataType == DataType.HEX)
                {
                    msg = HexUtil.ByteArrayToHex(datas);
                }
                else
                {
                    if (ReceiveEncode == SupportEncode.GBK)
                    {
                        msg = Encoding.Default.GetString(datas);
                    }
                    else if (ReceiveEncode == SupportEncode.ASCII)
                    {
                        msg = Encoding.ASCII.GetString(datas);
                    }
                    else
                    {
                        msg = Encoding.UTF8.GetString(datas);
                    }
                }
                Add("接收", msg, Brushes.Green);
            }
            catch (Exception ex)
            {
                //log.Error($"TcpReceived Error.", ex);
            }
            return EasyTask.CompletedTask;
        }
        private byte[] GetBytes(out string msg)
        {
            msg = null;
            if (string.IsNullOrEmpty(SendText))
            {
                return null;
            }
            if (SendDataType == DataType.String)
            {
                msg = SendText + GetEndMark();
                if (SendEncode == SupportEncode.GBK)
                {
                    return Encoding.Default.GetBytes(msg);
                }
                if (SendEncode == SupportEncode.ASCII)
                {
                    return Encoding.ASCII.GetBytes(msg);
                }
                if (SendEncode == SupportEncode.UTF8)
                {
                    return Encoding.UTF8.GetBytes(msg);
                }
                return null;
            }
            msg = SendText;
            if (msg.Length % 2 != 0)
            {
                msg = msg.Insert(msg.Length - 1, "0");
            }
            msg = ByteStringConverter.GroupByTwo(msg);
            return HexUtil.HexToByteArray(msg);
        }
        private string GetEndMark()
        {
            if (EndMark == -1)
            {
                return string.Empty;
            }
            if (EndMark == 0)
            {
                return "\r";
            }
            if (EndMark == 1)
            {
                return "\r\n";
            }
            if (EndMark == 2)
            {
                return CustomEndMark;
            }
            return string.Empty;
        }
        public void TrySend()
        {
            if (!IsConnected)
            {
                MessageBox.Show("请先连接", "提示");
                return;
            }
            var datas = GetBytes(out string msg);
            if (datas != null)
            {
                client.SendAsync(datas).Wait();
                Add("发送", msg, Brushes.Blue);
                // SendText = string.Empty;
            }
        }
        public void Add(string type, string text, Brush color)
        {
            Dispatcher.BeginInvoke(() =>
            {
                string time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                Run run = new Run();
                run.Text = $"{time} {type}\r\n";
                run.Foreground = Brushes.Gray;
                Paragraph paragraph = new Paragraph(run)
                {
                    LineHeight = 1
                };
                run = new Run();
                run.Text = $"{text}\r\n";
                run.Foreground = color;
                paragraph.Inlines.Add(run);
                //也可以插入到最后面
                console.Document.Blocks.InsertAfter(console.Document.Blocks.LastBlock, paragraph);
                //插入到最前面
                //rtxtLog.Document.Blocks.Add(paragraph);
                // 滚动至最后行
                //rtxtLog.ScrollToEnd();
                //Console.ScrollToHome();
                console.ScrollToEnd();
                // 删除
                //if (Console.Document.Blocks.Count > 10000)
                //{
                //    for (int i = 1000; i < Console.Document.Blocks.Count; i++)
                //    {
                //        _ = Console.Document.Blocks.Remove(Console.Document.Blocks.FirstBlock);
                //    }
                //}
            }, System.Windows.Threading.DispatcherPriority.Normal);
        }
        private static readonly Regex HexRegex = new Regex("^[0-9A-Fa-f]+$");
        private void TextBox_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
        {
            if (SendDataType != DataType.HEX)
            {
                return;
            }
            var tb = (TextBox)sender;
            string segment = e.Text;
            if (!HexRegex.IsMatch(segment))
            {
                tb.Text = tb.Text.Replace(segment, "");
                tb.CaretIndex = tb.Text.Length;
                e.Handled = true;
                return;
            }
            // tb.Text = tb.Text.ToUpper();
            base.OnPreviewTextInput(e);
        }
        private void OnPasting(object sender, DataObjectPastingEventArgs e)
        {
            if (SendDataType != DataType.HEX)
            {
                return;
            }
            if (e.DataObject.GetDataPresent(typeof(string)))
            {
                string text = (string)e.DataObject.GetData(typeof(string));
                text = text.Replace(" ", "");
                if (!HexRegex.IsMatch(text))
                {
                    e.CancelCommand();
                }
            }
        }
    }
}
SimpleUtil/HMI.Simple.Util/Parameter.cs
@@ -5,6 +5,7 @@
        public string Id { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }
        public bool IsArray { get; set; }
        public string Value { get; set; }
        public string Unit { get; set; }
        public string Description { get; set; }
SimpleUtil/HMI.Simple.Util/ParameterUtil.cs
@@ -59,21 +59,72 @@
        private static void WriteParameterData(SequenceContext context, ParameterData data)
        {
            string prefix = "SystemSetting";
            if (data.Type == "Bool")
            if (data.IsArray)
            {
                context.StationGlobals.SetValBoolean($"{prefix}.{data.Name}", 1, bool.Parse(data.Value));
            }
            else if (data.Type == "Int")
            {
                context.StationGlobals.SetValNumber($"{prefix}.{data.Name}", 1, int.Parse(data.Value));
            }
            else if (data.Type == "Double")
            {
                context.StationGlobals.SetValNumber($"{prefix}.{data.Name}", 1, double.Parse(data.Value));
                PropertyValueTypes type;
                if (data.Type == "Bool")
                {
                    type = PropertyValueTypes.PropValType_Boolean;
                }
                else if (data.Type == "Int")
                {
                    type = PropertyValueTypes.PropValType_Number;
                }
                else if (data.Type == "Double")
                {
                    type = PropertyValueTypes.PropValType_Number;
                }
                else
                {
                    type = PropertyValueTypes.PropValType_String;
                }
                string[] arr = data.Value?.Split(',') ?? new string[0];
                if (arr.Length > 0)
                {
                    context.StationGlobals.DeleteSubProperty($"{prefix}.{data.Name}", 2);
                    context.StationGlobals.NewSubProperty($"{prefix}.{data.Name}", type, true, null, 1);
                    context.StationGlobals.GetPropertyObject($"{prefix}.{data.Name}", 0).SetNumElements(arr.Length);
                    for (int i = 0; i < arr.Length; i++)
                    {
                        if (type == PropertyValueTypes.PropValType_String)
                        {
                            context.StationGlobals.SetValString($"{prefix}.{data.Name}[{i}]", 0, arr[i]);
                        }
                        if (data.Type == "Int")
                        {
                            context.StationGlobals.SetValNumber($"{prefix}.{data.Name}[{i}]", 0, int.Parse(arr[i]));
                        }
                        if (data.Type == "Double")
                        {
                            context.StationGlobals.SetValNumber($"{prefix}.{data.Name}[{i}]", 0, double.Parse(arr[i]));
                        }
                        if (data.Type == "Bool")
                        {
                            context.StationGlobals.SetValBoolean($"{prefix}.{data.Name}[{i}]", 0, bool.Parse(arr[i]));
                        }
                    }
                }
            }
            else
            {
                context.StationGlobals.SetValString($"{prefix}.{data.Name}", 1, data.Value);
                if (data.Type == "Bool")
                {
                    context.StationGlobals.SetValBoolean($"{prefix}.{data.Name}", 1, bool.Parse(data.Value));
                }
                else if (data.Type == "Int")
                {
                    context.StationGlobals.SetValNumber($"{prefix}.{data.Name}", 1, int.Parse(data.Value));
                }
                else if (data.Type == "Double")
                {
                    context.StationGlobals.SetValNumber($"{prefix}.{data.Name}", 1, double.Parse(data.Value));
                }
                else
                {
                    context.StationGlobals.SetValString($"{prefix}.{data.Name}", 1, data.Value);
                }
            }
        }
SimpleUtil/HMI.Simple.Util/ProductUtil.cs
@@ -10,22 +10,86 @@
        private static void WriteParameterData(object instance, ParameterData data)
        {
            SequenceContext context = (SequenceContext)instance;
            if (data.Type == "Bool")
            string name = $"DUTSetting.{data.Name}";
            if (data.IsArray)
            {
                context.FileGlobals.SetValBoolean($"DUTSetting.{data.Name}", 1, bool.Parse(data.Value));
            }
            else if (data.Type == "Int")
            {
                context.FileGlobals.SetValNumber($"DUTSetting.{data.Name}", 1, int.Parse(data.Value));
            }
            else if (data.Type == "Double")
            {
                context.FileGlobals.SetValNumber($"DUTSetting.{data.Name}", 1, double.Parse(data.Value));
                PropertyValueTypes type;
                if (data.Type == "Bool")
                {
                    type = PropertyValueTypes.PropValType_Boolean;
                }
                else if (data.Type == "Int")
                {
                    type = PropertyValueTypes.PropValType_Number;
                }
                else if (data.Type == "Double")
                {
                    type = PropertyValueTypes.PropValType_Number;
                }
                else
                {
                    type = PropertyValueTypes.PropValType_String;
                }
                string[] arr = data.Value?.Split(',') ?? new string[0];
                if (arr.Length > 0)
                {
                    context.FileGlobals.DeleteSubProperty(name, 2);
                    context.FileGlobals.NewSubProperty(name, type, true, null, 1);
                    context.FileGlobals.GetPropertyObject(name, 0).SetNumElements(arr.Length);
                    for (int i = 0; i < arr.Length; i++)
                    {
                        if (type == PropertyValueTypes.PropValType_String)
                        {
                            context.FileGlobals.SetValString($"{name}[{i}]", 0, arr[i]);
                        }
                        if (data.Type == "Int")
                        {
                            context.FileGlobals.SetValNumber($"{name}[{i}]", 0, int.Parse(arr[i]));
                        }
                        if (data.Type == "Double")
                        {
                            context.FileGlobals.SetValNumber($"{name}[{i}]", 0, double.Parse(arr[i]));
                        }
                        if (data.Type == "Bool")
                        {
                            context.FileGlobals.SetValBoolean($"{name}[{i}]", 0, bool.Parse(arr[i]));
                        }
                    }
                }
            }
            else
            {
                context.FileGlobals.SetValString($"DUTSetting.{data.Name}", 1, data.Value);
                if (data.Type == "Bool")
                {
                    context.FileGlobals.SetValBoolean(name, 1, bool.Parse(data.Value));
                }
                else if (data.Type == "Int")
                {
                    context.FileGlobals.SetValNumber(name, 1, int.Parse(data.Value));
                }
                else if (data.Type == "Double")
                {
                    context.FileGlobals.SetValNumber(name, 1, double.Parse(data.Value));
                }
                else
                {
                    context.FileGlobals.SetValString(name, 1, data.Value);
                }
            }
        }
        private static string GetDataFilePath()
        {
            string path = ParameterUtil.GetCommonParameterValue("ShareConfig", out _);
            if (path == null)
            {
                path = Path.Combine(RegUtil.GetLocaltion(), "Config");
            }
            return path;
        }
        /// <summary>
@@ -40,8 +104,7 @@
            errMsg = string.Empty;
            try
            {
                string dir = RegUtil.GetLocaltion();
                string path = Path.Combine(dir, "Config", "Product", "Parameter", $"Parameter_{model}.data");
                string path = Path.Combine(GetDataFilePath(), "Product", "Parameter", $"Parameter_{model}.data");
                return WriteConfigToContextByPath(path, instance, out errMsg);
            }
            catch (Exception ex)
SimpleUtil/HMI.Simple.Util/Properties/AssemblyInfo.cs
@@ -1,5 +1,4 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 有关程序集的一般信息由以下
@@ -29,5 +28,5 @@
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.2")]
[assembly: AssemblyVersion("1.0.0.3")]
[assembly: AssemblyFileVersion("1.0.0.3")]
Util.ManualDebug/AssemblyInfo.cs
New file
@@ -0,0 +1,10 @@
using System.Windows;
[assembly:ThemeInfo(
    ResourceDictionaryLocation.None,            //where theme specific resource dictionaries are located
                                                //(used if a resource is not found in the page,
                                                // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly   //where the generic resource dictionary is located
                                                //(used if a resource is not found in the page,
                                                // app, or any theme specific resource dictionaries)
)]
Util.ManualDebug/ManualDebug.xaml
New file
@@ -0,0 +1,133 @@
<UserControl x:Class="Util.SB.ManualDebug"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Util.SB"
        mc:Ignorable="d"
        x:Name="self"
        MinHeight="400" MinWidth="500">
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/Util.ManualDebug;component/ManualDebugStyle.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>
    <Grid>
        <Grid Margin="10">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="5"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="5"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <WindowsFormsHost x:Name="host" Visibility="Collapsed"></WindowsFormsHost>
            <!-- 按钮和状态区域 -->
            <Border Grid.Row="0" Background="{StaticResource TsPanelBrush}"
                BorderBrush="{StaticResource BorderBrush}"
                BorderThickness="1" CornerRadius="4" Padding="15">
                <StackPanel>
                    <TextBlock Text="手动调试控制" FontSize="14" FontWeight="SemiBold"
                           Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,15"/>
                    <WrapPanel x:Name="btnPanel">
                    </WrapPanel>
                </StackPanel>
            </Border>
            <!-- 分隔条1 -->
            <GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch"
                      Background="Transparent" Cursor="SizeNS"/>
            <!-- TX/RX 数据区域 -->
            <Border Grid.Row="2" Background="{StaticResource TsPanelBrush}"
                BorderBrush="{StaticResource BorderBrush}"
                BorderThickness="1" CornerRadius="4" Padding="10">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinitions>
                    <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,10">
                        <TextBlock Text="测试数据" FontSize="14" FontWeight="SemiBold"
                               Foreground="{StaticResource PrimaryBrush}" VerticalAlignment="Center"/>
                        <Button Content="清空" Style="{StaticResource ActionButtonStyle}"
                            Margin="15,0,0,0" Click="OnClearClick"/>
                    </StackPanel>
                    <DataGrid x:Name="DataGridComm" Grid.Row="1"
                          ItemsSource="{Binding TestData,ElementName=self}">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="测试项" Binding="{Binding Name}" Width="120" IsReadOnly="True"/>
                            <DataGridTextColumn Header="收" Binding="{Binding Receive}" Width="*" IsReadOnly="True"/>
                            <DataGridTextColumn Header="发" Binding="{Binding Send}" Width="*" IsReadOnly="True"/>
                            <DataGridTextColumn Header="单位" Binding="{Binding Unit}" Width="60" IsReadOnly="True"/>
                            <DataGridTextColumn Header="上限" Binding="{Binding UpperLimit}" Width="80" IsReadOnly="True"/>
                            <DataGridTextColumn Header="下限" Binding="{Binding LowerLimit}" Width="80" IsReadOnly="True"/>
                            <DataGridTextColumn Header="结果" Binding="{Binding Result}" Width="70" IsReadOnly="True"/>
                            <DataGridTextColumn Header="时间" Binding="{Binding Time}" Width="90" IsReadOnly="True"/>
                        </DataGrid.Columns>
                        <DataGrid.RowStyle>
                            <Style TargetType="DataGridRow">
                                <Style.Triggers>
                                    <DataTrigger Value="True">
                                        <DataTrigger.Binding>
                                            <MultiBinding Converter="{x:Static local:ResultFailedConverter.Instance}">
                                                <Binding Path="Result"/>
                                            </MultiBinding>
                                        </DataTrigger.Binding>
                                        <Setter Property="Background" Value="#FFE0E0"/>
                                        <Setter Property="Foreground" Value="#C53030"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </DataGrid.RowStyle>
                    </DataGrid>
                </Grid>
            </Border>
            <!-- 分隔条2 -->
            <GridSplitter Grid.Row="3" Height="5" HorizontalAlignment="Stretch"
                      Background="Transparent" Cursor="SizeNS"/>
            <!-- 日志区域 -->
            <Border Grid.Row="4" Background="{StaticResource TsPanelBrush}"
                BorderBrush="{StaticResource BorderBrush}"
                BorderThickness="1" CornerRadius="4" Padding="10">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinitions>
                    <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,8">
                        <TextBlock Text="日志" FontSize="14" FontWeight="SemiBold"
                               Foreground="{StaticResource PrimaryBrush}" VerticalAlignment="Center"/>
                        <Button Content="清空日志" Style="{StaticResource ActionButtonStyle}"
                            Margin="15,0,0,0" Click="OnClearLogClick"/>
                    </StackPanel>
                    <RichTextBox x:Name="LogRichTextBox" Grid.Row="1"
                             IsReadOnly="True"
                             VerticalScrollBarVisibility="Auto"
                             FontFamily="Consolas"
                             FontSize="11"
                             BorderThickness="1"
                             BorderBrush="Black"
                             Padding="4"/>
                </Grid>
            </Border>
        </Grid>
        <Border Background="#AAFFFFFF" x:Name="mask">
            <TextBlock Text="初始化中..."
                       FontSize="16" FontWeight="Bold" Foreground="{StaticResource PrimaryBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
    </Grid>
</UserControl>
Util.ManualDebug/ManualDebug.xaml.cs
New file
@@ -0,0 +1,467 @@
using Plugin.TsSupport;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
namespace Util.SB
{
    public class BtnConfig
    {
        public string Title { get; set; }
        public string Name { get; set; }
    }
    // 结果失败判断转换器(支持不区分大小写)
    public class ResultFailedConverter : IMultiValueConverter
    {
        public static readonly ResultFailedConverter Instance = new ResultFailedConverter();
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values.Length > 0 && values[0] is string)
            {
                string result = (string)values[0];
                string upper = result.ToUpperInvariant();
                return upper == "NG" || upper == "FAIL";
            }
            return false;
        }
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    // 日志级别枚举
    public enum LogLevel
    {
        Debug = 0,
        Info = 1,
        Warn = 2,
        Error = 3
    }
    public partial class ManualDebug : UserControl
    {
        public static readonly DependencyProperty OvenNameProperty = DependencyProperty.Register(
                            "OvenName",
                            typeof(string),
                            typeof(ManualDebug),
                            new PropertyMetadata(""));
        public static readonly DependencyProperty CellNameProperty = DependencyProperty.Register(
                            "CellName",
                            typeof(string),
                            typeof(ManualDebug),
                            new PropertyMetadata(""));
        public static readonly DependencyProperty OvenIndexProperty = DependencyProperty.Register(
                            "OvenIndex",
                            typeof(int),
                            typeof(ManualDebug),
                            new PropertyMetadata(0));
        public static readonly DependencyProperty CellIndexProperty = DependencyProperty.Register(
                            "CellIndex",
                            typeof(int),
                            typeof(ManualDebug),
                            new PropertyMetadata(0));
        public string OvenName
        {
            get { return (string)GetValue(OvenNameProperty); }
            set
            {
                SetValue(OvenNameProperty, value);
            }
        }
        public string CellName
        {
            get { return (string)GetValue(CellNameProperty); }
            set { SetValue(CellNameProperty, value); }
        }
        public int OvenIndex
        {
            get { return (int)GetValue(OvenIndexProperty); }
            set { SetValue(OvenIndexProperty, value); }
        }
        public int CellIndex
        {
            get { return (int)GetValue(CellIndexProperty); }
            set { SetValue(CellIndexProperty, value); }
        }
        public ObservableCollection<TestDataItem> TestData { get; private set; } = new ObservableCollection<TestDataItem>();
        private Dictionary<string, StatusBtn> btnCache = new Dictionary<string, StatusBtn>();
        // 日志级别颜色
        private readonly SolidColorBrush _debugBrush;
        private readonly SolidColorBrush _infoBrush;
        private readonly SolidColorBrush _warnBrush;
        private readonly SolidColorBrush _errorBrush;
        private ITestExecutionWrapper executionWrapper;
        public string seqPath { get; set; }
        public string modelPath { get; set; } = "C:\\Program Files\\National Instruments\\TestStand 2020\\Components\\Models\\TestStandModels\\ParallelModel.seq";
        public ManualDebug()
        {
            _debugBrush = new SolidColorBrush(Color.FromRgb(0x71, 0x80, 0x8A)); // 灰色
            _infoBrush = new SolidColorBrush(Colors.Black);
            _warnBrush = new SolidColorBrush(Color.FromRgb(0xD6, 0x9E, 0x2E));  // 橙黄色
            _errorBrush = new SolidColorBrush(Color.FromRgb(0xC5, 0x30, 0x30));   // 红色
            this.Loaded += ManualDebug_Loaded;
            InitializeComponent();
            RefreshButtons(null);
        }
        private void ManualDebug_Loaded(object sender, RoutedEventArgs e)
        {
            Task.Run(() =>
            {
                System.Threading.Thread.Sleep(500);
                Dispatcher.BeginInvoke(() =>
                {
                    InitWrapper();
                    StartSeq();
                    mask.Visibility = Visibility.Collapsed;
                });
            });
        }
        public void RefreshButtons(List<BtnConfig> settings)
        {
            if (settings == null) return;
            for (int i = 0; i < settings.Count; i++) {
                var setting = settings[i];
                var margin = new Thickness(0, 0, 20, 0);
                if (i == settings.Count - 1)
                {
                    margin = new Thickness(0);
                }
                var btn = new StatusBtn
                {
                    Title = setting.Title,
                    Tag = setting,
                    Margin = margin
                };
                btn.BtnClick += Btn_BtnClick;
                btnCache[setting.Name] = btn;
                btnPanel.Children.Add(btn);
            }
        }
        public void StartSeq()
        {
            try
            {
                executionWrapper?.StartSequence(seqPath, modelPath);
            }
            catch (Exception ex)
            {
                AppendLog(LogLevel.Error, $"启动测试序列失败 {ex.Message} {ex.StackTrace}");
            }
        }
        private void InitWrapper()
        {
            try
            {
                var wrapper = new TeststandWrapper();
                var AxApplicationMgr = wrapper.AxApplicationMgr;
                ((System.ComponentModel.ISupportInitialize)(AxApplicationMgr)).BeginInit();
                host.Child = AxApplicationMgr;
                ((System.ComponentModel.ISupportInitialize)(AxApplicationMgr)).EndInit();
                wrapper.Init();
                executionWrapper = wrapper;
                wrapper.UIMessageEvent += Wrapper_UIMessageEvent;
            }
            catch (Exception ex)
            {
                AppendLog(LogLevel.Error, $"初始化Teststand {ex.Message} {ex.StackTrace}");
            }
        }
        private void Wrapper_UIMessageEvent(object sender, UIMessageEvent e)
        {
            try
            {
                if (e.EventCode == 10000)
                {
                    executionWrapper?.SetContext(e.Data);
                    return;
                }
                if (e.EventCode == 10001)
                {
                    string[] arr = e.StrData.Split(',');
                    Dispatcher.BeginInvoke(() =>
                    {
                        AddTestData(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], arr[6], arr[7]);
                    });
                    return;
                }
                if (e.EventCode == 10002)
                {
                    string[] arr = e.StrData.Split(',');
                    LogLevel level = (LogLevel)int.Parse(arr[0]);
                    Dispatcher.BeginInvoke(() =>
                    {
                        AppendLog(level, arr[1]);
                    });
                    return;
                }
                if (e.EventCode == 10003)
                {
                    string[] arr = e.StrData.Split(',');
                    DebugStatus status = (DebugStatus)int.Parse(arr[1]);
                    Dispatcher.BeginInvoke(() =>
                    {
                        SetStatus(arr[0], status);
                    });
                    return;
                }
            }
            catch (Exception ex)
            {
                AppendLog(LogLevel.Error, $"处理UI信息失败 {ex.Message} {ex.StackTrace}");
            }
        }
        private void Btn_BtnClick(object sender, RoutedEventArgs e)
        {
            var btn = (StatusBtn)sender;
            BtnConfig config = (BtnConfig)btn.Tag;
            StartSingleTest(config.Name);
        }
        private void StartSingleTest(string name)
        {
            try
            {
                string key = $"Running.OvenName";
                executionWrapper?.SetFileGlobalValue(key, 1, OvenName);
                key = $"Running.CellName";
                executionWrapper?.SetFileGlobalValue(key, 1, CellName);
                key = $"Running.OvenIndex";
                executionWrapper?.SetFileGlobalValue(key, 1, OvenIndex);
                key = $"Running.CellIndex";
                executionWrapper?.SetFileGlobalValue(key, 1, CellIndex);
                key = $"Running.ActionName";
                executionWrapper?.SetFileGlobalValue(key, 1, name);
            }
            catch (Exception ex)
            {
                AppendLog(LogLevel.Error, $"启动功能失败 {ex.Message} {ex.StackTrace}");
            }
        }
        // 设置状态显示
        private void SetStatus(string labelName, DebugStatus status)
        {
            if (btnCache.ContainsKey(labelName))
            {
                var btn = btnCache[labelName];
                btn.Status = status;
            }
        }
        // 重置状态显示
        private void ResetStatus(string labelName)
        {
            SetStatus(labelName, DebugStatus.Idle);
        }
        // 设置测试中状态并禁用按钮
        private void SetTestingStatus(string labelName, Button button)
        {
            SetStatus(labelName, DebugStatus.Testing);
            button.IsEnabled = false;
        }
        private void OnClearClick(object sender, RoutedEventArgs e)
        {
            TestData.Clear();
        }
        private void OnClearLogClick(object sender, RoutedEventArgs e)
        {
            LogRichTextBox.Document.Blocks.Clear();
        }
        private void AddTestData(string name, string send, string receive, string unit, string upper, string lower, string result, string time)
        {
            TestData.Add(new TestDataItem
            {
                Name = name,
                Send = send,
                Receive = receive,
                Unit = unit,
                UpperLimit = upper,
                LowerLimit = lower,
                Result = result,
                Time = time
            });
            if (DataGridComm.Items.Count > 0)
            {
                DataGridComm.ScrollIntoView(DataGridComm.Items[DataGridComm.Items.Count - 1]);
            }
        }
        // 获取日志级别对应的颜色
        private Brush GetLogLevelBrush(LogLevel level)
        {
            switch (level)
            {
                case LogLevel.Debug:
                    return _debugBrush;
                case LogLevel.Info:
                    return _infoBrush;
                case LogLevel.Warn:
                    return _warnBrush;
                case LogLevel.Error:
                    return _errorBrush;
                default:
                    return _infoBrush;
            }
        }
        // 添加带级别的日志
        private void AppendLog(LogLevel level, string message)
        {
            Dispatcher.BeginInvoke(() =>
            {
                string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
                string levelStr = level.ToString().ToUpper();
                Paragraph? paragraph = LogRichTextBox.Document.Blocks.LastBlock as Paragraph;
                if (paragraph == null)
                {
                    paragraph = new Paragraph();
                    LogRichTextBox.Document.Blocks.Add(paragraph);
                }
                // 时间戳(灰色)
                Run timestampRun = new Run("[" + timestamp + "] ") { Foreground = _debugBrush };
                paragraph.Inlines.Add(timestampRun);
                // 级别标签
                Brush levelBrush = GetLogLevelBrush(level);
                Run levelRun = new Run("[" + levelStr + "] ") { Foreground = levelBrush, FontWeight = FontWeights.SemiBold };
                paragraph.Inlines.Add(levelRun);
                // 消息内容
                Run messageRun = new Run(message + "\n") { Foreground = levelBrush };
                paragraph.Inlines.Add(messageRun);
                LogRichTextBox.ScrollToEnd();
            });
        }
        // 兼容旧调用方式,默认 Info 级别
        private void AppendLog(string message)
        {
            AppendLog(LogLevel.Info, message);
        }
        public void Stop()
        {
            try
            {
                executionWrapper?.SetFileGlobalValue("Running.StopTest", 1, true);
                executionWrapper?.WaitForExit();
            }
            catch (Exception ex)
            {
            }
        }
    }
    public class TestDataItem : INotifyPropertyChanged
    {
        private string _name = "";
        private string _receive = "";
        private string _send = "";
        private string _unit = "";
        private string _upperLimit = "";
        private string _lowerLimit = "";
        private string _result = "";
        private string _time = "";
        public string Name
        {
            get { return _name; }
            set { _name = value; OnPropertyChanged("Name"); }
        }
        public string Receive
        {
            get { return _receive; }
            set { _receive = value; OnPropertyChanged("Receive"); }
        }
        public string Send
        {
            get { return _send; }
            set { _send = value; OnPropertyChanged("Send"); }
        }
        public string Unit
        {
            get { return _unit; }
            set { _unit = value; OnPropertyChanged("Unit"); }
        }
        public string UpperLimit
        {
            get { return _upperLimit; }
            set { _upperLimit = value; OnPropertyChanged("UpperLimit"); }
        }
        public string LowerLimit
        {
            get { return _lowerLimit; }
            set { _lowerLimit = value; OnPropertyChanged("LowerLimit"); }
        }
        public string Result
        {
            get { return _result; }
            set { _result = value; OnPropertyChanged("Result"); }
        }
        public string Time
        {
            get { return _time; }
            set { _time = value; OnPropertyChanged("Time"); }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
Util.ManualDebug/ManualDebugStyle.xaml
New file
@@ -0,0 +1,130 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <!-- 颜色定义 -->
    <SolidColorBrush x:Key="TsBgBrush" Color="#F0F2F5"/>
    <SolidColorBrush x:Key="TsPanelBrush" Color="#FFFFFF"/>
    <SolidColorBrush x:Key="BorderBrush" Color="#A0AEC0"/>
    <SolidColorBrush x:Key="PrimaryBrush" Color="#2E5C8A"/>
    <!-- 状态颜色画刷 -->
    <SolidColorBrush x:Key="StatusOKBrush" Color="#2D8659"/>
    <SolidColorBrush x:Key="StatusNGBrush" Color="#C53030"/>
    <SolidColorBrush x:Key="StatusTestingBrush" Color="#3182CE"/>
    <SolidColorBrush x:Key="StatusDefaultBrush" Color="#E0E0E0"/>
    <SolidColorBrush x:Key="BorderDarkBrush" Color="#718096"/>
    <!-- ActionButtonStyle 按钮样式 -->
    <Style x:Key="ActionButtonStyle" TargetType="Button">
        <Setter Property="Height" Value="22"/>
        <Setter Property="Padding" Value="8,0"/>
        <Setter Property="Background" Value="White"/>
        <Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="FontSize" Value="11"/>
        <Setter Property="Cursor" Value="Hand"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            CornerRadius="2"
                            Padding="{TemplateBinding Padding}">
                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Background" Value="#F0F0F0"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <!-- ManualDebug 窗口专用按钮样式 -->
    <Style x:Key="DebugButtonStyle" TargetType="Button">
        <Setter Property="Width" Value="100"/>
        <Setter Property="Height" Value="32"/>
        <Setter Property="Background" Value="White"/>
        <Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="FontSize" Value="11"/>
        <Setter Property="Cursor" Value="Hand"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            CornerRadius="4"
                            Padding="{TemplateBinding Padding}">
                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Background" Value="#E8E8E8"/>
                        </Trigger>
                        <Trigger Property="IsPressed" Value="True">
                            <Setter Property="Background" Value="#D0D0D0"/>
                        </Trigger>
                        <Trigger Property="IsEnabled" Value="False">
                            <Setter Property="Background" Value="LightGray"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <!-- DataGrid样式 -->
    <Style TargetType="DataGrid">
        <Setter Property="AutoGenerateColumns" Value="False"/>
        <Setter Property="CanUserAddRows" Value="False"/>
        <Setter Property="CanUserDeleteRows" Value="False"/>
        <Setter Property="HeadersVisibility" Value="Column"/>
        <Setter Property="GridLinesVisibility" Value="All"/>
        <Setter Property="HorizontalGridLinesBrush" Value="{StaticResource BorderBrush}"/>
        <Setter Property="VerticalGridLinesBrush" Value="{StaticResource BorderBrush}"/>
        <Setter Property="BorderBrush" Value="{StaticResource BorderDarkBrush}"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Background" Value="White"/>
        <Setter Property="RowBackground" Value="White"/>
        <Setter Property="AlternatingRowBackground" Value="#F5F5F5"/>
        <Setter Property="FontSize" Value="11"/>
    </Style>
    <Style TargetType="DataGridColumnHeader">
        <Setter Property="Background" Value="#F5F5F5"/>
        <Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
        <Setter Property="BorderThickness" Value="0,0,1,1"/>
        <Setter Property="Padding" Value="4"/>
        <Setter Property="Height" Value="26"/>
        <Setter Property="FontWeight" Value="Normal"/>
    </Style>
    <Style TargetType="DataGridCell">
        <Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
        <Setter Property="BorderThickness" Value="0"/>
        <Setter Property="Padding" Value="2,4"/>
        <Style.Triggers>
            <Trigger Property="IsSelected" Value="True">
                <Setter Property="Background" Value="#BBDEFB"/>
                <Setter Property="Foreground" Value="Black"/>
            </Trigger>
        </Style.Triggers>
    </Style>
    <Style TargetType="DataGridRow">
        <Style.Triggers>
            <Trigger Property="IsSelected" Value="True">
                <Setter Property="Background" Value="#BBDEFB"/>
            </Trigger>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background" Value="#E3F2FD"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>
Util.ManualDebug/StatusBtn.xaml
New file
@@ -0,0 +1,75 @@
<UserControl x:Class="Util.SB.StatusBtn"
             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:Util.SB"
             mc:Ignorable="d"
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/Util.ManualDebug;component/ManualDebugStyle.xaml"/>
            </ResourceDictionary.MergedDictionaries>
            <Style x:Key="StatusTextStyle" TargetType="TextBlock">
                <Setter Property="HorizontalAlignment" Value="Center"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
                <Setter Property="FontWeight" Value="SemiBold"/>
                <Setter Property="FontSize" Value="12"/>
                <Setter Property="Text" Value="-"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Status}" Value="{x:Static local:DebugStatus.NG}">
                        <Setter Property="Text" Value="NG"/>
                        <Setter Property="Foreground" Value="#FFF"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Status}" Value="{x:Static local:DebugStatus.OK}">
                        <Setter Property="Text" Value="OK"/>
                        <Setter Property="Foreground" Value="#FFF"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Status}" Value="{x:Static local:DebugStatus.Testing}">
                        <Setter Property="Text" Value="TESTING"/>
                        <Setter Property="Foreground" Value="#FFF"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
            <!-- 状态边框样式 -->
            <Style x:Key="StatusBorderStyle" TargetType="Border">
                <Setter Property="Background" Value="{StaticResource StatusDefaultBrush}"/>
                <Setter Property="CornerRadius" Value="2"/>
                <Setter Property="HorizontalAlignment" Value="Center"/>
                <Setter Property="Width" Value="100"/>
                <Setter Property="Height" Value="24"/>
                <Setter Property="Margin" Value="0,5,0,0"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Status}" Value="{x:Static local:DebugStatus.NG}">
                        <Setter Property="Background" Value="{StaticResource StatusNGBrush}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Status}" Value="{x:Static local:DebugStatus.OK}">
                        <Setter Property="Background" Value="{StaticResource StatusOKBrush}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Status}" Value="{x:Static local:DebugStatus.Testing}">
                        <Setter Property="Background" Value="{StaticResource StatusTestingBrush}"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
            <Style x:Key="btnStyle" TargetType="Button" BasedOn="{StaticResource DebugButtonStyle}">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Status}" Value="{x:Static local:DebugStatus.Testing}">
                        <Setter Property="IsEnabled" Value="False"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ResourceDictionary>
    </UserControl.Resources>
    <StackPanel>
        <Button x:Name="btn" Content="{Binding Title}" Style="{StaticResource btnStyle}" Click="Button_Click"/>
        <Border Style="{StaticResource StatusBorderStyle}">
            <TextBlock Style="{StaticResource StatusTextStyle}"/>
        </Border>
    </StackPanel>
</UserControl>
Util.ManualDebug/StatusBtn.xaml.cs
New file
@@ -0,0 +1,40 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace Util.SB
{
    // 状态枚举
    public enum DebugStatus
    {
        Idle = 0,    // 空闲
        Testing = 1, // 测试中
        OK = 2,      // 通过
        NG = 3       // 失败
    }
    /// <summary>
    /// StatusBtn.xaml 的交互逻辑
    /// </summary>
    [ObservableObject]
    public partial class StatusBtn : UserControl
    {
        public event RoutedEventHandler BtnClick;
        public StatusBtn()
        {
            DataContext = this;
            InitializeComponent();
        }
        [ObservableProperty]
        private string title = string.Empty;
        [ObservableProperty]
        private DebugStatus status = DebugStatus.Idle;
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            BtnClick?.Invoke(this, e);
        }
    }
}
Util.ManualDebug/Util.ManualDebug.csproj
New file
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
      <TargetFramework>net472</TargetFramework>
      <Nullable>enable</Nullable>
      <LangVersion>10.0</LangVersion>
      <ImplicitUsings>enable</ImplicitUsings>
      <UseWPF>true</UseWPF>
      <Platforms>AnyCPU;x64;x86</Platforms>
      <FileVersion>1.0.0.0</FileVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
    <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\Plugin.TsSupport\Plugin.TsSupport.csproj" />
  </ItemGroup>
  <ItemGroup>
    <Reference Include="NationalInstruments.TestStand.Interop.API">
      <HintPath>..\Libs\NationalInstruments.TestStand.Interop.API.dll</HintPath>
    </Reference>
    <Reference Include="NationalInstruments.TestStand.Interop.UI.AxControls">
      <HintPath>..\Libs\NationalInstruments.TestStand.Interop.UI.AxControls.dll</HintPath>
    </Reference>
    <Reference Include="System.Windows.Forms" />
    <Reference Include="WindowsFormsIntegration" />
  </ItemGroup>
</Project>
WPFProject/App.config
@@ -9,7 +9,7 @@
    </startup>
    <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
            <probing privatePath="Libs;ThirdParty;Plugin;3rds;Api;" />
            <probing privatePath="Libs;ThirdParty;Plugin;3rds;Api;OpenTap;" />
            <dependentAssembly>
                <assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
                <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
WPFProject/App.xaml.cs
@@ -93,7 +93,7 @@
        {
            var process = GetProcess();
            mutex = new Mutex(true, "TestPlatform_OnlyRun_CRNS");
            mutex = new Mutex(true, "TestPlatform_OnlyRun_CRNS__");
            if (mutex.WaitOne(0, false))//&& process == null
            {
                WriteRegister();
WPFProject/Controls/MainTabControl.xaml
@@ -159,7 +159,7 @@
            <DockPanel Background="Transparent">
                <Border DockPanel.Dock="Left" BorderThickness="1,1,0,1" Background="Transparent" BorderBrush="{StaticResource PrimaryColor}" Width="50"></Border>
                <ListBox DockPanel.Dock="Left" Grid.Row="0" x:Name="Menu"
 Style="{StaticResource ListBoxItemStyle1}" FontSize="16"
 Style="{StaticResource ListBoxItemStyle1}" FontSize="14"
 BorderBrush="{StaticResource PrimaryColor}"
 BorderThickness="1"
 SelectionChanged="Menu_SelectionChanged"
WPFProject/Controls/MainTabControl.xaml.cs
@@ -1,6 +1,7 @@
using HMI.Base;
using HMI.Core.Api;
using Panuon.WPF.UI;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
@@ -11,6 +12,12 @@
namespace WPFProject.Controls
{
    public class TabChangedEventArg
    {
        public IPlugin oldValue { get; set; }
        public IPlugin newValue { get; set; }
    }
    /// <summary>
    /// MainTabControl.xaml 的交互逻辑
    /// </summary>
@@ -46,21 +53,7 @@
        }
        // Tab切换事件
        public static readonly RoutedEvent OnTabChangeEvent =
             EventManager.RegisterRoutedEvent("OnTabChange", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MenuTabControl));
        public event RoutedEventHandler OnTabChange
        {
            add
            {
                AddHandler(OnTabChangeEvent, value);
            }
            remove
            {
                RemoveHandler(OnTabChangeEvent, value);
            }
        }
        public event EventHandler<TabChangedEventArg> TabChanged;
        private ObservableCollection<PluginInfo> _MenuItems;
@@ -193,6 +186,7 @@
                {
                    page.Child = (UIElement)content;
                }
                TabChanged?.Invoke(this, new TabChangedEventArg { oldValue = last, newValue = newPlugin });
            }
        }
WPFProject/Lang/en-US/TestResult.json
@@ -50,5 +50,7 @@
  "Tr.CantClearSNR": "Program is running, can't clear snr!",
  "Tr.MakeSureStopOvenTest": "Are you sure you want stop [{0}] test?",
  "Tr.PositionCantClearSNR": "Position [{0}] can't clear snr now!",
  "Tr.MakeSureStopAllTest": "Are you sure you want to stop test?",
  "Tr.OvenIsTesting": "Oven is testing, can't stop test.'",
  "Tr.TestProgram": "Test Program"
}
WPFProject/Lang/en-US/app.json
@@ -104,7 +104,7 @@
  "App.MenuLoadFailed": "{0} load failed: {1} {2}",
  "App.MenuCount": "There are {0} menus is loading...",
  "App.SelectIcon": "Select icon",
  "App.Welcome": "Welcom! {0}",
  "App.Welcome": "Welcome! {0}",
  "App.MakeSureLogout": "Are you sure you want to logout?",
  "App.MakeSureRestartApp": "Are you sure you want to restart?",
  "App.NoExportData": "No Data To Export",
@@ -159,5 +159,11 @@
  "App.ScannerSetting": "Scanner Setting",
  "App.SequenceIsStarting": "Starting Test,Please Wait...",
  "App.Status": "Status",
  "App.LoadPluginError": "Load plugin error,please check."
  "App.LoadPluginError": "Load plugin error,please check.",
  "App.Version": "Ver.",
  "App.PluginVersion": "Plugin Ver.",
  "App.CPUUsage": "CPU Usage",
  "App.RemUsage": "Rem Usage",
  "App.TotalCPUUsage": "PC CPU Usage",
  "App.TotalRemUsage": "PC Rem Usage"
}
WPFProject/Lang/zh-CN/TestResult.json
@@ -50,5 +50,7 @@
  "Tr.CantClearSNR": "程序运行中,不能清空条码",
  "Tr.MakeSureStopOvenTest": "确实停止老化炉[{0}]的测试吗?",
  "Tr.PositionCantClearSNR": "穴位[{0}]当前不允许清除条码!",
  "Tr.MakeSureStopAllTest": "确定要停止所有测试吗?",
  "Tr.OvenIsTesting": "有仓位正在测试中,不能停止测试",
  "Tr.TestProgram": "测试程序"
}
WPFProject/Lang/zh-CN/app.json
@@ -158,5 +158,11 @@
  "App.ScannerSetting": "扫码枪设置",
  "App.SequenceIsStarting": "正在启动测试序列,请稍后...",
  "App.Status": "状态",
  "App.LoadPluginError": "加载出错,请确认"
  "App.LoadPluginError": "加载出错,请确认",
  "App.Version": "软件版本:",
  "App.PluginVersion": "插件版本:",
  "App.CPUUsage": "CPU使用",
  "App.RemUsage": "内存使用",
  "App.TotalCPUUsage": "总CPU使用",
  "App.TotalRemUsage": "总内存使用"
}
WPFProject/PanuonWindow.xaml
@@ -22,7 +22,7 @@
            pu:WindowXCaption.HeaderAlignment="Stretch"
            Title="TestPlatform"
            pu:WindowXCaption.Background="Transparent"
            pu:WindowXCaption.Height="80"
            pu:WindowXCaption.Height="60"
            IsOverlayerVisible="True"
            pu:WindowXCaption.Foreground="#FFF">
    <Window.Resources>
@@ -40,7 +40,6 @@
            <Setter Property="pu:MenuHelper.TopLevelItemsHoverBackground" Value="{x:Null}"/>
            <Setter Property="pu:MenuHelper.TopLevelDropDownPlacement" Value="BottomLeft"/>
        </Style>
    </Window.Resources>
    <Window.Background>
        <ImageBrush Stretch="Uniform" AlignmentX="Left" AlignmentY="Top"  
@@ -49,9 +48,10 @@
    <pu:WindowXCaption.HeaderTemplate>
        <DataTemplate>
            <DockPanel DataContext="{Binding ElementName=window}" WindowChrome.IsHitTestVisibleInChrome="True" VerticalAlignment="Center" Height="100">
            <DockPanel DataContext="{Binding ElementName=window}" WindowChrome.IsHitTestVisibleInChrome="True"
                       VerticalAlignment="Center" Height="80">
                <StackPanel Orientation="Horizontal">
                    <Image DockPanel.Dock="Left" Source="{Binding WindowLogo}" Margin="5,0,20,0" Height="60" VerticalAlignment="Center" Stretch="Fill"
                    <Image DockPanel.Dock="Left" Source="{Binding WindowLogo}" Margin="5,0,20,0" Height="40" VerticalAlignment="Center" Stretch="Fill"
  RenderOptions.BitmapScalingMode="HighQuality" />
                    <TextBlock FontSize="30" VerticalAlignment="Center" Text="{Binding WindowTitle}">
                        <TextBlock.Effect>
@@ -71,9 +71,7 @@
                    </Grid.RowDefinitions>
                    <StackPanel Grid.Row="0" VerticalAlignment="Top" Margin="10,10,0,0" Orientation="Horizontal">
                        <Menu Margin="0" Padding="0" VerticalAlignment="Top"
  VerticalContentAlignment="Center" Style="{StaticResource UserMenu}"
  Background="Transparent">
  VerticalContentAlignment="Center" Style="{StaticResource UserMenu}" Background="Transparent">
                            <MenuItem x:Name="userMenu" Header="{Binding WelcomeTitle}" FontSize="16">
                                <MenuItem Icon="&#xe61d;" Cursor="Hand" FontSize="14" Header="退出登录" 
                                      x:Name="MiLogout" Click="MenuItem_Click"/>
@@ -131,11 +129,14 @@
                            </Button.Style>
                        </Button>
                    </StackPanel>
                    <Button Grid.Row="1" Margin="20,0,0,20" Foreground="#FFF"
                            Visibility="{Binding Alarms.Count,Converter={StaticResource cv}}"
                            HorizontalContentAlignment="Left" Style="{StaticResource LinkButton}"
                            Click="Clear_AllAlarm_Click"
                            Content="清除全部报警"/>
                    <DockPanel Margin="20,0,0,20" Grid.Row="1" LastChildFill="False">
                        <Button DockPanel.Dock="Left" Foreground="#FFF"
    Visibility="{Binding Alarms.Count,Converter={StaticResource cv}}"
    HorizontalContentAlignment="Left" Style="{StaticResource LinkButton}"
    Click="Clear_AllAlarm_Click"
    Content="清除全部报警"/>
                        <!--<local:ResourceUI x:Name="resourceUI" Loaded="resourceUI_Loaded" DockPanel.Dock="Right"/>-->
                    </DockPanel>
                </Grid>
                <ListBox x:Name="list"
@@ -234,7 +235,7 @@
                            <Setter Property="Padding" Value="0"/>
                            <Setter Property="VerticalContentAlignment" Value="Center"/>
                            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                            <Setter Property="Height" Value="70"/>
                            <Setter Property="Height" Value="50"/>
                            <Setter Property="Margin" Value="0,0,0,1"/>
                            <Setter Property="BorderThickness" Value="0"/>
                        </Style>
@@ -254,13 +255,27 @@
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ctl:MainTabControl Grid.Row="0" x:Name="menuTab">
        <ctl:MainTabControl Grid.Row="0" x:Name="menuTab" TabChanged="menuTab_TabChanged">
        </ctl:MainTabControl>
        <!--<ctl:MenuTabControl Grid.Row="1" x:Name="menuTab" OnTabChange="menuTab_OnTabChange"> </ctl:MenuTabControl>-->
        <!--<StatusBar Grid.Row="1" Background="{StaticResource HoriBar}"></StatusBar>-->
        <DockPanel Grid.Row="1" Height="20" Background="{StaticResource PrimaryColor}" LastChildFill="False">
            <TextBlock Margin="20,0,0,0" FontSize="14" VerticalAlignment="Center" Foreground="#FFF">
<Run Text="{DynamicResource App.Version}"/>
<Run Text="{Binding SoftwareVersion}"/>
            </TextBlock>
            <TextBlock Margin="20,0,0,0" FontSize="14" VerticalAlignment="Center" Foreground="#FFF">
<Run Text="{DynamicResource App.PluginVersion}"/>
<Run Text="{Binding PluginVersion}"/>
            </TextBlock>
            <local:ResourceUI FontSize="14" Margin="0,1,20,1" DockPanel.Dock="Right" x:Name="resourceUI"/>
        </DockPanel>
    </Grid>
</pu:WindowX>
WPFProject/PanuonWindow.xaml.cs
@@ -5,12 +5,14 @@
using HMI.Core.Util;
using HMI.UI.Util;
using HMI.Util.Alarm;
using log4net;
using Panuon.WPF.UI;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
@@ -33,9 +35,11 @@
    /// </summary>
    public partial class PanuonWindow : WindowX, IPluginContainer, IAlarmShow
    {
        private ILog log = LogManager.GetLogger("MainWindow");
        public PanuonWindow()
        {
            this.Loaded += PanuonWindow_Loaded;
            SoftwareVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
        }
        private UserInfo currentUser;
@@ -73,6 +77,26 @@
            get => Alarms?.Count ?? 0;
        }
        private string softwareVersion;
        public string SoftwareVersion
        {
            get => softwareVersion;
            set
            {
                Set(ref softwareVersion, value);
            }
        }
        private string pluginVersion;
        public string PluginVersion
        {
            get => pluginVersion;
            set
            {
                Set(ref pluginVersion, value);
            }
        }
        public ListBox sb
        {
            get; set;
@@ -80,17 +104,28 @@
        private void PanuonWindow_Loaded(object sender, RoutedEventArgs e)
        {
            DataContext = this;
            RefrshConfig();
            InitializeComponent();
            AlarmShowHelper.Register(this);
            var sb = WindowXCaption.GetHeaderTemplate(this);
            //sb.FindName("list",this.Con);
            try
            {
                DataContext = this;
                RefrshConfig();
                InitializeComponent();
                AlarmShowHelper.Register(this);
            WelcomeTitle = this.lang("App.Welcome", UserInfo.User.Name);
            ShowMenu = false;
            Application.Current.MainWindow = this;
            SystemSetting.SystemSettingChanged += SystemSetting_SystemSettingChanged;
                WelcomeTitle = this.lang("App.Welcome", UserInfo.User.Name);
                ShowMenu = false;
                Application.Current.MainWindow = this;
                SystemSetting.SystemSettingChanged += SystemSetting_SystemSettingChanged;
                Task.Run(() =>
                {
                    ResourceListener.Instance.RefreshEvent += this.resourceUI.Refresh;
                    ResourceListener.Instance.Start();
                });
            }
            catch (Exception ex)
            {
                log.Error("Window Loaded Error.", ex);
            }
        }
        private void RefrshConfig()
@@ -223,6 +258,7 @@
        {
            try
            {
                ResourceListener.Instance.Stop();
                AlarmUtil.Release();
                PluginLoader.Reset();
                UserInfo.Logout();
@@ -232,7 +268,7 @@
            {
            }
            Application.Current.Shutdown();
            Application.Current.Shutdown(0);
        }
        private void Button_Click(object sender, RoutedEventArgs e)
@@ -296,6 +332,21 @@
            Alarms?.Clear();
        }
        private void menuTab_TabChanged(object sender, Controls.TabChangedEventArg e)
        {
            Dispatcher.BeginInvoke(() =>
            {
                if (e.newValue == null)
                {
                    PluginVersion = string.Empty;
                }
                else
                {
                    PluginVersion = e.newValue.GetType().Assembly.GetName().Version.ToString();
                }
            }, System.Windows.Threading.DispatcherPriority.Normal);
        }
        //private void BaseWindow_Loaded(object sender, RoutedEventArgs e)
        //{
        //    var s = (Style)this.Resources["WindowButton"];
WPFProject/Properties/AssemblyInfo.cs
@@ -53,6 +53,6 @@
//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
//通过使用 "*",如下所示:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyVersion("1.0.0.1")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: XmlConfigurator(ConfigFile = "Config/log4net.config", Watch = true)]
WPFProject/ResourceListener.cs
New file
@@ -0,0 +1,135 @@
using HMI.Simple.Util;
using log4net;
using System;
using System.Diagnostics;
using System.Management;
using System.Timers;
using System.Xml.Linq;
namespace WPFProject
{
    public class RunTimeState
    {
        public float cpuRate { get; set; }
        public double remRate { get; set; }
        public float totalCpuRate { get; set; }
        public double totalRemRate { get; set; }
    }
    public class ResourceListener
    {
        public event EventHandler<RunTimeState> RefreshEvent;
        private Timer timer;
        private System.Threading.CancellationTokenSource _can;
        private static ResourceListener instance;
        public static ResourceListener Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new ResourceListener();
                }
                return instance;
            }
        }
        private ILog log = LogManager.GetLogger("ResourceListener");
        // CPU占用
        private PerformanceCounter totalCpuCounter;
        private PerformanceCounter cpuCounter;
        private ResourceListener()
        {
            totalCpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
            cpuCounter = new PerformanceCounter("Process", "% Processor Time", Process.GetCurrentProcess().ProcessName, true);
        }
        public void Start()
        {
            _can = new System.Threading.CancellationTokenSource();
            Refresh();
            timer = new Timer();
            timer.AutoReset = false;
            timer.Interval = 30000;
            timer.Elapsed += Timer_Elapsed;
            timer.Enabled = true;
        }
        private void Refresh()
        {
            try
            {
                var _cpuCount = Environment.ProcessorCount;
                ObjectQuery wql = new ObjectQuery("SELECT * FROM Win32_OperatingSystem");
                ManagementObjectSearcher searcher = new ManagementObjectSearcher(wql);
                string prcName = Process.GetCurrentProcess().ProcessName;
                var counter_Exec = new PerformanceCounter("Process", "Working Set - Private", prcName);
                ulong dWsp_Exec = (ulong)(counter_Exec.RawValue / 1024);
                var totalCpuRate = totalCpuCounter.NextValue();
                var cpuRate = cpuCounter.NextValue() / _cpuCount;
                foreach (ManagementObject obj in searcher.Get())
                {
                    ulong totalVisibleMemorySize = (ulong)obj["TotalVisibleMemorySize"];
                    ulong freePhysicalMemory = (ulong)obj["FreePhysicalMemory"];
                    ulong usedMemory = totalVisibleMemorySize - freePhysicalMemory;
                    string info = $"\r\n总内存: {totalVisibleMemorySize / 1024} MB\r\n";
                    info += $"已使用内存: {usedMemory / 1024} MB\r\n";
                    double usagePercent = usedMemory * 100d / totalVisibleMemorySize;
                    info += $"内存占用率: {usagePercent:F2}%\r\n";
                    info += $"应用占用内存: {dWsp_Exec / 1024} MB\r\n";
                    double appPercent = dWsp_Exec * 100d / totalVisibleMemorySize;
                    info += $"应用内存占用率: {appPercent:F2}%";
                    RefreshEvent?.Invoke(this, new RunTimeState
                    {
                        cpuRate = cpuRate,
                        remRate = appPercent,
                        totalCpuRate = totalCpuRate,
                        totalRemRate = usagePercent
                    });
                    log.Info(info);
                    if (usagePercent > 80)
                    {
                        // AlarmUtil.CreateAlarm("App", "内存占用过高,超80%", out _);
                    }
                }
            }
            catch (Exception ex)
            {
                log.Error("Refresh Resource Error.", ex);
            }
        }
        private void Timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            try
            {
                timer.Enabled = false;
                _can.Token.ThrowIfCancellationRequested();
                Refresh();
                timer.Enabled = true;
            }
            catch
            {
            }
        }
        public void Stop()
        {
            _can?.Cancel();
            _can?.Dispose();
            instance = null;
        }
    }
}
WPFProject/Settings.ini
New file
@@ -0,0 +1,6 @@
[Btns]
;名称=ActionName
上电激活=Active
CAN测试=CanTest
LIN测试=LinTest
DoIP测试=DOIPTest
WPFProject/UI/ResourceUI.xaml
New file
@@ -0,0 +1,63 @@
<hb:NotifyControl x:Class="WPFProject.ResourceUI"
             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:WPFProject.UI"
             mc:Ignorable="d"
             xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
             xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
             xmlns:pu="https://opensource.panuon.com/wpf-ui"
             d:DesignHeight="450" d:DesignWidth="800">
    <hb:NotifyControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <pu:StyleDictionary Includes="Label" />
            </ResourceDictionary.MergedDictionaries>
            <Style x:Key="def" TargetType="Label" BasedOn="{StaticResource {x:Static pu:StyleKeys.LabelStyle}}">
                <Setter Property="pu:LabelHelper.CornerRadius" Value="3"/>
                <Setter Property="Padding" Value="5,0"/>
                <Setter Property="Margin" Value="0"/>
                <Setter Property="VerticalContentAlignment" Value="Center"/>
                <Setter Property="Foreground" Value="#FFF"/>
            </Style>
        </ResourceDictionary>
    </hb:NotifyControl.Resources>
    <Grid dp:GridHelper.Space="10" dp:GridHelper.SpaceIgnore="ALL">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Label Grid.Column="0" Style="{StaticResource def}" x:Name="l1">
            <TextBlock>
    <Run Text="{DynamicResource App.CPUUsage}"/>
    <Run Text="{Binding CpuRate}"/>
            </TextBlock>
        </Label>
        <Label Grid.Column="1" Style="{StaticResource def}" x:Name="l2">
            <TextBlock>
    <Run Text="{DynamicResource App.RemUsage}"/>
    <Run Text="{Binding RemRate}"/>
            </TextBlock>
        </Label>
        <Label Grid.Column="2" Style="{StaticResource def}" x:Name="l3">
            <TextBlock Grid.Column="2">
    <Run Text="{DynamicResource App.TotalCPUUsage}"/>
    <Run Text="{Binding TotalCpuRate}"/>
            </TextBlock>
        </Label>
        <Label Grid.Column="3" Style="{StaticResource def}" x:Name="l4">
            <TextBlock Grid.Column="3">
    <Run Text="{DynamicResource App.TotalRemUsage}"/>
    <Run Text="{Binding TotalRemRate}"/>
            </TextBlock>
        </Label>
    </Grid>
</hb:NotifyControl>
WPFProject/UI/ResourceUI.xaml.cs
New file
@@ -0,0 +1,91 @@
using HMI.Base;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace WPFProject
{
    /// <summary>
    /// ResourceListener.xaml 的交互逻辑
    /// </summary>
    public partial class ResourceUI : NotifyControl
    {
        private string cpuRate;
        public string CpuRate
        {
            get => cpuRate;
            set
            {
                Set(ref cpuRate, value);
            }
        }
        private string remRate;
        public string RemRate
        {
            get => remRate;
            set
            {
                Set(ref remRate, value);
            }
        }
        private string totalCpuRate;
        public string TotalCpuRate
        {
            get => totalCpuRate;
            set
            {
                Set(ref totalCpuRate, value);
            }
        }
        private string totalRemRate;
        public string TotalRemRate
        {
            get => totalRemRate;
            set
            {
                Set(ref totalRemRate, value);
            }
        }
        public ResourceUI()
        {
            DataContext = this;
            InitializeComponent();
        }
        public void Refresh(object sender, RunTimeState state)
        {
            Dispatcher.BeginInvoke(() =>
            {
                CpuRate = $"{state.cpuRate:F1}%";
                SetColor(state.cpuRate, l1);
                RemRate = $"{state.remRate:F1}%";
                SetColor(state.remRate, l2);
                TotalCpuRate = $"{state.totalCpuRate:F1}%";
                SetColor(state.totalCpuRate, l3);
                TotalRemRate = $"{state.totalRemRate:F1}%";
                SetColor(state.totalRemRate, l4);
            }, System.Windows.Threading.DispatcherPriority.Normal);
        }
        private void SetColor(double val, Label label)
        {
            if (val > 80)
            {
                label.Background = Brushes.Red;
            }
            else if (val > 60)
            {
                label.Background = Brushes.Orange;
            }
            else
            {
                label.Background = Application.Current.FindResource("SecondPrimaryColor") as Brush;
            }
        }
    }
}
WPFProject/WPFProject.csproj
@@ -198,6 +198,10 @@
    <Compile Include="PanuonWindow.xaml.cs">
      <DependentUpon>PanuonWindow.xaml</DependentUpon>
    </Compile>
    <Compile Include="ResourceListener.cs" />
    <Compile Include="UI\ResourceUI.xaml.cs">
      <DependentUpon>ResourceUI.xaml</DependentUpon>
    </Compile>
    <Compile Include="UI\TestWindow.xaml.cs">
      <DependentUpon>TestWindow.xaml</DependentUpon>
    </Compile>
@@ -239,6 +243,10 @@
      <DependentUpon>MenuTabControl.xaml</DependentUpon>
    </Compile>
    <Page Include="PanuonWindow.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Page Include="UI\ResourceUI.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
@@ -299,6 +307,9 @@
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="变更记录\2025-12-24.txt" />
    <None Include="Settings.ini">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <Resource Include="Icon\b1.jpg" />
    <Resource Include="Icon\b2.jpg" />
    <Resource Include="Icon\b3.jpg" />
@@ -437,6 +448,10 @@
    <ProjectReference Include="..\ModBusTcpController\ModBusTcpController.csproj">
      <Project>{49db0c9b-b239-4608-a91e-f5ed43afaf96}</Project>
      <Name>ModBusTcpController</Name>
    </ProjectReference>
    <ProjectReference Include="..\Plugin.DataViewer\Plugin.DataViewer.csproj">
      <Project>{dbfe5040-963f-4bbf-93fb-26b3f18fda90}</Project>
      <Name>Plugin.DataViewer</Name>
    </ProjectReference>
    <ProjectReference Include="..\Plugin.SeqEditor\Plugin.SeqEditor.csproj">
      <Project>{1260f995-1ae9-17b6-feb5-c430c1416bec}</Project>
@@ -587,6 +602,8 @@
move /y "$(TargeDir)Plugin.ModelManage.dll" $(TargeDir)Plugin\Plugin.ModelManage.dll"
move /y "$(TargeDir)Plugin.CurrentSensorControl.dll" $(TargeDir)Plugin\Plugin.CurrentSensorControl.dll"
move /y "$(TargeDir)Plugin.TsSupport.dll" $(TargeDir)Plugin\Plugin.TsSupport.dll"
move /y "$(TargeDir)Plugin.DataViewer.dll" $(TargeDir)Plugin\Plugin.DataViewer.dll"
move /y "$(TargeDir)Util.ManualDebug.dll" $(TargeDir)Plugin\Util.ManualDebug.dll"
setlocal enabledelayedexpansion
@@ -609,7 +626,7 @@
)
for %25%25F in ("%25source_folder%25\*") do (
    if /I not "%25%25~nxF"=="$(TargetFileName)" if /I not "%25%25~nxF"=="$(TargetFileName).config" if /I not "%25%25~nxF"=="$(TargetName).pdb" if /I not "%25%25~nxF"=="clear.bat" if /I not "%25%25~nxF"=="安装MSMQ.bat" (
    if /I not "%25%25~nxF"=="$(TargetFileName)" if /I not "%25%25~nxF"=="$(TargetFileName).config" if /I not "%25%25~nxF"=="$(TargetName).pdb" if /I not "%25%25~nxF"=="clear.bat" if /I not "%25%25~nxF"=="安装MSMQ.bat" if /I not "%25%25~nxF"=="Settings.ini" (
        move /Y "%25%25F" "%25destination_folder%25"
    )
)
WPFProject/WPFProject.sln
@@ -75,6 +75,16 @@
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.SeqEditor", "..\Plugin.SeqEditor\Plugin.SeqEditor.csproj", "{1260F995-1AE9-17B6-FEB5-C430C1416BEC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.DataViewer", "..\Plugin.DataViewer\Plugin.DataViewer.csproj", "{DBFE5040-963F-4BBF-93FB-26B3F18FDA90}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.UICollection", "..\Plugin.UICollection\Plugin.UICollection.csproj", "{22DC8D91-50B2-47CA-86A8-29A6DA48F388}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtilLib", "..\..\UtilLib\UtilLib.csproj", "{A132F114-A26B-4428-BAEC-4851F38C0E14}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UILib", "..\..\UILib\UILib.csproj", "{27325FE0-3CE2-4CD5-8EB5-7E45CC441479}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Util.ManualDebug", "..\Util.ManualDebug\Util.ManualDebug.csproj", "{D0944E62-EE52-4ABE-832A-A09D98814DB1}"
EndProject
Global
    GlobalSection(SolutionConfigurationPlatforms) = preSolution
        Debug|Any CPU = Debug|Any CPU
@@ -505,6 +515,66 @@
        {1260F995-1AE9-17B6-FEB5-C430C1416BEC}.Release|x64.Build.0 = Release|x64
        {1260F995-1AE9-17B6-FEB5-C430C1416BEC}.Release|x86.ActiveCfg = Release|x86
        {1260F995-1AE9-17B6-FEB5-C430C1416BEC}.Release|x86.Build.0 = Release|x86
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Debug|x64.ActiveCfg = Debug|Any CPU
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Debug|x64.Build.0 = Debug|Any CPU
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Debug|x86.ActiveCfg = Debug|Any CPU
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Debug|x86.Build.0 = Debug|Any CPU
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Release|Any CPU.Build.0 = Release|Any CPU
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Release|x64.ActiveCfg = Release|Any CPU
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Release|x64.Build.0 = Release|Any CPU
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Release|x86.ActiveCfg = Release|Any CPU
        {DBFE5040-963F-4BBF-93FB-26B3F18FDA90}.Release|x86.Build.0 = Release|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Debug|x64.ActiveCfg = Debug|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Debug|x64.Build.0 = Debug|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Debug|x86.ActiveCfg = Debug|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Debug|x86.Build.0 = Debug|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Release|Any CPU.Build.0 = Release|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Release|x64.ActiveCfg = Release|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Release|x64.Build.0 = Release|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Release|x86.ActiveCfg = Release|Any CPU
        {22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Release|x86.Build.0 = Release|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x64.ActiveCfg = Debug|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x64.Build.0 = Debug|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x86.ActiveCfg = Debug|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x86.Build.0 = Debug|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|Any CPU.Build.0 = Release|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x64.ActiveCfg = Release|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x64.Build.0 = Release|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x86.ActiveCfg = Release|Any CPU
        {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x86.Build.0 = Release|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x64.ActiveCfg = Debug|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x64.Build.0 = Debug|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x86.ActiveCfg = Debug|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x86.Build.0 = Debug|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|Any CPU.Build.0 = Release|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x64.ActiveCfg = Release|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x64.Build.0 = Release|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x86.ActiveCfg = Release|Any CPU
        {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x86.Build.0 = Release|Any CPU
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Debug|x64.ActiveCfg = Debug|x64
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Debug|x64.Build.0 = Debug|x64
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Debug|x86.ActiveCfg = Debug|x86
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Debug|x86.Build.0 = Debug|x86
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|Any CPU.Build.0 = Release|Any CPU
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|x64.ActiveCfg = Release|Any CPU
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|x64.Build.0 = Release|Any CPU
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|x86.ActiveCfg = Release|Any CPU
        {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|x86.Build.0 = Release|Any CPU
    EndGlobalSection
    GlobalSection(SolutionProperties) = preSolution
        HideSolutionNode = FALSE