正式开始BDU项目前对设备指令的第一次备份

This commit is contained in:
ouyangrui 2026-03-13 17:12:20 +08:00
parent 27be073925
commit cdf7fed4a4
112 changed files with 18812 additions and 0 deletions

4
.editorconfig Normal file
View File

@ -0,0 +1,4 @@
[*.cs]
# CA1416: 验证平台兼容性
dotnet_diagnostic.CA1416.severity = silent

66
ATS.sln Normal file
View File

@ -0,0 +1,66 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36221.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATS", "ATS\ATS.csproj", "{000A2999-6535-43D3-94DA-EE372D19702F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{AEA71207-A57F-424A-9AA0-9AC9A4B681F1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceCommand", "DeviceCommand\DeviceCommand.csproj", "{13E7EF22-4595-D29C-1E97-91B321696C07}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Command", "Command\Command.csproj", "{59379C4C-4B0B-43FE-A56E-9532BAD20F9C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TOSUNCAN", "TOSUNCAN\TOSUNCAN.csproj", "{7810B83D-B705-B118-EA4E-CAD8F65E25F7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATS_DBContext", "ATS_DBContext\ATS_DBContext.csproj", "{D1356D14-DC7D-42B1-BDF2-EC37D09562E0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "加密狗", "加密狗\加密狗.csproj", "{E4D76F6D-39C5-4F4F-B81A-31D9828A8CC6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{000A2999-6535-43D3-94DA-EE372D19702F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{000A2999-6535-43D3-94DA-EE372D19702F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{000A2999-6535-43D3-94DA-EE372D19702F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{000A2999-6535-43D3-94DA-EE372D19702F}.Release|Any CPU.Build.0 = Release|Any CPU
{AEA71207-A57F-424A-9AA0-9AC9A4B681F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AEA71207-A57F-424A-9AA0-9AC9A4B681F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AEA71207-A57F-424A-9AA0-9AC9A4B681F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AEA71207-A57F-424A-9AA0-9AC9A4B681F1}.Release|Any CPU.Build.0 = Release|Any CPU
{13E7EF22-4595-D29C-1E97-91B321696C07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13E7EF22-4595-D29C-1E97-91B321696C07}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13E7EF22-4595-D29C-1E97-91B321696C07}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13E7EF22-4595-D29C-1E97-91B321696C07}.Release|Any CPU.Build.0 = Release|Any CPU
{59379C4C-4B0B-43FE-A56E-9532BAD20F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{59379C4C-4B0B-43FE-A56E-9532BAD20F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{59379C4C-4B0B-43FE-A56E-9532BAD20F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{59379C4C-4B0B-43FE-A56E-9532BAD20F9C}.Release|Any CPU.Build.0 = Release|Any CPU
{7810B83D-B705-B118-EA4E-CAD8F65E25F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7810B83D-B705-B118-EA4E-CAD8F65E25F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7810B83D-B705-B118-EA4E-CAD8F65E25F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7810B83D-B705-B118-EA4E-CAD8F65E25F7}.Release|Any CPU.Build.0 = Release|Any CPU
{D1356D14-DC7D-42B1-BDF2-EC37D09562E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1356D14-DC7D-42B1-BDF2-EC37D09562E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1356D14-DC7D-42B1-BDF2-EC37D09562E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1356D14-DC7D-42B1-BDF2-EC37D09562E0}.Release|Any CPU.Build.0 = Release|Any CPU
{E4D76F6D-39C5-4F4F-B81A-31D9828A8CC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E4D76F6D-39C5-4F4F-B81A-31D9828A8CC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E4D76F6D-39C5-4F4F-B81A-31D9828A8CC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E4D76F6D-39C5-4F4F-B81A-31D9828A8CC6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F2C112EA-AA28-490A-B794-5E0D540A92C9}
EndGlobalSection
EndGlobal

44
ATS/ATS.csproj Normal file
View File

@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="CoreCLR-NCalc" Version="3.1.253" />
<PackageReference Include="gong-wpf-dragdrop" Version="4.0.0" />
<PackageReference Include="MahApps.Metro" Version="2.4.10" />
<PackageReference Include="MahApps.Metro.IconPacks" Version="5.1.0" />
<PackageReference Include="MaterialDesignColors" Version="5.2.1" />
<PackageReference Include="MaterialDesignThemes" Version="5.2.1" />
<PackageReference Include="MaterialDesignThemes.MahApps" Version="5.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NModbus" Version="3.0.81" />
<PackageReference Include="NModbus4" Version="2.1.0" />
<PackageReference Include="NPOI" Version="2.7.4" />
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ATS_DBContext\ATS_DBContext.csproj" />
<ProjectReference Include="..\TOSUNCAN\TOSUNCAN.csproj" />
<ProjectReference Include="..\Command\Command.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
<ProjectReference Include="..\TOSUNCAN\TOSUNCAN.csproj" />
<ProjectReference Include="..\加密狗\加密狗.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Interop.TSMasterAPI">
<HintPath>..\TOSUNCAN\Interop.TSMasterAPI.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

170
ATS/App.xaml Normal file
View File

@ -0,0 +1,170 @@
<Application x:Class="ATS.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ATS"
xmlns:converters="clr-namespace:ATS.Converters"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
StartupUri="Windows/Login.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.MahApps;component/Themes/MaterialDesignTheme.MahApps.Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
<materialDesign:BundledTheme BaseTheme="Light"
PrimaryColor="DeepPurple"
SecondaryColor="Lime" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.MahApps;component/Themes/MaterialDesignTheme.MahApps.Flyout.xaml" />
</ResourceDictionary.MergedDictionaries>
<converters:ParameterCategoryToStringConverter x:Key="ParameterCategoryToStringConverter" />
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<converters:ParameterCategoryToVisibilityConverter x:Key="ParameterCategoryToVisibilityConverter" />
<converters:StepResultToStringConverter x:Key="StepResultToStringConverter" />
<converters:ParameterValueToStringConverter x:Key="ParameterValueToStringConverter" />
<converters:DeviceSettingWindowConverter x:Key="DeviceSettingWindowConverter" />
<converters:IsEnumTypeConverter x:Key="IsEnumTypeConverter" />
<converters:EnumValuesConverter x:Key="EnumValuesConverter" />
<converters:EnumValueConverter x:Key="EnumValueConverter" />
<converters:BoolInverseConverter x:Key="BoolInverseConverter" />
<converters:GuidToParameterNameConverter x:Key="GuidToParameterNameConverter" />
<converters:ParameterTypeToBoolConverter x:Key="ParameterTypeToBoolConverter" />
<converters:ParameterToGotoSettingStringConverter x:Key="ParameterToGotoSettingStringConverter" />
<converters:FilteredParametersConverter x:Key="FilteredParametersConverter" />
<converters:StringToVisibilityConverter x:Key="StringToVisibilityConverter" />
<converters:DeviceNameConverter x:Key="DeviceNameConverter" />
<converters:HexConverter x:Key="HexConverter" />
<converters:HexToDecimalConverter x:Key="HexToDecimalConverter"/>
<SolidColorBrush x:Key="HighlightBrush"
Color="{DynamicResource Primary700}" />
<SolidColorBrush x:Key="AccentBaseColorBrush"
Color="{DynamicResource Primary600}" />
<SolidColorBrush x:Key="AccentColorBrush"
Color="{DynamicResource Primary500}" />
<SolidColorBrush x:Key="AccentColorBrush2"
Color="{DynamicResource Primary400}" />
<SolidColorBrush x:Key="AccentColorBrush3"
Color="{DynamicResource Primary300}" />
<SolidColorBrush x:Key="AccentColorBrush4"
Color="{DynamicResource Primary200}" />
<SolidColorBrush x:Key="WindowTitleColorBrush"
Color="{DynamicResource Primary700}" />
<SolidColorBrush x:Key="AccentSelectedColorBrush"
Color="{DynamicResource Primary500Foreground}" />
<LinearGradientBrush x:Key="ProgressBrush"
EndPoint="0.001,0.5"
StartPoint="1.002,0.5">
<GradientStop Color="{DynamicResource Primary700}"
Offset="0" />
<GradientStop Color="{DynamicResource Primary300}"
Offset="1" />
</LinearGradientBrush>
<SolidColorBrush x:Key="CheckmarkFill"
Color="{DynamicResource Primary500}" />
<SolidColorBrush x:Key="RightArrowFill"
Color="{DynamicResource Primary500}" />
<SolidColorBrush x:Key="IDealForegroundColorBrush"
Color="{DynamicResource Primary500Foreground}" />
<SolidColorBrush x:Key="IDealForegroundDisabledBrush"
Color="{DynamicResource Primary500}"
Opacity="0.4" />
<SolidColorBrush x:Key="MahApps.Metro.Brushes.ToggleSwitchButton.OnSwitchBrush.Win10"
Color="{DynamicResource Primary500}" />
<SolidColorBrush x:Key="MahApps.Metro.Brushes.ToggleSwitchButton.OnSwitchMouseOverBrush.Win10"
Color="{DynamicResource Primary400}" />
<SolidColorBrush x:Key="MahApps.Metro.Brushes.ToggleSwitchButton.ThumbIndicatorCheckedBrush.Win10"
Color="{DynamicResource Primary500Foreground}" />
<FontFamily x:Key="misans">pack://application:,,,/BVC;component/字体/MiSans-Normal.ttf#misans</FontFamily>
<Style TargetType="{x:Type ListViewItem}"
BasedOn="{StaticResource MahApps.Styles.ListViewItem}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type ListView}"
BasedOn="{StaticResource MaterialDesignListView}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource MaterialDesignListBoxItem}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type ListBox}"
BasedOn="{StaticResource MaterialDesignListBox}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type TreeViewItem}"
BasedOn="{StaticResource MaterialDesignTreeViewItem}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type TreeView}"
BasedOn="{StaticResource MaterialDesignTreeView}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type Button}"
BasedOn="{StaticResource MaterialDesignRaisedButton}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type Label}"
BasedOn="{StaticResource MaterialDesignLabel}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type TextBox}"
BasedOn="{StaticResource MaterialDesignTextBox}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type PasswordBox}"
BasedOn="{StaticResource MaterialDesignPasswordBox}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type ComboBox}"
BasedOn="{StaticResource MaterialDesignComboBox}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type GroupBox}"
BasedOn="{StaticResource MaterialDesignGroupBox}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type TabControl}"
BasedOn="{StaticResource MaterialDesignTabControl}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type TabItem}"
BasedOn="{StaticResource MaterialDesignTabItem}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type Run}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type Menu}"
BasedOn="{StaticResource MaterialDesignMenu}">
<Setter Property="FontFamily"
Value="{StaticResource misans}" />
</Style>
<Style TargetType="{x:Type ScrollViewer}"
BasedOn="{StaticResource MaterialDesignScrollViewer}">
<Setter Property="VerticalScrollBarVisibility"
Value="Auto" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

25
ATS/App.xaml.cs Normal file
View File

@ -0,0 +1,25 @@
using ATS.Tools;
using System.Configuration;
using System.Data;
using System.Windows;
namespace ATS
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
if (SecurityDongle.Verify() <= 0)
{
MessageBox.Show("加密狗到期或检测失败!\n");
App.Current.Shutdown();
return;
}
base.OnStartup(e);
}
}
}

10
ATS/AssemblyInfo.cs Normal file
View 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)
)]

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace ATS.Converters
{
public class BoolInverseConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null)
{
return value is bool b ? !b : value;
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is bool b ? !b : value;
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace ATS.Converters
{
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
// 处理反转逻辑
if (parameter?.ToString() == "Inverse")
{
boolValue = !boolValue;
}
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace ATS.Converters
{
public class DeviceNameConverter : IValueConverter
{
private readonly string[] specialName = { "奇偶" };
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string name)
{
if (specialName.Contains(name))
{
if (parameter?.ToString() == "Inverse")
{
return Visibility.Visible;
}
else if (parameter?.ToString() == "Items")
{
switch (name)
{
case "奇偶":
return new List<string> { "无", "奇", "偶" };
}
}
return Visibility.Collapsed;
}
}
if (parameter?.ToString() == "Inverse")
{
return Visibility.Collapsed;
}
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,33 @@
using ATS.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace ATS.Converters
{
public class DeviceSettingWindowConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(parameter?.ToString()== "ToList")
{
return JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(value.ToString());
}
else
{
return value;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,82 @@
using System;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
namespace ATS.Converters
{
public class EnumValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// 验证输入参数
if (values.Length < 2 || values[0] == null || values[1] == null)
{
return null;
}
try
{
// 获取枚举类型
Type enumType = values[0] as Type;
if (enumType == null || !enumType.IsEnum)
{
return null;
}
// 获取数值
object value = values[1];
// 确保数值类型匹配枚举的底层类型
Type underlyingType = Enum.GetUnderlyingType(enumType);
object convertedValue;
try
{
convertedValue = System.Convert.ChangeType(value, underlyingType);
}
catch
{
// 如果转换失败,尝试直接使用原始值
convertedValue = value;
}
// 将数值转换为枚举值
return Enum.ToObject(enumType, convertedValue);
}
catch
{
// 发生任何异常时返回null
return null;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
if (value == null)
{
return [null, null];
}
try
{
// 获取枚举值的底层数值
Type enumType = value.GetType();
if (!enumType.IsEnum)
{
return [null, null];
}
Type underlyingType = Enum.GetUnderlyingType(enumType);
object numericValue = System.Convert.ChangeType(value, underlyingType);
// 返回枚举类型和对应的数值
return [enumType, numericValue];
}
catch
{
return [null, null];
}
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace ATS.Converters
{
public class EnumValuesConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Type type && type.IsEnum)
{
return Enum.GetValues(type);
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,76 @@
using ATS.Models;
using System;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
namespace ATS.Converters
{
public class FilteredParametersConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length < 2 || values[0] == null || values[1] == null)
return null;
Type currentParamType = values[0] as Type;
var allParameters = values[1] as System.Collections.IEnumerable;
if (currentParamType == null || allParameters == null)
return allParameters;
// 过滤出类型匹配的参数
return allParameters.Cast<ParameterModel>()
.Where(p => IsTypeMatch(currentParamType, p.Type))
.ToList();
}
private bool IsTypeMatch(Type currentType, Type candidateType)
{
if (candidateType == null) return false;
// 如果候选参数类型是 object则匹配所有类型
if (candidateType == typeof(object)) return true;
// 如果当前参数类型是 object则匹配所有类型
if (currentType == typeof(object)) return true;
// 如果类型完全相同,则匹配
if (candidateType == currentType) return true;
// 处理数值类型的兼容性
if (IsNumericType(currentType) && IsNumericType(candidateType))
return true;
return false;
}
private bool IsNumericType(Type type)
{
if (type == null) return false;
switch (Type.GetTypeCode(type))
{
case TypeCode.Byte:
case TypeCode.SByte:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Single:
return true;
default:
return false;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,29 @@
using ATS.Windows;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace ATS.Converters
{
public class GuidToParameterNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is Guid id)
{
var para = MainWindow.Instance.Program.Parameters.FirstOrDefault(x => x.ID == id);
if(para != null) return para.Name;
}
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace ATS.Converters
{
public class HexConverter : IValueConverter
{
// 显示时int → hex 字符串
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is int i)
return $"0x{i:X}"; // 例如 255 → 0xFF
return "0x0";
}
// 用户输入时hex 字符串 → int
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var str = value?.ToString()?.Trim();
if (string.IsNullOrWhiteSpace(str))
return 0;
if (str.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
str = str.Substring(2);
if (int.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int result))
return result;
return 0; // 或 return DependencyProperty.UnsetValue;
}
}
}

View File

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace ATS.Converters
{
public class HexToDecimalConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// 将模型中的十进制值转换为十六进制字符串显示
if (value != null)
{
if (value.GetType() == typeof(int) && value is int intValue)
{
return $"0x{intValue:X}";
}
}
return value!;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// 将用户输入的十六进制字符串(如 "0x13A")转换回十进制整数
if (value is string stringValue && !string.IsNullOrEmpty(stringValue))
{
// 移除可能存在的空格
stringValue = stringValue.Trim();
// 检查是否以 "0x" 或 "0X" 开头
if (stringValue.StartsWith("0x", StringComparison.OrdinalIgnoreCase)&& stringValue.Length>2)
{
try
{
// 移除 "0x" 前缀,然后解析为整数
var hexString = stringValue.Substring(2);
return System.Convert.ToInt32(hexString, 16);
}
catch (FormatException)
{
// 如果格式错误,返回 null 或者抛出异常,取决于您的需求
// 这里我们返回 null让绑定系统保持原值或触发验证
return Binding.DoNothing;
}
catch (OverflowException)
{
return Binding.DoNothing;
}
}
else
{
// 如果没有 "0x" 前缀,则尝试按十进制解析
try
{
return System.Convert.ToInt32(stringValue);
}
catch (FormatException)
{
return Binding.DoNothing;
}
catch (OverflowException)
{
return Binding.DoNothing;
}
}
}
return Binding.DoNothing;
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace ATS.Converters
{
public class IsEnumTypeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Type type)
{
// 检查是否为枚举类型
bool isEnum = type.IsEnum;
// 根据参数决定返回值类型
if (parameter is string strParam && strParam == "Collapse")
{
return isEnum ? Visibility.Collapsed : Visibility.Visible;
}
return isEnum ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using static ATS.Models.ParameterModel;
namespace ATS.Converters
{
public class ParameterCategoryToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is ParameterCategory category)
{
switch (category)
{
case ParameterCategory.Input:
return "输入";
case ParameterCategory.Output:
return "输出";
case ParameterCategory.Temp:
return "缓存";
default:
return "未知";
}
}
return "未知";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using static ATS.Models.ParameterModel;
namespace ATS.Converters
{
public class ParameterCategoryToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is ParameterCategory category)
{
if (parameter?.ToString() == "Item")
{
if(category == ParameterCategory.Temp) { return Visibility.Collapsed; }
else { return Visibility.Visible; }
}
bool boolValue = category == ParameterCategory.Input;
if (parameter?.ToString() == "Inverse")
{
boolValue = !boolValue;
}
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,119 @@
using ATS.Windows;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace ATS.Converters
{
internal class ParameterToGotoSettingStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Guid stepID && stepID == MainWindow.Instance.SelectedStep!.ID)
{
if (MainWindow.Instance.SelectedStep == null)
{
return "";
}
if (MainWindow.Instance.SelectedStep!.OKGotoStepID == null && MainWindow.Instance.SelectedStep!.NGGotoStepID == null)
{
return "0/0";
}
else
{
string gotoString = "";
if (MainWindow.Instance.SelectedStep!.OKGotoStepID != null)
{
var OKGotoStep = MainWindow.Instance.Program.StepCollection.FirstOrDefault(x => x.ID == MainWindow.Instance.SelectedStep!.OKGotoStepID);
if (OKGotoStep != null)
{
gotoString = OKGotoStep.Index.ToString() + "/";
}
else
{
gotoString = "0/";
}
}
else
{
gotoString = "0/";
}
if (MainWindow.Instance.SelectedStep!.NGGotoStepID != null)
{
var NGGotoStep = MainWindow.Instance.Program.StepCollection.FirstOrDefault(x => x.ID == MainWindow.Instance.SelectedStep!.NGGotoStepID);
if (NGGotoStep != null)
{
gotoString += NGGotoStep.Index.ToString();
}
else
{
gotoString += "0";
}
}
else
{
gotoString += "0";
}
return gotoString;
}
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string gotoSettingstring)
{
gotoSettingstring = gotoSettingstring.Replace(" ", "").Replace("/n", "").Replace("/r", "");
if (gotoSettingstring == "0/0")
{
MainWindow.Instance.SelectedStep!.OKGotoStepID = null;
MainWindow.Instance.SelectedStep!.NGGotoStepID = null;
}
else
{
try
{
var list = gotoSettingstring.Split("/");
var okindex = System.Convert.ToInt32(list[0]);
var ngindex = System.Convert.ToInt32(list[1]);
if (okindex == 0)
{
MainWindow.Instance.SelectedStep!.OKGotoStepID = null;
}
else
{
if (okindex > MainWindow.Instance.Program.StepCollection.Count)
{
throw new Exception("步骤序号超出最大值");
}
MainWindow.Instance.SelectedStep!.OKGotoStepID = MainWindow.Instance.Program.StepCollection.FirstOrDefault(x => x.Index == okindex)?.ID;
}
if (ngindex == 0)
{
MainWindow.Instance.SelectedStep!.NGGotoStepID = null;
}
else
{
if (ngindex > MainWindow.Instance.Program.StepCollection.Count)
{
throw new Exception("步骤序号超出最大值");
}
MainWindow.Instance.SelectedStep!.NGGotoStepID = MainWindow.Instance.Program.StepCollection.FirstOrDefault(x => x.Index == ngindex)?.ID;
}
}
catch (Exception ex)
{
MessageBox.Show($"跳转表达式错误:{ex.Message}");
}
}
}
return MainWindow.Instance.SelectedStep!.ID;
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace ATS.Converters
{
public class ParameterTypeToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is Type type)
{
if(type == typeof(CancellationToken))
{
return false;
}
}
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,38 @@
using ControlzEx.Standard;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace ATS.Converters
{
public class ParameterValueToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is IEnumerable enumerable && !(value is string))
{
var elements = enumerable.Cast<object>().Select(item => item?.ToString() ?? "null");
return $"[{string.Join(", ", elements)}]";
}
else if(value != null)
{
return value.ToString()!;
}
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null)
{
return value.ToString()!;
}
return "";
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace ATS.Converters
{
public class StepResultToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is int result)
{
if (result == -1)
{
return "";
}
else if(result == 1)
{
return "PASS";
}
else if(result == 2)
{
return "FAIL";
}
}
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace ATS.Converters
{
public class StringToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is string str)
{
if (string.IsNullOrEmpty(str))
{
return Visibility.Collapsed;
}
else
{
return Visibility.Visible;
}
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

3
ATS/FodyWeavers.xml Normal file
View File

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<PropertyChanged />
</Weavers>

433
ATS/Logic/DeviceConnect.cs Normal file
View File

@ -0,0 +1,433 @@
using ATS.Models;
using ATS.Tools;
using ATS.Windows;
using DeviceCommand.Base;
using DocumentFormat.OpenXml.Drawing.Charts;
using Newtonsoft.Json;
using Org.BouncyCastle.Utilities;
using System.Collections.ObjectModel;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using TSMasterCAN;
namespace ATS.Logic
{
public static class DeviceConnect
{
private static ParameterModel devicePara = new();
public static async Task InitAndConnectDevice(ProgramModel program, DeviceModel device, bool isDeviceAdd = false)
{
try
{
devicePara = new()
{
Name = device.Name,
IsVisible = false
};
if (isDeviceAdd)
{
device.ParameterID = devicePara.ID;
program.Devices.Add(device);
}
else
{
devicePara.ID = device.ParameterID;
}
program.Parameters.Add(devicePara);
switch (device.Type)
{
case "Tcp":
program.Parameters.First(x => x.ID == device.ParameterID).Type = typeof(Tcp);
Tcp tcp = new();
var DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(device.ConnectString)!;
tcp.CreateDevice(DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value),
Convert.ToInt32(DeviceConnectSettings[2].Value), Convert.ToInt32(DeviceConnectSettings[3].Value));
device.CommunicationProtocol = tcp;
var tmpDevice = program.Devices.FirstOrDefault(x => x.ID == device.ID);
var tmpParameter = program.Parameters.FirstOrDefault(x => x.ID == device.ParameterID);
if (tmpDevice != null && tmpParameter != null)
{
tmpDevice.CommunicationProtocol = device.CommunicationProtocol;
tmpParameter.Value = tcp;
_ = DeviceConnectedStatusWatchTask(tmpDevice.ID);
}
break;
case "Udp":
program.Parameters.First(x => x.ID == device.ParameterID).Type = typeof(Udp);
Udp udp = new();
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(device.ConnectString)!;
udp.CreateDevice(DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value),
Convert.ToInt32(DeviceConnectSettings[2].Value), Convert.ToInt32(DeviceConnectSettings[3].Value));
device.CommunicationProtocol = udp;
tmpDevice = program.Devices.FirstOrDefault(x => x.ID == device.ID);
tmpParameter = program.Parameters.FirstOrDefault(x => x.ID == device.ParameterID);
if (tmpDevice != null && tmpParameter != null)
{
tmpDevice.CommunicationProtocol = device.CommunicationProtocol;
tmpParameter.Value = udp;
_ = DeviceConnectedStatusWatchTask(tmpDevice.ID);
}
break;
case "ModbusTcp":
program.Parameters.First(x => x.ID == device.ParameterID).Type = typeof(ModbusTcp);
ModbusTcp modbusTcp = new();
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(device.ConnectString)!;
modbusTcp.CreateDevice(DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value),
Convert.ToInt32(DeviceConnectSettings[2].Value), Convert.ToInt32(DeviceConnectSettings[3].Value));
device.CommunicationProtocol = modbusTcp;
tmpDevice = program.Devices.FirstOrDefault(x => x.ID == device.ID);
tmpParameter = program.Parameters.FirstOrDefault(x => x.ID == device.ParameterID);
if (tmpDevice != null && tmpParameter != null)
{
tmpDevice.CommunicationProtocol = device.CommunicationProtocol;
tmpParameter.Value = modbusTcp;
_ = DeviceConnectedStatusWatchTask(tmpDevice.ID);
}
break;
case "CAN":
program.Parameters.First(x => x.ID == device.ParameterID).Type = typeof(CAN);
tmpDevice = program.Devices.FirstOrDefault(x => x.ID == device.ID);
if (tmpDevice != null)
{
tmpDevice.CommunicationProtocol = device.CommunicationProtocol;
CAN.DisConnect();
var re1 = CAN.Init("ATS测试系统");
var re2 = CAN.LoadDBC(SystemConfig.Instance.DBCFilePath, [0, 1, 2, 3], out _);
var re3 = CAN.Connect();
if (re1 == 0 && re2 == 0 && re3 == 0)
{
tmpDevice.Connected = true;
}
else
{
tmpDevice.Connected = false;
}
_ = DeviceConnectedStatusWatchTask(tmpDevice.ID);
}
break;
case "串口":
program.Parameters.First(x => x.ID == device.ParameterID).Type = typeof(Serial_Port);
// 串口设备初始化
Serial_Port serialPort = new ();
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(device.ConnectString)!;
StopBits stopBitsEnum = GetstopBitsEnum(DeviceConnectSettings[3].Value);
Parity parityEnum = GetparityEnum(DeviceConnectSettings[4].Value);
serialPort.CreateDevice(DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value)
, Convert.ToInt32(DeviceConnectSettings[2].Value), stopBitsEnum, parityEnum
, Convert.ToInt32(DeviceConnectSettings[5].Value), Convert.ToInt32(DeviceConnectSettings[6].Value));
// 将串口通信协议赋值给设备
device.CommunicationProtocol = serialPort;
// 更新设备和参数
tmpDevice = program.Devices.FirstOrDefault(x => x.ID == device.ID);
tmpParameter = program.Parameters.FirstOrDefault(x => x.ID == device.ParameterID);
if (tmpDevice != null && tmpParameter != null)
{
tmpDevice.CommunicationProtocol = device.CommunicationProtocol;
tmpParameter.Value = serialPort;
_ = DeviceConnectedStatusWatchTask(tmpDevice.ID);
}
break;
case "ModbusRtu_Tcp":
program.Parameters.First(x => x.ID == device.ParameterID).Type = typeof(ModbusRtu_Tcp);
ModbusRtu_Tcp modbusRtu = new();
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(device.ConnectString)!;
modbusRtu.CreateDevice(DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value),
Convert.ToInt32(DeviceConnectSettings[2].Value), Convert.ToInt32(DeviceConnectSettings[3].Value));
device.CommunicationProtocol = modbusRtu;
tmpDevice = program.Devices.FirstOrDefault(x => x.ID == device.ID);
tmpParameter = program.Parameters.FirstOrDefault(x => x.ID == device.ParameterID);
if (tmpDevice != null && tmpParameter != null)
{
tmpDevice.CommunicationProtocol = device.CommunicationProtocol;
tmpParameter.Value = modbusRtu;
_ = DeviceConnectedStatusWatchTask(tmpDevice.ID);
}
break;
case "ModbusRtu_Udp":
program.Parameters.First(x => x.ID == device.ParameterID).Type = typeof(ModbusRtu_Udp);
ModbusRtu_Udp modbusRtuUdp = new();
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(device.ConnectString)!;
modbusRtuUdp.CreateDevice(DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value),
Convert.ToInt32(DeviceConnectSettings[2].Value));
device.CommunicationProtocol = modbusRtuUdp;
tmpDevice = program.Devices.FirstOrDefault(x => x.ID == device.ID);
tmpParameter = program.Parameters.FirstOrDefault(x => x.ID == device.ParameterID);
if (tmpDevice != null && tmpParameter != null)
{
tmpDevice.CommunicationProtocol = device.CommunicationProtocol;
tmpParameter.Value = modbusRtuUdp;
_ = DeviceConnectedStatusWatchTask(tmpDevice.ID);
}
break;
case "ModbusRtu_Serial":
program.Parameters.First(x => x.ID == device.ParameterID).Type = typeof(ModbusRtu_Udp);
ModbusRtu_Serial modbusRtu_Serial = new();
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(device.ConnectString)!;
stopBitsEnum = GetstopBitsEnum(DeviceConnectSettings[3].Value);
parityEnum = GetparityEnum(DeviceConnectSettings[4].Value);
modbusRtu_Serial.CreateDevice(DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value)
, Convert.ToInt32(DeviceConnectSettings[2].Value), stopBitsEnum, parityEnum
, Convert.ToInt32(DeviceConnectSettings[5].Value), Convert.ToInt32(DeviceConnectSettings[6].Value));
device.CommunicationProtocol = modbusRtu_Serial;
tmpDevice = program.Devices.FirstOrDefault(x => x.ID == device.ID);
tmpParameter = program.Parameters.FirstOrDefault(x => x.ID == device.ParameterID);
if (tmpDevice != null && tmpParameter != null)
{
tmpDevice.CommunicationProtocol = device.CommunicationProtocol;
tmpParameter.Value = modbusRtu_Serial;
_ = DeviceConnectedStatusWatchTask(tmpDevice.ID);
}
break;
}
}
catch (Exception ex)
{
Log.Error($"设备 [ {device.Name} ] 初始化连接失败:{ex.Message}");
}
}
private static async Task DeviceConnectedStatusWatchTask(Guid deviceID)
{
DeviceModel? watchedDevice = MainWindow.Instance.Program.Devices.FirstOrDefault(x => x.ID == deviceID);
while (watchedDevice != null)
{
try
{
switch (watchedDevice.Type)
{
case "Tcp":
Tcp tcp = (Tcp)watchedDevice.CommunicationProtocol!;
var DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(watchedDevice.ConnectString)!;
Tcp.ChangeDeviceConfig(tcp, DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value),
Convert.ToInt32(DeviceConnectSettings[2].Value), Convert.ToInt32(DeviceConnectSettings[3].Value));
var re = await Tcp.ConnectAsync(tcp);
await Tcp.SendAsync(tcp, "");
if (re && tcp.TcpClient.Connected)
{
watchedDevice.Connected = true;
}
else
{
watchedDevice.Connected = false;
}
break;
case "Udp":
Udp udp = (Udp)watchedDevice.CommunicationProtocol!;
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(watchedDevice.ConnectString)!;
Udp.ChangeDeviceConfig(udp, DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value),
Convert.ToInt32(DeviceConnectSettings[2].Value), Convert.ToInt32(DeviceConnectSettings[3].Value));
re = await Udp.ConnectAsync(udp);
await Udp.SendAsync(udp, Encoding.UTF8.GetBytes(""));
if (re && udp.UdpClient.Client.Connected)
{
watchedDevice.Connected = true;
}
else
{
watchedDevice.Connected = false;
}
break;
case "ModbusTcp":
ModbusTcp modbusTcp = (ModbusTcp)watchedDevice.CommunicationProtocol!;
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(watchedDevice.ConnectString)!;
ModbusTcp.ChangeDeviceConfig(modbusTcp, DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value),
Convert.ToInt32(DeviceConnectSettings[2].Value), Convert.ToInt32(DeviceConnectSettings[3].Value));
re = await ModbusTcp.ConnectAsync(modbusTcp);
await modbusTcp.TcpClient.Client.SendAsync(Encoding.UTF8.GetBytes(""));
if (re && modbusTcp.TcpClient.Connected)
{
watchedDevice.Connected = true;
}
else
{
watchedDevice.Connected = false;
}
break;
case "CAN":
var re1 = CAN.Init("ATS测试系统");
//var re2 = CAN.LoadDBC(SystemConfig.Instance.DBCFilePath, [0, 1, 2, 3], out _);
//var re3 = CAN.Connect();
if (re1 == 0 && CAN.ConnectFlag)
{
watchedDevice.Connected = true;
}
else
{
watchedDevice.Connected = false;
}
break;
case "串口":
Serial_Port serialPort = (Serial_Port)watchedDevice.CommunicationProtocol!;
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(watchedDevice.ConnectString)!;
StopBits stopBitsEnum = GetstopBitsEnum(DeviceConnectSettings[3].Value);
Parity parityEnum = GetparityEnum(DeviceConnectSettings[4].Value);
Serial_Port.ChangeDeviceConfig(serialPort, DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value)
, Convert.ToInt32(DeviceConnectSettings[2].Value), stopBitsEnum, parityEnum
, Convert.ToInt32(DeviceConnectSettings[5].Value), Convert.ToInt32(DeviceConnectSettings[6].Value));
re = await Serial_Port.ConnectAsync(serialPort);
await Serial_Port.SendAsync(serialPort, "");
if (re)
{
watchedDevice.Connected = true;
}
else
{
watchedDevice.Connected = false;
}
break;
case "ModbusRtu_Tcp":
ModbusRtu_Tcp modbusRtu = (ModbusRtu_Tcp)watchedDevice.CommunicationProtocol!;
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(watchedDevice.ConnectString)!;
stopBitsEnum = GetstopBitsEnum(DeviceConnectSettings[3].Value);
//parityEnum = GetparityEnum(DeviceConnectSettings[4].Value);
ModbusRtu_Tcp.ChangeDeviceConfig(modbusRtu, DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value),
Convert.ToInt32(DeviceConnectSettings[2].Value), Convert.ToInt32(DeviceConnectSettings[3].Value));
re = await ModbusRtu_Tcp.ConnectAsync(modbusRtu);
await modbusRtu.TcpClient.Client.SendAsync(Encoding.UTF8.GetBytes(""));
if (re && modbusRtu.TcpClient.Connected)
{
watchedDevice.Connected = true;
}
else
{
watchedDevice.Connected = false;
}
break;
case "ModbusRtu_Udp":
ModbusRtu_Udp modbusRtuUdp = (ModbusRtu_Udp)watchedDevice.CommunicationProtocol!;
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(watchedDevice.ConnectString)!;
//stopBitsEnum = GetstopBitsEnum(DeviceConnectSettings[3].Value);
//parityEnum = GetparityEnum(DeviceConnectSettings[4].Value);
ModbusRtu_Udp.ChangeDeviceConfig(modbusRtuUdp, DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value)
, Convert.ToInt32(DeviceConnectSettings[2].Value));
CancellationToken ct = default;
re = await modbusRtuUdp.ConnectAsync(ct);
if (re)
{
await modbusRtuUdp.SendRequestAndReceiveAsync(Encoding.UTF8.GetBytes(""), CancellationToken.None);
watchedDevice.Connected = true;
}
else
{
watchedDevice.Connected = false;
}
break;
case "ModbusRtu_Serial":
ModbusRtu_Serial modbusRtu_Serial = (ModbusRtu_Serial)watchedDevice.CommunicationProtocol!;
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(watchedDevice.ConnectString)!;
stopBitsEnum = GetstopBitsEnum(DeviceConnectSettings[3].Value);
parityEnum = GetparityEnum(DeviceConnectSettings[4].Value);
ModbusRtu_Serial.ChangeDeviceConfig(modbusRtu_Serial, DeviceConnectSettings[0].Value, Convert.ToInt32(DeviceConnectSettings[1].Value)
, Convert.ToInt32(DeviceConnectSettings[2].Value), stopBitsEnum, parityEnum
, Convert.ToInt32(DeviceConnectSettings[5].Value), Convert.ToInt32(DeviceConnectSettings[6].Value));
CancellationToken ctSerial = default;
re = await ModbusRtu_Serial.ConnectAsync(modbusRtu_Serial, ctSerial);
if (re)
{
watchedDevice.Connected = true;
}
else
{
watchedDevice.Connected = false;
}
break;
}
}
catch (Exception ex)
{
watchedDevice.Connected = false;
watchedDevice.ErrorMessage = ex.Message;
}
finally
{
await Task.Delay(1000);
if (watchedDevice.Connected && !string.IsNullOrEmpty(watchedDevice.ErrorMessage))
{
watchedDevice.ErrorMessage = "";
}
watchedDevice = MainWindow.Instance.Program.Devices.FirstOrDefault(x => x.ID == deviceID);
}
}
}
private static StopBits GetstopBitsEnum(string Value)
{
StopBits stopBitsEnum;
// 使用 Enum.TryParse 转换字符串到 StopBits 枚举
if (!Enum.TryParse(Value, true, out stopBitsEnum))
{
throw new TimeoutException($"转换失败!无效的停止位字符串");
}
return stopBitsEnum;
}
private static Parity GetparityEnum(string Value)
{
string ParityEnumValue = GetParityEnumValue(Value);
Parity parityEnum;
// 使用 Enum.TryParse 转换字符串到 Parity 枚举
if (!Enum.TryParse(ParityEnumValue, true, out parityEnum))
{
throw new TimeoutException($"转换失败!无效的校验位字符串");
}
return parityEnum;
}
private static string GetParityEnumValue(string Value)
{
return Value switch
{
"无" => "None",
"奇" => "Odd",
"偶" => "Even",
_ => "",
};
}
}
}

799
ATS/Logic/StepRunning.cs Normal file
View File

@ -0,0 +1,799 @@
using ATS.Models;
using ATS.Tools;
using ATS.Views;
using ATS.Windows;
using ATS_DBContext;
using ATS_DBContext.Models;
using ControlzEx.Standard;
using MaterialDesignThemes.Wpf;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using static ATS.Models.ParameterModel;
namespace ATS.Logic
{
public static class StepRunning
{
private static readonly Dictionary<Guid, ParameterModel> tmpParameters = [];
private static readonly Stopwatch stepStopwatch = new();
private static readonly Stack<Stopwatch> loopStopwatchStack = new();
private static readonly Stack<LoopContext> loopStack = new();
public static CancellationTokenSource stepCTS = new();
private static bool SubSingleStep = false;
private static Guid TestRoundID;
public static async Task<bool> ExecuteSteps(ProgramModel program, int depth = 0, CancellationToken cancellationToken = default, string subProgramPath = "")
{
int index = 0;
bool overallSuccess = true;
var ReportList = ReportModelList.ReportList;
if (ReportList == null)
{
ReportList = new List<ReportModel>();
}
if (depth == 0)
{
loopStack.Clear();
loopStopwatchStack.Clear();
ResetAllStepStatus(program);
tmpParameters.Clear();
TestRoundID = Guid.NewGuid();
}
foreach (var item in program.Parameters)
{
tmpParameters.TryAdd(item.ID, item);
}
while (index < program.StepCollection.Count)
{
while (ToolBar.Instance.IsStop == true)
{
await Task.Delay(50);
}
if (cancellationToken.IsCancellationRequested)
{
break;
}
var step = program.StepCollection[index];
if (!step.IsUsed)
{
index++;
continue;
}
step.Result = 0;
if (step.StepType == "循环开始")
{
var endStep = program.StepCollection.FirstOrDefault(x => x.LoopStartStepId == step.ID);
if (endStep != null)
{
endStep.Result = 0;
}
else
{
Log.Error("程序循环指令未闭合,请检查后重试");
break;
}
}
// 处理循环开始
if (step.StepType == "循环开始")
{
Stopwatch loopStopwatch = new();
loopStopwatch.Start();
loopStopwatchStack.Push(loopStopwatch);
var context = new LoopContext
{
LoopCount = step.LoopCount ?? 1,
CurrentLoop = 0,
StartIndex = index,
LoopStartStep = step
};
loopStack.Push(context);
step.CurrentLoopCount = context.LoopCount;
Log.Info($"循环开始,共{context.LoopCount}次", depth);
index++;
}
// 处理循环结束
else if (step.StepType == "循环结束")
{
if (loopStack.Count == 0)
{
Log.Error("未匹配的循环结束指令", depth);
step.Result = 2;
index++;
continue;
}
var context = loopStack.Peek();
context.CurrentLoop++;
// 更新循环开始步骤的显示
context.LoopStartStep!.CurrentLoopCount = context.LoopCount - context.CurrentLoop;
if (context.CurrentLoop < context.LoopCount)
{
// 继续循环:跳转到循环开始后的第一条指令
index = context.StartIndex + 1;
Log.Info($"循环第{context.CurrentLoop}次结束,跳回开始,剩余{context.LoopCount - context.CurrentLoop}次", depth);
}
else
{
// 循环结束
loopStack.Pop();
var loopStopwatch = loopStopwatchStack.Peek();
index++;
Log.Info($"循环结束,共执行{context.LoopCount}次", depth);
if (depth == 0 && loopStopwatch.IsRunning)
{
loopStopwatch.Stop();
step.RunTime = (int)loopStopwatch.ElapsedMilliseconds;
step.Result = 1;
program.StepCollection.First(x => x.ID == step.LoopStartStepId).Result = 1;
loopStopwatchStack.Pop();
}
}
}
// 处理普通步骤
else
{
if (depth == 0)
{
stepStopwatch.Restart();
}
if (step.SubProgram != null)
{
if (ToolBar.Instance.SingleStep)//子程序的单步执行将执行完保存下的所有Method
{
SubSingleStep = true;
ToolBar.Instance.SingleStep = false;
}
// 构建子程序路径
string currentSubProgramPath = string.IsNullOrEmpty(subProgramPath)
? step.Name!
: $"{subProgramPath} -> {step.Name}";
Log.Info($"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
// 传递子程序路径给下一级
bool subProgramSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken, currentSubProgramPath);
UpdateCurrentStepResult(step, true, subProgramSuccess, depth);
overallSuccess &= subProgramSuccess;
// 新增:在子程序执行完成后,插入一个总结行
if (string.IsNullOrEmpty(subProgramPath) && !subProgramPath.Contains(" -> ")) // 如果是子程序内部执行完成(即不是主程序),才插入总结行
{
ReportModelList.ReportList.Add(new ReportModel
{
stepModel = new StepModel()
{
Index = -1,
Name = $"子程序 [{step.Name}] 执行结果:{(subProgramSuccess ? "PASS" : "FAIL")}"
}, // 没有对应的实际步骤,这里用于装在结果
User = null,
ExcuteTime = null,
IsPass = subProgramSuccess ? IsPass.PASS : IsPass.FAIL,
Result = "",
SubProgramPath = "",
IsSubProgramSummary = true,
SubProgramName = ""
});
Log.Success($"子程序 [{step.Name}] 总结行已写入");
}
if (SubSingleStep)
{
SubSingleStep = false;
ToolBar.Instance.SingleStep = true;
}
}
else if (step.Method != null)
{
Log.Info($"开始执行指令 [ {step.Index} ] [ {step.Method!.FullName}.{step.Method.Name} ] ", depth);
await ExecuteMethodStep(step, tmpParameters, depth, cancellationToken, subProgramPath); // 传递路径
bool stepSuccess = step.Result == 1;
overallSuccess &= stepSuccess;
if (step.NGGotoStepID != null && !stepSuccess)
{
var tmp = program.StepCollection.FirstOrDefault(x => x.ID == step.NGGotoStepID);
if (tmp != null)
{
index = tmp.Index - 2;
Log.Info($"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth);
}
}
if (step.OKGotoStepID != null && stepSuccess)
{
var tmp = program.StepCollection.FirstOrDefault(x => x.ID == step.OKGotoStepID);
if (tmp != null)
{
index = tmp.Index - 2;
Log.Info($"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth);
}
}
}
index++;
if (ToolBar.Instance.SingleStep||step.isBrokenpoint==true)
{
ToolBar.Instance.IsStop = true;
ToolBar.Instance.RunState = "运行";
ToolBar.Instance.RunIcon = PackIconKind.Play;
}
}
}
bool finalResult = loopStack.Count == 0 && overallSuccess;
if (depth > 0) // 子程序
{
return finalResult;
}
return finalResult;
}
public static async Task ExecuteMethodStep(StepModel step, Dictionary<Guid, ParameterModel> parameters, int depth, CancellationToken cancellationToken = default, string subProgramPath = "")
{
try
{
MainWindow.Instance.SelectedStep = null;
await Task.Delay(SystemConfig.Instance.PerformanceLevel);
// 1. 查找类型
Type? targetType = null;
foreach (var assembly in CommandTreeView.Instance!.Assemblies)
{
targetType = assembly.GetType(step.Method!.FullName!);
if (targetType != null) break;
}
if (targetType == null)
{
Log.Error($"指令 [ {step.Index} ] 执行错误:未找到类型 {step.Method!.FullName}", depth);
step.Result = 2;
}
// 2. 创建实例(仅当方法不是静态时才需要)
object? instance = null;
bool isStaticMethod = false;
// 3. 准备参数
var inputParams = new List<object?>();
var paramTypes = new List<Type>();
ParameterModel? outputParam = null;
foreach (var param in step.Method!.Parameters)
{
if (param.Category == ParameterCategory.Input)
{
if (param.Type == typeof(CancellationToken))
{
inputParams.Add(stepCTS.Token);
paramTypes.Add(param.Type!);
continue;
}
var actualValue = param.GetActualValue(tmpParameters);
// 类型转换处理
if (actualValue != null)
{
if (string.IsNullOrEmpty(actualValue.ToString()))
{
actualValue = null;
}
if (actualValue != null && param.Type != null && actualValue.GetType() != param.Type)
{
try
{
if (param.Type.IsArray)
{
// --- 原有的数组处理逻辑 ---
// 获取数组元素类型
Type elementType = param.Type.GetElementType()!;
// 解析字符串为字符串数组
string[] stringArray = actualValue.ToString()!
.Trim('[', ']')
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.ToArray();
// 创建目标类型数组
Array array = Array.CreateInstance(elementType, stringArray.Length);
// 转换每个元素
for (int i = 0; i < stringArray.Length; i++)
{
try
{
// 特殊处理字符串类型
if (elementType == typeof(string))
{
array.SetValue(stringArray[i], i);
}
// 特殊处理枚举类型
else if (elementType.IsEnum)
{
array.SetValue(Enum.Parse(elementType, stringArray[i]), i);
}
// 常规类型转换
{
if (stringArray[i] is string s && s.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
// 先转成整数
var intValue = Convert.ToInt64(s, 16);
// 再转成目标类型
array.SetValue(Convert.ChangeType(intValue, elementType), i);
}
else
{
array.SetValue(Convert.ChangeType(stringArray[i], elementType), i);
}
}
}
catch
{
throw new InvalidCastException($"指令 [ {step.Index} ] 执行错误:元素 '{stringArray[i]}' 无法转换为 {elementType.Name}[]");
}
}
actualValue = array;
}
// --- 修改:处理 Type 为 object 且 Value 为字符串的情况 ---
else if (param.Type == typeof(object) && actualValue is string strValue)
{
// 尝试将字符串解析为常见的数组类型
object parsedArray = TryParseStringToCommonArrays(strValue);
if (parsedArray != null)
{
actualValue = parsedArray; // 使用解析出的数组
}
// 如果解析失败 (parsedArray is null)actualValue 保持原字符串值
// 让后续的 Convert.ChangeType 尝试处理 (通常会失败,但这是预期的)
}
// --- 原有的非数组类型处理逻辑 ---
else if (param.Type.BaseType == typeof(Enum))
{
actualValue = Enum.Parse(param.Type, actualValue.ToString()!);
}
else if(actualValue is string s && s.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
// 先转成整数
var intValue = Convert.ToInt64(s, 16);
// 再转成目标类型
actualValue = Convert.ChangeType(intValue, param.Type);
}
else
{
actualValue = Convert.ChangeType(actualValue, param.Type);
}
}
catch (Exception ex)
{
Log.Warning($"指令 [ {step.Index} ] 执行错误:参数 {param.Name} 类型转换失败: {ex.Message}", depth);
// 可以选择在此处记录错误并设置 step.Result或者继续执行
// 这里我们选择继续,但实际应用中可能需要更严格的错误处理
}
}
}
inputParams.Add(actualValue);
paramTypes.Add(param.Type!);
}
else if (param.Category == ParameterCategory.Output)
{
outputParam = param;
}
}
// 4. 获取方法
var method = targetType!.GetMethod(
step.Method.Name!,
BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance,
null,
paramTypes.ToArray(),
null
);
if (method == null)
{
Log.Error($"指令 [ {step.Index} ] 执行错误:未找到方法{step.Method.Name}", depth);
step.Result = 2;
}
// 检查是否是静态方法
isStaticMethod = method!.IsStatic;
// 如果是实例方法,需要创建实例
if (!isStaticMethod)
{
try
{
instance = Activator.CreateInstance(targetType);
}
catch (Exception ex)
{
Log.Error($"指令 [ {step.Index} ] 执行错误:创建实例失败 - {ex.Message}", depth);
step.Result = 2;
}
}
// 5. 执行方法
object? returnValue = method.Invoke(instance, inputParams.ToArray());
try
{
// 处理异步方法
if (returnValue is Task task)
{
await task.ConfigureAwait(false);
// 获取结果如果是Task<T>
if (task.GetType().IsGenericType)
{
var returnValueProperty = task.GetType().GetProperty("Result");
returnValue = returnValueProperty?.GetValue(task);
}
else
{
returnValue = null;
}
}
// 处理VoidTaskreturnValue类型
if (returnValue != null && returnValue.GetType().FullName == "System.Threading.Tasks.VoidTaskreturnValue")
{
returnValue = null;
}
}
catch (OperationCanceledException)
{
return;
}
catch (Exception ex)
{
Log.Error($"指令 [ {step.Index} ] 执行错误: {ex.InnerException?.Message ?? ex.Message}", depth);
step.Result = 2;
//给报告添加子程序执行结果
ReportModelList.ReportList.Add(new ReportModel
{
stepModel = step,
User = MainWindow.Instance.User.UserName,
ExcuteTime = DateTime.Now,
IsPass = step.Result == 1 ? IsPass.PASS : IsPass.FAIL,
Result = $"指令 [ {step.Index} ] 执行错误: {ex.InnerException?.Message ?? ex.Message}"
});
Log.Success($"指令 [ {step.Index} ]报告已写入");
return;
}
// 6. 处理输出
bool paraResult = true;
string resultMsg = "";
if (outputParam != null)
{
outputParam.Value = returnValue;
var currentPara = outputParam.GetCurrentParameter(tmpParameters);
if (currentPara != null)
{
currentPara.Value = returnValue;
var tmp = currentPara.GetResult();
currentPara.Result = tmp.Item1;
if (tmp.Item2 != null)
{
Log.Warning(tmp.Item2);
}
if (currentPara.IsSave && MainWindow.Instance.Program.Parameters.FirstOrDefault(x => x.ID == currentPara.ID) != null)
{
_ = SaveDataToDatabase(MainWindow.Instance.Program.ID, currentPara);
}
}
var returnType = returnValue?.GetType();
if (returnType != null)
{
if (!returnType.IsArray)
{
Log.Success($"输出 [ {outputParam.Name} ] = {returnValue} ({returnType.Name})", depth);
// 只有勾选了IsOutputToReport才记录到报告的Result中
if (outputParam.IsOutputToReport)
{
resultMsg = $"输出 [ {outputParam.Name} ] = {returnValue} ({returnType.Name})";
}
}
else
{
if (returnValue is IEnumerable enumerable)
{
var elements = enumerable.Cast<object>().Select(item => item?.ToString() ?? "null");
Log.Success($"输出 [ {outputParam.Name} ] = [ {string.Join(", ", elements)} ] ({returnType.Name})", depth);
// 只有勾选了IsOutputToReport才记录到报告的Result中
if (outputParam.IsOutputToReport)
{
resultMsg = $"输出 [ {outputParam.Name} ] = [ {string.Join(", ", elements)} ] ({returnType.Name})";
}
}
}
}
}
Log.Success($"指令 [ {step.Index} ] 执行成功", depth);
UpdateCurrentStepResult(step, paraResult: paraResult, depth: depth);
if (ReportModelList.ReportList == null)
{
ReportModelList.ReportList = new List<ReportModel>();
}
// 始终写入报告但Result内容根据输出参数的IsOutputToReport控制
ReportModelList.ReportList.Add(new ReportModel
{
stepModel = step,
User = MainWindow.Instance.User.UserName,
ExcuteTime = DateTime.Now,
IsPass = step.Result == 1 ? IsPass.PASS : IsPass.FAIL,
Result = resultMsg, // resultMsg只在IsOutputToReport=true时才有值
SubProgramPath = subProgramPath
});
Log.Success($"指令 [ {step.Index} ]报告已写入");
}
catch (OperationCanceledException)
{
return;
}
catch (Exception ex)
{
Log.Error($"指令 [ {step.Index} ] 执行错误: {ex.InnerException?.Message ?? ex.Message}", depth);
step.Result = 2;
return;
}
}
public static void ResetAllStepStatus(ProgramModel program)
{
foreach (var step in program.StepCollection)
{
step.Result = -1;
step.RunTime = null;
}
}
private static void UpdateCurrentStepResult(StepModel step, bool paraResult = true, bool stepResult = true, int depth = 0)
{
if (stepResult && paraResult)
{
if (string.IsNullOrEmpty(step.OKExpression))
{
step.Result = 1;
}
else
{
Dictionary<string, object> paraDic = [];
foreach (var item in tmpParameters)
{
paraDic.TryAdd(item.Value.Name, item.Value.Value!);
}
if (step.SubProgram != null)
{
foreach (var item in step.SubProgram.Parameters.Where(x => x.Category == ParameterCategory.Output))
{
paraDic.TryAdd(item.Name, item.Value!);
}
}
else if (step.Method != null)
{
foreach (var item in step.Method.Parameters.Where(x => x.Category == ParameterCategory.Output))
{
paraDic.TryAdd(item.Name, item.Value!);
}
}
bool re = ExpressionEvaluator.EvaluateExpression(step.OKExpression, paraDic);
step.Result = re ? 1 : 2;
if (step.Result == 2)
{
Log.Warning($"指令 [ {step.Index} ] FAIL:条件表达式验证失败", depth);
}
}
}
else
{
if (!paraResult)
{
Log.Warning("参数限值校验失败", depth);
}
step.Result = 2;
}
}
private static async Task SaveDataToDatabase(Guid programID, ParameterModel currentPara)
{
using (ATS_DB db = new())
{
db.TestData.Add(new()
{
ProgramID = programID,
TestRoundID = TestRoundID,
ParameterID = currentPara.ID,
Value = currentPara.Value?.ToString() ?? "",
LowerLimit = currentPara.LowerLimit?.ToString(),
UpperLimit = currentPara.UpperLimit?.ToString(),
Result = currentPara.Result
});
try
{
var row = await db.SaveChangesAsync().ConfigureAwait(false);
if (row == 0)
{
Log.Error($"测试数据保存失败:未知的错误");
}
}
catch (Exception ex)
{
Log.Error($"测试数据保存失败{ex.InnerException}");
}
}
}
// 添加一个辅助方法来尝试解析字符串为不同类型的数组
private static object TryParseStringToCommonArrays(string strValue)
{
// 尝试解析常见的数组格式
strValue = strValue.Trim('[', ']');
string[] parts = System.Text.RegularExpressions.Regex.Split(strValue, @"[,\s]+").Where(s => !string.IsNullOrEmpty(s)).ToArray();
if (parts.Length == 0) return null; // 空数组或无效格式
//尝试解析为byte
if (parts.Length == 1)
{
if (byte.TryParse(parts[0], System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out byte singleByteValue))
{
return singleByteValue;
}
}
// --- 新增:先尝试解析为 byte[] (十六进制) ---
if (TryParseStringArrayToType(parts, out byte[] hexByteResult, s => byte.TryParse(s, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out _)))
{
return hexByteResult;
}
// --- 新增:再尝试解析为 byte[] (十进制) ---
if (TryParseStringArrayToType(parts, out byte[] decByteResult, s => byte.TryParse(s, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out _)))
{
return decByteResult;
}
// --- 原有的其他类型解析逻辑 ---
// 尝试解析为 int[]
if (TryParseStringArrayToType(parts, out int[] intResult, s => int.TryParse(s, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out _)))
{
return intResult;
}
// 尝试解析为 double[]
if (TryParseStringArrayToType(parts, out double[] doubleResult, s => double.TryParse(s, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out _)))
{
return doubleResult;
}
// 尝试解析为 string[] (处理带引号的字符串)
string[] originalParts = parts; // 保留原始分割结果
string[] stringParts = originalParts.Select(s => s.Trim('\"', '\'')).ToArray();
bool allPartsAreNonNumericStrings = stringParts.All(s => !double.TryParse(s, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out _));
if (allPartsAreNonNumericStrings && stringParts.All(s => !string.IsNullOrEmpty(s)))
{
return stringParts;
}
// 尝试解析为 float[]
if (TryParseStringArrayToType(parts, out float[] floatResult, s => float.TryParse(s, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out _)))
{
return floatResult;
}
// 尝试解析为 short[]
if (TryParseStringArrayToType(parts, out short[] shortResult, s => short.TryParse(s, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out _)))
{
return shortResult;
}
// 尝试解析为 long[]
if (TryParseStringArrayToType(parts, out long[] longResult, s => long.TryParse(s, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out _)))
{
return longResult;
}
// 尝试解析为 uint[]
if (TryParseStringArrayToType(parts, out uint[] uintResult, s => uint.TryParse(s, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out _)))
{
return uintResult;
}
// 尝试解析为 ushort[]
if (TryParseStringArrayToType(parts, out ushort[] ushortResult, s => ushort.TryParse(s, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out _)))
{
return ushortResult;
}
// 尝试解析为 ulong[]
if (TryParseStringArrayToType(parts, out ulong[] ulongResult, s => ulong.TryParse(s, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out _)))
{
return ulongResult;
}
// 尝试解析为 decimal[]
if (TryParseStringArrayToType(parts, out decimal[] decimalResult, s => decimal.TryParse(s, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out _)))
{
return decimalResult;
}
// 尝试解析为 bool[]
if (TryParseStringArrayToType(parts, out bool[] boolResult, s => bool.TryParse(s, out _)))
{
return boolResult;
}
// 如果以上所有尝试都失败,返回 null
return null;
}
// 一个通用的辅助方法,用于尝试将字符串数组解析为特定类型的数组
private static bool TryParseStringArrayToType<T>(string[] parts, out T[] result, Func<string, bool> tryParseFunc)
{
result = null;
T[] tempArray = new T[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
if (!tryParseFunc(parts[i]))
{
return false; // 解析失败
}
// 使用 Convert.ChangeType 确保类型安全
try
{
tempArray[i] = (T)Convert.ChangeType(parts[i], typeof(T), System.Globalization.CultureInfo.InvariantCulture);
}
catch
{
return false; // 转换失败
}
}
result = tempArray;
return true; // 解析成功
}
#region
private class LoopContext
{
public int LoopCount { get; set; }
public int CurrentLoop { get; set; }
public int StartIndex { get; set; }
public StepModel? LoopStartStep { get; set; }
}
#endregion
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ATS.Models
{
public class CANSignalModel
{
public Guid CatchID { get; set; }//采集ID
public byte Channel { get; set; }//通道
public int MessageID { get; set; }//报文ID
public string? MessageName { get; set; }//报文名称
public string? SignalName { get; set; }//信号名称
public TimeSpan LogInterval { get; set; }//记录间隔
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ATS.Models
{
public class DeviceConnectSettingModel
{
public string Name { get; set; } = "";
public string Value { get; set; } = "";
}
}

54
ATS/Models/DeviceModel.cs Normal file
View File

@ -0,0 +1,54 @@
using Newtonsoft.Json;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Tasks;
namespace ATS.Models
{
[AddINotifyPropertyChangedInterface]
public class DeviceModel
{
public DeviceModel()
{
}
public DeviceModel(DeviceModel source)
{
ID = source.ID;
ParameterID = source.ParameterID;
Name = source.Name;
Connected = source.Connected;
ErrorMessage = source.ErrorMessage;
Type = source.Type;
ConnectString = source.ConnectString;
CommunicationProtocol = source.CommunicationProtocol;
Description = source.Description;
}
public Guid ID { get; set; } = Guid.NewGuid();
public Guid ParameterID { get; set; }
public string Name { get; set; } = "";
[JsonIgnore]
public bool Connected { get; set; }
[JsonIgnore]
public string? ErrorMessage { get; set; }
public string Type { get; set; } = "Tcp";
public string ConnectString { get; set; } = "";
[JsonIgnore]
public object? CommunicationProtocol { get; set; }
public string Description { get; set; } = "";
}
}

42
ATS/Models/MethodModel.cs Normal file
View File

@ -0,0 +1,42 @@
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ATS.Models
{
[AddINotifyPropertyChangedInterface]
public class MethodModel
{
#region
public MethodModel()
{
}
public MethodModel(MethodModel source)
{
if (source == null) return;
Name = source.Name;
FullName = source.FullName;
// 深拷贝参数
Parameters = new ObservableCollection<ParameterModel>(
source.Parameters.Select(p => new ParameterModel(p)));
}
#endregion
public string? Name { get; set; }
public string? FullName { get; set; }
public ObservableCollection<ParameterModel> Parameters { get; set; } = [];
}
}

View File

@ -0,0 +1,287 @@
using Newtonsoft.Json;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ATS.Models
{
[AddINotifyPropertyChangedInterface]
public class ParameterModel
{
#region
public ParameterModel()
{
}
public ParameterModel(ParameterModel source)
{
if (source == null) return;
ID = source.ID;
Name = source.Name;
Type = source.Type;
Category = source.Category;
IsUseVar = source.IsUseVar;
IsSave = source.IsSave;
VariableName = source.VariableName;
VariableID = source.VariableID;
IsGlobal = source.IsGlobal;
Value = source.Value;
LowerLimit = source.LowerLimit;
UpperLimit = source.UpperLimit;
IsOutputToReport = source.IsOutputToReport;
}
#endregion
public Guid ID { get; set; } = Guid.NewGuid();
public bool IsVisible { get; set; } = true;
public string Name { get; set; }
public Type? Type { get; set; } = typeof(string);
public ParameterCategory Category { get; set; } = ParameterCategory.Temp;
public bool IsGlobal { get; set; }
public object? Value { get; set; }
public object? LowerLimit { get; set; }
public object? UpperLimit { get; set; }
public bool Result { get; set; } = true;
public bool IsUseVar { get; set; }
public bool IsSave { get; set; }
public string? VariableName { get; set; }
public Guid? VariableID { get; set; }
/// <summary>
/// 是否输出到报告仅对输出参数有效默认为false
/// </summary>
public bool IsOutputToReport { get; set; } = false;
public enum ParameterCategory
{
Input,
Output,
Temp
}
public object? GetActualValue(Dictionary<Guid, ParameterModel> paraList)
{
// 用于检测循环引用的哈希集合
HashSet<Guid> visitedIds = [];
ParameterModel current = this;
while (current != null)
{
// 如果没有使用变量,则返回值
if (!current.IsUseVar)
{
return current.Value;
}
// 检测循环引用
if (visitedIds.Contains(current.ID))
{
return null;
}
visitedIds.Add(current.ID);
// 如果当前参数没有关联变量,则返回其实际值
if (current.VariableID == null)
{
if (Type != null && Value != null)
{
try
{
return Convert.ChangeType(Value, Type);
}
catch
{
return Value;
}
}
}
// 查找下一个关联参数
ParameterModel? next = paraList[(Guid)current.VariableID!];
if (next == null)
{
return null;
}
current = next;
}
return null;
}
/// <summary>
/// 获取方法步骤中的返回值映射的主程序参数
/// </summary>
/// <param name="paraList"></param>
/// <returns></returns>
public ParameterModel? GetCurrentParameter(Dictionary<Guid, ParameterModel> paraList)
{
// 用于检测循环引用的哈希集合
HashSet<Guid> visitedIds = new HashSet<Guid>();
ParameterModel current = this;
while (current != null)
{
// 如果没有使用变量,则返回值
if (current.VariableID == null)
{
return current;
}
// 检测循环引用
if (visitedIds.Contains(current.ID))
{
return null;
}
visitedIds.Add(current.ID);
// 如果当前参数没有关联变量,则返回其实际值
if (current.VariableID == null)
{
if (Type != null && Value != null)
{
try
{
return current;
}
catch
{
return null;
}
}
}
// 查找下一个关联参数
ParameterModel? next = paraList[(Guid)current.VariableID!];
if (next == null)
{
return null;
}
current = next;
}
return null;
}
public (bool, string?) GetResult()
{
if (Type == typeof(string) && (!string.IsNullOrWhiteSpace(LowerLimit?.ToString()) || !string.IsNullOrWhiteSpace(UpperLimit?.ToString())))
{
return (true, $"参数 [ {Name}({Type}) ] 不可比较");
}
// 当值或限制条件不存在时返回true
if (Value == null || (LowerLimit == null && UpperLimit == null))
{
return (true, null);
}
// 检查空字符串情况
if (string.IsNullOrWhiteSpace(Value?.ToString()) || (string.IsNullOrWhiteSpace(LowerLimit?.ToString()) && string.IsNullOrWhiteSpace(UpperLimit?.ToString())))
{
return (true, null);
}
// 尝试将值转换为可比较的类型
try
{
object? comparableValue = ConvertToComparable(Value);
object? comparableLower = LowerLimit != null ? ConvertToComparable(LowerLimit) : null;
object? comparableUpper = UpperLimit != null ? ConvertToComparable(UpperLimit) : null;
if (comparableValue == null)
{
return (true, $"参数 [ {Name}({Type}) ] 不可比较");
}
bool lowerValid = true;
bool upperValid = true;
// 处理下限比较
if (comparableLower != null)
{
lowerValid = CompareValues(comparableValue, comparableLower) >= 0;
}
// 处理上限比较
if (comparableUpper != null)
{
upperValid = CompareValues(comparableValue, comparableUpper) <= 0;
}
return (lowerValid && upperValid, null);
}
catch (Exception ex)
{
return (true, $"参数 [ {Name}({Type}) ] 上下限比较失败:{ex.Message}");
}
}
private static object? ConvertToComparable(object value)
{
if (value is IConvertible convertible)
{
// 尝试转换为最适合比较的类型
try
{
return convertible.ToDouble(null);
}
catch
{
}
try
{
return convertible.ToDateTime(null);
}
catch
{
}
}
return null;
}
private static int CompareValues(object a, object b)
{
// 优先尝试数值比较
if (a is double aDouble && b is double bDouble)
{
return aDouble.CompareTo(bDouble);
}
// 尝试日期比较
if (a is DateTime aDate && b is DateTime bDate)
{
return aDate.CompareTo(bDate);
}
return 0;
}
}
}

View File

@ -0,0 +1,41 @@
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace ATS.Models
{
[AddINotifyPropertyChangedInterface]
public class ProgramModel
{
#region
public ProgramModel()
{
}
public ProgramModel(ProgramModel source)
{
ID = source.ID;
StepCollection = new(source.StepCollection.Select(p => new StepModel(p)));
Devices = new(source.Devices.Select(p => new DeviceModel(p)));
Parameters = new(source.Parameters.Select(p => new ParameterModel(p)));
}
#endregion
public Guid ID { get; set; } = Guid.NewGuid();
public ObservableCollection<StepModel> StepCollection { get; set; } = [];
public ObservableCollection<ParameterModel> Parameters { get; set; } = [];
public ObservableCollection<DeviceModel> Devices { get; set; } = [];
}
}

54
ATS/Models/ReportModel.cs Normal file
View File

@ -0,0 +1,54 @@
using NPOI.SS.Formula.Functions;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ATS.Models
{
public enum IsPass
{
PASS = 0,
FAIL = 1,
NULL = 2
}
[AddINotifyPropertyChangedInterface]
public class ReportModel
{
//步骤
public StepModel? stepModel { get; set; }
//执行人
public string? User { get; set; } = "";
//执行时间
public DateTime? ExcuteTime { get; set; }
//是否通过
public IsPass? IsPass { get; set; }
//结果
public string? Result { get; set; }
// 新增:记录所属子程序路径
public string? SubProgramPath { get; set; } = "";
// 新增:标记是否为子程序总结行
public bool? IsSubProgramSummary { get; set; } = false;
// 可选:子程序名称(用于总结行)
public string? SubProgramName { get; set; } = "";
}
public static class ReportModelList
{
public static List<ReportModel> ReportList { get; set; } = new();
}
}

106
ATS/Models/StepModel.cs Normal file
View File

@ -0,0 +1,106 @@
using Newtonsoft.Json;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ATS.Models
{
[AddINotifyPropertyChangedInterface]
public class StepModel
{
#region
public StepModel()
{
}
public StepModel(StepModel source)
{
if (source == null) return;
ID = source.ID;
Index = source.Index;
Name = source.Name;
StepType = source.StepType;
LoopCount = source.LoopCount;
LoopStartStepId = source.LoopStartStepId;
OKExpression = source.OKExpression;
OKGotoStepID = source.OKGotoStepID;
NGGotoStepID = source.NGGotoStepID;
Description = source.Description;
IsUsed = source.IsUsed;
if (source.Method != null)
{
Method = new(source.Method);
}
if (source.SubProgram != null)
{
SubProgram = new(source.SubProgram);
}
}
#endregion
public Guid? ID { get; set; } = Guid.NewGuid();
public bool IsUsed { get; set; } = true;
public int Index { get; set; }
public string? Name { get; set; }
public string? StepType { get; set; }
public MethodModel? Method { get; set; }
public ProgramModel? SubProgram { get; set; }
/// <summary>
/// 仅LoopStart使用
/// </summary>
public int? LoopCount { get; set; }
/// <summary>
/// 运行时循环计数
/// </summary>
[JsonIgnore]
public int? CurrentLoopCount { get; set; }
/// <summary>
/// 仅LoopEnd使用关联LoopStart
/// </summary>
public Guid? LoopStartStepId { get; set; }
/// <summary>
/// 初始:-1 运行中0 成功1 异常2
/// </summary>
[JsonIgnore]
public int Result { get; set; } = -1;
[JsonIgnore]
public int? RunTime { get; set; }
public string? OKExpression { get; set; }
/// <summary>
/// 默认为0/0不跳转第一位为OK跳转的步骤序号第二位为NG跳转的步骤序号
/// </summary>
public string GotoSettingString { get; set; } = "";
public Guid? OKGotoStepID { get; set; }
public Guid? NGGotoStepID { get; set; }
public string? Description { get; set; }
[JsonIgnore]
public bool? isBrokenpoint { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ATS.Models
{
public class SubProgramItem
{
public string Name { get; set; } = "";
public string FilePath { get; set; } = "";
}
}

45
ATS/Models/UserModel.cs Normal file
View File

@ -0,0 +1,45 @@
using Newtonsoft.Json;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ATS.Models
{
[AddINotifyPropertyChangedInterface]
public class UserModel
{
public UserModel()
{
}
public UserModel(UserModel source)
{
UserId = source.UserId;
UserName = source.UserName;
UserAccount = source.UserAccount;
PassWord = source.PassWord;
Role = source.Role;
}
public Guid UserId { get; set; } = Guid.NewGuid();
public string UserName { get; set; } = "";
public string UserAccount { get; set; } = "";
public string PassWord { get; set; }
/// <summary>
/// 0用户1管理员2超级管理员
/// </summary>
public int Role { get; set; } = 0;
public DateTime LoginTime { get; set; }
public int LoginCount { get; set; }
}
}

80
ATS/PreDefineDevices.json Normal file
View File

@ -0,0 +1,80 @@
[
{
"Name": "示例AC负载-WS_68070",
"Type": "ModbusTcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.0.0\"},{\"Name\":\"端口号\",\"Value\":\"502\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "ModbusTcp连接示例"
},
{
"Name": "示例直流负载-EA_PSI10080",
"Type": "ModbusTcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.0.0\"},{\"Name\":\"端口号\",\"Value\":\"502\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "ModbusTcp连接示例"
},
{
"Name": "示例高压双向直流源-PSB11500_60",
"Type": "ModbusTcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.0.0\"},{\"Name\":\"端口号\",\"Value\":\"502\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "ModbusTcp连接示例"
},
{
"Name": "示例IO板卡-IOBoardCard",
"Type": "ModbusTcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.0.0\"},{\"Name\":\"端口号\",\"Value\":\"502\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "ModbusTcp连接示例"
},
{
"Name": "示波器-MDO34",
"Type": "Tcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.5.5\"},{\"Name\":\"端口号\",\"Value\":\"4000\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "预配置MDO34示波器"
},
{
"Name": "示例功率分析仪-PW8001",
"Type": "Tcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.0.0\"},{\"Name\":\"端口号\",\"Value\":\"502\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "Tcp连接示例"
},
{
"Name": "示例AC源-SQ0090G1D1",
"Type": "Tcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.0.0\"},{\"Name\":\"端口号\",\"Value\":\"502\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "Tcp连接示例"
},
{
"Name": "示例AC源-AFV33030",
"Type": "Tcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.0.0\"},{\"Name\":\"端口号\",\"Value\":\"502\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "Tcp连接示例"
},
{
"Name": "示例温度采集仪-DAQ970A",
"Type": "Tcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.0.0\"},{\"Name\":\"端口号\",\"Value\":\"502\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "Tcp连接示例"
},
{
"Name": "示例信号发生器-KeySight_33509B",
"Type": "Tcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.0.0\"},{\"Name\":\"端口号\",\"Value\":\"502\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "Tcp连接示例"
},
{
"Name": "示例低压直流源-E36233A",
"Type": "Tcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.0.0\"},{\"Name\":\"端口号\",\"Value\":\"502\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "Tcp连接示例"
},
{
"Name": "示例直流源-NGI_N3410",
"Type": "Tcp",
"ConnectString": "[{\"Name\":\"IP地址\",\"Value\":\"192.168.0.0\"},{\"Name\":\"端口号\",\"Value\":\"502\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "Tcp连接示例"
},
{
"Name": "示例冷水机-LQ7500_D",
"Type": "ModbusRtu_Serial",
"ConnectString": "[{\"Name\":\"COM口\",\"Value\":\"COM5\"},{\"Name\":\"波特率\",\"Value\":\"19200\"},{\"Name\":\"数据位\",\"Value\":\"8\"},{\"Name\":\"停止位\",\"Value\":\"1\"},{\"Name\":\"奇偶\",\"Value\":\"无\"},{\"Name\":\"读超时\",\"Value\":\"3000\"},{\"Name\":\"写超时\",\"Value\":\"3000\"}]",
"Description": "ModbusRtu_Serial连接示例"
}
]

101
ATS/SystemConfig.cs Normal file
View File

@ -0,0 +1,101 @@
using ATS.Tools;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Reflection;
namespace ATS
{
public class SystemConfig
{
private static readonly object _lock = new();
private static SystemConfig? _instance;
public static SystemConfig Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
_instance = new();
_instance.LoadFromFile();
}
return _instance;
}
}
}
[JsonIgnore]
public string SystemPath { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ATS系统");
public int PerformanceLevel { get; set; } = 50;
public string LogFilePath { get; set; } = @"D:\ATS\日志\";
public string DLLFilePath { get; set; } = @"D:\ATS\指令\";
public string SubProgramFilePath { get; set; } = @"D:\ATS\子程序\";
public string PreDefineDevicesPath { get; set; } = @"D:\ATS\设备预设\PreDefineDevices.json";
public string DBCFilePath { get; set; } = @"D:\ATS\DBC文件\ZSDB123500_HY11_HS11_800V_ADCU20_HVEnergeCAN_230901_Fit.dbc";
public string DefaultSubProgramFilePath { get; set; } = "";
// 配置加载方法
public void LoadFromFile()
{
string configPath = Path.Combine(SystemPath, "system.config");
if (!File.Exists(configPath))
{
string json = JsonConvert.SerializeObject(Instance, Formatting.Indented);
File.WriteAllText(configPath, json);
return;
}
try
{
string json = File.ReadAllText(configPath);
var loadedConfig = JsonConvert.DeserializeObject<SystemConfig>(json);
// 复制所有可写属性排除JsonIgnore属性
PropertyInfo[] properties = typeof(SystemConfig).GetProperties();
foreach (var prop in properties)
{
if (prop.CanWrite && !Attribute.IsDefined(prop, typeof(JsonIgnoreAttribute)))
{
prop.SetValue(this, prop.GetValue(loadedConfig));
}
}
}
catch (Exception ex)
{
Log.Error($"配置加载失败: {ex.Message}");
}
}
public void SaveToFile()
{
lock (_lock)
{
try
{
if (!Directory.Exists(SystemPath))
Directory.CreateDirectory(SystemPath);
string configPath = Path.Combine(SystemPath, "system.config");
string json = JsonConvert.SerializeObject(this, Formatting.Indented);
File.WriteAllText(configPath, json);
Log.Info("系统配置已保存。");
}
catch (Exception ex)
{
Log.Error($"配置保存失败: {ex.Message}");
}
}
}
}
}

View File

@ -0,0 +1,195 @@
using DocumentFormat.OpenXml.Spreadsheet;
using Microsoft.Win32;
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using VerticalAlignment = NPOI.SS.UserModel.VerticalAlignment;
namespace ATS.Tools
{
public static class DataGridExportToExcel
{
public static DataTable DataGridToDataTable(DataGrid dataGrid)
{
DataTable dt = new DataTable();
// 添加列(仅可见列)
foreach (var column in dataGrid.Columns.Where(c => c.Visibility == Visibility.Visible))
{
dt.Columns.Add(column.Header.ToString());
}
// 提取数据
foreach (var item in dataGrid.Items)
{
DataRow row = dt.NewRow();
int colIndex = 0;
foreach (var column in dataGrid.Columns.Where(c => c.Visibility == Visibility.Visible))
{
if (column is DataGridBoundColumn boundColumn)
{
// 新增强型数据提取逻辑
row[colIndex] = GetBoundColumnValue(boundColumn, item);
}
else
{
// 其他列类型处理
row[colIndex] = GetCellContentValue(column, item);
}
colIndex++;
if (colIndex > dt.Columns.Count - 1)
{
break;
}
}
dt.Rows.Add(row);
}
return dt;
}
private static string GetBoundColumnValue(DataGridBoundColumn column, object item)
{
try
{
// 获取绑定路径
var bindingPath = (column.Binding as Binding)?.Path.Path;
if (string.IsNullOrEmpty(bindingPath)) return "";
// 支持嵌套属性(如 "Address.City"
var value = item;
foreach (var prop in bindingPath.Split('.'))
{
var propInfo = value?.GetType().GetProperty(prop);
value = propInfo?.GetValue(value);
}
return value?.ToString() ?? "";
}
catch
{
return "";
}
}
private static string? GetCellContentValue(DataGridColumn column, object item)
{
// 备选方案尝试通过UI元素获取内容
try
{
var content = column.GetCellContent(item);
return content switch
{
TextBlock tb => tb.Text,
CheckBox cb => cb.IsChecked.ToString(),
ComboBox cmb => cmb.SelectedValue?.ToString(),
_ => content?.ToString() ?? ""
};
}
catch
{
return "";
}
}
private static string? OpenExcelSaveFileDialog(string? excelTitle = null)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "Excel Files(*.xls)|*.xls|Excel Files(*.xlsx)|*.xlsx";
saveFileDialog.Title = $"导出{excelTitle}为表格";
saveFileDialog.ShowDialog();
if (!string.IsNullOrEmpty(saveFileDialog.FileName))
{
return saveFileDialog.FileName;
}
else
{
return null;
}
}
public static void ExportDataGridToExcel(DataGrid dataGrid, string? title = null)
{
try
{
string? filePath = OpenExcelSaveFileDialog(title);
if (!string.IsNullOrEmpty(filePath))
{
IWorkbook workbook = new HSSFWorkbook();
ISheet sheet = workbook.CreateSheet(title ?? "sheet1");
DataTable dataTable = DataGridToDataTable(dataGrid);
IRow headerRow = sheet.CreateRow(0);
// 创建可重用的单元格样式
ICellStyle headerStyle = workbook.CreateCellStyle();
headerStyle.VerticalAlignment = VerticalAlignment.Center;
// 创建数据单元格基础样式
ICellStyle dataCellStyle = workbook.CreateCellStyle();
dataCellStyle.VerticalAlignment = VerticalAlignment.Center;
// 创建带自动换行的样式
ICellStyle wrappedCellStyle = workbook.CreateCellStyle();
wrappedCellStyle.VerticalAlignment = VerticalAlignment.Center;
wrappedCellStyle.WrapText = true;
// 创建表头
for (int i = 0; i < dataTable.Columns.Count; i++)
{
ICell cell = headerRow.CreateCell(i);
cell.SetCellValue(dataTable.Columns[i].ColumnName);
cell.CellStyle = headerStyle; // 重用表头样式
}
// 填充数据行
for (int i = 0; i < dataTable.Rows.Count; i++)
{
IRow row = sheet.CreateRow(i + 1);
for (int j = 0; j < dataTable.Columns.Count; j++)
{
ICell cell = row.CreateCell(j);
var value = dataTable.Rows[i][j]?.ToString() ?? string.Empty;
cell.SetCellValue(value);
// 根据内容决定使用哪种样式
if (value.Contains("\n"))
{
cell.CellStyle = wrappedCellStyle; // 重用带换行的样式
}
else
{
cell.CellStyle = dataCellStyle; // 重用基础样式
}
}
}
// 自动调整列宽
for (int i = 0; i < dataTable.Columns.Count; i++)
{
sheet.AutoSizeColumn(i);
}
// 保存文件
using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
workbook.Write(fileStream);
MessageBox.Show($"{title}表格导出成功");
}
}
}
catch (Exception ex)
{
MessageBox.Show($"{title}表格导出失败:{ex.Message}");
}
}
}
}

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NCalc;
namespace ATS.Tools
{
public class ExpressionEvaluator
{
public static bool EvaluateExpression(string expression, Dictionary<string, object>? variables = null)
{
try
{
// 预处理:替换中文变量名为英文别名
var (processedExpression, processedVariables) = PreprocessExpression(expression, variables);
var expr = new Expression(processedExpression, EvaluateOptions.IgnoreCase);
if (processedVariables != null)
{
foreach (var kvp in processedVariables)
{
expr.Parameters[kvp.Key] = kvp.Value;
}
}
if (expr.HasErrors())
{
throw new ArgumentException($"条件表达式格式错误: {expr.Error}");
}
var result = expr.Evaluate();
return Convert.ToBoolean(result);
}
catch (Exception ex)
{
throw new ArgumentException($"条件表达式异常: {ex.Message}");
}
}
private static (string, Dictionary<string, object>) PreprocessExpression(
string expression,
Dictionary<string, object>? variables)
{
if (variables == null || variables.Count == 0)
return (expression, variables ?? new Dictionary<string, object>());
// 生成变量名映射 (中文 -> 英文别名)
var chineseToAlias = new Dictionary<string, string>();
var aliasToValue = new Dictionary<string, object>();
int counter = 1;
foreach (var key in variables.Keys)
{
if (ContainsChinese(key))
{
string alias = $"var_{counter}";
counter++;
chineseToAlias[key] = alias;
aliasToValue[alias] = variables[key];
}
}
// 如果没有中文变量名,直接返回原始数据
if (chineseToAlias.Count == 0)
return (expression, variables);
// 替换表达式中的中文变量名
string processedExpression = expression;
foreach (var pair in chineseToAlias)
{
// 使用正则确保完整匹配变量名
string pattern = $@"\b{Regex.Escape(pair.Key)}\b";
processedExpression = Regex.Replace(
processedExpression,
pattern,
pair.Value,
RegexOptions.Compiled
);
}
// 创建新变量字典(英文别名 + 原始英文变量)
var newVariables = new Dictionary<string, object>(aliasToValue);
foreach (var key in variables.Keys)
{
if (!ContainsChinese(key))
{
newVariables[key] = variables[key];
}
}
return (processedExpression, newVariables);
}
// 检查字符串是否包含中文字符
private static bool ContainsChinese(string text)
{
return text.Any(c => c >= 0x4E00 && c <= 0x9FFF);
}
}
}

114
ATS/Tools/Log.cs Normal file
View File

@ -0,0 +1,114 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows.Media;
using System.Windows.Threading;
namespace ATS.Tools
{
public static class Log
{
private static readonly object _lock = new();
private static string _currentLogFile = "";
public static Dispatcher? Dispatcher { get; set; }
public static event Action<string, SolidColorBrush, int>? LogAdded;
private static readonly StringBuilder _logBuffer = new();
private static readonly System.Timers.Timer _flushTimer = new(1000);
static Log()
{
_flushTimer.Elapsed += (s, e) => FlushBuffer();
_flushTimer.Start();
}
private static void FlushBuffer()
{
lock (_lock)
{
if (_logBuffer.Length > 0)
{
File.AppendAllText(_currentLogFile, _logBuffer.ToString());
_logBuffer.Clear();
}
}
}
public static void Info(string message, int depth = 0)
{
Write("INFO", message, Colors.DodgerBlue, depth);
}
public static void Success(string message, int depth = 0)
{
Write("SUCCESS", message, Colors.LimeGreen, depth);
}
public static void Error(string message, int depth = 0)
{
Write("ERROR", message, Colors.Red, depth);
}
public static void Warning(string message, int depth = 0)
{
Write("WARNING", message, Colors.Orange, depth);
}
private static void Write(string level, string message, Color color, int depth = 0)
{
lock (_lock)
{
try
{
// 创建按年月组织的目录
string monthDir = Path.Combine(SystemConfig.Instance.LogFilePath, DateTime.Now.ToString("yyyy-MM"));
Directory.CreateDirectory(monthDir);
// 创建当天的日志文件
_currentLogFile = Path.Combine(monthDir, $"{DateTime.Now:yyyy-MM-dd}.log");
string logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}";
// 写入文件
File.AppendAllTextAsync(_currentLogFile, logEntry + Environment.NewLine).ConfigureAwait(false);
// 使用 Dispatcher 安全触发事件
if (Dispatcher != null && !Dispatcher.CheckAccess())
{
Dispatcher.Invoke(() =>
{
LogAdded?.Invoke(logEntry, new SolidColorBrush(color), depth);
});
}
else
{
LogAdded?.Invoke(logEntry, new SolidColorBrush(color), depth);
}
}
catch
{
// 防止日志写入失败导致程序崩溃
}
}
}
}
public class LogEntry
{
public string Message { get; }
public SolidColorBrush Color { get; }
public int Depth { get; } // 用于缩进显示
public LogEntry(string message, SolidColorBrush color, int depth = 0)
{
Message = new string(' ', depth * 20) + message;
Color = color;
Depth = depth;
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using ;
namespace ATS.Tools
{
public class SecurityDongle
{
public static double Verify()
{
//第一次使用时需要写入加密狗,之后注释掉写入代码即可
//加密狗驱动类.写入加密狗(0, "00000000", "ATS项目");//00000000表示管理员密码ATS项目表示校验密码
//使用用户密码和校验密码寻找加密狗返回值小于0表示未找到或验证失败
var re = .("11111111", "ATS项目");
if (re < 0) return 0;
//在这里设置到期时间20260401
// 设置到期时间2026年4月1日
DateTime = new DateTime(2026, 4, 1, 0, 0, 0);
DateTime = DateTime.Now;
// 返回剩余毫秒数(或秒数、分钟数,根据需求)
return (long)( - ).TotalHours;
}
}
}

BIN
ATS/ViKey.dll Normal file

Binary file not shown.

View File

@ -0,0 +1,43 @@
<mah:MetroWindow x:Class="ATS.Views.CANCatchSingalView"
xmlns:metro="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
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:ATS.Views" xmlns:local1="clr-namespace:ATS.Converters"
mc:Ignorable="d"
Title="CAN采集信号配置界面" Height="450" Width="1100">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<DataGrid x:Name="配置表格" AutoGenerateColumns="False" ItemsSource="{Binding 配置}" IsReadOnly="True">
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="删除" Click="MenuItem_Click"/>
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn Header="通道" Binding="{Binding 通道}"/>
<DataGridTextColumn Header="报文ID" Binding="{Binding 报文ID,Converter={StaticResource HexConverter}}"/>
<DataGridTextColumn Header="报文名称" Binding="{Binding 报文名称}"/>
<DataGridTextColumn Header="信号名称" Binding="{Binding 信号名称}"/>
<DataGridTextColumn Header="采集间隔" Binding="{Binding 记录间隔}"/>
<DataGridTextColumn Header="采集id" Binding="{Binding 采集id}"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel Orientation="Horizontal" Grid.Row="1" Margin="10">
<Label Content="通道(从0开始)" VerticalAlignment="Center"/>
<TextBox Text="{Binding 通道}" TextChanged="TextBox_TextChanged" MinWidth="100" VerticalAlignment="Center"/>
<Label Content="报文" VerticalAlignment="Center"/>
<ComboBox MinWidth="100" SelectedIndex="{Binding 选中报文}" ItemsSource="{Binding 报文}" SelectionChanged="ComboBox_SelectionChanged" />
<Label Content="信号" VerticalAlignment="Center"/>
<ComboBox SelectedIndex="{Binding 选中信号}" ItemsSource="{Binding 信号}" MinWidth="100" />
<Label Content="采集间隔(ms)" VerticalAlignment="Center"/>
<TextBox Text="{Binding 采集间隔}" MinWidth="100" VerticalAlignment="Center"/>
<Button Content="添加" Margin="10" Click="Button_Click"/>
</StackPanel>
</Grid>
</mah:MetroWindow>

View File

@ -0,0 +1,116 @@
using ATS.Models;
using MahApps.Metro.Controls;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.ComponentModel;
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;
using TSMasterCAN;
namespace ATS.Views
{
/// <summary>
/// CANCatchSingalView.xaml 的交互逻辑
/// </summary>
[AddINotifyPropertyChangedInterface]
public partial class CANCatchSingalView : MetroWindow
{
public CANCatchSingalView()
{
InitializeComponent();
DataContext ??= this;
Loaded += (_, _) =>
{
= [.. DBCParse.MsgDatabase[].Select(s => $"[0x{s.msg_id:X}]{s.msg_name}")];
};
}
public BindingList<CANSignalModel> { get; set; } = new();
public byte { get; set; } = 0;
public List<string> { get; set; }
public List<string> { get; set; }
public int { get; set; }
public int { get; set; }
public int { get; set; } = 1000;
private void Button_Click(object sender, RoutedEventArgs e)
{
var find = .FirstOrDefault(s => s.Channel ==
&& s.MessageName == DBCParse.MsgDatabase[][].msg_name
&& s.SignalName == DBCParse.MsgDatabase[][].signal_Name[]);
try
{
if (find is null)
{
.Add(new()
{
Channel = ,
MessageName = DBCParse.MsgDatabase[][].msg_name,
SignalName = DBCParse.MsgDatabase[][].signal_Name[],
CatchID = Guid.NewGuid(),
LogInterval = TimeSpan.FromMilliseconds(),
MessageID = DBCParse.MsgDatabase[][].msg_id
});
}
else
{
find.LogInterval = TimeSpan.FromMilliseconds();
}
}
catch (Exception)
{
throw;
}
finally
{
}
}
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
var cbb = sender as ComboBox;
if (cbb.SelectedIndex < 0) return;
= DBCParse.MsgDatabase[][cbb.SelectedIndex].signal_Name.ToList();
= 0;
}
catch (Exception)
{
}
}
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
if (.SelectedIndex < 0) return;
.RemoveAt(.SelectedIndex);
}
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (sender is not TextBox o) return;
int temp = 0;
try
{
temp = Convert.ToInt32(o.Text);
= 0;
}
catch (Exception)
{
}
= [.. DBCParse.MsgDatabase[temp].Select(s => $"[0x{s.msg_id:X}]{s.msg_name}")];
}
}
}

View File

@ -0,0 +1,46 @@
<UserControl x:Class="ATS.Views.CommandTreeView"
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:ATS.Views"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
Loaded="UserControl_Loaded"
d:DesignHeight="450"
d:DesignWidth="800">
<Grid>
<GroupBox Header="指令">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0"
Orientation="Horizontal"
Margin="10,0,7,12"
VerticalAlignment="Center">
<TextBox x:Name="SearchTextBox"
MinWidth="150"
Height="25"
Margin="0,0,5,0"
VerticalContentAlignment="Center"
KeyDown="SearchTextBox_KeyDown"
TextChanged="SearchTextBox_TextChanged" />
</StackPanel>
<TreeView Grid.Row="1"
x:Name="InstructionTreeView"
MouseDoubleClick="InstructionTreeView_MouseDoubleClick">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="重新加载"
Click="RelodCommand_Click" />
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
</Grid>
</GroupBox>
</Grid>
</UserControl>

View File

@ -0,0 +1,841 @@
using ATS.Logic;
using ATS.Models;
using ATS.Tools;
using ATS.Windows;
using Common.Attributes;
using GongSolutions.Wpf.DragDrop.Utilities;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
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.Navigation;
using System.Windows.Shapes;
using System.Xml;
using static ATS.Models.ParameterModel;
using static Common.Attributes.ATSCommandAttribute;
using Path = System.IO.Path;
namespace ATS.Views
{
/// <summary>
/// CommandTreeView.xaml 的交互逻辑
/// </summary>
public partial class CommandTreeView : UserControl
{
#region
public static CommandTreeView? Instance { get; private set; }
public ObservableCollection<Assembly> Assemblies { get; set; } = [];
public ObservableCollection<SubProgramItem> SubPrograms { get; set; } = [];
public ProgramModel Program => MainWindow.Instance.Program ?? new();
private Dictionary<object, TreeViewItem> _treeViewItemMap = [];
private TreeViewItem _subProgramRootNode;
private readonly Dictionary<string, XmlDocument> _xmlDocumentCache = [];
#endregion
public CommandTreeView()
{
InitializeComponent();
Instance = this;
DataContext = this;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
Directory.CreateDirectory(SystemConfig.Instance.DLLFilePath);
Directory.CreateDirectory(SystemConfig.Instance.SubProgramFilePath);
LoadAllAssemblies();
LoadSubPrograms();
LoadInstructionsToTreeView();
}
#region
/// <summary>
/// 加载指定目录下的所有DLL
/// </summary>
private void LoadAllAssemblies()
{
Assemblies.Clear();
foreach (var dllPath in Directory.GetFiles(SystemConfig.Instance.DLLFilePath, "*.dll"))
{
try
{
var assembly = Assembly.LoadFrom(dllPath);
Assemblies.Add(assembly);
// 加载对应的XML注释文件
string xmlPath = Path.ChangeExtension(dllPath, ".xml");
if (File.Exists(xmlPath))
{
try
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(xmlPath);
_xmlDocumentCache[assembly.FullName!] = xmlDoc;
}
catch (Exception xmlEx)
{
Log.Warning($"加载XML注释失败: {Path.GetFileName(xmlPath)} - {xmlEx.Message}");
}
}
}
catch (Exception ex)
{
Log.Warning($"无法加载程序集 {Path.GetFileName(dllPath)}: {ex.Message}");
}
}
}
// 添加获取注释的方法
private string? GetMethodDocumentation(MethodInfo method)
{
if (method.DeclaringType == null) return null;
try
{
string assemblyName = method.DeclaringType.Assembly.FullName!;
if (!_xmlDocumentCache.TryGetValue(assemblyName, out XmlDocument? xmlDoc)) return null;
// 生成XML文档中的成员ID
string memberName = $"M:{method.DeclaringType.FullName}.{method.Name}";
var parameters = method.GetParameters();
if (parameters.Length > 0)
{
memberName += "(" + string.Join(",", parameters.Select(p => p.ParameterType.FullName)) + ")";
}
// 查找注释节点
XmlNode memberNode = xmlDoc.SelectSingleNode($"//member[@name='{memberName}']")!;
if (memberNode == null) return null;
// 获取摘要summary
var summaryNode = memberNode.SelectSingleNode("summary");
string documentation = "";
if (summaryNode != null)
{
documentation += CleanXmlContent(summaryNode.InnerXml);
}
// 获取参数注释param
var paramNodes = memberNode.SelectNodes("param");
if (paramNodes != null && paramNodes.Count > 0)
{
documentation += "\n\n参数:";
foreach (XmlNode paramNode in paramNodes)
{
string? paramName = paramNode.Attributes?["name"]?.Value;
if (!string.IsNullOrEmpty(paramName))
{
documentation += $"\n • {paramName}: {CleanXmlContent(paramNode.InnerXml)}";
}
}
}
// 获取返回值注释returns
var returnsNode = memberNode.SelectSingleNode("returns");
if (returnsNode != null)
{
documentation += $"\n\n返回值: {CleanXmlContent(returnsNode.InnerXml)}";
}
return string.IsNullOrWhiteSpace(documentation)
? null
: System.Net.WebUtility.HtmlDecode(documentation.Trim());
}
catch (Exception ex)
{
Log.Warning($"获取注释失败: {method.Name} - {ex.Message}");
return null;
}
}
// 辅助方法清理XML内容
private string CleanXmlContent(string xmlContent)
{
return xmlContent
.Replace("<see cref=\"", "")
.Replace("\"/>", "")
.Replace("<para>", "\n")
.Replace("</para>", "")
.Replace("<seealso", "")
.Replace("/>", "")
.Replace("<c>", "") // 处理代码标签
.Replace("</c>", "")
.Replace("<code>", "")
.Replace("</code>", "")
.Trim();
}
// 子程序加载方法
private void LoadSubPrograms()
{
SubPrograms.Clear();
if (!Directory.Exists(SystemConfig.Instance.SubProgramFilePath))
{
Directory.CreateDirectory(SystemConfig.Instance.SubProgramFilePath);
return;
}
foreach (var filePath in Directory.GetFiles(SystemConfig.Instance.SubProgramFilePath, "*.ats"))
{
try
{
SubPrograms.Add(new SubProgramItem
{
Name = Path.GetFileNameWithoutExtension(filePath),
FilePath = filePath
});
}
catch (Exception ex)
{
Log.Warning($"加载子程序错误: {filePath} - {ex.Message}");
}
}
}
// ATS/Views/CommandTreeView.xaml.cs - 修正 LoadInstructionsToTreeView 方法
/// <summary>
/// 加载指令集到TreeView
/// </summary>
private void LoadInstructionsToTreeView()
{
InstructionTreeView.Items.Clear();
_treeViewItemMap.Clear(); // 清空旧的映射
// 添加控制指令节点
var controlRootNode = new TreeViewItem
{
Header = "系统指令",
Tag = "ControlRoot"
};
InstructionTreeView.Items.Add(controlRootNode);
// 添加循环开始节点
var loopStartNode = new TreeViewItem
{
Header = "循环开始",
Tag = "循环开始"
};
controlRootNode.Items.Add(loopStartNode);
// 添加循环结束节点
var loopEndNode = new TreeViewItem
{
Header = "循环结束",
Tag = "循环结束"
};
controlRootNode.Items.Add(loopEndNode);
// 创建子程序根节点
_subProgramRootNode = new TreeViewItem
{
Header = "子程序",
Tag = "SubProgramRoot"
};
InstructionTreeView.Items.Add(_subProgramRootNode);
// 添加子程序节点
foreach (var subProgram in SubPrograms)
{
var subProgramNode = new TreeViewItem
{
Header = subProgram.Name,
Tag = subProgram,
ToolTip = subProgram.FilePath
};
_subProgramRootNode.Items.Add(subProgramNode);
}
// 添加dll中的方法指令 - 按分类组织
foreach (var assembly in Assemblies)
{
List<Type> validTypes = new List<Type>();
try
{
// 类型过滤条件
var types = assembly.GetTypes().Where(t =>
t.IsPublic &&
!t.IsNested &&
(t.IsClass || t.IsValueType) &&
(!t.IsAbstract || t.IsSealed) &&
t.GetCustomAttribute<ATSCommandAttribute>() != null);
foreach (var type in types)
{
if (type.GetCustomAttribute<BrowsableAttribute>()?.Browsable == false)
continue;
// 检查类型是否有有效方法(包括继承的方法)
var allMethods = new HashSet<MethodInfo>();
GetPublicMethods(type, allMethods);
if (allMethods.Count > 0)
{
validTypes.Add(type);
}
}
}
catch (Exception ex)
{
Log.Error($"加载类型错误: {assembly.FullName} - {ex.Message}");
}
// 只有当程序集包含有效类型时才添加节点
if (validTypes.Count > 0)
{
var assemblyNode = new TreeViewItem
{
Header = assembly.GetName().Name,
Tag = assembly
};
InstructionTreeView.Items.Add(assemblyNode);
_treeViewItemMap[assembly] = assemblyNode;
// 按分类组织类型 - 修改这部分逻辑
foreach (var type in validTypes)
{
// 获取该类型的所有 DeviceCategoryAttribute 实例
var categoryAttributes = type.GetCustomAttributes<DeviceCategoryAttribute>().ToArray();
// 获取所有分类名称,如果没有则归为 "未分类"
string[] categories = categoryAttributes.Length > 0 ?
categoryAttributes.Select(attr => attr.Category).ToArray() :
new[] { "未分类" };
// 获取该类型的所有公共方法,供后续在不同分类下复用
var allMethods = new HashSet<MethodInfo>();
GetPublicMethods(type, allMethods);
// 为该类型的每个分类创建节点
foreach (var category in categories)
{
// 检查该程序集下是否已存在该分类节点
var categoryNode = assemblyNode.Items.OfType<TreeViewItem>()
.FirstOrDefault(item => item.Header?.ToString() == category);
// 如果不存在,则创建新的分类节点
if (categoryNode == null)
{
categoryNode = new TreeViewItem
{
Header = category,
Tag = $"Category_{category}"
};
assemblyNode.Items.Add(categoryNode);
}
// 为当前分类创建一个 typeNode 实例
var typeNode = new TreeViewItem
{
Header = type.Name,
Tag = type, // Tag 仍然指向原始类型,方便后续处理
ToolTip = type.FullName
};
// 添加到当前分类节点
categoryNode.Items.Add(typeNode);
// 重要:将这个 *特定的* TreeViewItem 实例映射到 *原始的* Type 对象
// 这样在双击时可以通过 SelectedItem.Tag 找到原始 Type
// 如果有多个分类,最后添加的那个分类下的 typeNode 会覆盖前面的映射
// 如果需要精确映射到具体哪个分类下的节点,需要更复杂的结构
_treeViewItemMap[type] = typeNode;
// 为当前分类下的 typeNode 创建方法节点
foreach (var method in allMethods)
{
// 跳过特殊名称方法(属性访问器等)
if (method.IsSpecialName) continue;
// 跳过系统对象方法
if (method.DeclaringType == typeof(object)) continue;
// 跳过特定名称的系统方法
string[] ignoreMethods = { "GetType", "ToString", "Equals", "GetHashCode" };
if (ignoreMethods.Contains(method.Name)) continue;
// 跳过不可浏览的方法
if (method.GetCustomAttribute<BrowsableAttribute>()?.Browsable == false) continue;
// 对于静态类,只显示静态方法
if (type.IsAbstract && type.IsSealed && !method.IsStatic) continue;
var parameters = method.GetParameters();
var paramText = string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"));
// 获取方法注释
string summary = GetMethodDocumentation(method) ?? "";
string tooltipContent = $"{method.DeclaringType?.FullName}.{method.Name}";
if (!string.IsNullOrEmpty(summary)) tooltipContent = $"{tooltipContent}\n\n{summary}";
// 为当前分类下的 typeNode 创建一个 methodNode 实例
var methodNode = new TreeViewItem
{
Header = $"{method.Name}({paramText})",
Tag = new MethodInfoWrapper(method),
ToolTip = tooltipContent
};
// 添加到当前分类下的 typeNode
typeNode.Items.Add(methodNode);
// 重要:将这个 *特定的* TreeViewItem 实例映射到 *原始的* MethodInfo 对象
// 如果有多个分类,最后添加的那个分类下的 methodNode 会覆盖前面的映射
// 如果需要精确映射到具体哪个分类下的节点,需要更复杂的结构
_treeViewItemMap[method] = methodNode;
}
}
}
}
}
}
/// <summary>
/// 递归获取类型的所有公共方法(包括继承的方法),但跳过被重写的方法
/// </summary>
/// <param name="type">要处理的目标类型</param>
/// <param name="methods">存储方法的集合</param>
private void GetPublicMethods(Type type, HashSet<MethodInfo> methods)
{
// 获取当前类型的所有公共方法(包括继承的)
var allMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
.Where(m => !m.IsSpecialName &&
m.DeclaringType != typeof(object))
.ToList();
// 按方法签名分组
var groupedMethods = allMethods
.GroupBy(m => new { m.Name, Parameters = string.Join(",", m.GetParameters().Select(p => p.ParameterType.FullName)) });
foreach (var group in groupedMethods)
{
// 从组中选择声明类型最接近当前类型(即继承层次最深)的方法
MethodInfo? selectedMethod = null;
int minDepth = int.MaxValue;
foreach (var method in group)
{
// 计算声明类型的深度
int depth = 0;
Type? current = type;
Type declaringType = method.DeclaringType!;
while (current != null && current != declaringType)
{
depth++;
current = current.BaseType;
}
// 如果找到声明类型且在继承链上
if (current == declaringType)
{
if (selectedMethod == null || depth < minDepth)
{
selectedMethod = method;
minDepth = depth;
}
}
}
if (selectedMethod != null)
{
methods.Add(selectedMethod);
}
}
}
private void ReOrderProgramList()
{
for (int i = 0; i < Program.StepCollection.Count; i++)
{
Program.StepCollection[i].Index = i + 1;
}
}
private void UnselectTreeViewItems(ItemsControl itemsControl)
{
for (int i = 0; i < itemsControl.Items.Count; i++)
{
var item = itemsControl.Items[i];
if (itemsControl.ItemContainerGenerator.ContainerFromItem(item) is TreeViewItem container)
{
container.IsSelected = false;
UnselectTreeViewItems(container);
}
}
}
#region
private void AddMethodToProgram(MethodInfo method, int insertIndex = -1)
{
try
{
var newStep = new StepModel
{
Name = method.Name,
StepType = "方法",
Method = new MethodModel
{
FullName = method.DeclaringType?.FullName,
Name = method.Name
}
};
// 添加输入参数
foreach (var param in method.GetParameters())
{
newStep.Method.Parameters.Add(new ParameterModel
{
Name = param.Name!,
Type = param.ParameterType,
Category = ParameterCategory.Input
});
}
// 添加输出参数(返回值)
Type returnType = method.ReturnType;
if (returnType == typeof(Task))
{
// 不添加输出参数(无返回值)
}
else if (returnType.IsGenericType &&
returnType.GetGenericTypeDefinition() == typeof(Task<>))
{
// 提取实际返回类型(如 Task<bool> -> bool
Type actualType = returnType.GetGenericArguments()[0];
newStep.Method.Parameters.Add(new ParameterModel
{
Name = "Result",
Type = actualType, // 使用实际类型
Category = ParameterCategory.Output
});
}
else if (returnType != typeof(void))
{
// 同步方法正常添加
newStep.Method.Parameters.Add(new ParameterModel
{
Name = "Result",
Type = returnType,
Category = ParameterCategory.Output
});
}
// 添加到程序
if (insertIndex >= 0 && insertIndex <= Program.StepCollection.Count)
{
Program.StepCollection.Insert(insertIndex, newStep);
}
else
{
Program.StepCollection.Add(newStep);
}
ReOrderProgramList();
}
catch (Exception ex)
{
Log.Error($"添加方法失败: {method.Name} - {ex.Message}");
}
}
private void AddSubProgramToProgram(SubProgramItem subProgram, int insertIndex = -1)
{
try
{
var newStep = new StepModel
{
Name = subProgram.Name,
StepType = "子程序"
};
var jsonstr = File.ReadAllText($"{subProgram.FilePath}");
var tmp = JsonConvert.DeserializeObject<ProgramModel>(jsonstr);
if (tmp != null)
{
if (tmp.Devices != null && tmp.Devices.Count > 0)
{
foreach (var device in tmp.Devices)
{
_ = DeviceConnect.InitAndConnectDevice(tmp, device);
}
}
newStep.SubProgram = tmp;
}
// 添加到程序
if (insertIndex >= 0 && insertIndex <= Program.StepCollection.Count)
{
Program.StepCollection.Insert(insertIndex, newStep);
}
else
{
Program.StepCollection.Add(newStep);
}
ReOrderProgramList();
}
catch (Exception ex)
{
Log.Error($"添加子程序失败: {subProgram.Name} - {ex.Message}");
}
}
private void AddLoopStartStep(int insertIndex = -1)
{
var newStep = new StepModel
{
Name = "循环开始",
StepType = "循环开始",
LoopCount = 1,
Method = new() { Parameters = [new() { Name = "循环次数", Type = typeof(int), Category = ParameterCategory.Input }] }
};
if (insertIndex >= 0)
{
Program.StepCollection.Insert(insertIndex, newStep);
}
else
{
Program.StepCollection.Add(newStep);
}
ReOrderProgramList();
}
private void AddLoopEndStep(int insertIndex = -1)
{
// 查找最近的未匹配循环开始
StepModel? lastUnmatchedLoopStart = null;
for (int i = Program.StepCollection.Count - 1; i >= 0; i--)
{
if (Program.StepCollection[i].StepType == "循环开始")
{
bool isMatched = Program.StepCollection.Any(s => s.StepType == "循环结束" && s.LoopStartStepId == Program.StepCollection[i].ID);
if (!isMatched)
{
lastUnmatchedLoopStart = Program.StepCollection[i];
break;
}
}
}
var newStep = new StepModel
{
Name = "循环结束",
StepType = "循环结束",
LoopStartStepId = lastUnmatchedLoopStart?.ID
};
if (insertIndex >= 0)
{
Program.StepCollection.Insert(insertIndex, newStep);
}
else
{
Program.StepCollection.Add(newStep);
}
ReOrderProgramList();
}
#endregion
#endregion
#region
private class MethodInfoWrapper
{
public MethodInfo Method { get; }
public MethodInfoWrapper(MethodInfo method)
{
Method = method;
}
}
#endregion
private void InstructionTreeView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (MainWindow.Instance.User.Role < 1)
{
MessageBox.Show("当前登录用户无权限");
return;
}
if (InstructionTreeView.SelectedItem is TreeViewItem selectedItem)
{
int insertIndex = -1;
if (StepsManager.Instance!.SelectedIndex >= 0)
{
insertIndex = StepsManager.Instance.SelectedIndex + 1;
}
if (selectedItem.Tag is string tagString)
{
if (tagString == "循环开始")
{
AddLoopStartStep(insertIndex);
}
else if (tagString == "循环结束")
{
AddLoopEndStep(insertIndex);
}
}
else if (selectedItem.Tag is MethodInfoWrapper wrapper)
{
AddMethodToProgram(wrapper.Method, insertIndex);
}
else if (selectedItem.Tag is MethodInfo methodInfo)
{
AddMethodToProgram(methodInfo, insertIndex);
}
else if (selectedItem.Tag is SubProgramItem subProgram)
{
AddSubProgramToProgram(subProgram, insertIndex);
}
StepsManager.Instance.SelectedIndex = insertIndex;
UnselectTreeViewItems(InstructionTreeView);
}
}
private void RelodCommand_Click(object sender, RoutedEventArgs e)
{
LoadAllAssemblies();
LoadSubPrograms();
LoadInstructionsToTreeView();
}
// 在类中添加变量
private Dictionary<TreeViewItem, Brush> _originalBrushes = new();
private HashSet<TreeViewItem> _matchedItems = new();
// 添加搜索方法
private void SearchTreeView(string searchText)
{
// 清除上次搜索结果
ClearSearchHighlighting();
if (string.IsNullOrWhiteSpace(searchText)) ClearSearchButton_Click(null, null);
searchText = searchText.Trim().ToLower();
// 遍历所有节点
foreach (var item in InstructionTreeView.Items.OfType<TreeViewItem>())
{
SearchTreeViewItem(item, searchText);
}
// 展开所有包含匹配项的节点
foreach (var matchedItem in _matchedItems.ToList())
{
ExpandParents(matchedItem);
}
}
private void SearchTreeViewItem(TreeViewItem item, string searchText)
{
// 检查当前节点是否匹配
bool isMatch = false;
if (string.IsNullOrEmpty(searchText))
{
}
else if (item.Header is string headerText && headerText.Split("(")[0].ToLower().Contains(searchText))
{
isMatch = true;
}
if (isMatch)
{
// 保存原始背景色并设置高亮
if (!_originalBrushes.ContainsKey(item))
{
_originalBrushes[item] = item.Background;
}
item.Background = Brushes.Yellow;
_matchedItems.Add(item);
}
// 递归搜索子节点
foreach (var childItem in item.Items.OfType<TreeViewItem>())
{
SearchTreeViewItem(childItem, searchText);
}
}
private void ClearSearchHighlighting()
{
foreach (var item in _matchedItems)
{
if (_originalBrushes.TryGetValue(item, out var originalBrush))
{
item.Background = originalBrush;
}
else
{
item.ClearValue(TreeViewItem.BackgroundProperty);
}
}
_matchedItems.Clear();
_originalBrushes.Clear();
}
private void ExpandParents(TreeViewItem item)
{
var parent = ItemsControl.ItemsControlFromItemContainer(item) as TreeViewItem;
while (parent != null)
{
parent.IsExpanded = true;
parent = ItemsControl.ItemsControlFromItemContainer(parent) as TreeViewItem;
}
}
// 添加事件处理方法
private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(SearchTextBox.Text))
{
ClearSearchHighlighting();
}
}
private void SearchTextBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
SearchTreeView(SearchTextBox.Text);
}
}
private void ClearSearchButton_Click(object sender, RoutedEventArgs e)
{
SearchTextBox.Clear();
ClearSearchHighlighting();
}
}
}

33
ATS/Views/LogArea.xaml Normal file
View File

@ -0,0 +1,33 @@
<UserControl
x:Class="ATS.Views.LogArea"
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:local="clr-namespace:ATS.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
Loaded="UserControl_Loaded"
mc:Ignorable="d">
<Grid>
<GroupBox
Grid.Row="3"
Grid.Column="2"
Header="系统运行日志">
<ListView
x:Name="LogListView"
FontWeight="DemiBold"
ItemsSource="{Binding LogEntries}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock
Foreground="{Binding Color}"
Text="{Binding Message}"
TextWrapping="Wrap" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</GroupBox>
</Grid>
</UserControl>

96
ATS/Views/LogArea.xaml.cs Normal file
View File

@ -0,0 +1,96 @@
using ATS.Tools;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
namespace ATS.Views
{
public partial class LogArea : UserControl
{
// 日志配置常量
private const int MaxLogCount = 2000; // 最大日志条数
private const int PurgeThreshold = 1500; // 触发清理的阈值
private const int CleanupBatchSize = 100;// 添加批量清理阈值
// 异步处理状态标志
private bool _purgePending = false;
public ObservableCollection<LogEntry> LogEntries { get; } = [];
public LogArea()
{
InitializeComponent();
DataContext = this;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
Log.Dispatcher = Dispatcher;
Log.LogAdded += OnLogAdded;
}
private void OnLogAdded(string message, SolidColorBrush color, int depth = 0)
{
Dispatcher.Invoke(() =>
{
// 添加新日志
LogEntries.Add(new LogEntry(message, color, depth));
// 检查是否需要清理旧日志
if (LogEntries.Count > PurgeThreshold && !_purgePending)
{
_purgePending = true;
Dispatcher.BeginInvoke(new Action(PurgeExcessLogs), DispatcherPriority.Background);
}
// 延迟滚动到最新条目
Dispatcher.BeginInvoke(new Action(() =>
{
if (LogListView.Items.Count > 0)
{
try
{
LogListView.ScrollIntoView(LogListView.Items[LogListView.Items.Count - 1]);
}
catch (Exception ex)
{
Debug.WriteLine($"滚动错误: {ex.Message}");
}
}
}), DispatcherPriority.ContextIdle);
});
}
private void PurgeExcessLogs()
{
try
{
int excessCount = LogEntries.Count - MaxLogCount;
if (excessCount <= 0) return;
int removeCount = Math.Min(excessCount, CleanupBatchSize);
for (int i = 0; i < removeCount; i++)
{
LogEntries.RemoveAt(0);
}
}
finally
{
_purgePending = LogEntries.Count > PurgeThreshold;
if (_purgePending)
{
// 添加延迟防止递归过深
Dispatcher.BeginInvoke(new Action(PurgeExcessLogs), DispatcherPriority.Background, null);
}
}
}
}
}

View File

@ -0,0 +1,124 @@
<UserControl x:Class="ATS.Views.ParametersManager"
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:ATS.Views"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:ParametersManager}"
d:DesignHeight="450"
d:DesignWidth="800">
<UserControl.Resources>
<CollectionViewSource x:Key="VisibleParameters"
Source="{Binding Program.Parameters}">
<CollectionViewSource.LiveFilteringProperties>
<sys:String>IsVisible</sys:String>
</CollectionViewSource.LiveFilteringProperties>
</CollectionViewSource>
</UserControl.Resources>
<Grid>
<GroupBox Grid.Row="3"
Header="设备/参数"
Padding="0">
<TabControl>
<TabItem Header="参数">
<DataGrid x:Name="ParameterDataGrid"
Padding="10"
Background="Transparent"
ItemsSource="{Binding Source={StaticResource VisibleParameters}}"
AutoGenerateColumns="False"
CanUserAddRows="False"
SelectionMode="Extended"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTextColumn Header="类别"
Binding="{Binding Category, Converter={StaticResource ParameterCategoryToStringConverter}}"
IsReadOnly="True" />
<DataGridTextColumn Header="参数名"
Binding="{Binding Name}"
IsReadOnly="True" />
<DataGridTextColumn Header="类型"
Binding="{Binding Type}"
IsReadOnly="True" />
<DataGridTextColumn Header="值"
MinWidth="20"
Binding="{Binding Value, Converter={StaticResource ParameterValueToStringConverter}}"
IsReadOnly="True" />
</DataGrid.Columns>
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="新增"
Click="ParameterAdd_Click" />
<MenuItem Header="编辑"
Click="ParameterEdit_Click" />
<MenuItem Header="删除" Foreground="Red"
Click="ParameterDelete_Click" />
</ContextMenu>
</DataGrid.ContextMenu>
</DataGrid>
</TabItem>
<TabItem Header="设备">
<DataGrid x:Name="DeviceDataGrid"
Padding="10"
Background="Transparent"
ItemsSource="{Binding Program.Devices}"
AutoGenerateColumns="False"
CanUserAddRows="False"
IsReadOnly="True"
SelectionMode="Extended"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTemplateColumn Header="设备状态">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid Width="20"
Height="20">
<Ellipse Width="16"
Height="16"
StrokeThickness="1"
Stroke="DarkGray">
<Ellipse.Style>
<Style TargetType="Ellipse">
<Setter Property="Fill"
Value="Transparent" />
<Style.Triggers>
<DataTrigger Binding="{Binding Connected}"
Value="True">
<Setter Property="Fill"
Value="LimeGreen" />
</DataTrigger>
<DataTrigger Binding="{Binding Connected}"
Value="False">
<Setter Property="Fill"
Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="设备名称"
Binding="{Binding Name}" />
<DataGridTextColumn Header="备注"
Binding="{Binding Description}" />
</DataGrid.Columns>
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="新增"
Click="DeviceAdd_Click" />
<MenuItem Header="编辑"
Click="DeviceEdit_Click" />
<MenuItem Header="删除" Foreground="Red"
Click="DeviceDelete_Click" />
</ContextMenu>
</DataGrid.ContextMenu>
</DataGrid>
</TabItem>
</TabControl>
</GroupBox>
</Grid>
</UserControl>

View File

@ -0,0 +1,262 @@
using ATS.Logic;
using ATS.Models;
using ATS.Tools;
using ATS.Windows;
using DeviceCommand.Base;
using Newtonsoft.Json;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Net.Sockets;
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.Navigation;
using System.Windows.Shapes;
using static ATS.Models.ParameterModel;
using static MaterialDesignThemes.Wpf.Theme.ToolBar;
namespace ATS.Views
{
/// <summary>
/// ParametersManager.xaml 的交互逻辑
/// </summary>
public partial class ParametersManager : UserControl, INotifyPropertyChanged
{
public ProgramModel Program => MainWindow.Instance.Program;
public Dictionary<string, Task> DeviceStateWatchTask = [];
public ParametersManager()
{
InitializeComponent();
DataContext = this;
// 获取CollectionViewSource并设置过滤
var visibleParameters = (CollectionViewSource)this.Resources["VisibleParameters"];
visibleParameters.Filter += VisibleParameters_Filter;
visibleParameters.IsLiveFilteringRequested = true;
MainWindow.Instance.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(MainWindow.Program))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Program)));
}
};
}
#region
// 过滤方法:显示非设备参数
private void VisibleParameters_Filter(object sender, FilterEventArgs e)
{
if (e.Item is ParameterModel parameter)
{
e.Accepted = parameter.IsVisible;
}
else
{
e.Accepted = false;
}
}
private void CloseDeviceConnect(DeviceModel device)
{
if (device.CommunicationProtocol is Tcp tcp)
{
Tcp.Close(tcp);
}
}
#endregion
private void ParameterAdd_Click(object sender, RoutedEventArgs e)
{
if (MainWindow.Instance.User.Role < 1)
{
MessageBox.Show("当前登录用户无权限");
return;
}
ParameterSettingWindow window = new();
window.ShowDialog();
if (window.IsSaved && window.Parameter != null)
{
if (Program.Parameters.Any(p => p.Name == window.Parameter.Name && p.IsVisible))
{
MessageBox.Show($"参数 [ {window.Parameter.Name} ] 已存在", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
Program.Parameters.Add(window.Parameter);
Log.Success($"用户 [ {MainWindow.Instance.User.UserName} ] 新增参数 [ {window.Parameter.Name} ] 成功");
}
}
private void ParameterEdit_Click(object sender, RoutedEventArgs e)
{
if (MainWindow.Instance.User.Role < 1)
{
MessageBox.Show("当前登录用户无权限");
return;
}
if (ParameterDataGrid.SelectedItem is ParameterModel selectedPara)
{
ParameterModel parameter = new(selectedPara);
ParameterSettingWindow window = new(parameter);
window.ShowDialog();
if (window.IsSaved && window.Parameter != null)
{
if (Program.Parameters.Any(p => p.Name == window.Parameter.Name && p.ID != window.Parameter.ID && p.IsVisible))
{
MessageBox.Show($"参数 [ {window.Parameter.Name} ] 已存在", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var editPara = Program.Parameters.FirstOrDefault(x => x.ID == window.Parameter.ID);
if (editPara != null)
{
editPara.Name = window.Parameter.Name;
editPara.Type = window.Parameter.Type;
editPara.Category = window.Parameter.Category;
editPara.Value = window.Parameter.Value;
editPara.IsSave = window.Parameter.IsSave;
editPara.LowerLimit = window.Parameter.LowerLimit;
editPara.UpperLimit = window.Parameter.UpperLimit;
Log.Success($"用户 [ {MainWindow.Instance.User.UserName} ] 修改参数 [ {window.Parameter.Name} ] 成功 ");
}
else
{
Log.Error($"参数修改失败: 未找到源参数");
}
}
}
}
private void ParameterDelete_Click(object sender, RoutedEventArgs e)
{
if (MainWindow.Instance.User.Role < 1)
{
MessageBox.Show("当前登录用户无权限");
return;
}
MessageBoxResult result = MessageBox.Show($"确定执行删除操作?", "提示", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
List<ParameterModel> tmpList = [];
foreach (var item in ParameterDataGrid.SelectedItems)
{
if (item is ParameterModel selectedPara)
{
tmpList.Add(selectedPara);
}
}
foreach (var item in tmpList)
{
Program.Parameters.Remove(item);
Log.Success($"用户 [ {MainWindow.Instance.User.UserName} ] 删除参数 [ {item.Name} ] 成功");
}
}
}
private async void DeviceAdd_Click(object sender, RoutedEventArgs e)
{
if (MainWindow.Instance.User.Role < 1)
{
MessageBox.Show("当前登录用户无权限");
return;
}
DeviceSettingWindow window = new();
window.ShowDialog();
if (window.IsSaved && window.Device != null)
{
if (Program.Devices.Any(p => p.Name == window.Device.Name))
{
MessageBox.Show($"设备 [ {window.Device.Name} ] 已存在", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
await DeviceConnect.InitAndConnectDevice(MainWindow.Instance.Program, new(window.Device), true);
Log.Success($"用户 [ {MainWindow.Instance.User.UserName} ] 新增设备 [ {window.Device.Name} ] 成功");
}
}
private async void DeviceEdit_Click(object sender, RoutedEventArgs e)
{
if (MainWindow.Instance.User.Role < 1)
{
MessageBox.Show("当前登录用户无权限");
return;
}
if (DeviceDataGrid.SelectedItem is DeviceModel selectedDevice)
{
DeviceModel device = new(selectedDevice);
DeviceSettingWindow window = new(device);
window.ShowDialog();
if (window.IsSaved && window.Device != null)
{
if (Program.Devices.Any(p => p.Name == window.Device.Name && p.ID != window.Device.ID))
{
MessageBox.Show($"设备 [ {window.Device.Name} ] 已存在", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var editDevice = Program.Devices.FirstOrDefault(x => x.ID == window.Device.ID);
if (editDevice != null)
{
var devicePara = Program.Parameters.FirstOrDefault(x => x.ID == editDevice.ParameterID);
if (devicePara != null)
{
editDevice.Name = window.Device.Name;
editDevice.Type = window.Device.Type;
editDevice.ConnectString = window.Device.ConnectString;
editDevice.Description = window.Device.Description;
devicePara.Name = window.Device.Name;
devicePara.Value = window.Device.CommunicationProtocol;
Log.Success($"用户 [ {MainWindow.Instance.User.UserName} ] 修改设备参数 [ {window.Device.Name} ] 成功");
return;
}
}
Log.Error($"设备参数修改失败: 未找到源设备");
}
}
}
private void DeviceDelete_Click(object sender, RoutedEventArgs e)
{
if (MainWindow.Instance.User.Role < 1)
{
MessageBox.Show("当前登录用户无权限");
return;
}
MessageBoxResult result = MessageBox.Show($"确定执行删除操作?", "提示", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
List<DeviceModel> tmpList = [];
foreach (var item in DeviceDataGrid.SelectedItems)
{
if (item is DeviceModel selectedDevice)
{
tmpList.Add(selectedDevice);
}
}
foreach (var item in tmpList)
{
CloseDeviceConnect(item);
Program.Devices.Remove(item);
var devicePara = Program.Parameters.FirstOrDefault(x => x.ID == item.ParameterID);
if (devicePara != null) Program.Parameters.Remove(devicePara);
Log.Success($"用户 [ {MainWindow.Instance.User.UserName} ] 删除设备 [ {item.Name} ] 成功");
}
}
}
}
}

View File

@ -0,0 +1,404 @@
<UserControl
x:Class="ATS.Views.SingleStepEdit"
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:local="clr-namespace:ATS.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converters="clr-namespace:ATS.Converters"
d:DataContext="{d:DesignInstance Type=local:SingleStepEdit}"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.Resources>
<converters:FilteredParametersConverter x:Key="FilteredParametersConverter"/>
<converters:HexToDecimalConverter x:Key="HexToDecimalConverter"/>
<!-- 定义转换器资源 -->
</UserControl.Resources>
<Grid>
<GroupBox
Grid.Row="1"
Grid.Column="4"
Header="单步编辑">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<!-- 步骤名称 -->
<StackPanel Grid.Row="0">
<StackPanel
Height="30"
Margin="7"
Orientation="Horizontal">
<Label
Width="60"
VerticalAlignment="Bottom"
Content="名称" />
<TextBox
MinWidth="120"
VerticalAlignment="Bottom"
Text="{Binding SelectedStep.Name}" />
</StackPanel>
</StackPanel>
<!-- 合格条件 -->
<StackPanel Grid.Row="1">
<StackPanel
Height="30"
Margin="7"
Orientation="Horizontal">
<Label
Width="60"
VerticalAlignment="Bottom"
Content="合格条件" />
<TextBox
MinWidth="120"
VerticalAlignment="Bottom"
Text="{Binding SelectedStep.OKExpression}" />
</StackPanel>
</StackPanel>
<!-- 合格条件 -->
<StackPanel Grid.Row="2">
<StackPanel
Height="30"
Margin="7"
Orientation="Horizontal">
<Label
Width="60"
VerticalAlignment="Bottom"
Content="跳转"
ToolTip="格式OK跳转序号/NG跳转序号默认为0/0" />
<TextBox
MinWidth="120"
VerticalAlignment="Bottom"
Text="{Binding SelectedStep.ID, Converter={StaticResource ParameterToGotoSettingStringConverter}}" />
</StackPanel>
</StackPanel>
<!-- 备注 -->
<StackPanel Grid.Row="3">
<StackPanel
Height="30"
Margin="7"
Orientation="Horizontal">
<Label
Width="60"
VerticalAlignment="Bottom"
Content="备注" />
<TextBox
MinWidth="120"
VerticalAlignment="Bottom"
Text="{Binding SelectedStep.Description}" />
</StackPanel>
</StackPanel>
<!-- 参数编辑区 -->
<Grid Grid.Row="4">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Height="30" Orientation="Horizontal">
<Label
Margin="7,0"
VerticalAlignment="Bottom"
Content="参数" />
</StackPanel>
<ScrollViewer
Grid.Row="1"
Margin="40,0,0,0"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<StackPanel>
<ItemsControl ItemsSource="{Binding SelectedStep.Method.Parameters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="50" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
Margin="7,7,0,7"
Orientation="Horizontal">
<TextBlock VerticalAlignment="Bottom" Text="{Binding Name}" />
<TextBlock VerticalAlignment="Bottom" Text="(" />
<TextBlock VerticalAlignment="Bottom" Text="{Binding Type.Name}" />
<TextBlock VerticalAlignment="Bottom" Text=")" />
</StackPanel>
<StackPanel
Grid.Column="1"
Margin="7"
Orientation="Horizontal">
<TextBlock
Margin="5,0"
VerticalAlignment="Bottom"
Text="{Binding Category, Converter={StaticResource ParameterCategoryToStringConverter}}" />
</StackPanel>
<StackPanel
Grid.Column="2"
Margin="7"
Orientation="Horizontal">
<!-- 输入参数编辑 -->
<StackPanel
Height="30"
IsEnabled="{Binding Type, Converter={StaticResource ParameterTypeToBoolConverter}}"
Orientation="Horizontal"
Visibility="{Binding Category, Converter={StaticResource ParameterCategoryToVisibilityConverter}}">
<CheckBox VerticalAlignment="Bottom" IsChecked="{Binding IsUseVar}" />
<Grid>
<!-- 常量输入区域 -->
<Grid Visibility="{Binding IsUseVar, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Inverse}">
<!-- 非枚举类型 -->
<TextBox
Height="22"
MinWidth="120"
VerticalAlignment="Bottom"
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"
Visibility="{Binding Type, Converter={StaticResource IsEnumTypeConverter}, ConverterParameter=Collapse}" />
<!-- "{Binding Value, Converter={StaticResource HexToDecimalConverter}, UpdateSourceTrigger=PropertyChanged}" -->
<!-- 枚举类型 -->
<ComboBox
Height="22"
MinWidth="120"
VerticalAlignment="Bottom"
ItemsSource="{Binding Type, Converter={StaticResource EnumValuesConverter}}"
Visibility="{Binding Type, Converter={StaticResource IsEnumTypeConverter}}">
<ComboBox.SelectedItem>
<MultiBinding Converter="{StaticResource EnumValueConverter}">
<Binding Path="Type" />
<Binding Path="Value" />
</MultiBinding>
</ComboBox.SelectedItem>
</ComboBox>
</Grid>
<!-- 变量选择框 -->
<ComboBox
Height="22"
MinWidth="120"
VerticalAlignment="Bottom"
SelectedValue="{Binding VariableName}"
SelectedValuePath="Name"
Visibility="{Binding IsUseVar, Converter={StaticResource BooleanToVisibilityConverter}}">
<!-- 修改开始:使用 FilteredParametersConverter -->
<ComboBox.ItemsSource>
<MultiBinding Converter="{StaticResource FilteredParametersConverter}">
<Binding Path="Type" />
<Binding Path="DataContext.Program.Parameters" RelativeSource="{RelativeSource AncestorType=Window}" />
</MultiBinding>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="(" />
<TextBlock Text="{Binding Type.Name, TargetNullValue=Unknown}" />
<TextBlock Text=")" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
<!-- 修改结束 -->
</ComboBox>
</Grid>
</StackPanel>
<!-- 输出参数编辑 -->
<StackPanel Height="30" Orientation="Horizontal">
<CheckBox VerticalAlignment="Bottom"
IsChecked="{Binding IsOutputToReport}"
Visibility="{Binding Category, Converter={StaticResource ParameterCategoryToVisibilityConverter}, ConverterParameter=Inverse}"
ToolTip="勾选后,该输出参数的结果会写入到报告中" />
<ComboBox
Width="120"
Margin="0,0,0,0"
VerticalAlignment="Bottom"
SelectedValue="{Binding VariableName}"
SelectedValuePath="Name"
Visibility="{Binding Category, Converter={StaticResource ParameterCategoryToVisibilityConverter}, ConverterParameter=Inverse}">
<!-- 修改开始:使用 FilteredParametersConverter -->
<ComboBox.ItemsSource>
<MultiBinding Converter="{StaticResource FilteredParametersConverter}">
<Binding Path="Type" />
<Binding Path="DataContext.Program.Parameters" RelativeSource="{RelativeSource AncestorType=Window}" />
</MultiBinding>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="(" />
<TextBlock Text="{Binding Type.Name, TargetNullValue=Unknown}" />
<TextBlock Text=")" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
<!-- 修改结束 -->
</ComboBox>
</StackPanel>
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl ItemsSource="{Binding SelectedStep.SubProgram.Parameters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Visibility="{Binding Category, Converter={StaticResource ParameterCategoryToVisibilityConverter}, ConverterParameter=Item}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="50" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
Margin="7,7,0,7"
Orientation="Horizontal">
<TextBlock VerticalAlignment="Bottom" Text="{Binding Name}" />
<TextBlock VerticalAlignment="Bottom" Text="(" />
<TextBlock VerticalAlignment="Bottom" Text="{Binding Type.Name}" />
<TextBlock VerticalAlignment="Bottom" Text=")" />
</StackPanel>
<StackPanel
Grid.Column="1"
Margin="7"
Orientation="Horizontal">
<TextBlock
Margin="5,0"
VerticalAlignment="Bottom"
Text="{Binding Category, Converter={StaticResource ParameterCategoryToStringConverter}}" />
</StackPanel>
<StackPanel
Grid.Column="2"
Margin="7"
Orientation="Horizontal">
<!-- 输入参数编辑 -->
<StackPanel
Height="30"
Orientation="Horizontal"
Visibility="{Binding Category, Converter={StaticResource ParameterCategoryToVisibilityConverter}}">
<CheckBox VerticalAlignment="Bottom" IsChecked="{Binding IsUseVar}" />
<Grid>
<!-- 常量输入区域 -->
<Grid Visibility="{Binding IsUseVar, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Inverse}">
<!-- 非枚举类型 -->
<TextBox
Height="22"
MinWidth="120"
VerticalAlignment="Bottom"
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"
Visibility="{Binding Type, Converter={StaticResource IsEnumTypeConverter}, ConverterParameter=Collapse}" />
<!-- 枚举类型 -->
<ComboBox
Height="22"
MinWidth="120"
VerticalAlignment="Bottom"
ItemsSource="{Binding Type, Converter={StaticResource EnumValuesConverter}}"
SelectedItem="{Binding Value}"
Visibility="{Binding Type, Converter={StaticResource IsEnumTypeConverter}}" />
</Grid>
<!-- 变量选择框 -->
<ComboBox
Height="22"
MinWidth="120"
VerticalAlignment="Bottom"
SelectedValue="{Binding VariableName}"
SelectedValuePath="Name"
Visibility="{Binding IsUseVar, Converter={StaticResource BooleanToVisibilityConverter}}">
<!-- 修改开始:使用 FilteredParametersConverter -->
<ComboBox.ItemsSource>
<MultiBinding Converter="{StaticResource FilteredParametersConverter}">
<Binding Path="Type" />
<Binding Path="DataContext.Program.Parameters" RelativeSource="{RelativeSource AncestorType=Window}" />
</MultiBinding>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="(" />
<TextBlock Text="{Binding Type.Name, TargetNullValue=Unknown}" />
<TextBlock Text=")" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
<!-- 修改结束 -->
</ComboBox>
</Grid>
</StackPanel>
<!-- 输出参数编辑 -->
<StackPanel Height="30" Orientation="Horizontal">
<CheckBox VerticalAlignment="Bottom" IsChecked="{Binding IsUseVar}" Visibility="{Binding Category, Converter={StaticResource ParameterCategoryToVisibilityConverter}, ConverterParameter=Inverse}" />
<ComboBox
Width="120"
Margin="0,0,0,0"
VerticalAlignment="Bottom"
SelectedValue="{Binding VariableName}"
SelectedValuePath="Name"
Visibility="{Binding Category, Converter={StaticResource ParameterCategoryToVisibilityConverter}, ConverterParameter=Inverse}">
<!-- 修改开始:使用 FilteredParametersConverter -->
<ComboBox.ItemsSource>
<MultiBinding Converter="{StaticResource FilteredParametersConverter}">
<Binding Path="Type" />
<Binding Path="DataContext.Program.Parameters" RelativeSource="{RelativeSource AncestorType=Window}" />
</MultiBinding>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="(" />
<TextBlock Text="{Binding Type.Name, TargetNullValue=Unknown}" />
<TextBlock Text=")" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
<!-- 修改结束 -->
</ComboBox>
</StackPanel>
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Grid>
<!-- 按钮 -->
<StackPanel
Grid.Row="5"
Margin="5,0"
FlowDirection="RightToLeft"
Orientation="Horizontal">
<Button
Width="90"
Click="CancelStepEdit_Click"
Content="取消" />
<Button
Width="90"
Margin="20,0"
Click="SaveStep_Click"
Content="保存" />
</StackPanel>
</Grid>
</GroupBox>
</Grid>
</UserControl>

View File

@ -0,0 +1,130 @@
using ATS.Models;
using ATS.Tools;
using ATS.Windows;
using System;
using System.Collections.Generic;
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.Navigation;
using System.Windows.Shapes;
namespace ATS.Views
{
/// <summary>
/// SingleStepEdit.xaml 的交互逻辑
/// </summary>
public partial class SingleStepEdit : UserControl
{
public StepModel SelectedStep => MainWindow.Instance.SelectedStep ?? new();
public SingleStepEdit()
{
InitializeComponent();
}
private void SaveStep_Click(object sender, RoutedEventArgs e)
{
if (SelectedStep == null || (SelectedStep.Method == null && SelectedStep.SubProgram == null))
{
return;
}
if (SelectedStep.OKExpression != null)
{
Dictionary<string, object> paraDic = [];
foreach (var item in MainWindow.Instance.Program.Parameters)
{
paraDic.TryAdd(item.Name, item.Value!);
}
//if (!ExpressionEvaluator.IsExpressionValid(SelectedStep.OKExpression, paraDic))
//{
// MessageBox.Show("合格条件表达式非法,请检查后重试");
// return;
//}
}
if (StepsManager.Instance!.SelectedItem is StepModel originalStep)
{
originalStep.Name = SelectedStep.Name;
originalStep.OKExpression = SelectedStep.OKExpression;
originalStep.OKGotoStepID = SelectedStep.OKGotoStepID;
originalStep.NGGotoStepID = SelectedStep.NGGotoStepID;
originalStep.Description = SelectedStep.Description;
// 更新参数
if (originalStep.Method != null)
{
if (originalStep.StepType == "循环开始")
{
try
{
originalStep.LoopCount = Convert.ToInt32(SelectedStep.Method!.Parameters[0].Value);
}
catch
{
Log.Error("循环指令参数设置错误:类型转换失败");
}
}
else
{
for (int i = 0; i < originalStep.Method.Parameters.Count; i++)
{
try
{
var editedParam = SelectedStep.Method!.Parameters[i];
var originalParam = originalStep.Method.Parameters[i];
if (editedParam.IsUseVar)
{
originalParam.VariableName = editedParam.VariableName;
originalParam.VariableID = MainWindow.Instance.Program.Parameters.FirstOrDefault(x => x.Name == editedParam.VariableName)!.ID;
}
//originalParam.VariableName = editedParam.VariableName;
//originalParam.VariableID = MainWindow.Instance.Program.Parameters.FirstOrDefault(x => x.Name == editedParam.VariableName)!.ID;
originalParam.Value = editedParam.Value;
originalParam.IsUseVar = editedParam.IsUseVar;
originalParam.LowerLimit = editedParam.LowerLimit;
originalParam.UpperLimit = editedParam.UpperLimit;
originalParam.IsOutputToReport = editedParam.IsOutputToReport;
}
catch(Exception ex)
{
Log.Error(ex.Message);
}
}
}
}
else if (originalStep.SubProgram != null)
{
for (int i = 0; i < originalStep.SubProgram.Parameters.Count; i++)
{
var editedParam = SelectedStep.SubProgram!.Parameters[i];
var originalParam = originalStep.SubProgram.Parameters[i];
if (editedParam.IsUseVar)
{
originalParam.VariableName = editedParam.VariableName;
originalParam.VariableID = MainWindow.Instance.Program.Parameters.FirstOrDefault(x => x.Name == editedParam.VariableName)!.ID;
}
originalParam.Value = editedParam.Value;
originalParam.IsUseVar = editedParam.IsUseVar;
originalParam.LowerLimit = editedParam.LowerLimit;
originalParam.UpperLimit = editedParam.UpperLimit;
originalParam.IsOutputToReport = editedParam.IsOutputToReport;
}
}
Log.Success($"用户 [ {MainWindow.Instance.User.UserName} ] 修改步骤 [ {originalStep.Index} ] [ {originalStep.Name} ] ");
}
MainWindow.Instance.SelectedStep = null;
}
private void CancelStepEdit_Click(object sender, RoutedEventArgs e)
{
MainWindow.Instance.SelectedStep = null;
}
}
}

136
ATS/Views/StepsManager.xaml Normal file
View File

@ -0,0 +1,136 @@
<UserControl x:Class="ATS.Views.StepsManager"
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:dd="urn:gong-wpf-dragdrop"
xmlns:local="clr-namespace:ATS.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DataContext="{d:DesignInstance Type=local:StepsManager}"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<GroupBox Grid.Row="1"
Grid.Column="2"
Header="{Binding Title}">
<DataGrid x:Name="ProgramDataGrid"
dd:DragDrop.IsDragSource="{Binding IsAdmin}"
dd:DragDrop.IsDropTarget="{Binding IsAdmin}"
dd:DragDrop.UseDefaultDragAdorner="{Binding IsAdmin}"
AutoGenerateColumns="False"
Background="Transparent"
CanUserAddRows="False"
CanUserSortColumns="False"
ItemsSource="{Binding Program.StepCollection}"
PreviewKeyDown="StepManager_KeyDown"
SelectionMode="Extended"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTemplateColumn Header="断点"
Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<!-- 条件判断,决定是否显示断点 -->
<Border Visibility="{Binding isBrokenpoint, Converter={StaticResource BooleanToVisibilityConverter}}"
Background="Red"
BorderThickness="1"
Width="20"
Height="20"
CornerRadius="20"
Padding="5">
<TextBlock Text=""
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridCheckBoxColumn Width="58"
Binding="{Binding IsUsed, UpdateSourceTrigger=PropertyChanged}"
Header="启用" />
<DataGridTextColumn Binding="{Binding Index}"
Header="序号"
IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
Header="名称"
IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding StepType}"
Header="类型"
IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding Method.FullName}"
Header="指令类型"
IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding Method.Name}"
Header="指令"
IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding OKExpression}"
Header="合格条件"
IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding RunTime}"
Header="耗时(ms)"
IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding Result, Converter={StaticResource StepResultToStringConverter}}"
Header="结果"
IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding Description}"
Header="备注" />
</DataGrid.Columns>
<!-- 行样式定义 -->
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Foreground"
Value="Black" />
<Style.Triggers>
<DataTrigger Binding="{Binding Result}"
Value="0">
<Setter Property="Background"
Value="DodgerBlue" />
<Setter Property="Foreground"
Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding Result}"
Value="1">
<Setter Property="Background"
Value="LimeGreen" />
<Setter Property="Foreground"
Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding Result}"
Value="2">
<Setter Property="Background"
Value="Red" />
<Setter Property="Foreground"
Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding Result}"
Value="3">
<Setter Property="Background"
Value="Red" />
<Setter Property="Foreground"
Value="White" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Click="StepEdit_Click"
Header="编辑" />
<MenuItem Click="StepCopy_Click"
Header="复制" />
<MenuItem Click="StepPaste_Click"
Header="粘贴" />
<MenuItem Click="StepDelete_Click"
Header="删除"
Foreground="Red"/>
<MenuItem Click="SetOrCancelBroken_Click"
Header="添加/取消断点"
Foreground="Red"/>
</ContextMenu>
</DataGrid.ContextMenu>
</DataGrid>
</GroupBox>
</Grid>
</UserControl>

View File

@ -0,0 +1,311 @@
using ATS.Models;
using ATS.Tools;
using ATS.Windows;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
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.Navigation;
using System.Windows.Shapes;
using Timer = System.Timers.Timer;
namespace ATS.Views
{
/// <summary>
/// StepsManager.xaml 的交互逻辑
/// </summary>
public partial class StepsManager : UserControl, INotifyPropertyChanged
{
public static StepsManager? Instance { get; private set; }
private string _title = "主程序 NewProgram.ats";
public string Title
{
get => _title;
set
{
if (_title != value)
{
_title = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
}
}
}
/// <summary>
/// 更新 Title 显示,根据当前状态自动判断显示主程序或子程序名称
/// </summary>
public void UpdateTitle(string? subProgramName = null)
{
if (!string.IsNullOrEmpty(subProgramName))
{
// 如果传入了子程序名称,显示子程序标题
Title = $"子程序 {subProgramName}";
}
else
{
// 否则显示主程序标题
var tmp = MainWindow.Instance?.CurrentFilePath?.Split("\\");
if (tmp != null && tmp.Length > 1)
{
Title = "主程序 " + tmp[^1];
}
else
{
Title = "主程序 " + (tmp?.LastOrDefault() ?? "NewProgram.ats");
}
}
}
public ProgramModel Program => MainWindow.Instance.Program ?? new();
public object SelectedItem => ProgramDataGrid.SelectedItem;
public int SelectedIndex
{
get { return ProgramDataGrid.SelectedIndex; }
set { ProgramDataGrid.SelectedIndex = value; }
}
public bool IsAdmin => MainWindow.Instance.User.Role > 0;
private List<StepModel> tmpCopyList = [];
#region
private void ReOrderProgramList()
{
for (int i = 0; i < Program.StepCollection.Count; i++)
{
Program.StepCollection[i].Index = i + 1;
}
}
#endregion
public StepsManager()
{
Instance = this;
InitializeComponent();
DataContext = this;
MainWindow.Instance.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(MainWindow.Program))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Program)));
}
else if (e.PropertyName == nameof(MainWindow.CurrentFilePath))
{
// 当主程序文件路径变化时,更新 Title
UpdateTitle();
}
};
// 初始化 Title
UpdateTitle();
}
private void StepEdit_Click(object sender, RoutedEventArgs e)
{
if (MainWindow.Instance.User.Role < 1)
{
MessageBox.Show("当前登录用户无权限");
return;
}
if (MainWindow.Instance.SelectedStep != null)
{
MainWindow.Instance.SelectedStep = null;
}
if (ProgramDataGrid.SelectedItem is StepModel selectedStep)
{
// 如果是子程序类型,进入子程序编辑模式
if (selectedStep.StepType == "子程序" && selectedStep.SubProgram != null)
{
// 使用步骤的 Name 作为子程序的显示名称
string subProgramName = selectedStep.Name ?? "未命名子程序";
MainWindow.Instance.EnterSubProgramMode(
selectedStep.SubProgram, // 传递子程序模型
subProgramName, // 传递子程序名称(来自步骤名称)
selectedStep // 传递父步骤引用,用于保存时回写
);
// 更新 StepsManager 的 Title 显示子程序名称
UpdateTitle(subProgramName);
}
else
{
// 原有逻辑:编辑普通步骤
MainWindow.Instance.SelectedStep = new(selectedStep);
if (MainWindow.Instance.SelectedStep.Method != null)
{
if (MainWindow.Instance.SelectedStep.StepType == "循环开始")
{
MainWindow.Instance.SelectedStep.Method.Parameters[0].Value = MainWindow.Instance.SelectedStep.LoopCount;
}
else
{
foreach (var para in MainWindow.Instance.SelectedStep.Method.Parameters)
{
var tmppara = Program.Parameters.FirstOrDefault(x => x.ID == para.VariableID);
if (tmppara == null) continue;
para.VariableName = tmppara.Name;
}
}
}
else if (MainWindow.Instance.SelectedStep.SubProgram != null)
{
foreach (var para in MainWindow.Instance.SelectedStep.SubProgram.Parameters)
{
var tmppara = Program.Parameters.FirstOrDefault(x => x.ID == para.VariableID);
if (tmppara == null) continue;
para.VariableName = tmppara.Name;
}
}
}
}
}
private async void StepDelete_Click(object sender, RoutedEventArgs e)
{
if (MainWindow.Instance.User.Role < 1)
{
MessageBox.Show("当前登录用户无权限");
return;
}
MessageBoxResult result = MessageBox.Show($"确定执行删除操作?", "提示", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
List<StepModel> tmpList = [];
foreach (var item in ProgramDataGrid.SelectedItems)
{
if (item is StepModel selectedStep)
{
tmpList.Add(selectedStep);
}
}
List<int> deleteIndexList = [.. tmpList.OrderBy(x => x.Index).Select(x => x.Index)];
foreach (var item in tmpList)
{
Program.StepCollection.Remove(item);
}
await Task.Yield();
if (tmpList.Count == 1)
{
Log.Success($"用户 [ {MainWindow.Instance.User.UserName} ] 删除步骤 [ {tmpList[0].Index} ] ");
}
else if (tmpList.Count > 1)
{
Log.Success($"用户 [ {MainWindow.Instance.User.UserName} ] 删除步骤 [ {string.Join("", deleteIndexList)} ] ");
}
ReOrderProgramList();
}
}
private void StepCopy_Click(object sender, RoutedEventArgs e)
{
tmpCopyList.Clear();
foreach (var item in ProgramDataGrid.SelectedItems)
{
if (item is StepModel selectedStep)
{
tmpCopyList.Add(selectedStep);
}
}
}
private void StepPaste_Click(object sender, RoutedEventArgs e)
{
if (MainWindow.Instance.User.Role < 1)
{
MessageBox.Show("当前登录用户无权限");
return;
}
var insertIndex = SelectedIndex + 1;
List<int> copyIndexList = [.. tmpCopyList.OrderBy(x => x.Index).Select(x => x.Index)];
foreach (var item in tmpCopyList)
{
Program.StepCollection.Insert(insertIndex, new(item) { ID = Guid.NewGuid() });
insertIndex++;
}
if (tmpCopyList.Count == 1)
{
Log.Success($"用户 [ {MainWindow.Instance.User.UserName} ] 粘贴步骤 [ {tmpCopyList[0].Index} ] 到 [ {insertIndex} ] ");
}
else if (tmpCopyList.Count > 1)
{
Log.Success($"用户 [ {MainWindow.Instance.User.UserName} ] 粘贴步骤 [ {string.Join("", copyIndexList)} ] 到 [ {insertIndex + 1 - tmpCopyList.Count} - {insertIndex} ] ");
}
ReOrderProgramList();
}
private void SetOrCancelBroken_Click(object sender, RoutedEventArgs e)
{
foreach (var item in ProgramDataGrid.SelectedItems)
{
if (item is StepModel selectedStep)
{
selectedStep.isBrokenpoint = selectedStep.isBrokenpoint == null|| selectedStep.isBrokenpoint == false ? true : false;
}
}
}
private void StepManager_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Delete:
StepDelete_Click(sender, e);
break;
case Key.C:
if (Keyboard.Modifiers == ModifierKeys.Control)
{
StepCopy_Click(sender, e);
}
break;
case Key.V:
if (Keyboard.Modifiers == ModifierKeys.Control)
{
StepPaste_Click(sender, e);
}
break;
}
}
// 在 StepsManager 中添加
public ProgramModel CurrentProgram
{
get => (ProgramModel)GetValue(CurrentProgramProperty);
set => SetValue(CurrentProgramProperty, value);
}
public static readonly DependencyProperty CurrentProgramProperty =
DependencyProperty.Register("CurrentProgram", typeof(ProgramModel), typeof(StepsManager),
new PropertyMetadata(null, OnCurrentProgramChanged));
private static void OnCurrentProgramChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var stepsManager = (StepsManager)d;
// 更新绑定
stepsManager.DataContext = e.NewValue;
}
}
}

136
ATS/Views/ToolBar.xaml Normal file
View File

@ -0,0 +1,136 @@
<UserControl x:Class="ATS.Views.ToolBar"
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:local="clr-namespace:ATS.Views"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converters="clr-namespace:ATS.Tools"
d:DataContext="{d:DesignInstance Type=local:ToolBar}"
d:DesignWidth="800"
mc:Ignorable="d"
Height="50">
<Grid>
<Menu Grid.ColumnSpan="3"
Background="Transparent">
<MenuItem Header="返回上级程序"
FontSize="13"
Height="50"
Click="NavigateBack_Click">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ArrowLeft" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="文件"
FontSize="13"
Height="50">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="File" />
</MenuItem.Icon>
<MenuItem Click="File_New_Click"
Header="新建" />
<MenuItem Click="File_Open_Click"
Header="打开" />
<MenuItem Click="File_Save_Click"
Header="保存" />
<MenuItem Click="File_SaveAsOther_Click"
Header="另存为" />
<MenuItem Click="Set_DefaultProgram_Click"
Header="设置默认程序" />
</MenuItem>
<MenuItem Header="工具"
FontSize="13"
Height="50">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Tools" />
</MenuItem.Icon>
<MenuItem Click="SystemConfig_Click"
Header="系统设置"
IsEnabled="{Binding IsAdmin}" />
<MenuItem Header="设备管理"
Click="DeviceManage_Click"
IsEnabled="{Binding IsAdmin}">
</MenuItem>
<MenuItem Header="数据">
<MenuItem Click="TestDataInfo_Click"
Header="数据查询" />
</MenuItem>
<MenuItem Header="同星CAN"
IsEnabled="{Binding IsAdmin}">
<MenuItem Click="Connect_DisConnect_Click"
Header="连接/断开" />
<MenuItem Click="ChannelMapping_Click"
Header="通道映射" />
<MenuItem Click="CAN_DatabaseConnect_Click"
Header="加载数据库" />
<MenuItem Click="CAN_CatchConfig_Click"
Header="CAN采集信号配置"
IsEnabled="{Binding IsDebug}"/>
</MenuItem>
<MenuItem Header="调试"
IsEnabled="{Binding IsDebug}">
<MenuItem Header="自动运行"
Click="Debug_AutoRun_Click" />
<MenuItem Header="清空数据库"
Foreground="Red"
Click="Debug_ClearDataBase_Click" />
</MenuItem>
</MenuItem>
<MenuItem Click="ProgramRun_Click"
FontSize="13"
Height="50"
Header="{Binding RunState}"
IsEnabled="{Binding IsTerminate, Converter={StaticResource BoolInverseConverter}}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="{Binding RunIcon}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Click="SingleStepExecution_Click"
FontSize="13"
Height="50"
Header="单步执行"
IsEnabled="{Binding IsTerminate, Converter={StaticResource BoolInverseConverter}}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Arrow" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Click="Terminate_Click"
FontSize="13"
Height="50"
Header="停止"
IsEnabled="{Binding IsStop, Converter={StaticResource BoolInverseConverter}}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Stop" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Click="Reset_Click"
FontSize="13"
Height="50"
Header="复位"
IsEnabled="{Binding IsTerminate}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Restart" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Click="Report_Click"
FontSize="13"
Height="50"
Header="导出报告"
IsEnabled="{Binding CanReport, Converter={StaticResource BoolInverseConverter}}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Book" />
</MenuItem.Icon>
</MenuItem>
</Menu>
</Grid>
</UserControl>

613
ATS/Views/ToolBar.xaml.cs Normal file
View File

@ -0,0 +1,613 @@
using ATS.Logic;
using ATS.Models;
using ATS.Tools;
using ATS.Windows;
using ATS_DBContext;
using MaterialDesignThemes.Wpf;
using Microsoft.Win32;
using Newtonsoft.Json;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.IO;
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.Navigation;
using System.Windows.Shapes;
using TSMasterCAN;
using static ATS.Models.ParameterModel;
using Path = System.IO.Path;
namespace ATS.Views
{
/// <summary>
/// ToolBar.xaml 的交互逻辑
/// </summary>
[AddINotifyPropertyChangedInterface]
public partial class ToolBar : UserControl
{
public static ToolBar Instance { get; private set; }
public string CurSubProgramPath { get; set; }
public bool SingleStep = false;
public string RunState { get; set; } = "运行";
public PackIconKind RunIcon { get; set; } = PackIconKind.Play;
public bool? IsStop { get; set; } = null;
public bool IsTerminate { get; set; } = false;
public bool? CanReport => IsStop == false; // 是否可以导出报告
public bool IsAdmin => MainWindow.Instance.User.Role > 0;
public bool IsDebug => MainWindow.Instance.User.UserName == "开发者" && MainWindow.Instance.User.UserAccount == "Developer";
private Task? currentExecutionTask;
public ToolBar()
{
Instance = this;
InitializeComponent();
DataContext = this;
// 监听主窗口属性变化,特别是 Title 变化
if (MainWindow.Instance != null)
{
MainWindow.Instance.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(MainWindow.IsInSubProgramMode) ||
e.PropertyName == "Title") // 监听 Title 变化
{
OnPropertyChanged(nameof(IsInSubProgramMode));
OnPropertyChanged(nameof(CurrentPathDisplay));
}
};
}
this.Loaded += (s, e) => LoadDefaultProgramIfExists();
}
private void LoadDefaultProgramIfExists()
{
try
{
if (File.Exists(SystemConfig.Instance.DefaultSubProgramFilePath))
{
string json = File.ReadAllText(SystemConfig.Instance.DefaultSubProgramFilePath);
var tmp = JsonConvert.DeserializeObject<ProgramModel>(json);
if (tmp != null)
{
MainWindow.Instance.Program = tmp;
InitParameter(tmp);
InitDevice(tmp);
}
else
{
MainWindow.Instance.Program = new();
}
MainWindow.Instance.CurrentFilePath = SystemConfig.Instance.DefaultSubProgramFilePath;
Log.Success($"已打开文件: {SystemConfig.Instance.DefaultSubProgramFilePath}");
}
}
catch (Exception ex)
{
Log.Error($"默认程序:{SystemConfig.Instance.DefaultSubProgramFilePath} 文件打开失败:{ex.Message}");
}
}
#region
private void InitDevice(ProgramModel program)
{
if (program.Devices != null && program.Devices.Count > 0)
{
foreach (DeviceModel device in program.Devices)
{
_ = DeviceConnect.InitAndConnectDevice(program, device);
}
}
foreach (var step in program.StepCollection)
{
if (step.SubProgram != null)
{
InitDevice(step.SubProgram);
}
}
}
private void InitParameter(ProgramModel program)
{
if (program.Parameters != null && program.Parameters.Count > 0)
{
foreach (var parameter in program.Parameters)
{
if (parameter.Type!.BaseType == typeof(Enum))
{
parameter.Value = Enum.Parse(parameter.Type, parameter.Value!.ToString()!);
}
}
}
foreach (var step in program.StepCollection)
{
if (step.SubProgram != null)
{
InitDevice(step.SubProgram);
}
}
}
private void SaveProgramToFile(string filePath)
{
try
{
var tmp = ClearDeviceParameterValue(MainWindow.Instance.Program);
string json = JsonConvert.SerializeObject(tmp, Formatting.Indented);
File.WriteAllText(filePath, json);
Log.Success($"程序已保存: {filePath}");
}
catch (Exception ex)
{
Log.Error($"保存文件失败: {ex.Message}");
}
}
private ProgramModel ClearUnnecessaryParameterValue(ProgramModel program)
{
var tmp = new ProgramModel(program);
foreach (var step in tmp.StepCollection)
{
if (step.Method != null)
{
foreach (var para in step.Method.Parameters)
{
if (para.VariableID != null && para.Category == ParameterCategory.Output)
{
para.Value = null;
tmp.Parameters.First(x => x.ID == para.VariableID)!.Value = null;
}
}
}
else if (step.SubProgram != null)
{
var subTmp = ClearUnnecessaryParameterValue(step.SubProgram);
step.SubProgram = subTmp;
}
}
return tmp;
}
private ProgramModel ClearDeviceParameterValue(ProgramModel program)
{
var tmp = new ProgramModel(program);
foreach (var device in tmp.Devices)
{
tmp.Parameters.Remove(tmp.Parameters.First(x => x.ID == device.ParameterID));
}
foreach (var step in tmp.StepCollection)
{
if (step.SubProgram != null)
{
step.SubProgram = ClearDeviceParameterValue(step.SubProgram);
}
}
return tmp;
}
#endregion
private void File_New_Click(object sender, RoutedEventArgs e)
{
MainWindow.Instance.Program = new();
MainWindow.Instance.CurrentFilePath = "";
}
private void Set_DefaultProgram_Click(object sender, RoutedEventArgs e)
{
SystemConfig.Instance.DefaultSubProgramFilePath = CurSubProgramPath;
SystemConfig.Instance.SaveToFile();
}
private void File_Open_Click(object sender, RoutedEventArgs e)
{
var openFileDialog = new OpenFileDialog
{
Filter = "ATS程序文件|*.ats|所有文件|*.*",
Title = "打开程序文件"
};
if (openFileDialog.ShowDialog() == true)
{
try
{
CurSubProgramPath = openFileDialog.FileName;
string json = File.ReadAllText(openFileDialog.FileName);
var tmp = JsonConvert.DeserializeObject<ProgramModel>(json);
if (tmp != null)
{
MainWindow.Instance.Program = tmp;
MainWindow.Instance.Title = Path.GetFileName(openFileDialog.FileName).Split(".ats")[0];
InitParameter(tmp);
InitDevice(tmp);
}
else
{
MainWindow.Instance.Program = new();
}
MainWindow.Instance.SelectedStep = null;
MainWindow.Instance.CurrentFilePath = openFileDialog.FileName;
Log.Success($"已打开文件: {openFileDialog.FileName}");
}
catch (Exception ex)
{
Log.Error($"程序文件打开失败:{ex.Message}");
}
}
}
private void File_Save_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(MainWindow.Instance.CurrentFilePath))
{
File_SaveAsOther_Click(sender, e);
}
else
{
SaveProgramToFile(MainWindow.Instance.CurrentFilePath);
}
}
private void File_SaveAsOther_Click(object sender, RoutedEventArgs e)
{
var saveFileDialog = new SaveFileDialog
{
Filter = "ATS程序文件|*.ats|所有文件|*.*",
Title = "另存为",
FileName = "NewProgram.ats"
};
if (saveFileDialog.ShowDialog() == true)
{
MainWindow.Instance.CurrentFilePath = saveFileDialog.FileName;
SaveProgramToFile(MainWindow.Instance.CurrentFilePath);
}
}
private async void ProgramRun_Click(object sender, RoutedEventArgs e)
{
if (RunState == "运行")
{
SingleStep = false;
Log.Info($"用户 [ {MainWindow.Instance.User.UserName} ] 点击 [ 运行 ] ");
// 清空报告列表
ReportModelList.ReportList = new();
RunState = "暂停";
RunIcon = PackIconKind.Pause;
if (IsStop == null)
{
IsStop = false;
currentExecutionTask = StepRunning.ExecuteSteps(MainWindow.Instance.Program, cancellationToken: StepRunning.stepCTS.Token);
await currentExecutionTask;
RunState = "运行";
RunIcon = PackIconKind.Play;
IsStop = null;
}
else if (IsStop == true)
{
IsStop = false;
}
}
else
{
Log.Info($"用户 [ {MainWindow.Instance.User.UserName} ] 点击 [ 暂停 ] ");
IsStop = true;
RunState = "运行";
RunIcon = PackIconKind.Play;
}
}
private async void SingleStepExecution_Click(object sender, RoutedEventArgs e)
{
if (RunState == "运行")
{
SingleStep = true;
Log.Info($"用户 [ {MainWindow.Instance.User.UserName} ] 点击 [ 单步执行 ] ");
RunState = "暂停";
RunIcon = PackIconKind.Pause;
if (IsStop == null)
{
IsStop = false;
currentExecutionTask = StepRunning.ExecuteSteps(MainWindow.Instance.Program, cancellationToken: StepRunning.stepCTS.Token);
await currentExecutionTask;
RunState = "运行";
RunIcon = PackIconKind.Play;
IsStop = null;
}
else if (IsStop == true)
{
IsStop = false;
}
}
}
private void Terminate_Click(object sender, RoutedEventArgs e)
{
if (IsStop == false)
{
Log.Info($"用户 [ {MainWindow.Instance.User.UserName} ] 点击 [ 停止 ] ");
IsTerminate = true;
IsStop = null;
RunState = "运行";
RunIcon = PackIconKind.Play;
StepRunning.stepCTS.Cancel();
}
}
private void Reset_Click(object sender, RoutedEventArgs e)
{
Log.Info($"用户 [ {MainWindow.Instance.User.UserName} ] 点击 [ 复位 ] ");
IsTerminate = false;
currentExecutionTask = null;
StepRunning.stepCTS = new();
StepRunning.ResetAllStepStatus(MainWindow.Instance.Program);
}
//导出按钮
private void Report_Click(object sender, RoutedEventArgs e)
{
Log.Info($"用户 [ {MainWindow.Instance.User.UserName} ] 点击 [ 导出报告 ] ");
if (ReportModelList.ReportList != null && ReportModelList.ReportList.Count > 0)
{
//生成csv报告文件
GenerateCsvReport();
}
else
{
Log.Warning("当前无报告可导出");
MessageBox.Show("当前无报告可导出", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
private void ChannelMapping_Click(object sender, RoutedEventArgs e)
{
var re = CAN.ShowChannelMappingWindow(true);
if (re != 0)
{
var msg = CAN.GetErrorDescription(re);
Log.Error($"同星通道映射界面打开失败:{msg}");
}
}
private void TestDataInfo_Click(object sender, RoutedEventArgs e)
{
new TestDataInfomationWindow().Show();
}
private void SystemConfig_Click(object sender, RoutedEventArgs e)
{
new SystemConfigWindow().Show();
}
Task? DebugAutoRunTask;
CancellationTokenSource DebugAutoRunTaskCT;
private void Debug_AutoRun_Click(object sender, RoutedEventArgs e)
{
if (MainWindow.Instance.Program.StepCollection.Count == 0) return;
if (DebugAutoRunTask == null)
{
DebugAutoRunTaskCT = new();
try
{
DebugAutoRunTask = Task.Run(async () =>
{
while (true)
{
DebugAutoRunTaskCT.Token.ThrowIfCancellationRequested();
await StepRunning.ExecuteSteps(MainWindow.Instance.Program, cancellationToken: DebugAutoRunTaskCT.Token);
}
});
}
catch (OperationCanceledException)
{
}
}
else
{
DebugAutoRunTaskCT.Cancel();
DebugAutoRunTask = null;
}
}
private async void Debug_ClearDataBase_Click(object sender, RoutedEventArgs e)
{
MessageBoxResult result = MessageBox.Show($"确定执行删除操作?", "提示", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
using ATS_DB db = new();
db.TestData.RemoveRange(db.TestData);
await db.SaveChangesAsync();
}
}
private void CAN_DatabaseConnect_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog
{
Title = "请选择 DBC 文件",
Filter = "DBC 文件 (*.dbc)|*.dbc",
//InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
Multiselect = false
};
if (openFileDialog.ShowDialog() == true)
{
var re = CAN.LoadDBC(openFileDialog.FileName, [0, 1, 2, 3], out var DataBaseID);
if (re != 0)
{
Log.Error("CAN数据库加载失败" + CAN.GetErrorDescription(re));
}
else
{
Log.Success("CAN数据库加载成功");
}
}
}
private bool isConnect = true;
private void Connect_DisConnect_Click(object sender, RoutedEventArgs e)
{
if (isConnect)
{
var res = CAN.DisConnect();
if (res == 0)
{
Log.Success($"CAN卡断开连接成功,返回值:{res}");
isConnect = false;
}
else
{
Log.Error($"CAN卡断开连接失败,返回值:{res}");
}
}
else
{
var res = CAN.Connect();
if (res == 0)
{
Log.Success($"CAN卡连接成功,返回值:{res}");
isConnect = true;
}
else
{
Log.Error($"CAN卡连接失败,返回值:{res}");
}
}
}
private void GenerateCsvReport()
{
try
{
// 生成文件名(包含当前时间戳)
string fileName = $"Report_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Reports", fileName);
// 确保 Reports 目录存在
string directory = Path.GetDirectoryName(filePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 写入 CSV 文件
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8))
{
// 写入 CSV 头部(新增"所属子程序"列)
writer.WriteLine("步骤序号,步骤名称,所属子程序,备注,执行人,执行时间,是否通过,结果");
var allPASS = true;
// 写入数据行
foreach (var report in ReportModelList.ReportList)
{
string stepIndex = report.stepModel?.Index.ToString() ?? "";
string stepName = EscapeCsvField(report.stepModel?.Name ?? "");
string subProgramPath = EscapeCsvField(report.SubProgramPath); // 新增列
string stepRemark = EscapeCsvField(report.stepModel?.Description ?? "");
string user = EscapeCsvField(report.User);
string executeTime = report.ExcuteTime?.ToString("yyyy-MM-dd HH:mm:ss");
string isPass = report.IsPass.ToString(); // 根据你的 IsPass 枚举定义,可能需要调整
if (isPass == "FAIL") allPASS = false;
string result = EscapeCsvField(report.Result ?? "");
writer.WriteLine($"{stepIndex},{stepName},{subProgramPath},{stepRemark},{user},{executeTime},{isPass},{result}");
}
// 写入报告总结果
writer.WriteLine("");
writer.WriteLine($"测试总结果,,,,,,{(allPASS ? "PASS" : "FAIL")},");
}
Log.Success($"报告导出成功,路径:{filePath}");
MessageBox.Show($"报告导出成功!\n文件路径{filePath}", "导出成功", MessageBoxButton.OK, MessageBoxImage.Information);
// 可选:打开文件所在目录
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\"");
}
catch (Exception ex)
{
Log.Error($"导出报告失败:{ex.Message}");
MessageBox.Show($"导出报告失败:{ex.Message}", "导出失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 转义 CSV 字段中的特殊字符(逗号、双引号、换行符等)
/// </summary>
/// <param name="field">原始字段值</param>
/// <returns>转义后的字段值</returns>
private string EscapeCsvField(string field)
{
if (string.IsNullOrEmpty(field))
return "";
// 如果字段包含逗号、双引号或换行符,则用双引号包围,并将双引号转义为两个双引号
if (field.Contains(",") || field.Contains("\"") || field.Contains("\n") || field.Contains("\r"))
{
return "\"" + field.Replace("\"", "\"\"") + "\"";
}
return field;
}
private void CAN_CatchConfig_Click(object sender, RoutedEventArgs e)
{
new CANCatchSingalView().Show();
}
// 添加 IsInSubProgramMode 依赖属性或普通属性
// 如果 MainWindow.Instance.IsInSubProgramMode 是普通属性,可以用普通属性
public bool IsInSubProgramMode => MainWindow.Instance?.IsInSubProgramMode ?? false;
// 修改 CurrentPathDisplay 属性,直接从 MainWindow 获取 Title
public string CurrentPathDisplay
{
get
{
// 直接使用 MainWindow 的 Title它已经在 MainWindow 中被正确管理
return MainWindow.Instance?.Title ?? "ATS";
}
set
{
if (MainWindow.Instance != null)
{
value = MainWindow.Instance.Title;
}
}
}
private void NavigateBack_Click(object sender, RoutedEventArgs e)
{
MainWindow.Instance?.ExitSubProgramMode();
}
private void DeviceManage_Click(object sender, RoutedEventArgs e)
{
new DeviceManageWindow().Show();
}
}
}

View File

@ -0,0 +1,174 @@
<mah:MetroWindow x:Class="ATS.Windows.DeviceManageWindow"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
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:ATS.Windows"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:DeviceManageWindow}"
WindowStyle="None"
WindowStartupLocation="CenterScreen"
ShowTitleBar="False"
Title="DeviceManageWindow" Height="750" Width="900">
<Grid>
<GroupBox Header="设备管理界面"
Padding="0,10,0,0"
MouseLeftButtonDown="GroupBox_MouseLeftButtonDown">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧:设备列表 -->
<GroupBox Grid.Row="0" Grid.Column="0" Header="预配置设备列表" Margin="0,0,5,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 设备列表 -->
<ListBox Grid.Row="0"
ItemsSource="{Binding PreDefineDevices}"
SelectedItem="{Binding SelectedDevice}"
SelectionChanged="DeviceList_SelectionChanged"
Margin="5">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" Margin="5">
<TextBlock Text="{Binding Name}"
FontWeight="Bold"
FontSize="14"/>
<TextBlock Text="{Binding Type}"
FontSize="12"
Foreground="Gray"/>
<TextBlock Text="{Binding Description}"
FontSize="11"
Foreground="DarkGray"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- 按钮区 -->
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="5">
<Button Content="新增设备"
Width="100"
Margin="15,5,15,5"
Click="AddDevice_Click"/>
<Button Content="删除设备"
Width="100"
Margin="15,5,15,5"
Click="DeleteDevice_Click"
Foreground="Red"/>
</StackPanel>
</Grid>
</GroupBox>
<!-- 右侧:设备编辑区 -->
<GroupBox Grid.Row="0" Grid.Column="1" Header="设备配置" Margin="5,0,0,0">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="10" IsEnabled="{Binding SelectedDevice, Converter={StaticResource StringToVisibilityConverter}}">
<!-- 设备名称 -->
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="设备名称" Width="100" VerticalAlignment="Center"/>
<TextBox Text="{Binding SelectedDevice.Name, UpdateSourceTrigger=PropertyChanged}"
Width="250"
VerticalAlignment="Center"/>
</StackPanel>
<!-- 通讯类型 -->
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="通讯类型" Width="100" VerticalAlignment="Center"/>
<ComboBox ItemsSource="{Binding Types}"
SelectedItem="{Binding SelectedDevice.Type}"
Width="250"
SelectionChanged="DeviceType_SelectionChanged"
VerticalAlignment="Center"/>
</StackPanel>
<!-- 设备描述 -->
<StackPanel Orientation="Horizontal" Margin="0,10">
<Label Content="设备描述" Width="100" VerticalAlignment="Center"/>
<TextBox Text="{Binding SelectedDevice.Description, UpdateSourceTrigger=PropertyChanged}"
Width="250"
TextWrapping="Wrap"
AcceptsReturn="True"
VerticalAlignment="Top"/>
</StackPanel>
<!-- 连接参数 -->
<Label Content="连接参数"
FontWeight="Bold"
FontSize="14"
Margin="0,20,0,10"/>
<ItemsControl ItemsSource="{Binding DeviceConnectSettings}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0,5">
<Label Content="{Binding Name}"
Width="100"
VerticalAlignment="Center"/>
<!-- 文本输入框除COM口和奇偶校验外 -->
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"
Width="250"
VerticalAlignment="Center"
LostFocus="ConnectSettingValue_LostFocus"
Visibility="{Binding Name, Converter={StaticResource DeviceNameConverter}}"/>
<!-- 下拉框COM口和奇偶校验 -->
<ComboBox SelectedItem="{Binding Value}"
Width="250"
VerticalAlignment="Center"
ItemsSource="{Binding Name, Converter={StaticResource DeviceNameConverter}, ConverterParameter=Items}"
Visibility="{Binding Name, Converter={StaticResource DeviceNameConverter}, ConverterParameter=Inverse}"
SelectionChanged="ConnectSettingValue_LostFocus"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 提示信息 -->
<TextBlock Text="请选择左侧设备进行编辑"
Foreground="Gray"
FontStyle="Italic"
Margin="0,20"
HorizontalAlignment="Center"
Visibility="{Binding SelectedDevice, Converter={StaticResource BoolInverseConverter}}"/>
</StackPanel>
</ScrollViewer>
</GroupBox>
<!-- 底部按钮区 -->
<StackPanel Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="10">
<Button Content="保存"
Width="100"
Margin="15,5,15,5"
Click="Save_Click"/>
<Button Content="关闭"
Width="100"
Margin="15,5,15,5"
Click="Close_Click"/>
</StackPanel>
</Grid>
</GroupBox>
</Grid>
</mah:MetroWindow>

View File

@ -0,0 +1,286 @@
using ATS.Models;
using ATS.Tools;
using MahApps.Metro.Controls;
using Newtonsoft.Json;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
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 ATS.Windows
{
/// <summary>
/// DeviceManageWindow.xaml 的交互逻辑
/// </summary>
[AddINotifyPropertyChangedInterface]
public partial class DeviceManageWindow : MetroWindow
{
/// <summary>
/// 预配置设备列表
/// </summary>
public ObservableCollection<DeviceModel> PreDefineDevices { get; set; } = new();
/// <summary>
/// 当前选中的设备
/// </summary>
public DeviceModel? SelectedDevice { get; set; }
/// <summary>
/// 设备连接参数列表
/// </summary>
public ObservableCollection<DeviceConnectSettingModel> DeviceConnectSettings { get; set; } = new();
/// <summary>
/// 通讯类型列表
/// </summary>
public ObservableCollection<string> Types { get; set; } = new ObservableCollection<string>
{
"串口", "Tcp", "Udp", "ModbusRtu_Tcp", "ModbusRtu_Udp", "ModbusRtu_Serial", "ModbusTcp", "CAN"
};
private string _preDefineDevicesPath = "";
public DeviceManageWindow()
{
InitializeComponent();
DataContext = this;
LoadPreDefineDevices();
}
/// <summary>
/// 从 JSON 文件加载预配置设备
/// </summary>
private void LoadPreDefineDevices()
{
try
{
_preDefineDevicesPath = SystemConfig.Instance.PreDefineDevicesPath;
// 确保目录存在
string? directory = System.IO.Path.GetDirectoryName(_preDefineDevicesPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
if (!File.Exists(_preDefineDevicesPath))
{
// 创建空的配置文件
var emptyList = new ObservableCollection<DeviceModel>();
string json = JsonConvert.SerializeObject(emptyList, Formatting.Indented);
File.WriteAllText(_preDefineDevicesPath, json);
PreDefineDevices = emptyList;
return;
}
string fileContent = File.ReadAllText(_preDefineDevicesPath);
var devices = JsonConvert.DeserializeObject<ObservableCollection<DeviceModel>>(fileContent);
PreDefineDevices = devices ?? new ObservableCollection<DeviceModel>();
Log.Info($"已加载 {PreDefineDevices.Count} 个预配置设备");
}
catch (Exception ex)
{
MessageBox.Show($"加载预配置设备失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
PreDefineDevices = new ObservableCollection<DeviceModel>();
}
}
/// <summary>
/// 保存预配置设备到 JSON 文件
/// </summary>
private void SavePreDefineDevices()
{
try
{
string json = JsonConvert.SerializeObject(PreDefineDevices, Formatting.Indented);
File.WriteAllText(_preDefineDevicesPath, json);
Log.Success($"预配置设备已保存到: {_preDefineDevicesPath}");
MessageBox.Show("保存成功!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"保存失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
Log.Error($"保存预配置设备失败: {ex.Message}");
}
}
#region
/// <summary>
/// 设备选择变化
/// </summary>
private void DeviceList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
DeviceType_SelectionChanged(sender, e);
}
/// <summary>
/// 通讯类型变化
/// </summary>
private void DeviceType_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (SelectedDevice == null) return;
DeviceConnectSettings.Clear();
// 如果设备已有保存的连接参数,直接从 ConnectString 加载
if (!string.IsNullOrEmpty(SelectedDevice.ConnectString))
{
try
{
var settings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(SelectedDevice.ConnectString);
if (settings != null && settings.Count > 0)
{
DeviceConnectSettings = settings;
return;
}
}
catch (Exception ex)
{
Log.Error($"加载连接参数失败: {ex.Message}");
}
}
// 仅当设备没有保存的连接参数时,才初始化默认值
switch (SelectedDevice.Type)
{
case "Tcp":
case "ModbusRtu_Tcp":
case "ModbusTcp":
DeviceConnectSettings.Add(new() { Name = "IP地址", Value = "192.168.0.0" });
DeviceConnectSettings.Add(new() { Name = "端口号", Value = "502" });
break;
case "串口":
case "ModbusRtu_Serial":
DeviceConnectSettings.Add(new() { Name = "COM口", Value = "COM1" });
DeviceConnectSettings.Add(new() { Name = "波特率", Value = "9600" });
DeviceConnectSettings.Add(new() { Name = "数据位", Value = "8" });
DeviceConnectSettings.Add(new() { Name = "停止位", Value = "1" });
DeviceConnectSettings.Add(new() { Name = "奇偶", Value = "无" });
break;
case "ModbusRtu_Udp":
case "Udp":
DeviceConnectSettings.Add(new() { Name = "IP地址", Value = "192.168.0.0" });
DeviceConnectSettings.Add(new() { Name = "端口号", Value = "502" });
DeviceConnectSettings.Add(new() { Name = "本地端口号", Value = "8080" });
break;
}
DeviceConnectSettings.Add(new() { Name = "读超时", Value = "3000" });
DeviceConnectSettings.Add(new() { Name = "写超时", Value = "3000" });
// 更新设备的 ConnectString
UpdateDeviceConnectString();
}
/// <summary>
/// 更新设备连接字符串
/// </summary>
private void UpdateDeviceConnectString()
{
if (SelectedDevice != null)
{
SelectedDevice.ConnectString = JsonConvert.SerializeObject(DeviceConnectSettings);
}
}
/// <summary>
/// 连接参数值变化
/// </summary>
private void ConnectSettingValue_LostFocus(object sender, RoutedEventArgs e)
{
UpdateDeviceConnectString();
}
/// <summary>
/// 新增设备
/// </summary>
private void AddDevice_Click(object sender, RoutedEventArgs e)
{
var newDevice = new DeviceModel
{
Name = "新设备",
Type = "Tcp",
Description = "",
ConnectString = "[]"
};
PreDefineDevices.Add(newDevice);
SelectedDevice = newDevice;
// 触发类型选择,生成默认连接参数
DeviceType_SelectionChanged(sender, null!);
}
/// <summary>
/// 删除设备
/// </summary>
private void DeleteDevice_Click(object sender, RoutedEventArgs e)
{
if (SelectedDevice == null)
{
MessageBox.Show("请先选择要删除的设备!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var result = MessageBox.Show(
$"确定要删除设备 '{SelectedDevice.Name}' 吗?",
"确认删除",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
PreDefineDevices.Remove(SelectedDevice);
SelectedDevice = null;
DeviceConnectSettings.Clear();
}
}
/// <summary>
/// 保存按钮
/// </summary>
private void Save_Click(object sender, RoutedEventArgs e)
{
// 确保当前选中设备的连接参数已更新
UpdateDeviceConnectString();
SavePreDefineDevices();
this.Close();
}
/// <summary>
/// 关闭窗口
/// </summary>
private void Close_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
/// <summary>
/// 窗口拖动
/// </summary>
private void GroupBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
DragMove();
}
}
#endregion
}
}

View File

@ -0,0 +1,126 @@
<mah:MetroWindow x:Class="ATS.Windows.DeviceSettingWindow"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
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:ATS.Windows"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:DeviceSettingWindow}"
WindowStyle="None"
WindowStartupLocation="CenterScreen"
ShowTitleBar="False"
Title="ParameterSettingWindow"
Height="500"
Width="450">
<Grid>
<GroupBox Header="设备编辑界面"
Padding="10,15,10,0"
MouseLeftButtonDown="GroupBox_MouseLeftButtonDown">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<ScrollViewer>
<StackPanel>
<StackPanel Height="30"
Visibility="{Binding Device.ErrorMessage, Converter={StaticResource StringToVisibilityConverter}}"
Orientation="Horizontal"
Margin="7,7,0,7">
<TextBlock Text="{Binding Device.ErrorMessage}"
Padding="0,11"
Height="30"
Foreground="Red" />
</StackPanel>
<StackPanel Height="30"
Orientation="Horizontal"
Margin="7">
<Label Content="设备名称"
VerticalAlignment="Bottom"
Width="85" />
<TextBox Text="{Binding Device.Name}"
Visibility="{Binding IsInputMode}"
VerticalAlignment="Bottom"
Width="120" />
<ComboBox ItemsSource="{Binding PreSelectDevices}"
SelectedItem="{Binding SelectedPreDefineDevice}"
DisplayMemberPath="Name"
Visibility="{Binding ShowComboBox}"
VerticalAlignment="Bottom"
Width="120" />
<CheckBox IsChecked="{Binding IsSelectMode}"
Margin="10,0,0,0"
VerticalAlignment="Center" />
<Label Content="从预设中选择"
VerticalAlignment="Bottom"
Width="85" />
</StackPanel>
<StackPanel Height="30"
Orientation="Horizontal"
Margin="7">
<Label Content="通讯协议类型"
VerticalAlignment="Bottom"
Width="85" />
<ComboBox ItemsSource="{Binding Types}"
SelectedItem="{Binding Device.Type}"
VerticalAlignment="Bottom"
Width="120"
SelectionChanged="ComboBox_SelectionChanged"
IsEnabled="{Binding IsAdd}" />
</StackPanel>
<StackPanel Height="30"
Orientation="Horizontal"
Margin="7">
<Label Content="设备描述"
Width="85"
VerticalAlignment="Bottom" />
<TextBox Text="{Binding Device.Description}"
VerticalAlignment="Bottom"
Width="120" />
</StackPanel>
<Label Content="连接参数"
Height="30"
Margin="7"
VerticalContentAlignment="Bottom" />
<ItemsControl ItemsSource="{Binding DeviceConnectSettings}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal"
Height="30"
Margin="0,7">
<Label Content="{Binding Name}"
Width="50"
Margin="40,0,4,0"
VerticalContentAlignment="Bottom" />
<TextBox VerticalAlignment="Bottom"
Visibility="{Binding Name, Converter={StaticResource DeviceNameConverter}}"
Text="{Binding Value}"
Width="120" />
<ComboBox VerticalAlignment="Bottom"
Visibility="{Binding Name, Converter={StaticResource DeviceNameConverter}, ConverterParameter=Inverse}"
ItemsSource="{Binding Name, Converter={StaticResource DeviceNameConverter}, ConverterParameter=Items}"
SelectedItem="{Binding Value}"
Width="120" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
<StackPanel Grid.Row="1"
Orientation="Horizontal"
FlowDirection="RightToLeft"
Margin="5,10,5,15">
<Button Content="取消"
Width="70"
Click="Cancel_Click" />
<Button Content="保存"
Width="70"
Margin="20,0"
Click="Save_Click" />
</StackPanel>
</Grid>
</GroupBox>
</Grid>
</mah:MetroWindow>

View File

@ -0,0 +1,261 @@
using ATS.Models;
using MahApps.Metro.Controls;
using Newtonsoft.Json;
using NPOI.XSSF.Streaming.Values;
using PropertyChanged;
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 ATS.Windows
{
/// <summary>
/// DeviceSettingWindow.xaml 的交互逻辑
/// </summary>
[AddINotifyPropertyChangedInterface]
public partial class DeviceSettingWindow : MetroWindow
{
public bool IsSaved { get; private set; } = false;
//设备预选模式开关
public bool IsSelectMode { get; set; } = false;
public Visibility IsInputMode
{
get
{
if (IsSelectMode) return Visibility.Collapsed;
return Visibility.Visible;
}
set;
}
public Visibility ShowComboBox
{
get
{
if (IsSelectMode) return Visibility.Visible;
return Visibility.Collapsed;
}
set;
}
// 预选设备列表
public ObservableCollection<DeviceModel> PreSelectDevices { get; set; } = new();
// 当前选中的预配置设备
private DeviceModel? _selectedPreDefineDevice;
public DeviceModel? SelectedPreDefineDevice
{
get => _selectedPreDefineDevice;
set
{
_selectedPreDefineDevice = value;
if (value != null && IsSelectMode)
{
LoadPreDefineDevice(value);
}
}
}
public DeviceModel? Device { get; set; } = new();
public ObservableCollection<string> Types { get; set; } = ["串口", "Tcp", "Udp", "ModbusRtu_Tcp", "ModbusRtu_Udp", "ModbusRtu_Serial", "ModbusTcp", "CAN" ];
public ObservableCollection<DeviceConnectSettingModel> DeviceConnectSettings { get; set; } = [];
/// <summary>
/// 标记设备编辑操作是否为新增,如果不是,不能篡改设备类型
/// </summary>
public bool IsAdd { get; set; }
private string OriginalType;
public DeviceSettingWindow()
{
InitializeComponent();
DataContext = this;
IsAdd = true;
PreSelectDevices = GetPreSelectDevices();
}
public DeviceSettingWindow(DeviceModel device)
{
InitializeComponent();
DataContext = this;
this.Device = device;
OriginalType = device.Type;
IsAdd = false;
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
Device = null;
this.Close();
}
private void Save_Click(object sender, RoutedEventArgs e)
{
IsSaved = true;
Device!.ConnectString = JsonConvert.SerializeObject(DeviceConnectSettings);
this.Close();
}
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (Device == null) return;
if (Device!.Type == OriginalType)
{
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(Device.ConnectString)!;
return;
}
DeviceConnectSettings.Clear();
switch (Device!.Type)
{
case "Tcp":
case "ModbusRtu_Tcp":
case "ModbusTcp":
DeviceConnectSettings.Add(new()
{
Name = "IP地址",
Value = "127.0.0.1"
});
DeviceConnectSettings.Add(new()
{
Name = "端口号",
Value = "502"
});
break;
case "串口":
case "ModbusRtu_Serial":
DeviceConnectSettings.Add(new()
{
Name = "COM口",
//Value = ""
Value = "COM1"
});
DeviceConnectSettings.Add(new()
{
Name = "波特率",
Value = "9600"
});
DeviceConnectSettings.Add(new()
{
Name = "数据位",
Value = "8"
});
DeviceConnectSettings.Add(new()
{
Name = "停止位",
Value = "1"
});
DeviceConnectSettings.Add(new()
{
Name = "奇偶",
Value = "无"
});
break;
case "ModbusRtu_Udp":
case "Udp":
DeviceConnectSettings.Add(new()
{
Name = "IP地址",
Value = "127.0.0.1"
});
DeviceConnectSettings.Add(new()
{
Name = "端口号",
Value = "502"
});
DeviceConnectSettings.Add(new()
{
Name = "本地端口号",
Value = "8080"
});
break;
}
DeviceConnectSettings.Add(new()
{
Name = "读超时",
Value = "3000"
});
DeviceConnectSettings.Add(new()
{
Name = "写超时",
Value = "3000"
});
}
private void GroupBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left) DragMove();
}
//从预定义文件中获取预选设备列表
private ObservableCollection<DeviceModel> GetPreSelectDevices()
{
try
{
string filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, SystemConfig.Instance.PreDefineDevicesPath);
if (!System.IO.File.Exists(filePath))
{
// 如果文件不存在,创建一个空的配置文件
var emptyList = new ObservableCollection<DeviceModel>();
string json = JsonConvert.SerializeObject(emptyList, Formatting.Indented);
System.IO.File.WriteAllText(filePath, json);
return emptyList;
}
var devices = JsonConvert.DeserializeObject<ObservableCollection<DeviceModel>>(
System.IO.File.ReadAllText(filePath)) ?? new ObservableCollection<DeviceModel>();
return devices;
}
catch (Exception ex)
{
MessageBox.Show($"加载预配置设备失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return new ObservableCollection<DeviceModel>();
}
}
/// <summary>
/// 加载预配置设备信息到当前设备
/// </summary>
private void LoadPreDefineDevice(DeviceModel preDefineDevice)
{
if (Device == null) return;
// 复制预配置设备的信息除了ID使用新的ID
Device.Name = preDefineDevice.Name;
Device.Type = preDefineDevice.Type;
Device.Description = preDefineDevice.Description;
Device.ConnectString = preDefineDevice.ConnectString;
// 触发类型选择变化,加载连接参数
OriginalType = Device.Type;
if (!string.IsNullOrEmpty(Device.ConnectString))
{
try
{
DeviceConnectSettings = JsonConvert.DeserializeObject<ObservableCollection<DeviceConnectSettingModel>>(
Device.ConnectString) ?? new ObservableCollection<DeviceConnectSettingModel>();
}
catch
{
// 如果反序列化失败,使用默认配置
ComboBox_SelectionChanged(null!, null!);
}
}
}
}
}

154
ATS/Windows/Login.xaml Normal file
View File

@ -0,0 +1,154 @@
<mah:MetroWindow xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
x:Class="ATS.Windows.Login"
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:ATS.Windows"
mc:Ignorable="d"
Title="ATS系统"
WindowStartupLocation="CenterScreen"
Height="315"
Width="420"
ResizeMode="NoResize"
PreviewKeyDown="Login_PreviewKeyDown">
<Window.Resources>
<Style TargetType="TextBox"
BasedOn="{StaticResource MahApps.Styles.TextBox}">
<Setter Property="FontSize"
Value="14" />
<Setter Property="BorderThickness"
Value="0,0,0,2" />
<Setter Property="BorderBrush"
Value="#E0E0E0" />
<Setter Property="Padding"
Value="5,8" />
<Setter Property="Background"
Value="Transparent" />
</Style>
<Style TargetType="PasswordBox"
BasedOn="{StaticResource MahApps.Styles.PasswordBox}">
<Setter Property="FontSize"
Value="14" />
<Setter Property="BorderThickness"
Value="0,0,0,2" />
<Setter Property="BorderBrush"
Value="#E0E0E0" />
<Setter Property="Padding"
Value="5,8" />
<Setter Property="Background"
Value="Transparent" />
</Style>
<Style TargetType="Button"
BasedOn="{StaticResource MahApps.Styles.Button.Flat}">
<Setter Property="FontSize"
Value="15" />
<Setter Property="FontWeight"
Value="SemiBold" />
<Setter Property="Foreground"
Value="White" />
<Setter Property="Background"
Value="#2196F3" />
<Setter Property="BorderThickness"
Value="0" />
<Setter Property="Margin"
Value="0,20,0,0" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<!-- 表单区域 -->
<Border Grid.Row="0"
Background="White"
Margin="20,20,20,0"
CornerRadius="5"
BorderThickness="1"
BorderBrush="#E0E0E0"
Padding="30,20">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Content="用户登录"
HorizontalAlignment="Center"
FontSize="15"
Padding="3" />
<StackPanel Grid.Row="1">
<!-- 用户名输入 -->
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal"
Margin="17">
<iconPacks:PackIconMaterial Kind="Account"
Foreground="#2196F3"
VerticalAlignment="Center"
Margin="0,0,10,0" />
<TextBox x:Name="nameBox"
mah:TextBoxHelper.Watermark="请输入账号"
mah:TextBoxHelper.ClearTextButton="True"
VerticalContentAlignment="Center"
Width="180"
Height="30"
Padding="0" />
</StackPanel>
</StackPanel>
<!-- 密码输入 -->
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal"
Margin="7">
<iconPacks:PackIconMaterial Kind="Lock"
Foreground="#2196F3"
VerticalAlignment="Center"
Margin="0,0,10,0" />
<PasswordBox x:Name="pwdBox"
mah:TextBoxHelper.Watermark="请输入密码"
mah:TextBoxHelper.ClearTextButton="True"
VerticalContentAlignment="Center"
Width="180"
Height="30"
Padding="0" />
</StackPanel>
</StackPanel>
</StackPanel>
<!-- 登录按钮 -->
<Button Grid.Row="2"
Click="LoginClick"
Content="登 录"
Width="120"
Height="33"
mah:ControlsHelper.CornerRadius="5">
<Button.Effect>
<DropShadowEffect BlurRadius="8"
ShadowDepth="3"
Opacity="0.5" />
</Button.Effect>
</Button>
</Grid>
</Border>
<!-- 底部版权信息 -->
<TextBlock Grid.Row="2"
Text="© 2025 ATS系统"
Foreground="#777"
FontSize="12"
HorizontalAlignment="Center"
Margin="0,10" />
</Grid>
</mah:MetroWindow>

97
ATS/Windows/Login.xaml.cs Normal file
View File

@ -0,0 +1,97 @@
using ATS.Models;
using ATS.Windows;
using MahApps.Metro.Controls;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
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;
using Path = System.IO.Path;
namespace ATS.Windows
{
/// <summary>
/// Login.xaml 的交互逻辑
/// </summary>
public partial class Login : MetroWindow
{
public List<UserModel> UserList { get; set; } = new List<UserModel>();
private readonly string filePath = Path.Combine(SystemConfig.Instance.SystemPath, "Users.json");
public Login()
{
InitializeComponent();
FindUsersByJson();
}
//查询用户列表
public void FindUsersByJson()
{
if (File.Exists(filePath))
{
string listStr = File.ReadAllText(filePath);
//反序列化
UserList = JsonSerializer.Deserialize<List<UserModel>>(listStr)!;
}
else
{
UserList = [];
UserList.Add(new()
{
UserName = "超级管理员",
UserId = Guid.NewGuid(),
PassWord = "admin",
Role = 2
});
string listStr = JsonSerializer.Serialize(UserList, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(filePath, listStr);
}
}
private void LoginClick(object sender, RoutedEventArgs e)
{
if (!Regex.IsMatch(nameBox.Text, @"^[^\s\u4e00-\u9fa5]+$") || !Regex.IsMatch(pwdBox.Password, @"^[^\s\u4e00-\u9fa5]+$"))
{
MessageBox.Show("请输入正确格式的账号或密码");
return;
}
UserModel? userInfo = UserList.Where(item => item.UserAccount == nameBox.Text && item.PassWord == pwdBox.Password).FirstOrDefault();
if (userInfo != null)
{
userInfo.LoginCount += 1;
userInfo.LoginTime = DateTime.Now;
//保存文件
string listStr = JsonSerializer.Serialize(UserList, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(filePath, listStr);
new MainWindow(userInfo).Show();
this.Close();
}
else
{
MessageBox.Show("用户名或密码错误");
}
}
private void Login_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
LoginClick(sender, e);
}
}
}
}

102
ATS/Windows/MainWindow.xaml Normal file
View File

@ -0,0 +1,102 @@
<mah:MetroWindow x:Class="ATS.Windows.MainWindow"
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:ATS.Windows"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:vs="clr-namespace:ATS.Views"
mc:Ignorable="d"
Loaded="MetroWindow_Loaded"
d:DataContext="{d:DesignInstance Type=local:MainWindow}"
Title="ATS_MainWindow"
Height="580"
Width="1099">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="1.5*" />
<RowDefinition Height="7" />
<RowDefinition />
<RowDefinition Height="7" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="7" />
<ColumnDefinition Width="300" />
<ColumnDefinition Width="7" />
<ColumnDefinition />
<ColumnDefinition Width="7" />
<ColumnDefinition Width="400" />
<ColumnDefinition Width="7" />
</Grid.ColumnDefinitions>
<GridSplitter Grid.Row="1"
Grid.RowSpan="3"
Grid.Column="2"
Width="7"
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ResizeBehavior="PreviousAndNext"
ResizeDirection="Columns"
ShowsPreview="True" />
<GridSplitter Grid.Row="1"
Grid.RowSpan="3"
Grid.Column="4"
Width="7"
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ResizeBehavior="PreviousAndNext"
ResizeDirection="Columns"
ShowsPreview="True" />
<GridSplitter Grid.Row="2"
Grid.ColumnSpan="7"
Height="7"
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ResizeBehavior="PreviousAndNext"
ResizeDirection="Rows"
ShowsPreview="True" />
<vs:ToolBar Grid.Column="1"
Grid.ColumnSpan="5" />
<Grid Grid.Column="5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<Menu Grid.Column="1">
<MenuItem Header="{Binding User.UserName}"
FontSize="12"
Height="48"
VerticalContentAlignment="Center">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="User" />
</MenuItem.Icon>
<MenuItem Header="管理"
Click="UserManage_Click" />
<MenuItem Header="登出"
Click="LogOut_Click" />
</MenuItem>
</Menu>
</Grid>
</Grid>
<vs:CommandTreeView Grid.Row="1"
Grid.Column="1"
Grid.RowSpan="3" />
<vs:StepsManager Grid.Row="1"
Grid.Column="3" />
<vs:SingleStepEdit Grid.Row="1"
Grid.Column="5" />
<vs:LogArea Grid.Row="3"
Grid.Column="3" />
<vs:ParametersManager Grid.Row="3"
Grid.Column="5" />
</Grid>
</mah:MetroWindow>

View File

@ -0,0 +1,244 @@
using ATS.Models;
using ATS.Tools;
using MahApps.Metro.Controls;
using MathNet.Numerics;
using Microsoft.Win32;
using Newtonsoft.Json;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
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 ATS.Windows
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
[AddINotifyPropertyChangedInterface]
public partial class MainWindow : MetroWindow
{
public static MainWindow Instance { get; set; }
private ProgramModel _program = new();
public ProgramModel Program
{
get
{
return _program;
}
set
{
_program.StepCollection.CollectionChanged -= StepCollectionIndexChanged;
_program = value;
_program.StepCollection.CollectionChanged += StepCollectionIndexChanged;
}
}
public UserModel User { get; set; } = new();
public string? CurrentFilePath { get; set; } = "";
public StepModel? SelectedStep { get; set; }
public MainWindow()
{
Instance = this;
InitializeComponent();
DataContext = this;
User = new()
{
UserName = "开发者",
UserAccount = "Developer",
Role = 2
};
}
public MainWindow(UserModel user)
{
Instance = this;
InitializeComponent();
DataContext = this;
User = user;
}
private void MetroWindow_Loaded(object sender, RoutedEventArgs e)
{
Directory.CreateDirectory(SystemConfig.Instance.SystemPath);
Program = new();
}
#region
/// <summary>
/// 根据序号重新对步骤进行排序
/// </summary>
private void ReOrderProgramList()
{
for (int i = 0; i < Program.StepCollection.Count; i++)
{
Program.StepCollection[i].Index = i + 1;
}
}
#endregion
private void StepCollectionIndexChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset
|| e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Move)
{
ReOrderProgramList();
}
}
private void LogOut_Click(object sender, RoutedEventArgs e)
{
new Login().Show();
this.Close();
}
private void UserManage_Click(object sender, RoutedEventArgs e)
{
if (User.Role != 2)
{
MessageBox.Show("当前登录用户无权限");
return;
}
new UsersManage().ShowDialog();
}
private Stack<ProgramModel> _parentProgramStack = new();
private Stack<string> _programPathStack = new(); // 用于显示标题路径
private Stack<string> _currentSubProgramNameStack = new(); // 专门存储当前子程序的名称
private bool _isInSubProgramMode = false;
// 添加一个属性来获取当前程序名称(用于显示)
public string CurrentProgramDisplayName
{
get
{
if (_isInSubProgramMode && _currentSubProgramNameStack.Count > 0)
{
return _currentSubProgramNameStack.Peek();
}
else if (!string.IsNullOrEmpty(CurrentFilePath))
{
return System.IO.Path.GetFileName(CurrentFilePath);
}
return "NewProgram.ats"; // 默认名称
}
}
public void EnterSubProgramMode(ProgramModel subProgram, string subProgramName, StepModel parentStep)
{
// 保存当前状态
_parentProgramStack.Push(Program);
_programPathStack.Push(Title); // 保存当前标题/路径
_currentSubProgramNameStack.Push(subProgramName); // 保存当前子程序的名称
_parentStepStack.Push(parentStep); // 保存父步骤引用,用于保存时回写
// 进入子程序编辑模式 - 使用传入的 subProgram确保它是副本
Program = new ProgramModel(subProgram);
_isInSubProgramMode = true;
// 更新标题显示当前路径
string parentPath = _programPathStack.Count > 0 ? _programPathStack.Peek() : "主程序";
Title = $"{parentPath} > {subProgramName}";
// 通知 StepsManager 更新 Title 显示子程序名称
ATS.Views.StepsManager.Instance?.UpdateTitle(subProgramName);
}
public void ExitSubProgramMode()
{
if (_parentProgramStack.Count > 0)
{
// 先保存当前子程序的更改回父程序的对应步骤
SaveCurrentSubProgramBackToParent();
// 恢复父程序
Program = _parentProgramStack.Pop();
// 恢复其他状态
if (_programPathStack.Count > 0)
{
Title = _programPathStack.Pop();
}
else
{
Title = "ATS_MainWindow";
}
if (_currentSubProgramNameStack.Count > 0)
{
_currentSubProgramNameStack.Pop(); // 移除当前子程序名称
}
if (_parentStepStack.Count > 0)
{
_parentStepStack.Pop(); // 移除父步骤引用
}
_isInSubProgramMode = _parentProgramStack.Count > 0; // 根据栈是否为空判断是否还在子程序模式
// 通知 StepsManager 更新 Title返回主程序或上一层子程序
if (_isInSubProgramMode && _currentSubProgramNameStack.Count > 0)
{
// 如果还在子程序模式中(多层嵌套),显示上一层子程序名称
ATS.Views.StepsManager.Instance?.UpdateTitle(_currentSubProgramNameStack.Peek());
}
else
{
// 返回主程序
ATS.Views.StepsManager.Instance?.UpdateTitle();
}
}
}
// 需要一个方法来知道当前编辑的是哪个父步骤的子程序,以便保存
// 我们需要在进入子程序模式时也保存父步骤的引用
private Stack<StepModel> _parentStepStack = new(); // 添加这个栈
private void SaveCurrentSubProgramBackToParent()
{
if (_parentStepStack.Count > 0)
{
var parentStep = _parentStepStack.Peek();
if (parentStep.SubProgram != null)
{
// 将当前编辑的 Program (即子程序) 保存回父步骤的 SubProgram
// 注意:这里我们只更新 SubProgram 的内容,不改变其引用
// 如果 SubProgram 有 Name 等元数据,需要单独处理(但根据 ATS 文件结构,似乎没有)
parentStep.SubProgram = new ProgramModel(Program); // 创建一个新副本赋值回去
Log.Success($"用户 [ {User.UserName} ] 更新子程序 [ {parentStep.Name} ] ");
}
}
}
// 保持这些属性
public bool IsInSubProgramMode => _isInSubProgramMode;
public int SubProgramLevel => _parentProgramStack.Count; // 当前嵌套层级
}
}

View File

@ -0,0 +1,122 @@
<mah:MetroWindow x:Class="ATS.Windows.ParameterSettingWindow"
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:local="clr-namespace:ATS.Windows"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="ParameterSettingWindow"
Width="420"
Height="284"
d:DataContext="{d:DesignInstance Type=local:ParameterSettingWindow}"
ShowTitleBar="False"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
mc:Ignorable="d">
<Grid>
<GroupBox Padding="10,15,10,0"
Header="参数编辑界面"
MouseLeftButtonDown="GroupBox_MouseLeftButtonDown">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<ScrollViewer>
<StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="60"
VerticalAlignment="Bottom"
Content="参数名称*" />
<TextBox Width="120"
VerticalAlignment="Bottom"
Text="{Binding Parameter.Name}" />
</StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="60"
VerticalAlignment="Bottom"
Content="参数类型*" />
<ComboBox Width="120"
VerticalAlignment="Bottom"
ItemsSource="{Binding Types}"
SelectedItem="{Binding Parameter.Type}"
SelectionChanged="ParaType_SelectionChanged" />
</StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="60"
VerticalAlignment="Bottom"
Content="参数类别*" />
<ComboBox Width="120"
VerticalAlignment="Bottom"
ItemsSource="{Binding Categorys}"
Text="{Binding Parameter.Category}" />
</StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="60"
VerticalAlignment="Bottom"
Content="参数下限" />
<TextBox Width="120"
VerticalAlignment="Bottom"
Text="{Binding Parameter.LowerLimit}" />
</StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="60"
VerticalAlignment="Bottom"
Content="参数上限" />
<TextBox Width="120"
VerticalAlignment="Bottom"
Text="{Binding Parameter.UpperLimit}" />
</StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="60"
VerticalAlignment="Bottom"
Content="参数值" />
<!-- 非枚举类型时显示文本框 -->
<TextBox MinWidth="120"
VerticalAlignment="Bottom"
Text="{Binding Parameter.Value, Converter={StaticResource ParameterValueToStringConverter}}"
Visibility="{Binding Parameter.Type, Converter={StaticResource IsEnumTypeConverter}, ConverterParameter=Collapse}" />
<!-- 枚举类型时显示下拉框 -->
<ComboBox MinWidth="120"
VerticalAlignment="Bottom"
ItemsSource="{Binding EnumValues}"
SelectedItem="{Binding Parameter.Value}"
Visibility="{Binding Parameter.Type, Converter={StaticResource IsEnumTypeConverter}}" />
<CheckBox Margin="10,0"
VerticalAlignment="Bottom"
Content="保存数据"
IsChecked="{Binding Parameter.IsSave}" />
</StackPanel>
</StackPanel>
</ScrollViewer>
<StackPanel Grid.Row="1"
Margin="5,10,5,15"
FlowDirection="RightToLeft"
Orientation="Horizontal">
<Button Width="70"
Click="Cancel_Click"
Content="取消" />
<Button Width="70"
Margin="20,0"
Click="Save_Click"
Content="保存" />
</StackPanel>
</Grid>
</GroupBox>
</Grid>
</mah:MetroWindow>

View File

@ -0,0 +1,107 @@
using ATS.Models;
using MahApps.Metro.Controls;
using PropertyChanged;
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;
using TSMaster;
using static ATS.Models.ParameterModel;
namespace ATS.Windows
{
/// <summary>
/// ParameterSettingWindow.xaml 的交互逻辑
/// </summary>
[AddINotifyPropertyChangedInterface]
public partial class ParameterSettingWindow : MetroWindow
{
public bool IsSaved { get; private set; } = false;
public ParameterModel? Parameter { get; set; } = new();
public ObservableCollection<Type> Types { get; set; } =
[
typeof(string), typeof(bool),
typeof(byte),typeof(short), typeof(int), typeof(long), typeof(float), typeof(double),typeof(DateTime),typeof(TimeSpan),typeof(TLIBCAN),typeof(TLIBCANFD),
typeof(byte[]), typeof(short[]), typeof(ushort[]), typeof(int[]), typeof(long[]), typeof(float[]), typeof(double[]),typeof(DateTime[]),typeof(TimeSpan[]),typeof(TLIBCAN[]),typeof(TLIBCANFD[]),
typeof(object)
];
public ObservableCollection<string> Categorys { get; set; } = new(Enum.GetNames(typeof(ParameterCategory)));
public Array? EnumValues { get; set; }
public ParameterSettingWindow()
{
InitializeComponent();
DataContext = this;
}
public ParameterSettingWindow(ParameterModel parameter)
{
InitializeComponent();
DataContext = this;
this.Parameter = parameter;
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
Parameter = null;
this.Close();
}
private void Save_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(Parameter?.Name) || Parameter.Type == null)
{
MessageBox.Show("缺少必填项", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
else if (Parameter!.Name == "Result")
{
MessageBox.Show("参数名不允许为\"Result\"", "警告", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (Parameter!.Value == null)
{
Parameter.Value = CreateDefaultValue(Parameter.Type);
}
IsSaved = true;
this.Close();
}
private static object? CreateDefaultValue(Type type)
{
if (type.IsValueType) return Activator.CreateInstance(type);
return null;
}
private void GroupBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
DragMove();
}
}
private void ParaType_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (Parameter == null) return;
if (Parameter!.Type != null && Parameter!.Type.BaseType == typeof(Enum))
{
EnumValues = Parameter?.Type?.IsEnum == true ? Enum.GetValues(Parameter.Type) : null;
}
}
}
}

View File

@ -0,0 +1,115 @@
<mah:MetroWindow x:Class="ATS.Windows.SystemConfigWindow"
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:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:local="clr-namespace:ATS.Windows"
mc:Ignorable="d"
Title="SystemConfigWindow"
Width="420"
Height="430"
d:DataContext="{d:DesignInstance Type=local:SystemConfigWindow}"
ShowTitleBar="False"
WindowStartupLocation="CenterScreen"
WindowStyle="None">
<Grid>
<GroupBox Padding="10,15,10,0"
Header="系统参数配置"
MouseLeftButtonDown="GroupBox_MouseLeftButtonDown">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<ScrollViewer>
<StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="100"
VerticalAlignment="Bottom"
Content="性能等级*"
ToolTip="最高等级为0易造成卡顿"/>
<TextBox Width="200"
VerticalAlignment="Bottom"
Text="{Binding ConfigCopy.PerformanceLevel}" />
</StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="100"
VerticalAlignment="Bottom"
Content="日志路径*" />
<TextBox Width="200"
VerticalAlignment="Bottom"
Text="{Binding ConfigCopy.LogFilePath}" />
</StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="100"
VerticalAlignment="Bottom"
Content="指令路径*" />
<TextBox Width="200"
VerticalAlignment="Bottom"
Text="{Binding ConfigCopy.DLLFilePath}" />
</StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="100"
VerticalAlignment="Bottom"
Content="子程序路径*" />
<TextBox Width="200"
VerticalAlignment="Bottom"
Text="{Binding ConfigCopy.SubProgramFilePath}" />
</StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="100"
VerticalAlignment="Bottom"
Content="设备预设路径*" />
<TextBox Width="200"
VerticalAlignment="Bottom"
Text="{Binding ConfigCopy.PreDefineDevicesPath}" />
</StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="100"
VerticalAlignment="Bottom"
Content="DBC文件路径*" />
<TextBox MinWidth="200"
VerticalAlignment="Bottom"
Text="{Binding ConfigCopy.DBCFilePath}" />
</StackPanel>
<StackPanel Height="30"
Margin="7"
Orientation="Horizontal">
<Label Width="100"
VerticalAlignment="Bottom"
Content="默认程序路径*" />
<TextBox Width="200"
VerticalAlignment="Bottom"
Text="{Binding ConfigCopy.DefaultSubProgramFilePath}" />
</StackPanel>
</StackPanel>
</ScrollViewer>
<StackPanel Grid.Row="1"
Margin="5,10,5,15"
FlowDirection="RightToLeft"
Orientation="Horizontal">
<Button Width="70"
Click="Cancel_Click"
Content="取消" />
<Button Width="70"
Margin="20,0"
Click="Save_Click"
Content="保存" />
</StackPanel>
</Grid>
</GroupBox>
</Grid>
</mah:MetroWindow>

View File

@ -0,0 +1,87 @@
using ATS;
using MahApps.Metro.Controls;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
namespace ATS.Windows
{
public partial class SystemConfigWindow : MetroWindow
{
// 配置副本(用于界面绑定)
public SystemConfig ConfigCopy { get; } = new();
public SystemConfigWindow()
{
InitializeComponent();
// 使用反射复制配置属性
CopyConfigProperties(SystemConfig.Instance, ConfigCopy);
DataContext = this;
}
// 反射复制属性方法(排除 JsonIgnore 属性)
private void CopyConfigProperties(SystemConfig source, SystemConfig target)
{
Type type = typeof(SystemConfig);
PropertyInfo[] properties = type.GetProperties();
foreach (PropertyInfo property in properties)
{
// 跳过 JsonIgnore 属性和只读属性
if (Attribute.IsDefined(property, typeof(JsonIgnoreAttribute)) || !property.CanWrite)
{
continue;
}
object value = property.GetValue(source)!;
property.SetValue(target, value);
}
}
// 保存配置到文件
private void SaveConfigToFile()
{
try
{
// 使用实例的SystemPath确保路径正确
string configPath = Path.Combine(ConfigCopy.SystemPath, "system.config");
// 确保目录存在
Directory.CreateDirectory(ConfigCopy.SystemPath);
// 序列化保存
string json = JsonConvert.SerializeObject(ConfigCopy, Formatting.Indented);
File.WriteAllText(configPath, json);
}
catch (Exception ex)
{
MessageBox.Show($"保存配置失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void GroupBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
DragMove();
}
}
private void Save_Click(object sender, RoutedEventArgs e)
{
CopyConfigProperties(ConfigCopy, SystemConfig.Instance);
SaveConfigToFile();
SystemConfig.Instance.LoadFromFile();
Close();
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
Close();
}
}
}

View File

@ -0,0 +1,147 @@
<mah:MetroWindow x:Class="ATS.Windows.TestDataInfomationWindow"
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:local="clr-namespace:ATS.Windows"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="TestDataInfomationWindow"
Width="800"
Height="450"
d:DataContext="{d:DesignInstance Type=local:TestDataInfomationWindow}"
PreviewKeyDown="TestDataWindow_PreviewKeyDown"
Loaded="MetroWindow_Loaded"
ShowTitleBar="False"
WindowStyle="None"
mc:Ignorable="d">
<Grid>
<GroupBox Header="测试数据记录"
Padding="10"
MouseLeftButtonDown="GroupBox_MouseLeftButtonDown">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<StackPanel Margin="7">
<StackPanel Orientation="Horizontal">
<Label Content="参数名称"
VerticalAlignment="Bottom"
Margin="7,7,0,7" />
<TextBox Width="80"
Text="{Binding Search_ParaName}"
Margin="0,7,7,7" />
<Label Content="测试结果"
VerticalAlignment="Bottom"
Margin="7,7,0,7" />
<ComboBox Width="80"
Text="{Binding Search_Result}"
Margin="0,7,7,7">
<ComboBoxItem Content="" />
<ComboBoxItem Content="PASS" />
<ComboBoxItem Content="FAIL" />
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Search_IsUseTime}"
VerticalAlignment="Bottom"
Margin="7,7,0,10" />
<StackPanel Orientation="Horizontal">
<Label Content="启用时间筛选"
VerticalAlignment="Bottom"
Margin="0,7,7,7" />
<Label Content="开始时间"
Margin="7,7,0,7"
VerticalAlignment="Bottom" />
<TextBox Text="{Binding Search_StartTime,StringFormat='yyyy-MM-dd HH:mm:ss'}"
Margin="0,7,7,7"
Width="120"
VerticalAlignment="Bottom" />
<mah:DateTimePicker Width="25"
Background="Transparent"
SelectedDateTime="{Binding Search_StartTime}"
VerticalAlignment="Bottom"
Margin="0,7,7,7" />
<Label Content="结束时间"
Margin="7,7,0,7"
VerticalAlignment="Bottom" />
<TextBox Text="{Binding Search_EndTime,StringFormat='yyyy-MM-dd HH:mm:ss'}"
Margin="0,7,7,7"
Width="120"
VerticalAlignment="Bottom" />
<mah:DateTimePicker Width="25"
Background="Transparent"
SelectedDateTime="{Binding Search_EndTime}"
VerticalAlignment="Bottom"
Margin="0,7,7,7" />
</StackPanel>
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="1"
Margin="7"
Orientation="Horizontal">
<Button Content="查询"
Width="80"
VerticalAlignment="Center"
Margin="7"
Click="Search_Click" />
<Button Content="导出"
Width="80"
VerticalAlignment="Center"
Margin="7"
Click="Export_Click" />
</StackPanel>
<DataGrid Name="TestData_DataGrid"
Grid.Row="1"
Grid.ColumnSpan="2"
AutoGenerateColumns="False"
Background="Transparent"
IsReadOnly="True"
CanUserAddRows="False"
ItemsSource="{Binding DataList}"
Margin="15">
<DataGrid.Columns>
<DataGridTextColumn Header="参数名称"
Binding="{Binding ParameterID, Converter={StaticResource GuidToParameterNameConverter}}" />
<DataGridTextColumn Header="实际值"
Binding="{Binding Value}" />
<DataGridTextColumn Header="下限值"
Binding="{Binding LowerLimit}" />
<DataGridTextColumn Header="上限值"
Binding="{Binding UpperLimit}" />
<DataGridTemplateColumn Header="测试结果">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock x:Name="OKNG"></TextBlock>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Result}"
Value="true">
<Setter TargetName="OKNG"
Property="Text"
Value="PASS"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Result}"
Value="false">
<Setter TargetName="OKNG"
Property="Text"
Value="FAIL"></Setter>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="测试时间"
Binding="{Binding InsertTime, StringFormat=yyyy-MM-dd HH:mm:ss}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</GroupBox>
</Grid>
</mah:MetroWindow>

View File

@ -0,0 +1,188 @@
using ATS.Tools;
using ATS_DBContext;
using ATS_DBContext.Models;
using ClosedXML.Excel;
using MahApps.Metro.Controls;
using Microsoft.Win32;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace ATS.Windows
{
[AddINotifyPropertyChangedInterface]
public partial class TestDataInfomationWindow : MetroWindow
{
public ObservableCollection<DataModel> DataList { get; set; } = [];
public string Search_ParaName { get; set; }
public string Search_Result { get; set; }
public bool Search_IsUseTime { get; set; } = true;
public DateTime Search_StartTime { get; set; } = DateTime.Now.Date;
public DateTime Search_EndTime { get; set; } = DateTime.Now.AddDays(1).Date;
public TestDataInfomationWindow()
{
InitializeComponent();
DataContext = this;
}
private void MetroWindow_Loaded(object sender, RoutedEventArgs e)
{
Search_Click(sender, e);
}
private void GroupBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left) DragMove();
}
private async void Search_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
using (ATS_DB db = new())
{
// 始终过滤ProgramID
IQueryable<DataModel> query = db.TestData.Where(x => x.ProgramID == MainWindow.Instance.Program.ID);
// 参数名称搜索
if (!string.IsNullOrEmpty(Search_ParaName))
{
// 获取匹配的参数ID列表
var paramIds = MainWindow.Instance.Program.Parameters.Where(p => p.Name.Contains(Search_ParaName)).Select(p => p.ID).ToList();
query = query.Where(x => paramIds.Contains(x.ParameterID));
}
// 测试结果筛选
if (!string.IsNullOrEmpty(Search_Result))
{
if (Search_Result == "PASS")
{
query = query.Where(x => x.Result);
}
if (Search_Result == "FAIL")
{
query = query.Where(x => !x.Result);
}
}
// 时间范围过滤
if (Search_IsUseTime)
{
query = query.Where(x => x.InsertTime >= Search_StartTime && x.InsertTime < Search_EndTime);
}
List<DataModel>? results = query.OrderByDescending(x => x.InsertTime).ToList();
Application.Current.Dispatcher.Invoke(() =>
{
DataList = new(results);
});
}
});
}
private void TestDataWindow_PreviewKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Enter:
Search_Click(sender, e);
break;
}
}
private async void Export_Click(object sender, RoutedEventArgs e)
{
// 检查是否有数据可导出
if (DataList == null || DataList.Count == 0)
{
MessageBox.Show("没有数据可导出!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
// 显示保存文件对话框
var saveFileDialog = new SaveFileDialog
{
Filter = "Excel文件|*.xlsx",
FileName = $"测试数据_{DateTime.Now:yyyyMMddHHmmss}.xlsx",
Title = "导出测试数据"
};
if (saveFileDialog.ShowDialog() != true)
return;
string filePath = saveFileDialog.FileName;
try
{
// 异步执行导出操作
await Task.Run(() => ExportToExcel(filePath));
MessageBox.Show($"测试数据导出成功", "导出成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ExportToExcel(string filePath)
{
using (var workbook = new XLWorkbook())
{
var worksheet = workbook.Worksheets.Add("测试数据");
// 添加标题行
worksheet.Cell(1, 1).Value = "参数名称";
worksheet.Cell(1, 2).Value = "实际值";
worksheet.Cell(1, 3).Value = "下限值";
worksheet.Cell(1, 4).Value = "上限值";
worksheet.Cell(1, 5).Value = "测试结果";
worksheet.Cell(1, 6).Value = "测试时间";
// 设置标题行样式
var titleRow = worksheet.Row(1);
titleRow.Style.Font.Bold = true;
titleRow.Style.Fill.BackgroundColor = XLColor.LightGray;
// 填充数据
int row = 2;
foreach (var item in DataList)
{
// 转换参数ID为参数名称
string parameterName = "未知参数";
var parameter = MainWindow.Instance.Program.Parameters.FirstOrDefault(p => p.ID == item.ParameterID);
if (parameter != null)
{
parameterName = parameter.Name;
}
worksheet.Cell(row, 1).Value = parameterName;
worksheet.Cell(row, 2).Value = item.Value;
worksheet.Cell(row, 3).Value = item.LowerLimit;
worksheet.Cell(row, 4).Value = item.UpperLimit;
worksheet.Cell(row, 5).Value = item.Result ? "PASS" : "FAIL";
worksheet.Cell(row, 6).Value = item.InsertTime;
row++;
}
// 调整列宽
worksheet.Columns().AdjustToContents();
// 保存文件
workbook.SaveAs(filePath);
}
}
}
}

View File

@ -0,0 +1,85 @@
<mah:MetroWindow xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
x:Class="ATS.Windows.UserSettingWindow"
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:ATS.Windows"
d:DataContext="{d:DesignInstance Type=local:UserSettingWindow}"
mc:Ignorable="d"
ShowTitleBar="False"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
Height="281"
Width="417">
<Grid>
<GroupBox Header="用户编辑"
MouseLeftButtonDown="GroupBox_MouseLeftButtonDown"
Padding="10,15,10,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<StackPanel VerticalAlignment="Center">
<StackPanel Margin="7"
Orientation="Horizontal">
<TextBlock VerticalAlignment="Bottom"
Width="60">用户名</TextBlock>
<TextBox Background="#fff"
Foreground="#000"
Width="120"
Text="{Binding UserInfo.UserName}"></TextBox>
</StackPanel>
<StackPanel Margin="7"
Orientation="Horizontal">
<TextBlock VerticalAlignment="Bottom"
Width="60">账户</TextBlock>
<TextBox Background="#fff"
Width="120"
Foreground="#000"
Text="{Binding UserInfo.UserAccount}"
IsReadOnly="{Binding IsEdit}" />
</StackPanel>
<StackPanel Margin="7"
Orientation="Horizontal">
<TextBlock VerticalAlignment="Bottom"
Width="60">密码</TextBlock>
<TextBox Background="#fff"
Width="120"
Foreground="#000"
Text="{Binding UserInfo.PassWord}"></TextBox>
</StackPanel>
<StackPanel Margin="7"
Orientation="Horizontal">
<TextBlock VerticalAlignment="Bottom"
Width="60">用户权限</TextBlock>
<ComboBox x:Name="roleComBox"
SelectedIndex="{Binding UserInfo.Role}"
Width="120">
<ComboBoxItem IsSelected="False"
Tag="0">用户</ComboBoxItem>
<ComboBoxItem IsSelected="False"
Tag="1">管理员</ComboBoxItem>
<ComboBoxItem IsSelected="False"
Tag="2">超级管理员</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
<StackPanel Grid.Row="1"
Orientation="Horizontal"
FlowDirection="RightToLeft"
Margin="5,20,5,15">
<Button Content="取消"
Width="70"
Click="Cancel_Click" />
<Button Content="保存"
Width="70"
Margin="20,0"
Click="Save_Click" />
</StackPanel>
</Grid>
</GroupBox>
</Grid>
</mah:MetroWindow>

View File

@ -0,0 +1,77 @@
using ATS.Models;
using MahApps.Metro.Controls;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
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 ATS.Windows
{
/// <summary>
/// UsersAdd.xaml 的交互逻辑
/// </summary>
[AddINotifyPropertyChangedInterface]
public partial class UserSettingWindow : MetroWindow
{
public UserModel UserInfo { get; set; } = new();
public bool IsEdit { get; set; } = false;
public bool IsSave = false;
public UserSettingWindow()
{
InitializeComponent();
this.DataContext = this;
IsEdit = false;
}
public UserSettingWindow(UserModel user)
{
InitializeComponent();
this.DataContext = this;
UserInfo = user;
IsEdit = true;
}
private void Save_Click(object sender, RoutedEventArgs e)
{
if (UserInfo.PassWord == null || !Regex.IsMatch(UserInfo.PassWord, @"^[^\s\u4e00-\u9fa5]+$"))
{
MessageBox.Show("密码格式错误!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (UserInfo.UserAccount == null || !Regex.IsMatch(UserInfo.UserAccount, @"^[^\s\u4e00-\u9fa5]+$"))
{
MessageBox.Show("账号格式错误!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
IsSave = true;
this.Close();
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
IsSave = false;
this.Close();
}
private void GroupBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left) DragMove();
}
}
}

View File

@ -0,0 +1,98 @@
<mah:MetroWindow xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
x:Class="ATS.Windows.UsersManage"
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:ATS.Windows"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
d:DataContext="{d:DesignInstance Type=local:UsersManage}"
mc:Ignorable="d"
ShowTitleBar="False"
WindowStartupLocation="CenterScreen"
Height="450"
Width="820">
<Grid>
<GroupBox Header="用户管理"
MouseLeftButtonDown="GroupBox_MouseLeftButtonDown">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Menu>
<MenuItem Header="新增"
Click="AddUserClick"
FontSize="12">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="UserAdd" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="编辑"
Click="EditUserClick"
FontSize="12">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="UserEdit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="删除"
Foreground="Red"
Click="DelUserClick"
FontSize="12">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="UserOff" />
</MenuItem.Icon>
</MenuItem>
</Menu>
<DataGrid x:Name="userTable"
Grid.Row="1"
Grid.Column="0"
Background="Transparent"
IsReadOnly="True"
Margin="10"
AutoGenerateColumns="False"
ItemsSource="{Binding UserList}">
<DataGrid.Columns>
<DataGridTextColumn Header="用户名"
Binding="{Binding UserName}" />
<DataGridTextColumn Header="账户"
Binding="{Binding UserAccount}" />
<DataGridTextColumn Header="密码"
Binding="{Binding PassWord}" />
<DataGridTemplateColumn Header="用户权限">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock x:Name="roleName"></TextBlock>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Role}"
Value="0">
<Setter TargetName="roleName"
Property="Text"
Value="用户"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Role}"
Value="1">
<Setter TargetName="roleName"
Property="Text"
Value="管理员"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Role}"
Value="2">
<Setter TargetName="roleName"
Property="Text"
Value="超级管理员"></Setter>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="登录次数"
Binding="{Binding LoginCount}" />
<DataGridTextColumn Header="最后登录时间"
Binding="{Binding LoginTime,StringFormat=yyyy-MM-dd hh:mm:ss}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</GroupBox>
</Grid>
</mah:MetroWindow>

View File

@ -0,0 +1,154 @@
using ATS.Models;
using DocumentFormat.OpenXml.Spreadsheet;
using MahApps.Metro.Controls;
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
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.Navigation;
using System.Windows.Shapes;
using Path = System.IO.Path;
namespace ATS.Windows
{
/// <summary>
/// UsersManage.xaml 的交互逻辑
/// </summary>
[AddINotifyPropertyChangedInterface]
public partial class UsersManage : MetroWindow
{
public ObservableCollection<UserModel> UserList { get; set; }
public UserModel SelectedUser { get; set; } = new();
private readonly string filePath = Path.Combine(SystemConfig.Instance.SystemPath, "Users.json");
public UsersManage()
{
InitializeComponent();
this.DataContext = this;
LoadUserJson();
}
protected override void OnClosed(EventArgs e)
{
string listStr = JsonSerializer.Serialize(UserList, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(filePath, listStr);
base.OnClosed(e);
}
private void AddUserClick(object sender, RoutedEventArgs e)
{
UserSettingWindow tmp = new();
tmp.ShowDialog();
var userFind = UserList.FirstOrDefault(x => x.UserAccount == tmp.UserInfo.UserAccount);
if (userFind != null && userFind.UserId != tmp.UserInfo.UserId)
{
MessageBox.Show("用户添加失败:账号已存在");
return;
}
if (tmp.IsSave)
{
UserList.Add(tmp.UserInfo);
}
}
//反序列化用户列表
public void LoadUserJson()
{
if (File.Exists(filePath))
{
// 反序列化用户列表
string json = File.ReadAllText(filePath, Encoding.UTF8);
// 移除 /* 多行注释 */
json = Regex.Replace(json, @"/\*.*?\*/", "", RegexOptions.Singleline);
var list = JsonSerializer.Deserialize<List<UserModel>>(json);
UserList = new ObservableCollection<UserModel>(list!);
}
else
{
UserList = [];
UserList.Add(new()
{
UserName = "超级管理员",
UserAccount = "admin",
PassWord = "admin",
Role = 2
});
string listStr = JsonSerializer.Serialize(UserList, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(filePath, listStr);
}
}
private void DelUserClick(object sender, RoutedEventArgs e)
{
if (userTable.SelectedItems.Count < 1)
{
MessageBox.Show("请选择至少一名用户!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
MessageBoxResult result = MessageBox.Show($"确定执行删除操作?", "提示", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
List<UserModel> deleteUsers = [];
foreach (var item in userTable.SelectedItems)
{
if (item is UserModel user)
{
deleteUsers.Add(user);
}
}
foreach (var deleteUser in deleteUsers)
{
UserList.Remove(deleteUser);
}
}
}
private void GroupBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left) DragMove();
}
private void EditUserClick(object sender, RoutedEventArgs e)
{
if (userTable.SelectedItems.Count != 1)
{
MessageBox.Show("请选择一名用户!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (userTable.SelectedItem is UserModel user)
{
var tmp = new UserSettingWindow(new(user));
tmp.ShowDialog();
var userFind = UserList.FirstOrDefault(x => x.UserAccount == tmp.UserInfo.UserAccount);
if (userFind != null && userFind.UserId != tmp.UserInfo.UserId)
{
MessageBox.Show("用户添加失败:账号已存在");
return;
}
if (tmp.IsSave)
{
userFind!.UserName = tmp.UserInfo.UserName;
userFind!.PassWord = tmp.UserInfo.PassWord;
userFind!.LoginCount = tmp.UserInfo.LoginCount;
userFind!.Role = tmp.UserInfo.Role;
}
}
}
}
}

25
ATS_DBContext/ATS_DB.cs Normal file
View File

@ -0,0 +1,25 @@
using ATS_DBContext.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ATS_DBContext
{
public class ATS_DB : DbContext
{
public ATS_DB()
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql("Host=localhost;Port=5432;Database=ATS_DB;Username=postgres;Password=123456");
}
public DbSet<DataModel> TestData { get; set; }
}
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ATS_DBContext.Models
{
public class DataModel
{
[Key]
public Guid ID { get; set; } = Guid.NewGuid();
public Guid ProgramID { get; set; }
public Guid TestRoundID { get; set; }
public Guid ParameterID { get; set; }
public string Value { get; set; } = "";
public string? LowerLimit { get; set; }
public string? UpperLimit { get; set; }
public bool Result { get; set; } = false;
[Column(TypeName = "timestamp without time zone")]
public DateTime InsertTime { get; set; } = DateTime.Now;
}
}

14
Command/Command.csproj Normal file
View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,599 @@
using Common.Attributes;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace Command
{
[ATSCommand]
[DeviceCategory("应用指令")]
public static class CommandApplication
{
/// <summary>
/// 打开外部应用程序
/// </summary>
/// <param name="route">程序路径</param>
/// <param name="waitShutdown">是否等待关闭</param>
/// <param name="AdminRun">是否请求管理员运行</param>
public static void OpenApplication(string route, bool waitShutdown, bool AdminRun)
{
// 创建一个新的进程实例
Process process = new Process();
try
{
// 指定要启动的程序路径
process.StartInfo.FileName = route;
// 指定是否等待程序关闭
process.StartInfo.UseShellExecute = !waitShutdown;
if (AdminRun) process.StartInfo.Verb = "runas";
// 启动程序
process.Start();
// 如果需要等待程序关闭,则等待程序退出
if (waitShutdown)
{
process.WaitForExit();
}
}
catch (Exception ex)
{
Console.WriteLine("错误!: " + ex.Message);
}
finally
{
// 确保进程对象被释放
process.Dispose();
}
}
/// <summary>
/// 使用默认方式打开一个文件
/// </summary>
/// <param name="route">文件路径</param>
public static void OpenFile(string route)
{
// 创建一个新的进程实例
Process process = new Process();
try
{
// 指定要启动的程序路径
process.StartInfo.FileName = route;
process.StartInfo.UseShellExecute = true;
// 启动程序
process.Start();
}
catch (Exception ex)
{
Console.WriteLine("错误!: " + ex.Message);
}
finally
{
// 确保进程对象被释放
process.Dispose();
}
}
// Python引擎相关字段
private static Process _pythonProcess;
private static string _pythonPath = string.Empty;
private static bool _isPythonEngineInitialized = false;
/// <summary>
/// 初始化python引擎
/// </summary>
/// <param name="py路径">Python解释器路径</param>
public static void Initialization_Python(string py路径)
{
try
{
// 验证Python路径是否存在
if (string.IsNullOrEmpty(py路径) || !File.Exists(py路径))
{
throw new FileNotFoundException($"Python解释器路径不存在: {py路径}");
}
// 验证是否为Python可执行文件
var fileInfo = new FileInfo(py路径);
if (!fileInfo.Name.ToLower().Contains("python"))
{
Console.WriteLine("警告: 指定的文件可能不是Python解释器");
}
// 测试Python是否能正常运行
var testProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = py路径,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
testProcess.Start();
testProcess.WaitForExit(5000); // 5秒超时
if (testProcess.ExitCode != 0)
{
throw new InvalidOperationException($"Python解释器验证失败退出码: {testProcess.ExitCode}");
}
_pythonPath = py路径;
_isPythonEngineInitialized = true;
Console.WriteLine($"Python引擎初始化成功版本: {testProcess.StandardOutput.ReadToEnd().Trim()}");
}
catch (Exception ex)
{
_isPythonEngineInitialized = false;
_pythonPath = string.Empty;
Console.WriteLine($"初始化Python引擎失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 运行python脚本
/// </summary>
/// <param name="路径">Python脚本路径</param>
/// <returns>脚本执行结果</returns>
public static string Run_Python_Script(string )
{
if (!_isPythonEngineInitialized)
{
throw new InvalidOperationException("Python引擎未初始化请先调用初始化python引擎");
}
if (string.IsNullOrEmpty() || !File.Exists())
{
throw new FileNotFoundException($"Python脚本路径不存在: {路径}");
}
Process process = null;
try
{
process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = _pythonPath,
Arguments = $"\"{}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
var output = new StringBuilder();
var error = new StringBuilder();
// 设置输出和错误处理
process.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
output.AppendLine(e.Data);
};
process.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
error.AppendLine(e.Data);
};
process.Start();
// 异步读取输出
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
var outputResult = output.ToString().Trim();
var errorResult = error.ToString().Trim();
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"Python脚本执行失败退出码: {process.ExitCode}, 错误: {errorResult}");
}
return string.IsNullOrEmpty(outputResult) ? "执行成功" : outputResult;
}
catch (Exception ex)
{
Console.WriteLine($"运行Python脚本失败: {ex.Message}");
throw;
}
finally
{
process?.Dispose();
}
}
/// <summary>
/// 释放python引擎
/// </summary>
/// <param name="py">Python解释器路径可选用于验证</param>
public static void Release_Python(string py = "")
{
try
{
if (!string.IsNullOrEmpty(py) && py != _pythonPath)
{
Console.WriteLine($"警告: 释放的Python路径与当前初始化路径不匹配");
}
// 如果有正在运行的Python进程尝试终止它
if (_pythonProcess != null && !_pythonProcess.HasExited)
{
try
{
_pythonProcess.Kill();
_pythonProcess.WaitForExit(1000);
}
catch (Exception ex)
{
Console.WriteLine($"终止Python进程时出错: {ex.Message}");
}
}
_pythonPath = string.Empty;
_isPythonEngineInitialized = false;
_pythonProcess = null;
Console.WriteLine("Python引擎已释放");
}
catch (Exception ex)
{
Console.WriteLine($"释放Python引擎失败: {ex.Message}");
throw;
}
}
#region LabVIEW相关功能
// LabVIEW引擎相关字段
private static Process _labviewProcess;
private static string _labviewPath = string.Empty;
private static bool _isLabVIEWEngineInitialized = false;
/// <summary>
/// 初始化LabVIEW引擎
/// </summary>
/// <param name="labview路径">LabVIEW可执行文件路径</param>
public static void Initialization_LabVIEW(string labview路径)
{
try
{
// 验证LabVIEW路径是否存在
if (string.IsNullOrEmpty(labview路径) || !File.Exists(labview路径))
{
throw new FileNotFoundException($"LabVIEW可执行文件路径不存在: {labview路径}");
}
// 验证是否为LabVIEW可执行文件
var fileInfo = new FileInfo(labview路径);
if (!fileInfo.Name.ToLower().Contains("labview") && !fileInfo.Name.ToLower().Contains("vi"))
{
Console.WriteLine("警告: 指定的文件可能不是LabVIEW相关文件");
}
// 测试LabVIEW是否能正常运行
var testProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = labview路径,
Arguments = "--help", // 使用帮助参数进行快速测试
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
testProcess.Start();
testProcess.WaitForExit(5000); // 5秒超时
_labviewPath = labview路径;
_isLabVIEWEngineInitialized = true;
Console.WriteLine($"LabVIEW引擎初始化成功路径: {labview路径}");
}
catch (Exception ex)
{
_isLabVIEWEngineInitialized = false;
_labviewPath = string.Empty;
Console.WriteLine($"初始化LabVIEW引擎失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 运行LabVIEW VI
/// </summary>
/// <param name="路径">LabVIEW VI文件路径</param>
/// <param name="参数">传递给VI的参数</param>
/// <returns>运行结果</returns>
public static string Run_LabVIEW_VI(string , string = "")
{
if (!_isLabVIEWEngineInitialized && string.IsNullOrEmpty(_labviewPath))
{
// 如果没有初始化尝试使用默认LabVIEW路径
var defaultLabVIEWPath = FindDefaultLabVIEWPath();
if (string.IsNullOrEmpty(defaultLabVIEWPath))
{
throw new InvalidOperationException("LabVIEW引擎未初始化且未找到默认LabVIEW路径请先调用初始化LabVIEW引擎");
}
_labviewPath = defaultLabVIEWPath;
}
if (string.IsNullOrEmpty() || !File.Exists())
{
throw new FileNotFoundException($"LabVIEW VI文件路径不存在: {路径}");
}
Process process = null;
try
{
var viFileInfo = new FileInfo();
if (!viFileInfo.Extension.ToLower().Equals(".vi"))
{
Console.WriteLine("警告: 指定的文件可能不是VI文件");
}
process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = string.IsNullOrEmpty(_labviewPath) ? FindDefaultLabVIEWPath() : _labviewPath,
Arguments = $"\"{}\" {参数}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = false // LabVIEW VI通常需要窗口显示
}
};
var output = new StringBuilder();
var error = new StringBuilder();
// 设置输出和错误处理
process.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
output.AppendLine(e.Data);
};
process.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
error.AppendLine(e.Data);
};
process.Start();
// 异步读取输出
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
var outputResult = output.ToString().Trim();
var errorResult = error.ToString().Trim();
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"LabVIEW VI执行失败退出码: {process.ExitCode}, 错误: {errorResult}");
}
return string.IsNullOrEmpty(outputResult) ? "执行成功" : outputResult;
}
catch (Exception ex)
{
Console.WriteLine($"运行LabVIEW VI失败: {ex.Message}");
throw;
}
finally
{
process?.Dispose();
}
}
/// <summary>
/// 运行LabVIEW项目
/// </summary>
/// <param name="路径">LabVIEW项目文件路径</param>
/// <param name="启动VI">要启动的VI名称</param>
/// <param name="参数">传递给VI的参数</param>
/// <returns>运行结果</returns>
public static string Run_LabVIEW_Project(string , string VI = "", string = "")
{
if (!_isLabVIEWEngineInitialized && string.IsNullOrEmpty(_labviewPath))
{
// 如果没有初始化尝试使用默认LabVIEW路径
var defaultLabVIEWPath = FindDefaultLabVIEWPath();
if (string.IsNullOrEmpty(defaultLabVIEWPath))
{
throw new InvalidOperationException("LabVIEW引擎未初始化且未找到默认LabVIEW路径请先调用初始化LabVIEW引擎");
}
_labviewPath = defaultLabVIEWPath;
}
if (string.IsNullOrEmpty() || !File.Exists())
{
throw new FileNotFoundException($"LabVIEW项目文件路径不存在: {路径}");
}
Process process = null;
try
{
var projectFileInfo = new FileInfo();
if (!projectFileInfo.Extension.ToLower().Equals(".lvproj"))
{
Console.WriteLine("警告: 指定的文件可能不是LabVIEW项目文件");
}
string arguments = $"\"{}\"";
if (!string.IsNullOrEmpty(VI))
{
arguments += $" /vi:{启动VI}";
}
if (!string.IsNullOrEmpty())
{
arguments += $" {参数}";
}
process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = string.IsNullOrEmpty(_labviewPath) ? FindDefaultLabVIEWPath() : _labviewPath,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = false
}
};
var output = new StringBuilder();
var error = new StringBuilder();
// 设置输出和错误处理
process.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
output.AppendLine(e.Data);
};
process.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
error.AppendLine(e.Data);
};
process.Start();
// 异步读取输出
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
var outputResult = output.ToString().Trim();
var errorResult = error.ToString().Trim();
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"LabVIEW项目执行失败退出码: {process.ExitCode}, 错误: {errorResult}");
}
return string.IsNullOrEmpty(outputResult) ? "执行成功" : outputResult;
}
catch (Exception ex)
{
Console.WriteLine($"运行LabVIEW项目失败: {ex.Message}");
throw;
}
finally
{
process?.Dispose();
}
}
/// <summary>
/// 释放LabVIEW引擎
/// </summary>
/// <param name="labview">LabVIEW路径可选用于验证</param>
public static void Release_LabVIEW(string labview = "")
{
try
{
if (!string.IsNullOrEmpty(labview) && labview != _labviewPath)
{
Console.WriteLine($"警告: 释放的LabVIEW路径与当前初始化路径不匹配");
}
// 如果有正在运行的LabVIEW进程尝试终止它
if (_labviewProcess != null && !_labviewProcess.HasExited)
{
try
{
_labviewProcess.Kill();
_labviewProcess.WaitForExit(1000);
}
catch (Exception ex)
{
Console.WriteLine($"终止LabVIEW进程时出错: {ex.Message}");
}
}
_labviewPath = string.Empty;
_isLabVIEWEngineInitialized = false;
_labviewProcess = null;
Console.WriteLine("LabVIEW引擎已释放");
}
catch (Exception ex)
{
Console.WriteLine($"释放LabVIEW引擎失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 查找默认LabVIEW安装路径
/// </summary>
/// <returns>LabVIEW可执行文件路径</returns>
private static string FindDefaultLabVIEWPath()
{
// 常见的LabVIEW安装路径
var possiblePaths = new[]
{
@"C:\Program Files\National Instruments\LabVIEW 2025\LabVIEW.exe",
@"C:\Program Files\National Instruments\LabVIEW 2024\LabVIEW.exe",
@"C:\Program Files\National Instruments\LabVIEW 2023\LabVIEW.exe",
@"C:\Program Files\National Instruments\LabVIEW 2022\LabVIEW.exe",
@"C:\Program Files\National Instruments\LabVIEW 2021\LabVIEW.exe",
@"C:\Program Files\National Instruments\LabVIEW 2020\LabVIEW.exe",
@"C:\Program Files (x86)\National Instruments\LabVIEW 2025\LabVIEW.exe",
@"C:\Program Files (x86)\National Instruments\LabVIEW 2024\LabVIEW.exe",
@"C:\Program Files (x86)\National Instruments\LabVIEW 2023\LabVIEW.exe",
@"C:\Program Files (x86)\National Instruments\LabVIEW 2022\LabVIEW.exe",
@"C:\Program Files (x86)\National Instruments\LabVIEW 2021\LabVIEW.exe",
@"C:\Program Files (x86)\National Instruments\LabVIEW 2020\LabVIEW.exe"
};
foreach (var path in possiblePaths)
{
if (File.Exists(path))
{
return path;
}
}
return string.Empty;
}
#endregion
}
}

373
Command/CommandArray.cs Normal file
View File

@ -0,0 +1,373 @@
using Common.Attributes;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace Command
{
[ATSCommand]
[DeviceCategory("数组指令")]
public static class CommandArray
{
#region
/// <summary>
/// 从数组中获取指定位置的元素。
/// </summary>
/// <param name="Array">要索引的数组。</param>
/// <param name="Index">要获取的元素的位置。</param>
/// <returns>指定位置的元素。</returns>
public static object IndexArray(object Array, int Index)
{
return ((dynamic)Array)[Index];
}
/// <summary>
/// 设置数组中指定位置的元素为给定值。
/// </summary>
/// <param name="Array">要设置元素的数组。</param>
/// <param name="Location">要设置的元素的位置。</param>
/// <param name="Value">要设置的值。</param>
/// <returns>修改后的数组。</returns>
//public static object SetArrayElement(object Array, int Location, object Value)
//{
// ((dynamic)Array)[Location] = (dynamic)Value;
// return Array;
//}
/// <summary>
/// 设置数组中指定位置的元素为给定值。
/// </summary>
/// <param name="Array">要设置元素的数组。</param>
/// <param name="Location">要设置的元素的位置从0开始。</param>
/// <param name="Value">要设置的值。</param>
/// <returns>修改后的数组。</returns>
/// <exception cref="ArgumentNullException">如果Array为null。</exception>
/// <exception cref="ArgumentException">如果Array不是数组类型或为字符串。</exception>
/// <exception cref="IndexOutOfRangeException">如果Location超出数组边界。</exception>
public static object SetArrayElement(object Array, int Location, object Value)
{
// 检查输入是否为null
if (Array == null)
{
throw new ArgumentNullException(nameof(Array), "Array cannot be null.");
}
// 检查输入是否为数组类型,但排除字符串
if (Array is string)
{
throw new ArgumentException("Input 'Array' cannot be a string. Strings are immutable.", nameof(Array));
}
if (!Array.GetType().IsArray)
{
throw new ArgumentException("Input 'Array' must be an array type.", nameof(Array));
}
// 将对象转换为数组
Array arr = (Array)Array;
// 检查索引是否有效
if (Location < 0 || Location >= arr.Length)
{
throw new IndexOutOfRangeException($"Location {Location} is out of range for array of length {arr.Length}.");
}
// 检查Value的类型是否与数组元素类型兼容
Type arrayElementType = arr.GetType().GetElementType();
if (Value != null && !arrayElementType.IsAssignableFrom(Value.GetType()))
{
// 尝试进行类型转换,如果失败则抛出异常
try
{
Value = Convert.ChangeType(Value, arrayElementType);
}
catch (Exception ex)
{
throw new ArgumentException($"Value '{Value}' cannot be converted to type '{arrayElementType.Name}'.", nameof(Value), ex);
}
}
// 设置元素
arr.SetValue(Value, Location);
// 返回修改后的数组
return Array;
}
/// <summary>
/// 将给定数组的长度设置为指定值,并返回一个新的数组。
/// 如果指定的长度小于原始数组的长度,则截取原始数组的一部分作为新数组。
/// </summary>
/// <param name="Arr">要设置长度的数组。</param>
/// <param name="Length">新数组的长度。</param>
/// <returns>新的数组,或者原始数组(如果输入参数不是数组类型)。</returns>
public static object SetArrayLength(object Arr, int Length)
{
if (Arr is Array)
{
// 创建一个新的数组实例来存储结果
Array temp = Array.CreateInstance(Arr.GetType()!.GetElementType()!, Length);
// 确定复制的长度,取原始数组长度和目标长度的最小值
int CopyLength = Math.Min(((Array)Arr).Length, Length);
// 复制原始数组的元素到新数组中
Array.Copy((Array)Arr, temp, CopyLength);
return temp;
}
else
{
// 如果输入参数不是数组类型,则返回原始数组
return Arr;
}
}
/// <summary>
/// 在指定位置插入元素到数组中。
/// </summary>
/// <param name="Arr">要插入元素的数组。</param>
/// <param name="Location">要插入元素的位置。</param>
/// <param name="InsertElement">要插入的元素。</param>
/// <returns>插入元素后的数组。</returns>
public static object SetArrayInsertElement(object Arr, int Location, object InsertElement)
{
// 将数组转换为可编辑列表
var temp = Enumerable.ToList((dynamic)Arr);
// 在指定位置插入元素
temp.Insert(Location, (dynamic)InsertElement);
// 如果原数组是数组类型,则将可编辑列表转换回数组并返回
if (Arr is Array)
{
return Enumerable.ToArray(temp);
}
// 返回插入元素后的可编辑列表
return temp;
}
/// <summary>
/// 从数组中删除指定的元素。
/// </summary>
/// <param name="Arr">要删除元素的数组。</param>
/// <param name="DeleteElement">要从数组中删除的元素。</param>
/// <returns>删除元素后的数组。</returns>
public static object SetArrayDeleteElement(object Arr, object DeleteElement)
{
// 将数组转换为可编辑列表
var temp = Enumerable.ToList((dynamic)Arr);
// 从列表中移除指定元素
temp.Remove((dynamic)DeleteElement);
// 如果原数组是数组类型,则将可编辑列表转换回数组并返回
if (Arr is Array)
{
return Enumerable.ToArray(temp);
}
// 返回删除元素后的可编辑列表
return temp;
}
/// <summary>
/// 从数组中删除指定位置的元素。
/// </summary>
/// <param name="Arr">要删除元素的数组。</param>
/// <param name="DeleteElementLocation">要从数组中删除的元素的位置。</param>
/// <returns>删除元素后的数组。</returns>
public static object SetArrayDeleteLocationElement(object Arr, int DeleteElementLocation)
{
var temp = Enumerable.ToList((dynamic)Arr);
temp.RemoveAt(DeleteElementLocation);
if (Arr is Array)
{
return Enumerable.ToArray(temp);
}
return temp;
}
/// <summary>
/// 在数组中寻找第一个匹配的元素,并返回其索引位置。
/// </summary>
/// <param name="Arr">要搜索的数组。</param>
/// <param name="QueryElement">要寻找的元素。</param>
/// <returns>要寻找的元素在数组中的第一个匹配项的索引;如果未找到匹配项,则为 -1。</returns>
public static int ArrayQueryFirstMatchElement(object Arr, object QueryElement)
{
if (Arr is Array)
{
return Array.IndexOf((dynamic)Arr, (dynamic)QueryElement);
}
return ((dynamic)Arr).IndexOf((dynamic)QueryElement);
}
/// <summary>
/// 替换数组中第一个匹配的元素为指定的新元素。
/// </summary>
/// <param name="Arr">要进行替换操作的数组。</param>
/// <param name="FirstMatchElement">要替换的元素。</param>
/// <param name="ReplaceElement">要替换为的新元素。</param>
/// <returns>替换后的数组。</returns>
public static object ArrayReplaceFirstMatchElement(object Arr, object FirstMatchElement, object ReplaceElement)
{
var temp = Enumerable.ToList((dynamic)Arr);
int Location = ArrayQueryFirstMatchElement(Arr, FirstMatchElement);
temp[Location] = (dynamic)ReplaceElement;
return temp;
}
/// <summary>
/// 将数组中所有匹配的元素替换为指定的新元素。
/// </summary>
/// <param name="Arr">要进行替换操作的数组。</param>
/// <param name="MatchElement">要替换的元素。</param>
/// <param name="ReplaceElement">要替换为的新元素。</param>
/// <returns>替换后的数组。</returns>
public static object ArrayReplaceAllMatchElement(object Arr, object MatchElement, object ReplaceElement)
{
var temp = Enumerable.ToList((dynamic)Arr);
int = temp.Count;
for (int i = 0; i < ; i++)
{
if (((dynamic)Arr)[i] == (dynamic)MatchElement)
{
((dynamic)Arr)[i] = (dynamic)ReplaceElement;
}
}
return Arr;
}
/// <summary>
/// 反转数组中元素的顺序。
/// </summary>
/// <param name="Arr">要反转的数组。</param>
/// <returns>反转后的数组。</returns>
public static object ArrayReverse(object Arr)
{
var temp = Enumerable.ToList((dynamic)Arr);
temp.Reverse();
if (Arr is Array)
{
return Enumerable.ToArray(temp);
}
return temp;
}
/// <summary>
/// 对数组进行排序。
/// </summary>
/// <param name="Arr">要排序的数组。</param>
/// <returns>排序后的数组。</returns>
public static object ArraySorting(object Arr)
{
var temp = Enumerable.ToList((dynamic)Arr);
temp = Enumerable.ToList(Enumerable.Order(temp));
if (Arr is Array)
{
return Enumerable.ToArray(temp);
}
return temp;
}
/// <summary>
/// 获取数组的长度。
/// </summary>
/// <param name="Arr">要获取长度的数组。</param>
/// <returns>数组的长度。</returns>
public static int GetArrayLength(object Arr)
{
if (Arr is Array)
{
return ((Array)Arr).Length;
}
return ((ICollection)Arr).Count;
}
/// <summary>
/// 将对象转换为数组。
/// </summary>
/// <param name="Arr">要转换的对象。</param>
/// <returns>转换后的数组。</returns>
public static object objectConvertToArray(object Arr)
{
return Enumerable.ToArray((dynamic)Arr);
}
/// <summary>
/// 将对象转换为列表。
/// </summary>
/// <param name="Arr">要转换的对象。</param>
/// <returns>转换后的列表。</returns>
public static object objectConvertToList(object Arr)
{
return Enumerable.ToList((dynamic)Arr);
}
/// <summary>
/// 从数组中截取指定长度的子数组。
/// </summary>
/// <param name="Arr">要截取的数组。</param>
/// <param name="StartLocation">截取开始的位置。</param>
/// <param name="Length">要截取的长度。</param>
/// <returns>截取得到的子数组。</returns>
public static object ArrayCapture(object Arr, int StartLocation, int Length)
{
var temp = Enumerable.ToList((dynamic)Arr);
temp = Enumerable.ToList(Enumerable.Take(Enumerable.Skip(temp, StartLocation), Length));
if (Arr is Array)
{
return Enumerable.ToArray(temp);
}
return temp;
}
///<summary>
///计算数组的平均值
/// </summary>
/// <param name="Arr">要计算平均值的数组</param>
public static object? GetArrayAverage(object Arr)
{
try
{
return Enumerable.Average((dynamic)Arr);
}
catch
{
return null;
}
}
///<summary>
///获得数组的最大值
/// </summary>
/// <param name="Arr">要获得最大值的数组</param>
public static object? GetArrayMax(object Arr)
{
try
{
return Enumerable.Max((dynamic)Arr);
}
catch
{
return null;
}
}
///<summary>
///获得数组的最小值
/// </summary>
/// <param name="Arr">要获得最小值的数组</param>
public static object? GetArrayMin(object Arr)
{
try
{
return Enumerable.Min((dynamic)Arr);
}
catch
{
return null;
}
}
#endregion
}
}

489
Command/CommandMath.cs Normal file
View File

@ -0,0 +1,489 @@
using Common.Attributes;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace Command
{
public enum ComparisonFuncEnum
{
Big,
BigOrEqual,
Small,
SmallOrEqual,
Equal,
NotEqual
}
[ATSCommand]
[DeviceCategory("计算指令")]
public static class CommandMath
{
#region Double类型数据
/// <summary>
/// 两数相加(Param1 + Param2)
/// </summary>
/// <param name="Param1">传入值实数1</param>
/// <param name="Param2">传入值实数2</param>
/// <example>Param1:9 Param2:2 返回值:11</example>
/// <returns></returns>
public static double ParamAdd(double Param1, double Param2)
{
return Param1 + Param2;
}
/// <summary>
/// 两数相减(Param1 - Param2)
/// </summary>
/// <param name="Param1">传入值实数1</param>
/// <param name="Param2">传入值实数2</param>
/// <example>Param1:9 Param2:2 返回值:7</example>
/// <returns></returns>
public static double ParamReduce(double Param1, double Param2)
{
return Param1 - Param2;
}
/// <summary>
/// 两数相乘(Param1 * Param2)
/// </summary>
/// <param name="Param1">传入值实数1</param>
/// <param name="Param2">传入值实数2</param>
/// <example>Param1:9 Param2:2 返回值:18</example>
/// <returns></returns>
public static double ParamMult(double Param1, double Param2)
{
return Param1 * Param2;
}
/// <summary>
/// 两数相除(Param1 / Param2)
/// </summary>
/// <param name="Param1">传入值实数1</param>
/// <param name="Param2">传入值实数2</param>
/// <param name="Param3">传入值:保留几位小数</param>
/// <example>Param1:10 Param2:3 Param3:3 返回值3.333</example>
/// <example>Param1:11 Param2:3 Param3:2 返回值3.67</example>
/// <returns></returns>
public static double ParamDivide(double Param1, double Param2, int Param3)
{
return Math.Round((Param1 / Param2), Param3);
}
/// <summary>
/// 两数相除求余(Param1 % Param2)
/// </summary>
/// <param name="Param1">传入值实数1</param>
/// <param name="Param2">传入值实数2</param>
/// <example>Param1:9 Param2:2 返回值:1</example>
/// <returns></returns>
public static double ParamRemainder(double Param1, double Param2)
{
return Param1 % Param2;
}
/// <summary>
/// 实数开根号
/// </summary>
/// <param name="Param1">传入值实数1</param>
/// <example>Param1:16 返回值4</example>
/// <returns></returns>
public static double ParamSquareRoot(double Param1)
{
return Math.Sqrt(Param1);
}
/// <summary>
/// 实数的幂运算
/// </summary>
/// <param name="Param1">传入值实数1</param>
/// <param name="Param2">传入值实数1需要做几次方</param>
/// <example>Param1:3 Param22 返回值9</example>
/// <example>Param1:2 Param23 返回值8</example>
/// <example>Param1:5 Param24 返回值625</example>
/// <returns></returns>
public static double ParamPow(double Param1, double Param2)
{
return Math.Pow(Param1, Param2);
}
/// <summary>
/// 实数的绝对值
/// </summary>
/// <param name="Param1">传入值实数1</param>
/// <example>Param1:-3.7 返回值3.7</example>
/// <returns></returns>
public static double ParamAbs(double Param1)
{
return Math.Abs(Param1);
}
/// <summary>
/// 实数的向下取整
/// </summary>
/// <param name="Param1">传入值实数1</param>
/// <example>Param1:3.7 返回值3</example>
/// <returns></returns>
public static double ParamFloor(double Param1)
{
return Math.Floor(Param1);
}
/// <summary>
/// 实数的向上取整
/// </summary>
/// <param name="Param1">传入值实数1</param>
/// <example>Param1:3.7 返回值4</example>
/// <returns></returns>
public static double ParamCeiling(double Param1)
{
return Math.Ceiling(Param1);
}
#endregion
#region String类型的数据
/// <summary>
/// A与B转换为double后比较,返回A比较B的结果
/// </summary>
/// <param name="a">比较数1</param>
/// <param name="b">比较数2</param>
/// <param name="比较方法">比较方法</param>
/// <returns>返回A比较B</returns>
public static bool Comparison(string a, string b, ComparisonFuncEnum )
{
var aa = Convert.ToDouble(a);
var bb = Convert.ToDouble(b);
switch ()
{
case ComparisonFuncEnum.Big:
return aa > bb;
case ComparisonFuncEnum.BigOrEqual:
return aa >= bb;
case ComparisonFuncEnum.Small:
return aa < bb;
case ComparisonFuncEnum.SmallOrEqual:
return aa <= bb;
case ComparisonFuncEnum.Equal:
return aa == bb;
case ComparisonFuncEnum.NotEqual:
return aa != bb;
default:
return false;
}
}
/// <summary>
/// 加法指令,转换为double后返回o1+o2
/// </summary>
/// <param name="o1">运算数1</param>
/// <param name="o2">运算数2</param>
/// <returns>返回o1+o2</returns>
public static string TwoNumberAdd(string o1, string o2)
{
return (Convert.ToDecimal(o1) + Convert.ToDecimal(o2)).ToString();
}
/// <summary>
/// 减法指令,转换为double后返回o1-o2
/// </summary>
/// <param name="o1">运算数1</param>
/// <param name="o2">运算数2</param>
/// <returns>返回o1-o2</returns>
public static string TwoNumberReduce(string o1, string o2)
{
return (Convert.ToDecimal(o1) - Convert.ToDecimal(o2)).ToString();
}
/// <summary>
/// 乘法指令,转换为double后返回o1*o2
/// </summary>
/// <param name="o1">运算数1</param>
/// <param name="o2">运算数2</param>
/// <returns>返回o1*o2</returns>
public static string TwoNumberMult(string o1, string o2)
{
return (Convert.ToDecimal(o1) * Convert.ToDecimal(o2)).ToString();
}
/// <summary>
/// 除法指令,转换为double后返回o1/o2
/// </summary>
/// <param name="o1">运算数1</param>
/// <param name="o2">运算数2</param>
/// <returns>返回o1/o2</returns>
public static string TwoNumberDivide1(string o1, string o2)
{
return (Convert.ToDecimal(o1) / Convert.ToDecimal(o2)).ToString();
}
/// <summary>
/// 转换为double后返回o1/o2
/// </summary>
/// <param name="o1">运算数1</param>
/// <param name="o2">运算数2</param>
/// <param name="o3">保留几位小数</param>
/// <returns>返回o1/o2</returns>
public static string TwoNumberDivide2(string o1, string o2, int o3)
{
return (Math.Round((Convert.ToDecimal(o1) / Convert.ToDecimal(o2)), o3)).ToString();
}
/// <summary>
/// 返回o1%o2
/// </summary>
/// <param name="o1">运算数1</param>
/// <param name="o2">运算数2</param>
/// <returns>返回o1%o2</returns>
public static string TwoNumberRemainder(string o1, string o2)
{
return (Convert.ToDecimal(o1) % (Convert.ToDecimal(o2))).ToString();
}
/// <summary>
/// 计算两个数字的平方。
/// </summary>
/// <param name="o1">第一个数字的字符串表示。</param>
/// <param name="o2">第二个数字的字符串表示。</param>
/// <example>Param1:3 Param22 返回值9</example>
/// <example>Param1:2 Param23 返回值8</example>
/// <example>Param1:5 Param24 返回值625</example>
/// <returns>返回两个数字的平方的字符串表示。</returns>
public static string NumberPow(string o1, string o2)
{
// 将输入的字符串转换为双精度浮点数,并计算它们的平方
// 最后将结果转换为字符串并返回
return Math.Pow(Convert.ToDouble(o1), Convert.ToDouble(o2)).ToString();
}
/// <summary>
/// 计算一个数的平方根。
/// </summary>
/// <param name="o1">要计算平方根的数字的字符串表示。</param>
/// <returns>返回输入数字的平方根的字符串表示。</returns>
public static string NumberSqrt(string o1)
{
// 将输入的字符串转换为双精度浮点数,并计算其平方根。
// 最后将结果转换为字符串并返回。
return Math.Sqrt(Convert.ToDouble(o1)).ToString();
}
/// <summary>
/// 计算一个数的绝对值。
/// </summary>
/// <param name="o1">要计算绝对值的数字的字符串表示。</param>
/// <returns>返回输入数字的绝对值的字符串表示。</returns>
public static string NumberAbs(string o1)
{
// 将输入的字符串转换为双精度浮点数,并计算其绝对值。
// 最后将结果转换为字符串并返回。
return Math.Abs(Convert.ToDouble(o1)).ToString();
}
/// <summary>
/// 计算一个角度的正弦值。
/// </summary>
/// <param name="o1">要计算正弦值的角度的字符串表示(单位为弧度)。</param>
/// <returns>返回输入角度的正弦值的字符串表示。</returns>
public static string NumberSin(string o1)
{
// 将输入的字符串转换为双精度浮点数,并计算其正弦值。
// 最后将结果转换为字符串并返回。
return Math.Sin(Convert.ToDouble(o1)).ToString();
}
/// <summary>
/// 计算一个角度的余弦值。
/// </summary>
/// <param name="o1">要计算余弦值的角度的字符串表示(单位为弧度)。</param>
/// <returns>返回输入角度的余弦值的字符串表示。</returns>
public static string NumberCos(string o1)
{
// 将输入的字符串转换为双精度浮点数,并计算其余弦值。
// 最后将结果转换为字符串并返回。
return Math.Cos(Convert.ToDouble(o1)).ToString();
}
/// <summary>
/// 计算一个角度的正切值。
/// </summary>
/// <param name="o1">要计算正切值的角度的字符串表示(单位为弧度)。</param>
/// <returns>返回输入角度的正切值的字符串表示。</returns>
public static string NumberTan(string o1)
{
// 将输入的字符串转换为双精度浮点数,并计算其正切值。
// 最后将结果转换为字符串并返回。
return Math.Tan(Convert.ToDouble(o1)).ToString();
}
/// <summary>
/// 计算一个数的反正弦值。
/// </summary>
/// <param name="o1">要计算反正弦值的数的字符串表示。</param>
/// <returns>返回输入数的反正弦值的字符串表示(单位为弧度)。</returns>
public static string NumberASin(string o1)
{
// 将输入的字符串转换为双精度浮点数,并计算其反正弦值。
// 最后将结果转换为字符串并返回。
return Math.Asin(Convert.ToDouble(o1)).ToString();
}
/// <summary>
/// 计算一个数的反余弦值。
/// </summary>
/// <param name="o1">要计算反余弦值的数的字符串表示。</param>
/// <returns>返回输入数的反余弦值的字符串表示(单位为弧度)。</returns>
public static string NumberACos(string o1)
{
// 将输入的字符串转换为双精度浮点数,并计算其反余弦值。
// 最后将结果转换为字符串并返回。
return Math.Acos(Convert.ToDouble(o1)).ToString();
}
/// <summary>
/// 计算一个数的反正切值。
/// </summary>
/// <param name="o1">要计算反正切值的数的字符串表示。</param>
/// <returns>返回输入数的反正切值的字符串表示(单位为弧度)。</returns>
public static string NumberATan(string o1)
{
// 将输入的字符串转换为双精度浮点数,并计算其反正切值。
// 最后将结果转换为字符串并返回。
return Math.Atan(Convert.ToDouble(o1)).ToString();
}
/// <summary>
/// 计算 e 的给定次幂。
/// </summary>
/// <param name="o1">e 的指数的字符串表示。</param>
/// <returns>返回 e 的给定次幂的字符串表示。</returns>
public static string eIndex(string o1)
{
// 将输入的字符串转换为双精度浮点数,并计算 e 的给定次幂。
// 最后将结果转换为字符串并返回。
return Math.Exp(Convert.ToDouble(o1)).ToString();
}
/// <summary>
/// 计算一个数的自然对数。
/// </summary>
/// <param name="o1">要计算自然对数的数的字符串表示。</param>
/// <returns>返回输入数的自然对数的字符串表示。</returns>
public static string NumberNaturalLogarithm(string o1)
{
// 将输入的字符串转换为双精度浮点数,并计算其自然对数。
// 最后将结果转换为字符串并返回。
return Math.Log(Convert.ToDouble(o1)).ToString();
}
/// <summary>
/// 计算一个数在指定基数下的对数。
/// </summary>
/// <param name="o1">要计算对数的数的字符串表示。</param>
/// <param name="Base">对数的基数的字符串表示。</param>
/// <returns>返回输入数在指定基数下的对数的字符串表示。</returns>
public static string NumberLogarithm(string o1, string Base)
{
// 将输入的字符串转换为双精度浮点数,并计算其在指定基数下的对数。
// 最后将结果转换为字符串并返回。
return Math.Log(Convert.ToDouble(o1), Convert.ToDouble(Base)).ToString();
}
/// <summary>
/// 对输入数字进行四舍五入取整。
/// </summary>
/// <param name="n1">要进行取整操作的数字的字符串表示。</param>
/// <returns>返回经过四舍五入取整后的结果的字符串表示。</returns>
public static string NumberRoundToNearest(string n1)
{
// 将输入的字符串转换为双精度浮点数,并进行四舍五入取整。
// 最后将结果转换为字符串并返回。
return Math.Round(Convert.ToDouble(n1)).ToString();
}
/// <summary>
/// 对输入数字进行向上舍入取整。
/// </summary>
/// <param name="n1">要进行取整操作的数字的字符串表示。</param>
/// <returns>返回经过向上舍入取整后的结果的字符串表示。</returns>
public static string NumberRoundUp(string n1)
{
// 将输入的字符串转换为双精度浮点数,并进行向上舍入取整。
// 最后将结果转换为字符串并返回。
return Math.Ceiling(Convert.ToDouble(n1)).ToString();
}
/// <summary>
/// 对输入数字进行向下舍入取整。
/// </summary>
/// <param name="n1">要进行取整操作的数字的字符串表示。</param>
/// <returns>返回经过向下舍入取整后的结果的字符串表示。</returns>
public static string NumberRoundDown(string n1)
{
// 将输入的字符串转换为双精度浮点数,并进行向下舍入取整。
// 最后将结果转换为字符串并返回。
return Math.Floor(Convert.ToDouble(n1)).ToString();
}
/// <summary>
/// 获取圆周率π的字符串表示。
/// </summary>
/// <returns>返回圆周率π的字符串表示。</returns>
public static string PI()
{
// 返回圆周率π的字符串表示。
return Math.PI.ToString();
}
//public static Random random1 = new Random();
/// <summary>
/// 生成一个随机整数。
/// </summary>
/// <returns>返回生成的随机整数的字符串表示。</returns>
public static string RandomNumber_Int()
{
Random random = new Random();
// 使用随机数生成器生成一个随机整数,并将其转换为字符串表示。
return random.Next().ToString();
}
/// <summary>
/// 生成一个指定范围内的随机整数。
/// </summary>
/// <param name="最小">随机数生成范围的最小值。</param>
/// <param name="最大">随机数生成范围的最大值(不包含)。</param>
/// <returns>返回生成的指定范围内的随机整数的字符串表示。</returns>
public static string RandomNumber_RangeInt(int , int )
{
Random random = new Random();
// 使用随机数生成器生成一个指定范围内的随机整数,并将其转换为字符串表示。
return random.Next(, ).ToString();
}
/// <summary>
/// 生成一个随机小数。
/// </summary>
/// <returns>返回生成的随机小数的字符串表示。</returns>
public static string RandomNumber_Decimal()
{
Random random = new Random();
// 使用随机数生成器生成一个随机小数,并将其转换为字符串表示。
return random.NextDouble().ToString();
}
/// <summary>
/// 计算给定算式的结果。
/// </summary>
/// <param name="s">要计算的算式的字符串表示。</param>
/// <returns>返回计算结果的字符串表示。</returns>
public static string FormulaCalculation(string s)
{
// 创建一个 DataTable 实例来进行算式的计算。
DataTable dataTable = new DataTable();
// 使用 DataTable 的 Compute 方法计算给定的算式,并将结果转换为字符串表示。
return dataTable.Compute(s, "").ToString();
}
/// <summary>
/// 给double类型的变量赋值。
/// </summary>
/// <param name="Param1">被赋值的数值。</param>
/// <param name="Param2">数值。</param>
/// <returns>返回赋值后的值。</returns>
public static double AssignValue(double Param1, double Param2)
{
Param1 = Param2;
return Param1;
}
#endregion
}
}

View File

@ -0,0 +1,155 @@
using Common.Attributes;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace Command
{
[ATSCommand]
[DeviceCategory("进制转换指令")]
public static class CommandRadixChange
{
#region
/// <summary>
/// 二进制 转换成 八进制
/// </summary>
/// <param name="Param1">传入值:字符类型</param>
/// <example>Param1:"101010" 返回值:"52"</example>
/// <returns></returns>
public static string TwoRadix_ConvertTo_EightRadix(string Param1)
{
return Convert.ToString(Convert.ToInt32(Param1, 2),8);
}
/// <summary>
/// 二进制 转换成 十进制
/// </summary>
/// <param name="Param1">传入值:字符类型</param>
/// <example>Param1:"101010" 返回值:"42"</example>
/// <returns></returns>
public static string TwoRadix_ConvertTo_TenRadix(string Param1)
{
return Convert.ToString(Convert.ToInt32(Param1, 2));
}
/// <summary>
/// 二进制 转换成 十六进制
/// </summary>
/// <param name="Param1">传入值:字符类型</param>
/// <example>Param1:"101010" 返回值:"2A"</example>
/// <returns></returns>
public static string TwoRadix_ConvertTo_SixteenRadix(string Param1)
{
return Convert.ToString(Convert.ToInt32(Param1, 2), 16);
}
/// <summary>
/// 八进制 转换成 二进制
/// </summary>
/// <param name="Param1">传入值:字符类型</param>
/// <example>Param1:"88" 返回值:"1001000"</example>
/// <returns></returns>
public static string EightRadix_ConvertTo_TwoRadix(string Param1)
{
return Convert.ToString(Convert.ToInt32(Param1, 8), 2);
}
/// <summary>
/// 八进制 转换成 十进制
/// </summary>
/// <param name="Param1">传入值:字符类型</param>
/// <example>Param1:"88" 返回值:"72"</example>
/// <returns></returns>
public static double EightRadix_ConvertTo_TenRadix(string Param1)
{
double Value = Convert.ToInt32(Param1, 8);
return Value;
}
/// <summary>
/// 八进制 转换成 十六进制
/// </summary>
/// <param name="Param1">传入值:字符类型</param>
/// <example>Param1:"88" 返回值:"48"</example>
/// <returns></returns>
public static string EightRadix_ConvertTo_SixteenRadix(string Param1)
{
return Convert.ToString(Convert.ToInt32(Param1, 8), 16);
}
/// <summary>
/// 十进制 转换成 二进制
/// </summary>
/// <param name="Param1">传入值:整型数值</param>
/// <example>Param1:60 返回值:"111100"</example>
/// <returns></returns>
public static string TenRadix_ConvertTo_TwoRadix(int Param1)
{
return Convert.ToString(Param1, 2);
}
/// <summary>
/// 十进制 转换成 八进制
/// </summary>
/// <param name="Param1">传入值:整型数值</param>
/// <example>Param1:60 返回值:"74"</example>
/// <returns></returns>
public static string TenRadix_ConvertTo_EightRadix(int Param1)
{
return Convert.ToString(Param1, 8);
}
/// <summary>
/// 十进制 转换成 十六进制
/// </summary>
/// <param name="Param1">传入值:整型数值</param>
/// <example>Param1:60 返回值:"3C"</example>
/// <returns></returns>
public static string TenRadix_ConvertTo_SixteenRadix(int Param1)
{
return Convert.ToString(Param1, 16);
}
/// <summary>
/// 十六进制 转换成 二进制
/// </summary>
/// <param name="Param1">传入值:字符类型</param>
/// <example>Param1:"6F" 返回值:"1101111"</example>
/// <returns></returns>
public static string SixteenRadix_ConvertTo_Radix2(string Param1)
{
return Convert.ToString(Convert.ToInt32(Param1, 16), 2);
}
/// <summary>
/// 十六进制 转换成 八进制
/// </summary>
/// <param name="Param1">传入值:字符类型</param>
/// <example>Param1:"6F" 返回值:"157"</example>
/// <returns></returns>
public static string SixteenRadix_ConvertTo_EightRadix8(string Param1)
{
return Convert.ToString(Convert.ToInt32(Param1, 16), 8);
}
/// <summary>
/// 十六进制 转换成 十进制
/// </summary>
/// <param name="Param1">传入值:字符类型</param>
/// <example>Param1:"6F" 返回值:"111"</example>
/// <returns></returns>
public static string SixteenRadix_ConvertTo_TenRadix(string Param1)
{
return Convert.ToString(Convert.ToInt32(Param1, 16));
}
#endregion
}
}

View File

@ -0,0 +1,198 @@
using Common.Attributes;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace Command
{
[ATSCommand]
[DeviceCategory("字符串处理指令")]
public static class CommandStringProcessing
{
///<summary>
///获取值
///</summary>
///<param name="obj">要获取的值。</param>
///returns>获取值(字符串)。</returns>
public static string GetValue(object obj)
{
return Convert.ToString(obj) ?? string.Empty;
}
/// <summary>
/// 在原字符串中查找指定字符的位置。
/// </summary>
/// <param name="str">要查找的字符串。</param>
/// <param name="findStr">要查找的字符。</param>
/// <returns>字符在字符串中的位置,如果未找到则返回 -1。</returns>
public static int FindStr(string str, string findStr)
{
return str.IndexOf(findStr);
}
/// <summary>
/// 将原字符串中的指定字符替换为新字符。
/// </summary>
/// <param name="str">要替换的字符串。</param>
/// <param name="ReplaceStr">要被替换的字符。</param>
/// <param name="ReplacedStr">替换后的新字符。</param>
/// <returns>替换后的字符串。</returns>
public static string ReplaceStr(string str, string ReplaceStr, string ReplacedStr)
{
return str.Replace(ReplaceStr, ReplacedStr);
}
/// <summary>
/// 从原字符串中移除指定位置开始的指定个数的字符。
/// </summary>
/// <param name="str">要操作的字符串。</param>
/// <param name="startLocation">开始移除字符的位置。</param>
/// <param name="removeQty">要移除的字符个数。</param>
/// <returns>移除后的字符串。</returns>
public static string RemoveStr(string str, int startLocation, int removeQty)
{
return str.Remove(startLocation, removeQty);
}
/// <summary>
/// 在原字符串的指定位置插入新字符串。
/// </summary>
/// <param name="str">要操作的字符串。</param>
/// <param name="startLocation">要插入字符串的位置。</param>
/// <param name="newStr">要插入的字符串。</param>
/// <returns>插入后的字符串。</returns>
public static string InsertStr(string str, int startLocation, string newStr)
{
return str.Insert(startLocation, newStr);
}
/// <summary>
/// 从原字符串中截取指定位置开始的指定个数的字符。
/// </summary>
/// <param name="str">要操作的字符串。</param>
/// <param name="startLocation">要截取的起始位置。</param>
/// <param name="qty">要截取的字符个数。</param>
/// <returns>截取后的字符串。</returns>
public static string CaptureStr(string str, int startLocation, int qty)
{
return str.Substring(startLocation, qty);
}
/// <summary>
/// 将字符串转换为小写形式。
/// </summary>
/// <param name="str">要转换的字符串。</param>
/// <returns>转换为小写后的字符串。</returns>
public static string StrConvertToLower(string str)
{
return str.ToLower();
}
/// <summary>
/// 将字符串转换为大写形式。
/// </summary>
/// <param name="str">要转换的字符串。</param>
/// <returns>转换为大写后的字符串。</returns>
public static string StrConvertToUpper(string str)
{
return str.ToUpper();
}
/// <summary>
/// 获取字符串的长度。
/// </summary>
/// <param name="str">要获取长度的字符串。</param>
/// <returns>字符串的长度。</returns>
public static int GetStrLength(string str)
{
return str.Length;
}
/// <summary>
/// 去除字符串两端的空白字符。
/// </summary>
/// <param name="str">要去除空白字符的字符串。</param>
/// <returns>去除空白字符后的字符串。</returns>
public static string StrRemoveLeadingAndTrailingWhitespaces(string str)
{
return str.Trim();
}
/// <summary>
/// 将字节数组转换为字符串,使用 UTF-8 编码。
/// </summary>
/// <param name="arr">要转换的字节数组。</param>
/// <returns>转换后的字符串。</returns>
public static string ByteArrayToStr_UTF8(byte[] arr)
{
return Encoding.UTF8.GetString(arr);
}
/// <summary>
/// 将字节数组转换为字符串,使用 GB18030 编码。
/// </summary>
/// <param name="arr">要转换的字节数组。</param>
/// <returns>转换后的字符串。</returns>
public static string ByteArrayToStr_GB18030(byte[] arr)
{
return Encoding.GetEncoding("gb18030").GetString(arr);
}
/// <summary>
/// 将字节数组转换为字符串,使用自选的编码类型。
/// </summary>
/// <param name="arr">要转换的字节数组。</param>
/// <param name="encodingType">要使用的编码类型。</param>
/// <returns>转换后的字符串。</returns>
public static string ByteArrayToStr_SelfSelectedEncoding(byte[] arr, string encodingType)
{
return Encoding.GetEncoding(encodingType).GetString(arr);
}
/// <summary>
/// 将字节数组转换为16进制字符串
/// </summary>
/// <param name="str">要转换的字节数组。</param>
/// <returns>转换后的字符串。</returns>
public static string ByteArrayTo_HexStr(byte[] str)
{
return Convert.ToInt32(str.ToString(),2).ToString("X2");
}
/// <summary>
/// 将字符串转换为字节数组,使用 UTF-8 编码。
/// </summary>
/// <param name="str">要转换的字符串。</param>
/// <returns>转换后的字节数组。</returns>
public static byte[] StrToByteArray_UTF8(string str)
{
return Encoding.UTF8.GetBytes(str);
}
/// <summary>
/// 将字符串转换为字节数组,使用 GB18030 编码。
/// </summary>
/// <param name="str">要转换的字符串。</param>
/// <returns>转换后的字节数组。</returns>
public static byte[] StrToByteArray_GB18030(string str)
{
return Encoding.GetEncoding("gb18030").GetBytes(str);
}
/// <summary>
/// 将字符串转换为字节数组,使用自选的编码类型。
/// </summary>
/// <param name="str">要转换的字符串。</param>
/// <param name="encodingType">要使用的编码类型。</param>
/// <returns>转换后的字节数组。</returns>
public static byte[] StrToByteArray_SelfSelectedEncoding(string str, string encodingType)
{
return Encoding.GetEncoding(encodingType).GetBytes(str);
}
}
}

465
Command/CommandSystem.cs Normal file
View File

@ -0,0 +1,465 @@
using Common.Attributes;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using static Common.Attributes.ATSCommandAttribute;
namespace Command
{
[ATSCommand]
[DeviceCategory("系统指令")]
public static class CommandSystem
{
#region
/// <summary>
/// int转化为double
/// </summary>
/// <param name="Param1">传入值:整型数值</param>
/// <example>Param1:40 返回值40</example>
/// <returns></returns>
public static double Int_ConvertTo_Double(int Param1)
{
return Convert.ToDouble(Param1);
}
/// <summary>
/// double转化为Int16
/// </summary>
/// <param name="Param1">传入值:实数数值</param>
/// <example>Param1:4 返回值4</example>
/// <example>Param1:4.67 返回值4</example>
/// <returns></returns>
public static Int16 Double_ConvertTo_Int16(double Param1)
{
Int16 Value = Convert.ToInt16(Param1);
return Value;
}
/// <summary>
/// double转化为int,即Int32
/// </summary>
/// <param name="Param1">传入值:实数数值</param>
/// <example>Param1:4 返回值4</example>
/// <example>Param1:4.67 返回值4</example>
/// <returns></returns>
public static int Double_ConvertTo_Int(double Param1)
{
int Value = Convert.ToInt32(Param1);
return Value;
}
/// <summary>
/// double转化为Int64
/// </summary>
/// <param name="Param1">传入值:实数数值</param>
/// <example>Param1:4 返回值4</example>
/// <example>Param1:4.67 返回值4</example>
/// <returns></returns>
public static Int64 Double_ConvertTo_Int64(double Param1)
{
Int64 Value = Convert.ToInt64(Param1);
return Value;
}
/// <summary>
/// float转化为double
/// </summary>
/// <param name="Param1">传入值:浮点数值</param>
/// <example>Param1:4.0 返回值4</example>
/// <example>Param1:4.2 返回值4.2</example>
/// <returns></returns>
public static double Float_ConvertTo_Double(float Param1)
{
double Value = Convert.ToDouble(Param1);
return Value;
}
/// <summary>
/// 判断string转化为int转换成功返回 true失败返回 false
/// </summary>
/// <param name="Param1">传入值:字符串</param>
/// <example>Param1:"2" 返回值true</example>
/// <example>Param1:"abc" 返回值false</example>
/// <returns></returns>
public static bool BoolString_ConvertTo_Int(string Param1)
{
bool Value = int.TryParse(Param1, out int intValue);
return Value;
}
/// <summary>
/// string转化为int
/// </summary>
/// <param name="Param1">传入值:字符串</param>
/// <example>Param1:"2" 返回值2</example>
/// <returns></returns>
public static int String_ConvertTo_Int(string Param1)
{
int Value = Convert.ToInt32(Param1);
return Value;
}
/// <summary>
/// int转化为string
/// </summary>
/// <param name="Param1">传入值:整型数值</param>
/// <example>Param1:2 返回值:"2"</example>
/// <returns></returns>
public static string Int_ConvertTo_String(int Param1)
{
string Value = Convert.ToString(Param1);
return Value;
}
/// <summary>
/// 判断string转化为double转换成功返回 true失败返回 false
/// </summary>
/// <param name="Param1">传入值:字符串</param>
/// <example>Param1:"2" 返回值true</example>
/// <example>Param1:"2.1" 返回值true</example>
/// <example>Param1:"abc" 返回值false</example>
/// <returns></returns>
public static bool BoolString_ConvertTo_Double(string Param1)
{
bool Value = double.TryParse(Param1, out double intValue);
return Value;
}
/// <summary>
/// string转化为double
/// </summary>
/// <param name="Param1">传入值:字符串</param>
/// <example>Param1:"2" 返回值2</example>
/// <example>Param1:"2.1" 返回值2.1</example>
/// <returns></returns>
public static double String_ConvertTo_Double(string Param1)
{
double Value = Convert.ToDouble(Param1);
return Value;
}
/// <summary>
/// double转化为string
/// </summary>
/// <param name="Param1">传入值:实数数值</param>
/// <example>Param1:2 返回值:"2"</example>
/// <example>Param1:2.1 返回值:"2.1"</example>
/// <returns></returns>
public static string Double_ConvertTo_String(double Param1)
{
string Value = Convert.ToString(Param1);
return Value;
}
/// <summary>
/// float转化为string
/// </summary>
/// <param name="Param1">传入值:浮点数值</param>
/// <example>Param1:2.0 返回值:"2.0"</example>
/// <example>Param1:2.1 返回值:"2.1"</example>
/// <returns></returns>
public static string Float_ConvertTo_String(float Param1)
{
string Value = Convert.ToString(Param1);
return Value;
}
/// <summary>
/// 判断string转化为datetime转换成功返回 true失败返回 false
/// </summary>
/// <param name="Param1">传入值:时间字符串</param>
/// <example>Param1:"2025-08-14" 返回值true</example>
/// <example>Param1:"2025-08-14 13:14:15" 返回值true</example>
/// <example>Param1:"abc" 返回值false</example>
/// <returns></returns>
public static bool BoolString_ConvertTo_Datetime(string Param1)
{
bool Value = DateTime.TryParse(Param1, out DateTime DateTimeValue);
return Value;
}
/// <summary>
/// string转化为datetime
/// </summary>
/// <param name="Param1">传入值:时间字符串</param>
/// <example>Param1:"2025-08-14" 返回值2025/8/14 0:00:00</example>
/// <example>Param1:"2025-08-14 13:14:15" 返回值2025/8/14 13:14:15</example>
/// <returns></returns>
public static DateTime String_ConvertTo_Datetime(string Param1)
{
DateTime Value = Convert.ToDateTime(Param1);
return Value;
}
/// <summary>
/// datetime转化为string
/// </summary>
/// <param name="Param1">传入值:时间</param>
/// <example>Param1:2025/8/14 13:14:15 返回值:"2025/08/14 13:14:15"</example>
/// <returns></returns>
public static string Datetime_ConvertTo_String1(DateTime Param1)
{
string Value = Convert.ToString(Param1);
return Value;
}
/// <summary>
/// datetime转化为string
/// </summary>
/// <param name="Param1">传入值:时间</param>
/// <param name="DateTimeFormat">传入值:字符串时间格式</param>
/// <example>Param1:2025/8/14 13:14:15 DateTimeFormat:"yyyy-MM-dd" 返回值:"2025-08-14"</example>
/// <example>Param1:2025/8/14 13:14:15 DateTimeFormat:"yyyy-MM-dd hh:mm:ss" 返回值:"2025-08-14 01:14:15"</example>
/// <example>Param1:2025/8/14 13:14:15 DateTimeFormat:"yyyy-MM-dd HH:mm:ss" 返回值:"2025-08-14 13:14:15"</example>
/// <example>Param1:2025/8/14 13:14:15 DateTimeFormat:"yyyyMMdd" 返回值:"20250814"</example>
/// <returns></returns>
public static string Datetime_ConvertTo_String2(DateTime Param1,string DateTimeFormat)
{
string Value = Param1.ToString(DateTimeFormat);
return Value;
}
/// <summary>
/// bool转化为string
/// </summary>
/// <param name="Param1">传入值:布尔值</param>
/// <example>Param1:true 返回值:"true"</example>
/// <example>Param1:false 返回值:"false"</example>
/// <returns></returns>
public static string Bool_ConvertTo_String(bool Param1)
{
string Value = Convert.ToString(Param1);
return Value;
}
/// <summary>
/// string转化为bool
/// </summary>
/// <param name="Param1">传入值:字符串</param>
/// <example>Param1:"true" 返回值true</example>
/// <example>Param1:"false" 返回值false</example>
/// <example>Param1:"abc" 返回值false</example>
/// <returns></returns>
public static bool String_ConvertTo_Bool(string Param1)
{
bool Value = bool.TryParse(Param1, out bool boolValue);
return Value;
}
/// <summary>
/// 将无符号整型转换为整型
/// </summary>
/// <param name="u"></param>
/// <returns></returns>
public static int Uint_ConvertTo_Int(uint u)
{
return (int)u;
}
/// <summary>
/// 将对象转换为字符串
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static string Object_ConvertTo_String(object obj)
{
return Convert.ToString(obj);
}
/// <summary>
/// 将字符串内容转换为byte
/// </summary>
/// <param name="s">要转换的内容</param>
/// <returns></returns>
public static byte String_ConvertTo_Byte(string s)
{
return Convert.ToByte(s);
}
/// <summary>
/// 转换为short(int16)
/// </summary>
/// <param name="s">要转换的内容</param>
/// <returns></returns>
public static short String_ConvertTo_Short(string s)
{
return Convert.ToInt16(s);
}
/// <summary>
/// 将字符串转换为无符号整数。
/// </summary>
/// <param name="s">要转换的字符串。</param>
/// <returns>转换后的无符号整数。</returns>
public static uint String_ConvertTo_Uint(string s)
{
return Convert.ToUInt32(s);
}
/// <summary>
/// 将指定进制的字符串转换为整数。
/// </summary>
/// <param name="s">要转换的字符串。</param>
/// <param name="XRadix">源字符串的进制。只能为2,8,10,16</param>
/// <returns>转换后的整数。</returns>
public static int XRadixString_ConvertTo_Int(string s, int XRadix)
{
return Convert.ToInt32(s, XRadix);
}
#endregion
#region
/// <summary>
/// 执行整数的异或操作。
/// </summary>
/// <param name="a">第一个整数。</param>
/// <param name="b">第二个整数。</param>
/// <returns>异或操作结果。</returns>
public static int XOR(int a, int b)
{
return a ^ b;
}
/// <summary>
/// 执行整数的位或操作。
/// </summary>
/// <param name="a">第一个整数。</param>
/// <param name="b">第二个整数。</param>
/// <returns>位或操作结果。</returns>
public static int BitwiseOR(int a, int b)
{
return a | b;
}
/// <summary>
/// 执行整数的位与操作。
/// </summary>
/// <param name="a">第一个整数。</param>
/// <param name="b">第二个整数。</param>
/// <returns>位与操作结果。</returns>
public static int BitwiseAND(int a, int b)
{
return a & b;
}
/// <summary>
/// 将整数向左移动指定位数。
/// </summary>
/// <param name="a">要移动的整数。</param>
/// <param name="BitwiseLeftShift">左移的位数。</param>
/// <returns>移动后的结果。</returns>
public static int LeftShift(int a, int BitwiseLeftShift)
{
return a << BitwiseLeftShift;
}
/// <summary>
/// 将整数向右移动指定位数。
/// </summary>
/// <param name="a">要移动的整数。</param>
/// <param name="BitwiseRightShift">右移的位数。</param>
/// <returns>移动后的结果。</returns>
public static int RightShift(int a, int BitwiseRightShift)
{
return a >> BitwiseRightShift;
}
/// <summary>
/// 执行逻辑或操作。
/// </summary>
/// <param name="a">第一个逻辑值。</param>
/// <param name="b">第二个逻辑值。</param>
/// <returns>逻辑或操作结果。</returns>
public static bool LogicalOR(bool a, bool b)
{
return a || b;
}
/// <summary>
/// 执行逻辑与操作。
/// </summary>
/// <param name="a">第一个逻辑值。</param>
/// <param name="b">第二个逻辑值。</param>
/// <returns>逻辑与操作结果。</returns>
public static bool LogicalAND(bool a, bool b)
{
return a && b;
}
/// <summary>
/// 将字节的高低位对调。
/// </summary>
/// <param name="b">要对调的字节。</param>
/// <returns>对调后的字节。</returns>
public static byte ByteHighAndLowBitSwap(byte b)
{
return (byte)(((b & 0x0F) << 4) | ((b & 0xF0) >> 4));
}
/// <summary>
/// 翻转字节数组中的元素顺序。
/// </summary>
/// <param name="b">要翻转的字节数组。</param>
/// <returns>翻转后的字节数组。</returns>
public static byte[] ByteArrayReverse(byte[] b)
{
return b.Reverse().ToArray();
}
/// <summary>
/// 将当前线程挂起指定的时间。
/// </summary>
/// <param name="time">要延时的时间,以毫秒为单位。</param>
public static void Delay_ms(int time)
{
Thread.Sleep(time);
}
/// <summary>
/// 将当前线程挂起指定的时间。
/// </summary>
/// <param name="time">要延时的时间,以秒为单位。</param>
public static void Delay_s(int time)
{
Thread.Sleep(time * 1000);
}
/// <summary>
/// 将当前线程挂起指定的时间。
/// </summary>
/// <param name="time">要延时的时间,以分钟为单位。</param>
public static void Delay_min(int time)
{
Thread.Sleep(time * 1000 * 60);
}
/// <summary>
/// 将当前线程挂起指定的时间。
/// </summary>
/// <param name="time">要延时的时间,以小时为单位。</param>
public static void Delay_hour(int time)
{
Thread.Sleep(time * 1000 * 60 * 60);
}
/// <summary>
/// 将当前线程挂起指定的时间。
/// </summary>
/// <param name="time">要延时的时间,以天为单位。</param>
public static void Delay_day(int time)
{
Thread.Sleep(time * 1000 * 60 * 60 * 24);
}
#endregion
#region
#endregion
}
}

97
Command/CommandTime.cs Normal file
View File

@ -0,0 +1,97 @@
using Common.Attributes;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace Command
{
[ATSCommand]
[DeviceCategory("时间处理指令")]
public static class CommandTime
{
#region
/// <summary>
/// 获取给定时间段的总毫秒数。
/// </summary>
/// <param name="TimePeriod">要获取总毫秒数的时间段。</param>
/// <returns>返回时间段的总毫秒数。</returns>
public static double GetTimePeriodMilliseconds(TimeSpan TimePeriod)
{
// 返回时间段的总毫秒数。
return TimePeriod.TotalMilliseconds;
}
/// <summary>
/// 获取给定时间段的总秒数。
/// </summary>
/// <param name="TimePeriod">要获取总秒数的时间段。</param>
/// <returns>返回时间段的总秒数。</returns>
public static double GetTimePeriodSeconds(TimeSpan TimePeriod)
{
// 返回时间段的总秒数。
return TimePeriod.TotalSeconds;
}
/// <summary>
/// 获取给定时间段的总分钟数。
/// </summary>
/// <param name="TimePeriod">要获取总分钟数的时间段。</param>
/// <returns>返回时间段的总分钟数。</returns>
public static double GetTimePeriodMinutes(TimeSpan TimePeriod)
{
// 返回时间段的总分钟数。
return TimePeriod.TotalMinutes;
}
/// <summary>
/// 获取给定时间段的总小时数。
/// </summary>
/// <param name="TimePeriod">要获取总小时数的时间段。</param>
/// <returns>返回时间段的总小时数。</returns>
public static double GetTimePeriodHours(TimeSpan TimePeriod)
{
// 返回时间段的总小时数。
return TimePeriod.TotalHours;
}
/// <summary>
/// 获取给定时间段的总天数。
/// </summary>
/// <param name="TimePeriod">要获取总天数的时间段。</param>
/// <returns>返回时间段的总天数。</returns>
public static double GetTimePeriodDays(TimeSpan TimePeriod)
{
// 返回时间段的总天数。
return TimePeriod.TotalDays;
}
/// <summary>
/// 计算两个日期时间之间的时间差。
/// </summary>
/// <param name="date1">第一个日期时间。</param>
/// <param name="date2">第二个日期时间。</param>
/// <returns>返回两个日期时间之间的时间差。</returns>
public static TimeSpan GetDateReduce(DateTime date1, DateTime date2)
{
// 返回第一个日期时间减去第二个日期时间的时间差。
return date1 - date2;
}
/// <summary>
/// 获取当前系统时间。
/// </summary>
/// <returns>返回当前系统时间。</returns>
public static DateTime GetNowTime()
{
// 返回当前系统时间。
return DateTime.Now;
}
#endregion
}
}

60
Command/Delay.cs Normal file
View File

@ -0,0 +1,60 @@
using Common.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace Command
{
[ATSCommand]
[DeviceCategory("延时指令")]
public static class Delay
{
/// <summary>
/// 等待_毫秒
/// </summary>
/// <param name="millisecond"></param>
/// <param name="ct"></param>
/// <returns></returns>
public static async Task Delay_ms(int millisecond, CancellationToken ct)
{
await Task.Delay(millisecond, ct);
}
/// <summary>
/// 等待_秒
/// </summary>
/// <param name="second"></param>
/// <param name="ct"></param>
/// <returns></returns>
public static async Task Delay_s(float second, CancellationToken ct)
{
await Task.Delay((int)second * 1000, ct);
}
/// <summary>
/// 等待_分钟
/// </summary>
/// <param name="minnute"></param>
/// <param name="ct"></param>
/// <returns></returns>
public static async Task Delay_m(float minnute, CancellationToken ct)
{
await Task.Delay((int)minnute * 60 * 1000, ct);
}
/// <summary>
/// 等待_小时
/// </summary>
/// <param name="hour"></param>
/// <param name="ct"></param>
/// <returns></returns>
public static async Task Delay_h(float hour, CancellationToken ct)
{
await Task.Delay((int)hour* 60 * 60 * 1000, ct);
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Common.Attributes
{
/// <summary>
/// 标记可加载到指令集中的类或方法
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class ATSCommandAttribute : Attribute
{
/// <summary>
/// 指令描述(用于工具提示)
/// </summary>
public string Description { get; }
/// <summary>
/// 指令分类(用于树形视图分组)
/// </summary>
public string Category { get; set; } = "默认分类";
public ATSCommandAttribute() { }
public ATSCommandAttribute(string description)
{
Description = description;
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DeviceCategoryAttribute : Attribute
{
public string Category { get; }
public DeviceCategoryAttribute(string category)
{
Category = category;
}
}
}
}

9
Common/Common.csproj Normal file
View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,610 @@
using Common.Attributes;
using System;
using System.IO;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Base
{
/// <summary>
/// 提供标准串口 Modbus RTU 协议通信能力。
/// 该类直接管理 SerialPort并处理 Modbus RTU 帧的构建、发送、接收与 CRC16 校验。
/// </summary>
[ATSCommand]
[DeviceCategory("全部驱动")] // 添加分类属性
public class ModbusRtu_Serial : IDisposable
{
private SerialPort _serialPort;
private readonly object _lock = new();
private bool _disposed = false;
protected internal SemaphoreSlim semaphoreSlimLock { get; set; } = new(1, 1);
/// <summary>
/// 获取或设置串口名称(如 "COM1")。
/// </summary>
public string PortName { get; set; }
/// <summary>
/// 获取或设置波特率。
/// </summary>
public int BaudRate { get; set; }
/// <summary>
/// 获取或设置数据位数,默认为 8。
/// </summary>
public int DataBits { get; set; } = 8;
/// <summary>
/// 获取或设置停止位,默认为 One。
/// </summary>
public StopBits StopBits { get; set; } = StopBits.One;
/// <summary>
/// 获取或设置奇偶校验位,默认为 None。
/// </summary>
public Parity Parity { get; set; } = Parity.None;
/// <summary>
/// 获取或设置读取超时时间(毫秒),默认为 3000。
/// </summary>
public int ReadTimeout { get; set; } = 3000;
/// <summary>
/// 获取或设置写入超时时间(毫秒),默认为 3000。
/// </summary>
public int WriteTimeout { get; set; } = 3000;
/// <summary>
/// 获取或设置通信失败时的最大重试次数,默认为 3 次。
/// </summary>
public int MaxRetries { get; set; } = 3;
public SerialPort _SerialPort { get; set; } = new SerialPort();
/// <summary>
/// 初始化一个新的 Modbus RTU 串口实例。
/// </summary>
/// <param name="portName">串口名称,如 "COM1"。</param>
/// <param name="baudRate">波特率。</param>
/// <param name="dataBits">数据位,默认 8。</param>
/// <param name="stopBits">停止位,默认 One。</param>
/// <param name="parity">奇偶校验,默认 None。</param>
/// <param name="readTimeout">读取超时(毫秒)。</param>
/// <param name="writeTimeout">写入超时(毫秒)。</param>
public ModbusRtu_Serial CreateDevice(string portName, int baudRate
, int dataBits = 8, StopBits stopBits = StopBits.One, Parity parity = Parity.None
, int readTimeout = 3000, int writeTimeout = 3000)
{
PortName = portName;
BaudRate = baudRate;
DataBits = dataBits;
StopBits = stopBits;
Parity = parity;
//ReadTimeout = readTimeout;
//WriteTimeout = writeTimeout;
return this;
}
/// <summary>
/// 修改串口实例参数
/// </summary>
/// <param name="modbusRtu">ModbusRtu串口实例</param>
/// <param name="portName">串口名称(如"COM1"</param>
/// <param name="baudRate">波特率</param>
/// <param name="dataBits">数据位</param>
/// <param name="stopBits">停止位</param>
/// <param name="parity">校验位(根据设备需求设置)</param>
/// <param name="readTimeout">读取超时</param>
/// <param name="writeTimeout">写入超时</param>
public static void ChangeDeviceConfig(ModbusRtu_Serial modbusRtu, string portName, int baudRate,
int dataBits = 8, StopBits stopBits = StopBits.One, Parity parity = Parity.None,
int readTimeout = 3000, int writeTimeout = 3000)
{
// 更新配置实例参数
modbusRtu.PortName = portName;
modbusRtu.BaudRate = baudRate;
modbusRtu.DataBits = dataBits;
modbusRtu.StopBits = stopBits;
modbusRtu.Parity = parity;
if (readTimeout > 0)
modbusRtu.ReadTimeout = readTimeout;
if (writeTimeout > 0)
modbusRtu.WriteTimeout = writeTimeout;
// 如果串口已打开,需要重新配置 SerialPort 对象
if (modbusRtu._serialPort != null && modbusRtu._serialPort.IsOpen)
{
modbusRtu._serialPort.Close();
modbusRtu._serialPort.PortName = modbusRtu.PortName;
modbusRtu._serialPort.BaudRate = modbusRtu.BaudRate;
modbusRtu._serialPort.Parity = modbusRtu.Parity;
modbusRtu._serialPort.DataBits = modbusRtu.DataBits;
modbusRtu._serialPort.StopBits = modbusRtu.StopBits;
modbusRtu._serialPort.ReadTimeout = modbusRtu.ReadTimeout;
modbusRtu._serialPort.WriteTimeout = modbusRtu.WriteTimeout;
// 注意DtrEnable, RtsEnable 等其他属性也可以在此更新,如果需要的话
}
}
/// <summary>
/// 异步打开串口连接。
/// </summary>
/// <param name="ct">支持中途取消操作</param>
/// <returns>连接结果</returns>
public static Task<bool> ConnectAsync(ModbusRtu_Serial modbusRtu_Serial, CancellationToken ct = default)
{
if (modbusRtu_Serial._SerialPort.PortName != modbusRtu_Serial.PortName
|| modbusRtu_Serial._SerialPort.BaudRate != modbusRtu_Serial.BaudRate
|| modbusRtu_Serial._SerialPort.Parity != modbusRtu_Serial.Parity
|| modbusRtu_Serial._SerialPort.DataBits != modbusRtu_Serial.DataBits
|| modbusRtu_Serial._SerialPort.StopBits != modbusRtu_Serial.StopBits
|| modbusRtu_Serial._SerialPort.ReadTimeout != modbusRtu_Serial.ReadTimeout
|| modbusRtu_Serial._SerialPort.WriteTimeout != modbusRtu_Serial.WriteTimeout)
{
// 关闭现有连接并重新配置
modbusRtu_Serial._SerialPort.Close();
//更新串口配置
modbusRtu_Serial._SerialPort.PortName = modbusRtu_Serial.PortName;
modbusRtu_Serial._SerialPort.BaudRate = modbusRtu_Serial.BaudRate;
modbusRtu_Serial._SerialPort.Parity = modbusRtu_Serial.Parity;
modbusRtu_Serial._SerialPort.DataBits = modbusRtu_Serial.DataBits;
modbusRtu_Serial._SerialPort.StopBits = modbusRtu_Serial.StopBits;
modbusRtu_Serial._SerialPort.ReadTimeout = modbusRtu_Serial.ReadTimeout;
modbusRtu_Serial._SerialPort.WriteTimeout = modbusRtu_Serial.WriteTimeout;
// 重新打开串口
modbusRtu_Serial._SerialPort.Open();
}
else
{
// 检查串口是否已打开
if (!modbusRtu_Serial._SerialPort.IsOpen)
{
// 打开串口
modbusRtu_Serial._SerialPort.Open();
}
}
return Task.FromResult(true);
}
/// <summary>
/// 打开串口连接(同步方法)。
/// </summary>
public void Connect()
{
if (_disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Serial));
lock (_lock)
{
if (_serialPort == null)
{
_serialPort = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits)
{
ReadTimeout = ReadTimeout,
WriteTimeout = WriteTimeout,
// DtrEnable = true, // 可选:根据设备要求设置
// RtsEnable = true // 可选:根据设备要求设置
};
}
if (!_serialPort.IsOpen)
{
_serialPort.Open();
}
}
}
/// <summary>
/// 关闭串口连接。
/// </summary>
public void Disconnect()
{
lock (_lock)
{
if (_serialPort != null && _serialPort.IsOpen)
{
_serialPort.Close();
}
}
}
/// <summary>
/// 异步发送请求帧并接收响应帧,支持重试。
/// </summary>
/// <param name="requestFrame">待发送的完整 Modbus RTU 请求帧(含 CRC。</param>
/// <param name="ct">用于取消操作的取消令牌。</param>
/// <returns>接收到的响应帧字节数组。</returns>
private async Task<byte[]> SendAndReceiveFrameAsync(byte[] requestFrame, CancellationToken ct)
{
if (_disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Serial));
if (_serialPort == null || !_serialPort.IsOpen) throw new InvalidOperationException("串口未连接。");
for (int attempt = 0; attempt <= MaxRetries; attempt++)
{
try
{
lock (_lock)
{
_serialPort.DiscardInBuffer(); // 清空接收缓冲区
_serialPort.Write(requestFrame, 0, requestFrame.Length);
}
// 等待响应,使用超时
await Task.Delay(50, ct); // 短暂等待设备响应
byte[] responseBuffer = new byte[256]; // 预分配足够大的缓冲区
int receivedBytes = 0;
int totalBytesToRead = 0;
// 等待第一个字节,实现超时
var start = DateTime.Now;
while (_serialPort.BytesToRead == 0 && (DateTime.Now - start).TotalMilliseconds < ReadTimeout)
{
await Task.Delay(10, ct);
}
if (_serialPort.BytesToRead == 0)
{
if (attempt == MaxRetries)
throw new TimeoutException("读取响应超时。");
continue; // 重试
}
// 读取响应
while ((DateTime.Now - start).TotalMilliseconds < ReadTimeout)
{
int bytesAvailable = _serialPort.BytesToRead;
if (bytesAvailable > 0)
{
int read = _serialPort.Read(responseBuffer, receivedBytes, responseBuffer.Length - receivedBytes);
receivedBytes += read;
if (receivedBytes >= 5) // 最小响应帧长度 (地址 + 功能 + 数据 + CRC)
{
// 尝试解析帧长度(根据功能码和数据长度)
byte func = responseBuffer[1];
if ((func & 0x80) != 0) // 异常响应
{
totalBytesToRead = 5; // 异常响应固定长度
}
else
{
if (func == 0x01 || func == 0x02) // 读线圈/读离散输入
{
totalBytesToRead = 3 + responseBuffer[2] + 2; // 地址 + 功能 + 字节计数 + 数据 + CRC
}
else if (func == 0x03 || func == 0x04) // 读保持/输入寄存器
{
totalBytesToRead = 3 + (responseBuffer[2] * 2) + 2; // 地址 + 功能 + 字节计数 + (数据 * 2) + CRC
}
else if (func == 0x05 || func == 0x06) // 写单个线圈/寄存器
{
totalBytesToRead = 6; // 响应长度固定 (地址 + 功能 + 起始地址 + CRC)
}
else if (func == 0x10) // 写多个寄存器
{
totalBytesToRead = 6; // 响应长度固定 (地址 + 功能 + 起始地址 + 寄存器数量 + CRC)
}
else
{
totalBytesToRead = receivedBytes; // 无法预知长度,先读到当前为止
}
}
if (receivedBytes >= totalBytesToRead)
{
break; // 接收完成
}
}
}
await Task.Delay(10, ct);
}
if (receivedBytes == 0)
{
if (attempt == MaxRetries)
throw new TimeoutException("读取响应超时。");
continue; // 重试
}
byte[] response = new byte[receivedBytes];
Array.Copy(responseBuffer, response, receivedBytes);
return response;
}
catch (TimeoutException)
{
if (attempt == MaxRetries)
throw;
}
catch (Exception)
{
if (attempt == MaxRetries)
throw;
await Task.Delay(100, ct); // 重试前短暂延迟
}
}
throw new InvalidOperationException("通信失败。");
}
/// <summary>
/// 计算 Modbus RTU 帧的 CRC16 校验值(小端格式,多项式 0xA001
/// </summary>
/// <param name="data">待计算 CRC 的字节数组。</param>
/// <param name="offset">起始偏移量。</param>
/// <param name="length">数据长度。</param>
/// <returns>16 位 CRC 校验值。</returns>
private static ushort CalculateCRC16(byte[] data, int offset, int length)
{
ushort crc = 0xFFFF;
for (int i = 0; i < length; i++)
{
crc ^= data[offset + i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x0001) != 0)
{
crc >>= 1;
crc ^= 0xA001;
}
else
crc >>= 1;
}
}
return crc;
}
/// <summary>
/// 构建完整的 Modbus RTU 帧(含 CRC16 校验)。
/// </summary>
/// <param name="slaveAddress">从站地址1-247。</param>
/// <param name="functionCode">功能码(如 0x01、0x03 等)。</param>
/// <param name="data">PDU 数据部分(不含地址和功能码)。</param>
/// <returns>完整的 RTU 帧字节数组。</returns>
private static byte[] BuildRtuFrame(byte slaveAddress, byte functionCode, byte[] data)
{
byte[] pdu = new byte[2 + data.Length];
pdu[0] = slaveAddress;
pdu[1] = functionCode;
Array.Copy(data, 0, pdu, 2, data.Length);
ushort crc = CalculateCRC16(pdu, 0, pdu.Length);
byte[] frame = new byte[pdu.Length + 2];
Array.Copy(pdu, frame, pdu.Length);
frame[pdu.Length] = (byte)(crc & 0xFF);
frame[pdu.Length + 1] = (byte)(crc >> 8);
return frame;
}
/// <summary>
/// 验证接收到的 Modbus RTU 响应帧是否有效。
/// </summary>
/// <param name="response">接收到的完整响应帧。</param>
/// <param name="expectedSlave">期望的从站地址。</param>
/// <param name="expectedFunction">期望的功能码。</param>
/// <returns>若帧有效则返回 true否则抛出异常。</returns>
/// <exception cref="Exception">当收到异常响应或 CRC 校验失败时抛出。</exception>
private static bool ValidateResponse(byte[] response, byte expectedSlave, byte expectedFunction)
{
if (response.Length < 4) throw new InvalidDataException("响应帧长度不足。");
if (response[0] != expectedSlave) throw new InvalidDataException("从站地址不匹配。");
if (response[1] != expectedFunction && response[1] != (byte)(expectedFunction | 0x80))
throw new InvalidDataException("功能码不匹配。");
if ((response[1] & 0x80) != 0)
throw new Exception($"Modbus 异常响应:功能码 {response[1] & 0x7F},错误码 {response[2]}");
ushort receivedCrc = (ushort)(response[response.Length - 2] | (response[response.Length - 1] << 8));
ushort calculatedCrc = CalculateCRC16(response, 0, response.Length - 2);
if (receivedCrc != calculatedCrc)
throw new InvalidDataException("响应帧 CRC 校验失败。");
return true;
}
// ————————————————————————
// 公共 Modbus 功能方法
// ————————————————————————
/// <summary>
/// 异步读取从站的线圈状态(功能码 01
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始线圈地址0-based。</param>
/// <param name="numberOfPoints">要读取的线圈数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>布尔数组,表示每个线圈的 ON/OFF 状态。</returns>
public async Task<bool[]> ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfPoints >> 8), (byte)numberOfPoints
};
byte[] request = BuildRtuFrame(slaveAddress, 0x01, data);
byte[] response = await SendAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x01);
int byteCount = response[2];
bool[] result = new bool[numberOfPoints];
for (int i = 0; i < numberOfPoints; i++)
{
int byteIndex = i / 8;
int bitIndex = i % 8;
result[i] = (response[3 + byteIndex] & (1 << bitIndex)) != 0;
}
return result;
}
/// <summary>
/// 异步写入单个线圈状态(功能码 05
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="coilAddress">线圈地址0-based。</param>
/// <param name="value">目标值true = ON, false = OFF。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>任务完成表示写入成功。</returns>
public async Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default)
{
ushort coilValue = value ? (ushort)0xFF00 : (ushort)0x0000;
byte[] data = {
(byte)(coilAddress >> 8), (byte)coilAddress,
(byte)(coilValue >> 8), (byte)coilValue
};
byte[] request = BuildRtuFrame(slaveAddress, 0x05, data);
byte[] response = await SendAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x05);
}
/// <summary>
/// 异步读取从站的离散输入状态(功能码 02
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始输入地址0-based。</param>
/// <param name="numberOfPoints">要读取的输入点数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>布尔数组,表示每个输入的状态。</returns>
public async Task<bool[]> ReadDiscreteInputsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfPoints >> 8), (byte)numberOfPoints
};
byte[] request = BuildRtuFrame(slaveAddress, 0x02, data);
byte[] response = await SendAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x02);
int byteCount = response[2];
bool[] result = new bool[numberOfPoints];
for (int i = 0; i < numberOfPoints; i++)
{
int byteIndex = i / 8;
int bitIndex = i % 8;
result[i] = (response[3 + byteIndex] & (1 << bitIndex)) != 0;
}
return result;
}
/// <summary>
/// 异步读取从站的保持寄存器值(功能码 03
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始寄存器地址0-based。</param>
/// <param name="numberOfRegisters">要读取的寄存器数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>16 位无符号整数数组,表示寄存器值。</returns>
public async Task<ushort[]> ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfRegisters, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfRegisters >> 8), (byte)numberOfRegisters
};
byte[] request = BuildRtuFrame(slaveAddress, 0x03, data);
byte[] response = await SendAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x03);
ushort[] result = new ushort[numberOfRegisters];
for (int i = 0; i < numberOfRegisters; i++)
{
result[i] = (ushort)((response[3 + i * 2] << 8) | response[4 + i * 2]);
}
return result;
}
/// <summary>
/// 异步写入单个保持寄存器(功能码 06
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="registerAddress">寄存器地址0-based。</param>
/// <param name="value">要写入的值。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>任务完成表示写入成功。</returns>
public async Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default)
{
byte[] data = {
(byte)(registerAddress >> 8), (byte)registerAddress,
(byte)(value >> 8), (byte)value
};
byte[] request = BuildRtuFrame(slaveAddress, 0x06, data);
byte[] response = await SendAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x06);
}
/// <summary>
/// 异步写入多个保持寄存器(功能码 10 (0x10))。
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始寄存器地址0-based。</param>
/// <param name="values">要写入的值数组。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>任务完成表示写入成功。</returns>
public async Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] values, CancellationToken ct = default)
{
if (values == null || values.Length == 0)
throw new ArgumentException("写入值数组不能为空。", nameof(values));
int byteCount = values.Length * 2;
byte[] data = new byte[4 + byteCount]; // 起始地址(2) + 寄存器数量(2) + 字节计数(1) + 数据(N)
data[0] = (byte)(startAddress >> 8);
data[1] = (byte)(startAddress & 0xFF);
data[2] = (byte)(values.Length >> 8);
data[3] = (byte)(values.Length & 0xFF);
data[4] = (byte)byteCount; // 字节计数
for (int i = 0; i < values.Length; i++)
{
data[5 + i * 2] = (byte)(values[i] >> 8); // High byte
data[5 + i * 2 + 1] = (byte)(values[i] & 0xFF); // Low byte
}
byte[] request = BuildRtuFrame(slaveAddress, 0x10, data);
byte[] response = await SendAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x10);
}
/// <summary>
/// 异步读取从站的输入寄存器值(功能码 04
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始寄存器地址0-based。</param>
/// <param name="numberOfRegisters">要读取的寄存器数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>16 位无符号整数数组,表示输入寄存器值。</returns>
public async Task<ushort[]> ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfRegisters, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfRegisters >> 8), (byte)numberOfRegisters
};
byte[] request = BuildRtuFrame(slaveAddress, 0x04, data);
byte[] response = await SendAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x04);
ushort[] result = new ushort[numberOfRegisters];
for (int i = 0; i < numberOfRegisters; i++)
{
result[i] = (ushort)((response[3 + i * 2] << 8) | response[4 + i * 2]);
}
return result;
}
/// <summary>
/// 释放串口资源并标记为已处置。
/// </summary>
public void Dispose()
{
if (!_disposed)
{
Disconnect();
if (_serialPort != null)
{
_serialPort.Dispose();
_serialPort = null;
}
_disposed = true;
}
}
}
}

View File

@ -0,0 +1,518 @@
using Common.Attributes;
using NModbus;
using NModbus.Device;
using NModbus.Serial;
//using DeviceCommand.Interface;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Base
{
[ATSCommand]
[DeviceCategory("全部驱动")] // 添加分类属性
public class ModbusRtu_Tcp
{
private TcpClient _tcpClient;
private NetworkStream _networkStream;
private readonly object _lock = new();
private bool _disposed = false;
/// <summary>
/// 获取目标设备的 IP 地址。
/// </summary>
public string RemoteIpAddress { get; private set; }
/// <summary>
/// 获取目标设备的 TCP 端口号,默认为 502。
/// </summary>
public int RemotePort { get; private set; } = 502;
/// <summary>
/// 接收和发送的超时时间(毫秒)。
/// </summary>
public int ReceiveTimeout { get; set; } = 3000;
/// <summary>
/// 发送超时时间
/// </summary>
public int SendTimeout { get; set; } = 3000;
public TcpClient TcpClient
{
get => _tcpClient;
set => _tcpClient = value;
}
/// <summary>
/// 获取内部的 NetworkStream 对象。
/// </summary>
public NetworkStream NetworkStream => _networkStream;
/// <summary>
/// 初始化一个新的 Modbus RTU over TCP 实例。
/// </summary>
/// <param name="ipAddress">目标设备的 IPv4 地址。</param>
/// <param name="port">目标 TCP 端口,默认为 502。</param>
/// <param name="sendTimeout">发送超时时间。</param>
/// <param name="receiveTimeout">接收超时时间。</param>
public ModbusRtu_Tcp CreateDevice(string ipAddress, int port = 502, int sendTimeout = 3000, int receiveTimeout = 3000)
{
RemoteIpAddress = ipAddress;
RemotePort = port;
return this;
}
/// <summary>
/// 连接到 Modbus RTU over TCP 设备。
/// </summary>
/// <summary>
/// 异步连接到 Modbus RTU over TCP 设备。
/// </summary>
/// <param name="modbusTcp">Modbus RTU over TCP 设备对象实例。</param>
/// <param name="ct">用于取消操作的取消令牌。</param>
/// <returns>连接结果,成功为 true失败为 false。</returns>
public static async Task<bool> ConnectAsync(ModbusRtu_Tcp modbusTcp, CancellationToken ct = default)
{
if (modbusTcp._disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Tcp));
lock (modbusTcp._lock) // 确保线程安全
{
if (modbusTcp._tcpClient == null)
{
modbusTcp._tcpClient = new TcpClient();
}
}
// 检查连接状态和端点匹配
if (!modbusTcp._tcpClient.Connected)
{
// 未连接,直接创建新连接
lock (modbusTcp._lock)
{
modbusTcp._tcpClient.Close(); // 关闭可能存在的旧连接
modbusTcp._tcpClient.Dispose();
modbusTcp._tcpClient = new TcpClient();
modbusTcp._tcpClient.ReceiveTimeout = modbusTcp.ReceiveTimeout;
modbusTcp._tcpClient.SendTimeout = modbusTcp.SendTimeout;
}
await modbusTcp._tcpClient.ConnectAsync(modbusTcp.RemoteIpAddress, modbusTcp.RemotePort, ct);
}
else
{
// 已连接,检查端点是否匹配
var remoteEndPoint = (IPEndPoint)modbusTcp._tcpClient.Client.RemoteEndPoint!;
var ip = remoteEndPoint.Address.MapToIPv4().ToString();
bool isSameAddress = ip.Equals(modbusTcp.RemoteIpAddress);
bool isSamePort = remoteEndPoint.Port == modbusTcp.RemotePort;
if (!isSameAddress || !isSamePort)
{
// 端点不匹配,断开旧连接并创建新连接
lock (modbusTcp._lock)
{
modbusTcp._tcpClient.Close();
modbusTcp._tcpClient.Dispose();
modbusTcp._tcpClient = new TcpClient();
modbusTcp._tcpClient.ReceiveTimeout = modbusTcp.ReceiveTimeout;
modbusTcp._tcpClient.SendTimeout = modbusTcp.SendTimeout;
}
await modbusTcp._tcpClient.ConnectAsync(modbusTcp.RemoteIpAddress, modbusTcp.RemotePort, ct);
}
// 如果端点匹配,则无需操作
}
// 连接成功后,初始化 NetworkStream
lock (modbusTcp._lock)
{
if (modbusTcp._tcpClient.Connected)
{
modbusTcp._networkStream = modbusTcp._tcpClient.GetStream();
return true;
}
}
return false; // 理论上不应到达此处,除非连接状态判断有误
}
/// <summary>
/// 修改 Modbus RTU over TCP 设备的连接配置。
/// /// </summary>
/// <param name="modbusTcp">Modbus RTU over TCP 设备对象实例。</param>
/// <param name="ipAddress">目标设备的 IPv4 地址。</param>
/// <param name="port">目标 TCP 端口,默认为 502。</param>
/// <param name="timeoutMs">通信超时时间(毫秒)。</param>
public static void ChangeDeviceConfig(ModbusRtu_Tcp modbusTcp, string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
{
modbusTcp.RemoteIpAddress = ipAddress;
modbusTcp.RemotePort = port;
modbusTcp.ReceiveTimeout = receiveTimeout;
modbusTcp.SendTimeout = sendTimeout;
}
/// <summary>
/// 断开与 Modbus RTU over TCP 设备的连接。
/// </summary>
public void Disconnect()
{
lock (_lock)
{
_networkStream?.Close();
_networkStream = null;
_tcpClient?.Close();
_tcpClient = null;
}
}
/// <summary>
/// 执行设备初始化操作当前为占位实现TCP 无状态故无需特殊初始化)。
/// </summary>
public void InitializeDevice()
{
// TCP 连接建立后通常无需额外初始化,保留接口一致性
}
/// <summary>
/// 触发紧急停止:立即断开 TCP 连接。
/// </summary>
public void EmergencyStop()
{
Disconnect();
}
/// <summary>
/// 异步发送 Modbus RTU 请求并通过 TCP 接收响应,支持重试机制。
/// </summary>
/// <param name="requestFrame">待发送的完整 Modbus RTU 请求帧(含 CRC。</param>
/// <param name="ct">用于取消操作的取消令牌。</param>
/// <returns>接收到的响应帧字节数组。</returns>
private async Task<byte[]> SendRequestAndReceiveFrameAsync(byte[] requestFrame, CancellationToken ct)
{
if (_disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Tcp));
if (_networkStream == null || !_tcpClient.Connected) throw new InvalidOperationException("TCP 连接未建立。");
for (int attempt = 0; attempt <= 3; attempt++)
{
try
{
await _networkStream.WriteAsync(requestFrame, 0, requestFrame.Length, ct);
// 读取响应长度(最小响应帧长度为 5: 从站地址 + 功能码 + 字节计数 + CRC(2)
// 先读取至少 5 个字节
byte[] initialBuffer = new byte[5];
int totalRead = 0;
while (totalRead < 5)
{
int n = await _networkStream.ReadAsync(initialBuffer, totalRead, 5 - totalRead, ct);
if (n == 0) throw new IOException("连接中断。");
totalRead += n;
}
// 解析功能码和字节计数
byte func = initialBuffer[1];
int pduDataLength = 0;
int expectedTotalLength = 0;
if ((func & 0x80) != 0) // 异常响应
{
expectedTotalLength = 5; // 异常响应固定长度
}
else
{
if (func == 0x01 || func == 0x02) // 读线圈/读离散输入
{
pduDataLength = initialBuffer[2];
}
else if (func == 0x03 || func == 0x04) // 读保持/输入寄存器
{
pduDataLength = initialBuffer[2];
}
// 其他功能码可以继续扩展
expectedTotalLength = 2 + 1 + pduDataLength + 2; // 从站地址 + 功能码 + 字节计数(或数据) + CRC(2)
}
byte[] response = new byte[expectedTotalLength];
Array.Copy(initialBuffer, response, 5);
if (expectedTotalLength > 5)
{
totalRead = 5;
while (totalRead < expectedTotalLength)
{
int n = await _networkStream.ReadAsync(response, totalRead, expectedTotalLength - totalRead, ct);
if (n == 0) throw new IOException("连接中断。");
totalRead += n;
}
}
return response;
}
catch (Exception ex) when (ex is SocketException || ex is TimeoutException || ex is IOException)
{
if (attempt == 3)
throw new TimeoutException($"Modbus RTU over TCP 通信超时,已重试 3 次。");
await Task.Delay(100, ct);
}
}
throw new InvalidOperationException("通信失败。");
}
/// <summary>
/// 计算 Modbus RTU 帧的 CRC16 校验值(小端格式,多项式 0xA001
/// </summary>
/// <param name="data">待计算 CRC 的字节数组。</param>
/// <param name="offset">起始偏移量。</param>
/// <param name="length">数据长度。</param>
/// <returns>16 位 CRC 校验值。</returns>
private static ushort CalculateCRC16(byte[] data, int offset, int length)
{
ushort crc = 0xFFFF;
for (int i = 0; i < length; i++)
{
crc ^= data[offset + i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x0001) != 0)
{
crc >>= 1;
crc ^= 0xA001;
}
else
crc >>= 1;
}
}
return crc;
}
/// <summary>
/// 构建完整的 Modbus RTU 帧(含 CRC16 校验)。
/// </summary>
/// <param name="slaveAddress">从站地址1-247。</param>
/// <param name="functionCode">功能码(如 0x01、0x03 等)。</param>
/// <param name="data">PDU 数据部分(不含地址和功能码)。</param>
/// <returns>完整的 RTU 帧字节数组。</returns>
private static byte[] BuildRtuFrame(byte slaveAddress, byte functionCode, byte[] data)
{
byte[] pdu = new byte[2 + data.Length];
pdu[0] = slaveAddress;
pdu[1] = functionCode;
Array.Copy(data, 0, pdu, 2, data.Length);
ushort crc = CalculateCRC16(pdu, 0, pdu.Length);
byte[] frame = new byte[pdu.Length + 2];
Array.Copy(pdu, frame, pdu.Length);
frame[pdu.Length] = (byte)(crc & 0xFF);
frame[pdu.Length + 1] = (byte)(crc >> 8);
return frame;
}
/// <summary>
/// 验证接收到的 Modbus RTU 响应帧是否有效。
/// </summary>
/// <param name="response">接收到的完整响应帧。</param>
/// <param name="expectedSlave">期望的从站地址。</param>
/// <param name="expectedFunction">期望的功能码。</param>
/// <returns>若帧有效则返回 true否则抛出异常。</returns>
/// <exception cref="Exception">当收到异常响应或 CRC 校验失败时抛出。</exception>
private static bool ValidateResponse(byte[] response, byte expectedSlave, byte expectedFunction)
{
if (response.Length < 4) throw new InvalidDataException("响应帧长度不足。");
if (response[0] != expectedSlave) throw new InvalidDataException("从站地址不匹配。");
if (response[1] != expectedFunction && response[1] != (byte)(expectedFunction | 0x80))
throw new InvalidDataException("功能码不匹配。");
if ((response[1] & 0x80) != 0)
throw new Exception($"Modbus 异常响应:功能码 {response[1] & 0x7F},错误码 {response[2]}");
ushort receivedCrc = (ushort)(response[response.Length - 2] | (response[response.Length - 1] << 8));
ushort calculatedCrc = CalculateCRC16(response, 0, response.Length - 2);
if (receivedCrc != calculatedCrc)
throw new InvalidDataException("响应帧 CRC 校验失败。");
return true;
}
// ————————————————————————
// 公共 Modbus 功能方法 (编号 68-70)
// ————————————————————————
/// <summary>
/// 异步读取从站的线圈状态(功能码 01
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始线圈地址0-based。</param>
/// <param name="numberOfPoints">要读取的线圈数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>布尔数组,表示每个线圈的 ON/OFF 状态。</returns>
public async Task<bool[]> ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfPoints >> 8), (byte)numberOfPoints
};
byte[] request = BuildRtuFrame(slaveAddress, 0x01, data);
byte[] response = await SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x01);
int byteCount = response[2];
bool[] result = new bool[numberOfPoints];
for (int i = 0; i < numberOfPoints; i++)
{
int byteIndex = i / 8;
int bitIndex = i % 8;
result[i] = (response[3 + byteIndex] & (1 << bitIndex)) != 0;
}
return result;
}
/// <summary>
/// 异步写入单个线圈状态(功能码 05
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="coilAddress">线圈地址0-based。</param>
/// <param name="value">目标值true = ON, false = OFF。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>任务完成表示写入成功。</returns>
public async Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default)
{
ushort coilValue = value ? (ushort)0xFF00 : (ushort)0x0000;
byte[] data = {
(byte)(coilAddress >> 8), (byte)coilAddress,
(byte)(coilValue >> 8), (byte)coilValue
};
byte[] request = BuildRtuFrame(slaveAddress, 0x05, data);
byte[] response = await SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x05);
}
/// <summary>
/// 异步读取从站的离散输入状态(功能码 02
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始输入地址0-based。</param>
/// <param name="numberOfPoints">要读取的输入点数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>布尔数组,表示每个输入的状态。</returns>
public async Task<bool[]> ReadDiscreteInputsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfPoints >> 8), (byte)numberOfPoints
};
byte[] request = BuildRtuFrame(slaveAddress, 0x02, data);
byte[] response = await SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x02);
int byteCount = response[2];
bool[] result = new bool[numberOfPoints];
for (int i = 0; i < numberOfPoints; i++)
{
int byteIndex = i / 8;
int bitIndex = i % 8;
result[i] = (response[3 + byteIndex] & (1 << bitIndex)) != 0;
}
return result;
}
/// <summary>
/// 异步读取从站的保持寄存器值(功能码 03
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始寄存器地址0-based。</param>
/// <param name="numberOfRegisters">要读取的寄存器数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>16 位无符号整数数组,表示寄存器值。</returns>
public async Task<ushort[]> ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfRegisters, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfRegisters >> 8), (byte)numberOfRegisters
};
byte[] request = BuildRtuFrame(slaveAddress, 0x03, data);
byte[] response = await SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x03);
ushort[] result = new ushort[numberOfRegisters];
for (int i = 0; i < numberOfRegisters; i++)
{
result[i] = (ushort)((response[3 + i * 2] << 8) | response[4 + i * 2]);
}
return result;
}
/// <summary>
/// 异步写入单个保持寄存器(功能码 06
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="registerAddress">寄存器地址0-based。</param>
/// <param name="value">要写入的值。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>任务完成表示写入成功。</returns>
public async Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default)
{
byte[] data = {
(byte)(registerAddress >> 8), (byte)registerAddress,
(byte)(value >> 8), (byte)value
};
byte[] request = BuildRtuFrame(slaveAddress, 0x06, data);
byte[] response = await SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x06);
}
/// <summary>
/// 异步读取从站的输入寄存器值(功能码 04
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始寄存器地址0-based。</param>
/// <param name="numberOfRegisters">要读取的寄存器数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>16 位无符号整数数组,表示输入寄存器值。</returns>
public async Task<ushort[]> ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfRegisters, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfRegisters >> 8), (byte)numberOfRegisters
};
byte[] request = BuildRtuFrame(slaveAddress, 0x04, data);
byte[] response = await SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x04);
ushort[] result = new ushort[numberOfRegisters];
for (int i = 0; i < numberOfRegisters; i++)
{
result[i] = (ushort)((response[3 + i * 2] << 8) | response[4 + i * 2]);
}
return result;
}
/// <summary>
/// 释放 TCP 客户端资源并标记为已处置。
/// </summary>
public void Dispose()
{
if (!_disposed)
{
Disconnect();
_disposed = true;
}
}
}
}

View File

@ -0,0 +1,476 @@
using Common.Attributes;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Base
{
/// <summary>
/// 提供基于 UDP 传输的 Modbus RTU 协议通信能力。
/// 该类将标准 Modbus RTU 帧(含 CRC16 校验)封装在 UDP 数据包中进行传输,
/// 适用于支持此非标准模式的工业设备或仿真平台。
/// </summary>
[ATSCommand]
[DeviceCategory("全部驱动")] // 添加分类属性
public class ModbusRtu_Udp : IDisposable
{
private UdpClient _udpClient;
private IPEndPoint _remoteEndPoint;
private bool _isConnected = false;
private readonly object _lock = new();
private bool _disposed = false;
/// <summary>
/// 获取目标设备的 IP 地址。
/// </summary>
public string RemoteIpAddress { get; private set; }
/// <summary>
/// 获取目标设备的 UDP 端口号。
/// </summary>
public int RemotePort { get; private set; }
/// <summary>
/// 获取本地 UDP 端口号。如果为 0则表示使用系统自动分配的端口。
/// </summary>
public int LocalPort { get; private set; } = 0; // 0 表示自动分配
/// <summary>
/// 获取或设置每次通信的超时时间(毫秒),默认为 3000 毫秒。
/// </summary>
public int TimeoutMs { get; set; } = 3000;
/// <summary>
/// 获取或设置通信失败时的最大重试次数,默认为 3 次。
/// </summary>
public int MaxRetries { get; set; } = 3;
/// <summary>
/// 初始化一个新的 Modbus RTU over UDP 实例。
/// </summary>
/// <param name="ipAddress">目标设备的 IPv4 地址。</param>
/// <param name="remotePort">目标 UDP 端口。</param>
/// <param name="localPort">本地 UDP 端口。如果为 0则使用系统自动分配的端口默认为 0。</param>
/// <param name="timeoutMs">通信超时时间(毫秒)。</param>
public ModbusRtu_Udp CreateDevice(string ipAddress, int remotePort, int localPort = 0)
{
RemoteIpAddress = ipAddress;
RemotePort = remotePort;
LocalPort = localPort; // 存储本地端口
_remoteEndPoint = new IPEndPoint(IPAddress.Parse(ipAddress), remotePort);
return this;
}
/// <summary>
/// 修改 Modbus RTU over UDP 实例参数
/// </summary>
/// <param name="modbusUdp">ModbusRtu_Udp实例</param>
/// <param name="ipAddress">IP地址</param>
/// <param name="remotePort">远程端口号</param>
/// <param name="localPort">本地端口号。如果为 0则使用系统自动分配的端口。</param>
/// <param name="timeoutMs">超时时间(毫秒)</param>
public static void ChangeDeviceConfig(ModbusRtu_Udp modbusUdp, string ipAddress, int remotePort, int localPort = 0, int timeoutMs = 3000)
{
modbusUdp.RemoteIpAddress = ipAddress;
modbusUdp.RemotePort = remotePort;
modbusUdp.LocalPort = localPort; // 更新本地端口
if (timeoutMs > 0)
{
modbusUdp.TimeoutMs = timeoutMs;
}
// 更新远程端点
modbusUdp._remoteEndPoint = new IPEndPoint(IPAddress.Parse(ipAddress), remotePort);
// 如果客户端已连接,需要重新配置
if (modbusUdp._udpClient != null && modbusUdp._isConnected)
{
modbusUdp._udpClient.Client.ReceiveTimeout = modbusUdp.TimeoutMs;
}
}
/// <summary>
/// 异步初始化 UDP 客户端并标记为已连接状态。
/// 注意UDP 是无连接协议,此处“连接”仅为逻辑状态初始化。
/// 如果 LocalPort 不为 0则会绑定到指定的本地端口。
/// </summary>
/// <param name="ct">支持中途取消操作</param>
/// <returns>连接结果</returns>
public async Task<bool> ConnectAsync(CancellationToken ct = default)
{
await Task.Run(() =>
{
if (_disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Udp));
Disconnect();
lock (_lock)
{
// 修改:根据 LocalPort 创建 UdpClient
if (LocalPort != 0)
{
_udpClient = new UdpClient(LocalPort); // 绑定到指定本地端口
}
else
{
_udpClient = new UdpClient(); // 使用系统自动分配端口
}
_udpClient.Client.ReceiveTimeout = TimeoutMs;
_isConnected = true;
}
}, ct);
return true; // UdpClient 初始化成功即返回 true
}
/// <summary>
/// 初始化 UDP 客户端并标记为已连接状态。
/// 注意UDP 是无连接协议,此处“连接”仅为逻辑状态初始化。
/// 如果 LocalPort 不为 0则会绑定到指定的本地端口。
/// </summary>
public void Connect()
{
if (_disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Udp));
Disconnect();
lock (_lock)
{
// 修改:根据 LocalPort 创建 UdpClient
if (LocalPort != 0)
{
_udpClient = new UdpClient(LocalPort); // 绑定到指定本地端口
}
else
{
_udpClient = new UdpClient(); // 使用系统自动分配端口
}
_udpClient.Client.ReceiveTimeout = TimeoutMs;
_isConnected = true;
}
}
/// <summary>
/// 关闭 UDP 客户端并清除连接状态。
/// </summary>
public void Disconnect()
{
lock (_lock)
{
_udpClient?.Close();
_udpClient = null;
_isConnected = false;
}
}
/// <summary>
/// 执行设备初始化操作当前为占位实现UDP 无状态故无需特殊初始化)。
/// </summary>
public void InitializeDevice()
{
// UDP 无状态,保留接口一致性
}
/// <summary>
/// 触发紧急停止:立即断开通信连接。
/// </summary>
public void EmergencyStop()
{
Disconnect();
}
/// <summary>
/// 异步发送 Modbus RTU 请求帧并通过 UDP 接收响应,支持重试机制。
/// </summary>
/// <param name="request">完整的 Modbus RTU 请求帧(含 CRC。</param>
/// <param name="ct">用于取消操作的取消令牌。</param>
/// <returns>接收到的响应字节数组。</returns>
/// <exception cref="TimeoutException">在指定重试次数内未收到有效响应。</exception>
public async Task<byte[]> SendRequestAndReceiveAsync(byte[] request, CancellationToken ct)
{
if (!_isConnected) throw new InvalidOperationException("设备未连接。");
for (int attempt = 0; attempt <= MaxRetries; attempt++)
{
try
{
await _udpClient.SendAsync(request, request.Length, _remoteEndPoint);
var result = await _udpClient.ReceiveAsync().WaitAsync(TimeSpan.FromMilliseconds(TimeoutMs), ct);
return result.Buffer;
}
catch (Exception ex) when (ex is SocketException || ex is TimeoutException)
{
if (attempt == MaxRetries)
throw new TimeoutException($"Modbus RTU over UDP 通信超时,已重试 {MaxRetries} 次。");
await Task.Delay(100, ct);
}
}
throw new InvalidOperationException("通信失败。");
}
/// <summary>
/// 计算 Modbus RTU 帧的 CRC16 校验值(小端格式,多项式 0xA001
/// </summary>
/// <param name="data">待计算 CRC 的字节数组。</param>
/// <returns>16 位 CRC 校验值。</returns>
private static ushort CalculateCRC16(byte[] data)
{
ushort crc = 0xFFFF;
for (int i = 0; i < data.Length; i++)
{
crc ^= data[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x0001) != 0)
{
crc >>= 1;
crc ^= 0xA001;
}
else
crc >>= 1;
}
}
return crc;
}
/// <summary>
/// 构建完整的 Modbus RTU 帧(含 CRC16 校验)。
/// </summary>
/// <param name="slaveAddress">从站地址1-247。</param>
/// <param name="functionCode">功能码(如 0x01、0x03 等)。</param>
/// <param name="data">PDU 数据部分(不含地址和功能码)。</param>
/// <returns>完整的 RTU 帧字节数组。</returns>
private static byte[] BuildRtuFrame(byte slaveAddress, byte functionCode, byte[] data)
{
byte[] pdu = new byte[2 + data.Length];
pdu[0] = slaveAddress;
pdu[1] = functionCode;
Array.Copy(data, 0, pdu, 2, data.Length);
ushort crc = CalculateCRC16(pdu);
byte[] frame = new byte[pdu.Length + 2];
Array.Copy(pdu, frame, pdu.Length);
frame[pdu.Length] = (byte)(crc & 0xFF);
frame[pdu.Length + 1] = (byte)(crc >> 8);
return frame;
}
/// <summary>
/// 验证接收到的 Modbus RTU 响应帧是否有效。
/// </summary>
/// <param name="response">接收到的完整响应帧。</param>
/// <param name="expectedSlave">期望的从站地址。</param>
/// <param name="expectedFunction">期望的功能码。</param>
/// <returns>若帧有效则返回 true否则抛出异常。</returns>
/// <exception cref="Exception">当收到异常响应或 CRC 校验失败时抛出。</exception>
private static bool ValidateResponse(byte[] response, byte expectedSlave, byte expectedFunction)
{
if (response.Length < 4) return false;
if (response[0] != expectedSlave) return false;
if (response[1] != expectedFunction && response[1] != (byte)(expectedFunction | 0x80)) return false;
if ((response[1] & 0x80) != 0)
throw new Exception($"Modbus 异常响应:功能码 {response[1] & 0x7F},错误码 {response[2]}");
ushort receivedCrc = (ushort)(response[response.Length - 2] | (response[response.Length - 1] << 8));
ushort calculatedCrc = CalculateCRC16(response, 0, response.Length - 2);
return receivedCrc == calculatedCrc;
}
/// <summary>
/// 计算指定范围数据的 CRC16 校验值。
/// </summary>
/// <param name="data">源字节数组。</param>
/// <param name="offset">起始偏移量。</param>
/// <param name="length">数据长度。</param>
/// <returns>16 位 CRC 校验值。</returns>
private static ushort CalculateCRC16(byte[] data, int offset, int length)
{
ushort crc = 0xFFFF;
for (int i = 0; i < length; i++)
{
crc ^= data[offset + i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x0001) != 0)
{
crc >>= 1;
crc ^= 0xA001;
}
else
crc >>= 1;
}
}
return crc;
}
/// <summary>
/// 异步读取从站的线圈状态(功能码 01
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始线圈地址0-based。</param>
/// <param name="numberOfPoints">要读取的线圈数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>布尔数组,表示每个线圈的 ON/OFF 状态。</returns>
public async Task<bool[]> ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfPoints >> 8), (byte)numberOfPoints
};
byte[] request = BuildRtuFrame(slaveAddress, 0x01, data);
byte[] response = await SendRequestAndReceiveAsync(request, ct);
if (!ValidateResponse(response, slaveAddress, 0x01))
throw new InvalidDataException("响应校验失败");
int byteCount = response[2];
bool[] result = new bool[numberOfPoints];
for (int i = 0; i < numberOfPoints; i++)
{
int byteIndex = i / 8;
int bitIndex = i % 8;
result[i] = (response[3 + byteIndex] & (1 << bitIndex)) != 0;
}
return result;
}
/// <summary>
/// 异步写入单个线圈状态(功能码 05
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="coilAddress">线圈地址0-based。</param>
/// <param name="value">目标值true = ON, false = OFF。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>任务完成表示写入成功。</returns>
public async Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default)
{
ushort coilValue = value ? (ushort)0xFF00 : (ushort)0x0000;
byte[] data = {
(byte)(coilAddress >> 8), (byte)coilAddress,
(byte)(coilValue >> 8), (byte)coilValue
};
byte[] request = BuildRtuFrame(slaveAddress, 0x05, data);
byte[] response = await SendRequestAndReceiveAsync(request, ct);
if (!ValidateResponse(response, slaveAddress, 0x05))
throw new InvalidDataException("写入线圈响应校验失败");
}
/// <summary>
/// 异步读取从站的离散输入状态(功能码 02
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始输入地址0-based。</param>
/// <param name="numberOfPoints">要读取的输入点数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>布尔数组,表示每个输入的状态。</returns>
public async Task<bool[]> ReadDiscreteInputsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfPoints >> 8), (byte)numberOfPoints
};
byte[] request = BuildRtuFrame(slaveAddress, 0x02, data);
byte[] response = await SendRequestAndReceiveAsync(request, ct);
if (!ValidateResponse(response, slaveAddress, 0x02))
throw new InvalidDataException("读取离散输入响应校验失败");
int byteCount = response[2];
bool[] result = new bool[numberOfPoints];
for (int i = 0; i < numberOfPoints; i++)
{
int byteIndex = i / 8;
int bitIndex = i % 8;
result[i] = (response[3 + byteIndex] & (1 << bitIndex)) != 0;
}
return result;
}
/// <summary>
/// 异步读取从站的保持寄存器值(功能码 03
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始寄存器地址0-based。</param>
/// <param name="numberOfRegisters">要读取的寄存器数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>16 位无符号整数数组,表示寄存器值。</returns>
public async Task<ushort[]> ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfRegisters, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfRegisters >> 8), (byte)numberOfRegisters
};
byte[] request = BuildRtuFrame(slaveAddress, 0x03, data);
byte[] response = await SendRequestAndReceiveAsync(request, ct);
if (!ValidateResponse(response, slaveAddress, 0x03))
throw new InvalidDataException("读取保持寄存器响应校验失败");
ushort[] result = new ushort[numberOfRegisters];
for (int i = 0; i < numberOfRegisters; i++)
{
result[i] = (ushort)((response[3 + i * 2] << 8) | response[4 + i * 2]);
}
return result;
}
/// <summary>
/// 异步写入单个保持寄存器(功能码 06
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="registerAddress">寄存器地址0-based。</param>
/// <param name="value">要写入的值。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>任务完成表示写入成功。</returns>
public async Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default)
{
byte[] data = {
(byte)(registerAddress >> 8), (byte)registerAddress,
(byte)(value >> 8), (byte)value
};
byte[] request = BuildRtuFrame(slaveAddress, 0x06, data);
byte[] response = await SendRequestAndReceiveAsync(request, ct);
if (!ValidateResponse(response, slaveAddress, 0x06))
throw new InvalidDataException("写入寄存器响应校验失败");
}
/// <summary>
/// 异步读取从站的输入寄存器值(功能码 04
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始寄存器地址0-based。</param>
/// <param name="numberOfRegisters">要读取的寄存器数量。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>16 位无符号整数数组,表示输入寄存器值。</returns>
public async Task<ushort[]> ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfRegisters, CancellationToken ct = default)
{
byte[] data = {
(byte)(startAddress >> 8), (byte)startAddress,
(byte)(numberOfRegisters >> 8), (byte)numberOfRegisters
};
byte[] request = BuildRtuFrame(slaveAddress, 0x04, data);
byte[] response = await SendRequestAndReceiveAsync(request, ct);
if (!ValidateResponse(response, slaveAddress, 0x04))
throw new InvalidDataException("读取输入寄存器响应校验失败");
ushort[] result = new ushort[numberOfRegisters];
for (int i = 0; i < numberOfRegisters; i++)
{
result[i] = (ushort)((response[3 + i * 2] << 8) | response[4 + i * 2]);
}
return result;
}
/// <summary>
/// 释放 UDP 客户端资源并标记为已处置。
/// </summary>
public void Dispose()
{
if (!_disposed)
{
Disconnect();
_disposed = true;
}
}
}
}

View File

@ -0,0 +1,354 @@
using Common.Attributes;
using NModbus;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Base
{
/// <summary>
/// ModbusTcp协议
/// </summary>
[ATSCommand]
[DeviceCategory("全部驱动")] // 添加分类属性
public class ModbusTcp
{
public string IPAddress { get; set; } = "127.0.0.1";
public int Port { get; set; } = 502;
public int SendTimeout { get; set; } = 3000;
public int ReceiveTimeout { get; set; } = 3000;
public TcpClient TcpClient { get; set; } = new();
public IModbusMaster Modbus { get; set; }
/// <summary>
/// 创建ModbusTCP设备对象
/// </summary>
/// <param name="ipAddress">IP地址</param>
/// <param name="port">端口号</param>
/// <param name="sendTimeout">发送超时时间</param>
/// <param name="receiveTimeout">接收超时时间</param>
public ModbusTcp CreateDevice(string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
{
IPAddress = ipAddress;
Port = port;
SendTimeout = sendTimeout;
ReceiveTimeout = receiveTimeout;
return this;
}
/// <summary>
/// 修改ModbusTCP连接参数
/// </summary>
/// <param name="modbusTcp"></param>
/// <param name="ipAddress">IP地址</param>
/// <param name="port">端口号</param>
/// <param name="sendTimeout">发送超时时间</param>
/// <param name="receiveTimeout">接收超时时间</param>
public static void ChangeDeviceConfig(ModbusTcp modbusTcp, string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
{
modbusTcp.IPAddress = ipAddress;
modbusTcp.Port = port;
if (sendTimeout > 0)
{
modbusTcp.SendTimeout = sendTimeout;
}
if (receiveTimeout > 0)
{
modbusTcp.ReceiveTimeout = receiveTimeout;
}
}
/// <summary>
/// 连接
/// </summary>
/// <param name="modbusTcp"></param>
/// <param name="ct"></param>
/// <returns></returns>
public static async Task<bool> ConnectAsync(ModbusTcp modbusTcp, CancellationToken ct = default)
{
if (!modbusTcp.TcpClient.Connected)
{
modbusTcp.TcpClient.Close();
modbusTcp.TcpClient.Dispose();
modbusTcp.TcpClient = new TcpClient();
await modbusTcp.TcpClient.ConnectAsync(modbusTcp.IPAddress, modbusTcp.Port, ct);
modbusTcp.Modbus = new ModbusFactory().CreateMaster(modbusTcp.TcpClient);
}
else
{
// 获取当前连接的远程端点
var remoteEndPoint = (IPEndPoint)modbusTcp.TcpClient.Client.RemoteEndPoint!;
// 比较IP地址和端口
var ip = remoteEndPoint.Address.MapToIPv4().ToString();
bool isSameAddress = ip.Equals(modbusTcp.IPAddress);
bool isSamePort = remoteEndPoint.Port == modbusTcp.Port;
// 如果端点不匹配则断开重连
if (!isSameAddress || !isSamePort)
{
modbusTcp.TcpClient.Close();
modbusTcp.TcpClient.Dispose();
modbusTcp.TcpClient = new TcpClient();
await modbusTcp.TcpClient.ConnectAsync(modbusTcp.IPAddress, modbusTcp.Port, ct);
modbusTcp.Modbus = new ModbusFactory().CreateMaster(modbusTcp.TcpClient);
}
}
return true;
}
/// <summary>
/// 设备初始化
/// </summary>
/// <param name="modbusTcp">ModbusTcp实例</param>
public static void ModbusTcpInitialize(ModbusTcp modbusTcp)
{
try
{
if (modbusTcp.TcpClient.Connected)
{
Console.WriteLine("设备已初始化,无需重复初始化");
return;
}
modbusTcp.TcpClient = new TcpClient();
modbusTcp.TcpClient.SendTimeout = modbusTcp.SendTimeout;
modbusTcp.TcpClient.ReceiveTimeout = modbusTcp.ReceiveTimeout;
Console.WriteLine($"ModbusTCP设备初始化完成");
}
catch (Exception ex)
{
Console.WriteLine($"设备初始化失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 断开
/// </summary>
/// <param name="modbusTcp">ModbusTcp实例</param>
public static void Close(ModbusTcp modbusTcp)
{
try
{
if (modbusTcp.TcpClient.Connected)
{
modbusTcp.TcpClient.Close();
Console.WriteLine("ModbusTCP连接已断开");
}
}
catch (Exception ex)
{
Console.WriteLine($"断开连接失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 读保存寄存器
/// </summary>
/// <param name="modbusTcp"></param>
/// <param name="slaveAddress">设备地址</param>
/// <param name="startAddress">起始地址</param>
/// <param name="numberOfPoints">读取数量</param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="TimeoutException"></exception>
public static async Task<ushort[]> ReadHoldingRegistersAsync(ModbusTcp modbusTcp, byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
{
var readTask = modbusTcp.Modbus.ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints).WaitAsync(ct);
var timeoutTask = Task.Delay(modbusTcp.ReceiveTimeout, ct);
var completedTask = await Task.WhenAny(readTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"ModbusTCP通讯异常读取操作在 {modbusTcp.ReceiveTimeout} ms内未完成");
}
return await readTask;
}
/// <summary>
/// 写入单个寄存器
/// </summary>
/// <param name="modbusTcp"></param>
/// <param name="slaveAddress">从设备地址</param>
/// <param name="registerAddress">寄存器地址</param>
/// <param name="value">值</param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="TimeoutException"></exception>
public static async Task WriteSingleRegisterAsync(ModbusTcp modbusTcp, byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default)
{
var sendTask = modbusTcp.Modbus.WriteSingleRegisterAsync(slaveAddress, registerAddress, value).WaitAsync(ct);
var timeoutTask = Task.Delay(modbusTcp.ReceiveTimeout, ct);
var completedTask = await Task.WhenAny(sendTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"ModbusTCP通讯异常写入操作在 {modbusTcp.ReceiveTimeout} ms内未完成");
}
}
/// <summary>
/// 写入多个寄存器
/// </summary>
/// <param name="modbusTcp"></param>
/// <param name="slaveAddress">从设备地址</param>
/// <param name="startAddress">起始寄存器地址</param>
/// <param name="values">值列表</param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="TimeoutException"></exception>
public static async Task WriteMultipleRegistersAsync(ModbusTcp modbusTcp, byte slaveAddress, ushort startAddress, ushort[] values, CancellationToken ct = default)
{
var sendTask = modbusTcp.Modbus.WriteMultipleRegistersAsync(slaveAddress, startAddress, values).WaitAsync(ct);
var timeoutTask = Task.Delay(modbusTcp.ReceiveTimeout, ct);
var completedTask = await Task.WhenAny(sendTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"ModbusTCP通讯异常写入操作在 {modbusTcp.ReceiveTimeout} ms内未完成");
}
}
/// <summary>
/// 读取线圈
/// </summary>
/// <param name="modbusTcp">ModbusTcp实例</param>
/// <param name="slaveAddress">从机地址</param>
/// <param name="startAddress">起始地址</param>
/// <param name="numberOfPoints">读取数量</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns>布尔数组</returns>
public static async Task<bool[]> ReadCoilAsync(ModbusTcp modbusTcp, byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
{
try
{
var readTask = modbusTcp.Modbus.ReadCoilsAsync(slaveAddress, startAddress, numberOfPoints).WaitAsync(ct);
var timeoutTask = Task.Delay(modbusTcp.ReceiveTimeout, ct);
var completedTask = await Task.WhenAny(readTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"ModbusTCP读取线圈操作在 {modbusTcp.ReceiveTimeout} ms内未完成");
}
return await readTask;
}
catch (Exception ex)
{
Console.WriteLine($"读取线圈失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 写入单个线圈
/// </summary>
/// <param name="modbusTcp"></param>
/// <param name="slaveAddress">从设备地址</param>
/// <param name="coilAddress">线圈地址</param>
/// <param name="value">值</param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="TimeoutException"></exception>
public static async Task WriteSingleCoilAsync(ModbusTcp modbusTcp, byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default)
{
var sendTask = modbusTcp.Modbus.WriteSingleCoilAsync(slaveAddress, coilAddress, value).WaitAsync(ct);
var timeoutTask = Task.Delay(modbusTcp.ReceiveTimeout, ct);
var completedTask = await Task.WhenAny(sendTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"ModbusTCP通讯异常写入操作在 {modbusTcp.ReceiveTimeout} ms内未完成");
}
}
/// <summary>
/// 写入多个线圈
/// </summary>
/// <param name="modbusTcp"></param>
/// <param name="slaveAddress">从设备地址</param>
/// <param name="startAddress">起始线圈地址</param>
/// <param name="values">值列表</param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="TimeoutException"></exception>
public static async Task WriteMultipleCoilsAsync(ModbusTcp modbusTcp, byte slaveAddress, ushort startAddress, bool[] values, CancellationToken ct = default)
{
var sendTask = modbusTcp.Modbus.WriteMultipleCoilsAsync(slaveAddress, startAddress, values).WaitAsync(ct);
var timeoutTask = Task.Delay(modbusTcp.ReceiveTimeout, ct);
var completedTask = await Task.WhenAny(sendTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"ModbusTCP通讯异常写入操作在 {modbusTcp.ReceiveTimeout} ms内未完成");
}
}
/// <summary>
/// 紧急停止
/// </summary>
/// <param name="modbusTcp">ModbusTcp实例</param>
public static void ModbusTcpStopNow(ModbusTcp modbusTcp)
{
try
{
if (modbusTcp.TcpClient.Connected)
{
modbusTcp.TcpClient.Close();
Console.WriteLine("紧急停止ModbusTCP连接已断开");
}
}
catch (Exception ex)
{
Console.WriteLine($"紧急停止失败: {ex.Message}");
}
}
/// <summary>
/// 读取输入寄存器
/// </summary>
/// <param name="modbusTcp">ModbusTcp实例</param>
/// <param name="slaveAddress">从机地址</param>
/// <param name="startAddress">起始地址</param>
/// <param name="numberOfPoints">读取数量</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns>ushort数组</returns>
public static async Task<ushort[]> ReadInputRegisterAsync(ModbusTcp modbusTcp, byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
{
try
{
var readTask = modbusTcp.Modbus.ReadInputRegistersAsync(slaveAddress, startAddress, numberOfPoints).WaitAsync(ct);
var timeoutTask = Task.Delay(modbusTcp.ReceiveTimeout, ct);
var completedTask = await Task.WhenAny(readTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"ModbusTCP读取输入寄存器操作在 {modbusTcp.ReceiveTimeout} ms内未完成");
}
return await readTask;
}
catch (Exception ex)
{
Console.WriteLine($"读取输入寄存器失败: {ex.Message}");
throw;
}
}
}
}

View File

@ -0,0 +1,607 @@
using Common.Attributes;
//using DeviceCommand.Interface;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Base
{
[ATSCommand]
[DeviceCategory("全部驱动")] // 添加分类属性
public class Serial_Port
{
public string PortName { get; set; } = "COM1";
public int BaudRate { get; set; } = 9600;
public int DataBits { get; set; } = 8;
public StopBits StopBits { get; set; } = StopBits.One;
public Parity Parity { get; set; } = Parity.None;
public int ReadTimeout { get; set; } = 3000;
public int WriteTimeout { get; set; } = 3000;
public SerialPort _SerialPort { get; set; } = new SerialPort();
/// <summary>
/// 创建并配置串口实例
/// </summary>
/// <param name="portName">串口名称(如"COM1"</param>
/// <param name="baudRate">波特率</param>
/// <param name="dataBits">数据位,配置默认值8</param>
/// <param name="stopBits">停止位,配置默认值StopBits.One</param>
/// <param name="parity">校验位(根据设备需求设置),配置默认值Parity.None</param>
/// <param name="readTimeout">读取超时</param>
/// <param name="writeTimeout">写入超时</param>
public Serial_Port CreateDevice(string portName, int baudRate
, int dataBits = 8, StopBits stopBits = StopBits.One, Parity parity = Parity.None
, int readTimeout = 3000, int writeTimeout = 3000)
{
PortName = portName;
BaudRate = baudRate;
DataBits = dataBits;
StopBits = stopBits;
Parity = parity;
ReadTimeout = readTimeout;
WriteTimeout = writeTimeout;
return this;
}
/// <summary>
/// 修改串口实例参数
/// </summary>
/// <param name="serialPort">串口实例</param>
/// <param name="PortName">串口名称(如"COM1"</param>
/// <param name="BaudRate">波特率</param>
/// <param name="dataBits">数据位</param>
/// <param name="stopBits">停止位</param>
/// <param name="parity">校验位(根据设备需求设置)</param>
/// <param name="ReadTimeout">读取超时</param>
/// <param name="WriteTimeout">写入超时</param>
public static void ChangeDeviceConfig(Serial_Port serialPort, string PortName, int BaudRate
, int dataBits = 8, StopBits stopBits = StopBits.One, Parity parity = Parity.None
, int ReadTimeout = 3000, int WriteTimeout = 3000)
{
// 更新配置串口实例参数
serialPort.PortName = PortName; // 串口名称(如"COM1"
serialPort.BaudRate = BaudRate; // 波特率
//serialPort.ReadTimeout = ReadTimeout;
//serialPort.WriteTimeout = WriteTimeout;
serialPort.Parity = parity; // 校验位(根据设备需求设置)
serialPort.DataBits = dataBits; // 数据位
serialPort.StopBits = stopBits; // 停止位
if (ReadTimeout > 0)
serialPort.ReadTimeout = ReadTimeout; // 读取超时
if (WriteTimeout > 0)
serialPort.WriteTimeout = WriteTimeout;// 写入超时
}
/// <summary>
/// 串口连接
/// </summary>
/// <param name="serialPort">串口实例</param>
/// <param name="ct">支持中途取消发送指令</param>
public static async Task<bool> ConnectAsync(Serial_Port serialPort, CancellationToken ct = default)
{
if (serialPort._SerialPort.PortName != serialPort.PortName
|| serialPort._SerialPort.BaudRate != serialPort.BaudRate
|| serialPort._SerialPort.Parity != serialPort.Parity
|| serialPort._SerialPort.DataBits != serialPort.DataBits
|| serialPort._SerialPort.StopBits != serialPort.StopBits
|| serialPort._SerialPort.ReadTimeout != serialPort.ReadTimeout
|| serialPort._SerialPort.WriteTimeout != serialPort.WriteTimeout)
{
// 关闭现有连接并重新配置
serialPort._SerialPort.Close();
//更新串口配置
serialPort._SerialPort.PortName = serialPort.PortName;
serialPort._SerialPort.BaudRate = serialPort.BaudRate;
serialPort._SerialPort.Parity = serialPort.Parity;
serialPort._SerialPort.DataBits = serialPort.DataBits;
serialPort._SerialPort.StopBits = serialPort.StopBits;
serialPort._SerialPort.ReadTimeout = serialPort.ReadTimeout;
serialPort._SerialPort.WriteTimeout = serialPort.WriteTimeout;
// 重新打开串口
serialPort._SerialPort.Open();
}
else
{
// 检查串口是否已打开
if (!serialPort._SerialPort.IsOpen)
{
// 打开串口
serialPort._SerialPort.Open();
}
}
return true;
}
/// <summary>
/// 串口关闭
/// </summary>
/// <param name="serialPort">串口实例</param>
public static void Close(Serial_Port serialPort)
{
if (serialPort._SerialPort.IsOpen)
{
// 关闭串口连接
serialPort._SerialPort.Close();
//Console.WriteLine("串口已关闭");
}
}
/// <summary>
/// 串口发送byte类型数据信息
/// </summary>
/// <param name="serialPort">串口实例</param>
/// <param name="bytes">发送的数据信息为byte类型</param>
/// <param name="ct">支持中途取消发送指令</param>
public static async Task SendAsync(Serial_Port serialPort, byte[] bytes, CancellationToken ct = default)
{
if (!serialPort._SerialPort.IsOpen) return;
// 获取写入超时时间
var timeoutMs = serialPort.WriteTimeout;
if (timeoutMs <= 0)
{
serialPort._SerialPort.Write(bytes, 0, bytes.Length); // 向串口写入字节数据
return;
}
var sendTask = Task.Run(() =>
{
// 向串口写入字节数据
serialPort._SerialPort.Write(bytes, 0, bytes.Length);
}, ct);
var timeoutTask = Task.Delay(timeoutMs, ct);
var completedTask = await Task.WhenAny(sendTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"串口通讯异常:写入操作在 {timeoutMs} ms内未完成");
}
await sendTask;
//if (serialPort._SerialPort.IsOpen)
//{
//try
//{
// // 异步发送数据到串口
// await Task.Run(() =>
// {
// serialPort._SerialPort.Write(bytes, 0, bytes.Length); // 向串口写入字节数据
// }, ct);
//}
//catch (Exception ex)
//{
// // 异常处理
// //Console.WriteLine($"发送数据失败: {ex.Message}");
// MessageBox.Show($"串口发送数据失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
//}
//}
//else
//{
// //Console.WriteLine("串口未打开");
// MessageBox.Show($"串口未打开", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
//}
}
/// <summary>
/// 串口发送字符串类型数据信息
/// </summary>
/// <param name="serialPort">串口实例</param>
/// <param name="str">发送的数据信息为字符串类型</param>
/// <param name="ct">支持中途取消发送指令</param>
public static async Task SendAsync(Serial_Port serialPort, string str, CancellationToken ct = default)
{
if (!serialPort._SerialPort.IsOpen) return;
// 将字符串转换为字节数组
byte[] bytes = Encoding.UTF8.GetBytes(str);
// 获取写入超时时间
var timeoutMs = serialPort.WriteTimeout;
if (timeoutMs <= 0)
{
serialPort._SerialPort.Write(bytes, 0, bytes.Length);
return;
}
var sendTask = Task.Run(() =>
{
// 向串口写入字节数据
serialPort._SerialPort.Write(bytes, 0, bytes.Length);
}, ct);
var timeoutTask = Task.Delay(timeoutMs, ct);
var completedTask = await Task.WhenAny(sendTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"串口通讯异常:写入操作在 {timeoutMs} ms内未完成");
}
await sendTask;
}
/// <summary>
/// 串口读取byte类型数据信息
/// </summary>
/// <param name="serialPort">串口实例</param>
/// <param name="buffer">读取的数据信息为byte数组类型</param>
/// <param name="ct">支持中途取消发送指令</param>
public static async Task<byte[]> ReadAsync(Serial_Port serialPort, byte[] buffer, CancellationToken ct = default)
{
if (!serialPort._SerialPort.IsOpen) return null;
int bytesRead = 0;
// 获取写入超时时间
var timeoutMs = serialPort.ReadTimeout;
if (timeoutMs <= 0)
{
return await ReadByte(serialPort, buffer, ct);
//while (bytesRead < buffer.Length)
//{
// if (serialPort._SerialPort.BytesToRead > 0)
// {
// bytesRead += serialPort._SerialPort.Read(buffer, bytesRead, buffer.Length - bytesRead);
// }
//}
//return buffer;
}
//var readTask = Task.Run(() =>
//{
// while (bytesRead < buffer.Length)
// {
// if (serialPort._SerialPort.BytesToRead > 0)
// {
// bytesRead += serialPort._SerialPort.Read(buffer, bytesRead, buffer.Length - bytesRead);
// }
// }
//}, ct);
var readTask = ReadByte(serialPort, buffer, ct);
var timeoutTask = Task.Delay(timeoutMs, ct);
var completedTask = await Task.WhenAny(readTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"串口通讯异常:读取操作在 {timeoutMs} ms内未完成");
}
return buffer;
//try
//{
// // 异步读取串口数据
// await Task.Run(() =>
// {
// while (bytesRead < buffer.Length)
// {
// if (serialPort._SerialPort.BytesToRead > 0)
// {
// bytesRead += serialPort._SerialPort.Read(buffer, bytesRead, buffer.Length - bytesRead);
// }
// }
// }, ct);
//}
//catch (Exception ex)
//{
// //Console.WriteLine($"读取数据失败: {ex.Message}");
// MessageBox.Show($"读取串口数据失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
// return null;
//}
//return buffer;
}
public static async Task<byte[]> ReadByte(Serial_Port serialPort, byte[] buffer, CancellationToken ct)
{
int bytesRead = 0;
while (bytesRead < buffer.Length)
{
if (serialPort._SerialPort.BytesToRead > 0)
{
bytesRead += serialPort._SerialPort.Read(buffer, bytesRead, buffer.Length - bytesRead);
}
}
return buffer;
}
/// <summary>
/// 串口读取字符串类型数据信息
/// </summary>
/// <param name="serialPort">串口实例</param>
/// <param name="delimiter">读取的数据信息为字符串类型,string delimiter = "\n"</param>
/// <param name="ct">支持中途取消发送指令</param>
public static async Task<string> ReadAsync(Serial_Port serialPort, string delimiter = "\n", CancellationToken ct = default)
{
if (!serialPort._SerialPort.IsOpen) return null;
// 获取写入超时时间
var timeoutMs = serialPort.ReadTimeout;
if (timeoutMs <= 0)
{
return await ReadDefaultString(serialPort, delimiter, ct);
}
var readTask = ReadDefaultString(serialPort, delimiter, ct);
var timeoutTask = Task.Delay(timeoutMs, ct);
var completedTask = await Task.WhenAny(readTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"串口通讯异常:读取操作在 {timeoutMs} ms内未完成");
}
return await readTask;
//try
//{
// // 异步读取串口数据
// await Task.Run(() =>
// {
// while (true)
// {
// if (ct.IsCancellationRequested)
// return;
// if (serialPort._SerialPort.BytesToRead > 0)
// {
// bytesRead = serialPort._SerialPort.Read(buffer, 0, buffer.Length);
// memoryStream.Write(buffer, 0, bytesRead);
// data = Encoding.UTF8.GetString(memoryStream.ToArray());
// int lineEndIndex = data.IndexOf(delimiter);
// // 找到分隔符,则返回数据
// if (lineEndIndex >= 0)
// {
// data = data.Substring(0, lineEndIndex).Trim();
// return;
// }
// }
// }
// }, ct);
//}
//catch (Exception ex)
//{
// //Console.WriteLine($"读取串口数据失败: {ex.Message}");
// MessageBox.Show($"读取串口数据失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
// return null;
//}
//return data;
}
private static async Task<string> ReadDefaultString(Serial_Port serialPort, string delimiter, CancellationToken ct)
{
delimiter ??= "\n";
MemoryStream memoryStream = new();
byte[] buffer = new byte[2048];
int bytesRead = 0;
string data = string.Empty;
// 异步读取串口数据
await Task.Run(() =>
{
while (true)
{
if (ct.IsCancellationRequested)
return;
if (serialPort._SerialPort.BytesToRead > 0)
{
bytesRead = serialPort._SerialPort.Read(buffer, 0, buffer.Length);
memoryStream.Write(buffer, 0, bytesRead);
data = Encoding.UTF8.GetString(memoryStream.ToArray());
int lineEndIndex = data.IndexOf(delimiter);
// 找到分隔符,则返回数据
if (lineEndIndex >= 0)
{
data = data.Substring(0, lineEndIndex).Trim();
return;
}
}
}
}, ct);
return data;
}
/// <summary>
/// 串口读取字符串类型数据信息
/// </summary>
/// <param name="serialPort">串口实例</param>
/// <param name="delimiter">读取的数据信息为字符串类型,string delimiter</param>
/// <param name="ct">支持中途取消发送指令</param>
public static async Task<string> ReadStrAsync(Serial_Port serialPort, string delimiter, CancellationToken ct = default)
{
if (!serialPort._SerialPort.IsOpen)
{
return null; // 串口未打开,返回 null
}
// 获取写入超时时间
var timeoutMs = serialPort.ReadTimeout;
if (timeoutMs <= 0)
{
return await ReadString(serialPort, delimiter, ct);
}
var readTask = ReadString(serialPort, delimiter, ct);
var timeoutTask = Task.Delay(timeoutMs, ct);
var completedTask = await Task.WhenAny(readTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"串口通讯异常:读取操作在 {timeoutMs} ms内未完成");
}
return await readTask;
////delimiter ??= "\n"; // 默认分隔符为换行符
//MemoryStream memoryStream = new();
//byte[] buffer = new byte[2048];
//int bytesRead;
//string data = string.Empty;
//// 异步读取串口数据
//await Task.Run(() =>
//{
// while (true)
// {
// if (ct.IsCancellationRequested)
// return;
// if (serialPort._SerialPort.BytesToRead > 0)
// {
// // 从串口读取数据
// bytesRead = serialPort._SerialPort.Read(buffer, 0, buffer.Length);
// memoryStream.Write(buffer, 0, bytesRead);
// // 获取当前已读取的数据
// data = Encoding.UTF8.GetString(memoryStream.ToArray());
// // 查找分隔符
// int delimiterIndex = data.IndexOf(delimiter);
// if (delimiterIndex >= 0)
// {
// // 如果找到分隔符,则返回读取到的字符串
// data = data.Substring(0, delimiterIndex).Trim();
// return;
// }
// }
// }
//}, ct);
//try
//{
// // 异步读取串口数据
// await Task.Run(() =>
// {
// while (true)
// {
// if (ct.IsCancellationRequested)
// return;
// if (serialPort._SerialPort.BytesToRead > 0)
// {
// // 从串口读取数据
// bytesRead = serialPort._SerialPort.Read(buffer, 0, buffer.Length);
// memoryStream.Write(buffer, 0, bytesRead);
// // 获取当前已读取的数据
// data = Encoding.UTF8.GetString(memoryStream.ToArray());
// // 查找分隔符
// int delimiterIndex = data.IndexOf(delimiter);
// if (delimiterIndex >= 0)
// {
// // 如果找到分隔符,则返回读取到的字符串
// data = data.Substring(0, delimiterIndex).Trim();
// return;
// }
// }
// }
// }, ct);
//}
//catch (Exception ex)
//{
// //Console.WriteLine($"读取串口数据时发生错误: {ex.Message}");
// MessageBox.Show($"读取串口数据时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
// return null;
//}
//return data; // 如果没有数据返回,返回 null
}
private static async Task<string> ReadString(Serial_Port serialPort, string delimiter, CancellationToken ct)
{
//delimiter ??= "\n";
MemoryStream memoryStream = new();
byte[] buffer = new byte[2048];
int bytesRead = 0;
string data = string.Empty;
// 异步读取串口数据
await Task.Run(() =>
{
while (true)
{
if (ct.IsCancellationRequested)
return;
if (serialPort._SerialPort.BytesToRead > 0)
{
bytesRead = serialPort._SerialPort.Read(buffer, 0, buffer.Length);
memoryStream.Write(buffer, 0, bytesRead);
data = Encoding.UTF8.GetString(memoryStream.ToArray());
int lineEndIndex = data.IndexOf(delimiter);
// 找到分隔符,则返回数据
if (lineEndIndex >= 0)
{
data = data.Substring(0, lineEndIndex).Trim();
return;
}
}
}
}, ct);
return data;
}
/// <summary>
/// 串口连接
/// </summary>
/// <param name="ct">取消令牌</param>
/// <returns>连接结果</returns>
public async Task<bool> ConnectAsync(CancellationToken ct = default)
{
return await ConnectAsync(this, ct);
}
/// <summary>
/// 断开串口连接
/// </summary>
public void Disconnect()
{
Close(this);
}
/// <summary>
/// 发送字符串到串口
/// </summary>
/// <param name="str">要发送的字符串</param>
/// <param name="ct">取消令牌</param>
public async Task SendStringAsync(string str, CancellationToken ct = default)
{
await SendAsync(this, str, ct);
}
/// <summary>
/// 接收字符串直到遇到指定分隔符
/// </summary>
/// <param name="delimiter">分隔符</param>
/// <param name="ct">取消令牌</param>
/// <returns>接收到的字符串</returns>
public async Task<string> ReceiveStringToAsync(string delimiter, CancellationToken ct = default)
{
return await ReadAsync(this, delimiter, ct);
}
}
}

262
DeviceCommand/Base/Tcp.cs Normal file
View File

@ -0,0 +1,262 @@
using Common.Attributes;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Base
{
[ATSCommand]
[DeviceCategory("全部驱动")] // 添加分类属性
public class Tcp
{
public string IPAddress { get; set; } = "127.0.0.1";
public int Port { get; set; } = 502;
public int SendTimeout { get; set; } = 3000;
public int ReceiveTimeout { get; set; } = 3000;
public TcpClient TcpClient { get; set; } = new();
public Tcp CreateDevice(string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
{
IPAddress = ipAddress;
Port = port;
SendTimeout = sendTimeout;
ReceiveTimeout = receiveTimeout;
return this;
}
/// <summary>
/// 修改TCP连接参数
/// </summary>
/// <param name="tcp"></param>
/// <param name="ipAddress">IP地址</param>
/// <param name="port">端口号</param>
/// <param name="sendTimeout">发送超时时间</param>
/// <param name="receiveTimeout">接收超时时间</param>
public static void ChangeDeviceConfig(Tcp tcp, string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
{
tcp.IPAddress = ipAddress;
tcp.Port = port;
if (sendTimeout > 0)
{
tcp.SendTimeout = sendTimeout;
}
if (receiveTimeout > 0)
{
tcp.ReceiveTimeout = receiveTimeout;
}
}
/// <summary>
/// 连接TCP设备
/// </summary>
/// <param name="tcp">TCP设备对象</param>
/// <param name="ct">取消令牌</param>
/// <returns>连接结果</returns>
public static async Task<bool> ConnectAsync(Tcp tcp, CancellationToken ct = default)
{
if (!tcp.TcpClient.Connected)
{
tcp.TcpClient.Close();
tcp.TcpClient.Dispose();
tcp.TcpClient = new TcpClient();
await tcp.TcpClient.ConnectAsync(tcp.IPAddress, tcp.Port, ct);
}
else
{
// 获取当前连接的远程端点
var remoteEndPoint = (IPEndPoint)tcp.TcpClient.Client.RemoteEndPoint!;
// 比较IP地址和端口
var ip = remoteEndPoint.Address.MapToIPv4().ToString();
bool isSameAddress = ip.Equals(tcp.IPAddress);
bool isSamePort = remoteEndPoint.Port == tcp.Port;
// 如果端点不匹配则断开重连
if (!isSameAddress || !isSamePort)
{
tcp.TcpClient.Close();
tcp.TcpClient.Dispose();
tcp.TcpClient = new TcpClient();
await tcp.TcpClient.ConnectAsync(tcp.IPAddress, tcp.Port, ct);
}
}
return true;
}
/// <summary>
/// 关闭TCP连接
/// </summary>
/// <param name="tcp">TCP设备对象</param>
public static void Close(Tcp tcp)
{
tcp.TcpClient.Close();
}
/// <summary>
/// 发送字节数组到TCP设备
/// </summary>
/// <param name="tcp">TCP设备对象</param>
/// <param name="bytes">要发送的字节数组</param>
/// <param name="ct">取消令牌</param>
public static async Task SendAsync(Tcp tcp, byte[] bytes, CancellationToken ct = default)
{
var timeoutMs = tcp.SendTimeout;
if (timeoutMs <= 0)
{
await tcp.TcpClient.Client.SendAsync(bytes, ct);
return;
}
var sendTask = tcp.TcpClient.Client.SendAsync(bytes, ct).AsTask();
var timeoutTask = Task.Delay(timeoutMs, ct);
var completedTask = await Task.WhenAny(sendTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"TCP通讯异常写入操作在 {timeoutMs} ms内未完成");
}
await sendTask;
}
/// <summary>
/// 发送字符串到TCP设备
/// </summary>
/// <param name="tcp">TCP设备对象</param>
/// <param name="str">要发送的字符串</param>
/// <param name="ct">取消令牌</param>
public static async Task SendAsync(Tcp tcp, string str, CancellationToken ct = default)
{
await SendAsync(tcp, Encoding.UTF8.GetBytes(str), ct);
}
/// <summary>
/// 接收指定长度的字节数组
/// </summary>
/// <param name="tcp">TCP设备对象</param>
/// <param name="buffer">接收缓冲区</param>
/// <param name="ct">取消令牌</param>
/// <returns>接收到的字节数组</returns>
public static async Task<byte[]> ReadAsync(Tcp tcp, byte[] buffer, CancellationToken ct = default)
{
if (!tcp.TcpClient.Connected) return null;
var timeoutMs = tcp.ReceiveTimeout;
if (timeoutMs <= 0)
{
return await ReadBytes(tcp, buffer, ct);
}
var readTask = ReadBytes(tcp, buffer, ct);
var timeoutTask = Task.Delay(timeoutMs, ct);
var completedTask = await Task.WhenAny(readTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"TCP通讯异常读取操作在 {timeoutMs} ms内未完成");
}
return await readTask;
}
private static async Task<byte[]> ReadBytes(Tcp tcp, byte[] buffer, CancellationToken ct)
{
NetworkStream stream = tcp.TcpClient.GetStream();
int bytesRead = 0;
while (bytesRead < buffer.Length)
{
int read = await stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead, ct);
if (read == 0) return null;
bytesRead += read;
}
return buffer;
}
/// <summary>
/// 接收字符串直到遇到分隔符
/// </summary>
/// <param name="tcp">TCP设备对象</param>
/// <param name="delimiter">分隔符</param>
/// <param name="ct">取消令牌</param>
/// <returns>接收到的字符串</returns>
public static async Task<string> ReadAsync(Tcp tcp, string delimiter = "\n", CancellationToken ct = default)
{
delimiter ??= "\n";
var timeoutMs = tcp.ReceiveTimeout;
if (timeoutMs <= 0)
{
return await ReadString(tcp, delimiter, ct);
}
var readTask = ReadString(tcp, delimiter, ct);
var timeoutTask = Task.Delay(timeoutMs, ct);
var completedTask = await Task.WhenAny(readTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"TCP通讯异常读取操作在 {timeoutMs} ms内未完成");
}
return await readTask;
}
private static async Task<string> ReadString(Tcp tcp, string delimiter, CancellationToken ct)
{
NetworkStream stream = tcp.TcpClient.GetStream();
MemoryStream memoryStream = new();
byte[] buffer = new byte[2048];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, ct)) > 0)
{
memoryStream.Write(buffer, 0, bytesRead);
string data = Encoding.UTF8.GetString(memoryStream.ToArray());
int lineEndIndex = data.IndexOf(delimiter);
if (lineEndIndex >= 0)
{
return data[..lineEndIndex].Trim();
}
}
return null;
}
/// <summary>
/// 发送并接收数据
/// </summary>
/// <param name="tcp">TCP设备对象</param>
/// <param name="str">要发送的字符串</param>
/// <param name="endstr">结束符</param>
/// <param name="ct">取消令牌</param>
/// <returns>接收到的响应</returns>
public async Task<string> WriteRead(Tcp tcp, string str, string endstr, CancellationToken ct = default)
{
await SendAsync(tcp, str, ct);
return await ReadAsync(tcp, endstr, ct);
}
/// <summary>
/// 连接TCP设备
/// </summary>
/// <param name="ct">取消令牌</param>
/// <returns>连接结果</returns>
public async Task<bool> ConnectAsync(CancellationToken ct = default)
{
return await ConnectAsync(this, ct);
}
}
}

308
DeviceCommand/Base/Udp.cs Normal file
View File

@ -0,0 +1,308 @@
using Common.Attributes;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Base
{
[ATSCommand]
[DeviceCategory("全部驱动")] // 添加分类属性
public class Udp
{
public string RemoteIpAddress { get; set; } = "127.0.0.1";
public int RemotePort { get; set; } = 8080;
public int LocalPort { get; set; } = 0; // 0 表示系统自动分配
public int SendTimeout { get; set; } = 3000;
public int ReceiveTimeout { get; set; } = 3000;
public UdpClient UdpClient { get; set; } = new UdpClient();
/// <summary>
/// 创建UDP设备对象
/// </summary>
/// <param name="ipAddress">IP地址</param>
/// <param name="port">端口号</param>
/// <param name="sendTimeout">发送超时时间</param>
/// <param name="receiveTimeout">接收超时时间</param>
public Udp CreateDevice(string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
{
RemoteIpAddress = ipAddress;
RemotePort = port;
SendTimeout = sendTimeout;
ReceiveTimeout = receiveTimeout;
if (LocalPort > 0)
{
UdpClient = new UdpClient(LocalPort);
}
else
{
UdpClient = new UdpClient();
}
UdpClient.Client.SendTimeout = SendTimeout;
UdpClient.Client.ReceiveTimeout = ReceiveTimeout;
return this;
}
/// <summary>
/// 修改UDP连接参数
/// </summary>
/// <param name="udp">UDP设备对象</param>
/// <param name="ipAddress">IP地址</param>
/// <param name="port">端口号</param>
/// <param name="sendTimeout">发送超时时间</param>
/// <param name="receiveTimeout">接收超时时间</param>
public static void ChangeDeviceConfig(Udp udp, string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
{
udp.RemoteIpAddress = ipAddress;
udp.RemotePort = port;
if (sendTimeout > 0)
{
udp.SendTimeout = sendTimeout;
}
if (receiveTimeout > 0)
{
udp.ReceiveTimeout = receiveTimeout;
}
if (udp.UdpClient != null)
{
udp.UdpClient.Client.SendTimeout = udp.SendTimeout;
udp.UdpClient.Client.ReceiveTimeout = udp.ReceiveTimeout;
}
}
// ————————————————————————
// 功能 78
// ————————————————————————
/// <summary>
/// UDP连接功能78
/// UDP是无连接的此处主要进行初始化和设置默认远程端点
/// </summary>
/// <param name="udp">UDP设备对象</param>
/// <param name="ct">取消令牌</param>
/// <returns>连接结果</returns>
public static async Task<bool> ConnectAsync(Udp udp, CancellationToken ct = default)
{
if (udp.UdpClient == null)
{
if (udp.LocalPort > 0)
{
udp.UdpClient = new UdpClient(udp.LocalPort);
}
else
{
udp.UdpClient = new UdpClient();
}
}
udp.UdpClient.Client.SendTimeout = udp.SendTimeout;
udp.UdpClient.Client.ReceiveTimeout = udp.ReceiveTimeout;
var remoteEndpoint = new IPEndPoint(IPAddress.Parse(udp.RemoteIpAddress), udp.RemotePort);
//await udp.UdpClient.ConnectAsync(remoteEndpoint, ct);
return true;
}
/// <summary>
/// 紧急停止功能78
/// </summary>
/// <param name="udp">UDP设备对象</param>
public static void EmergencyStop(Udp udp)
{
Close(udp); // 复用 Close 逻辑
}
// ————————————————————————
// 功能 79
// ————————————————————————
/// <summary>
/// 设备初始化功能79
/// </summary>
/// <param name="udp">UDP设备对象</param>
public static void InitializeDevice(Udp udp)
{
if (udp.UdpClient == null)
{
if (udp.LocalPort > 0)
{
udp.UdpClient = new UdpClient(udp.LocalPort);
}
else
{
udp.UdpClient = new UdpClient();
}
}
udp.UdpClient.Client.SendTimeout = udp.SendTimeout;
udp.UdpClient.Client.ReceiveTimeout = udp.ReceiveTimeout;
}
/// <summary>
/// 断开功能79
/// </summary>
/// <param name="udp">UDP设备对象</param>
public static void Disconnect(Udp udp)
{
Close(udp); // 复用 Close 逻辑
}
/// <summary>
/// 连接功能79
/// </summary>
/// <param name="udp">UDP设备对象</param>
/// <param name="ct">取消令牌</param>
/// <returns>连接结果</returns>
public static async Task<bool> Connect(Udp udp, CancellationToken ct = default)
{
// 与 ConnectAsync 功能相同,可以复用
return await ConnectAsync(udp, ct);
}
// ————————————————————————
// 功能 80
// ————————————————————————
/// <summary>
/// 发送字符串功能80
/// </summary>
/// <param name="udp">UDP设备对象</param>
/// <param name="str">要发送的字符串</param>
/// <param name="ct">取消令牌</param>
public static async Task SendString(Udp udp, string str, CancellationToken ct = default)
{
await SendAsync(udp, str, ct); // 复用 SendAsync 逻辑
}
/// <summary>
/// 发送字节功能80
/// </summary>
/// <param name="udp">UDP设备对象</param>
/// <param name="bytes">要发送的字节数组</param>
/// <param name="ct">取消令牌</param>
public static async Task SendByte(Udp udp, byte[] bytes, CancellationToken ct = default)
{
await SendAsync(udp, bytes, ct); // 复用 SendAsync 逻辑
}
/// <summary>
/// 接收字节功能80
/// </summary>
/// <param name="udp">UDP设备对象</param>
/// <param name="ct">取消令牌</param>
/// <returns>接收到的字节数组</returns>
public static async Task<byte[]> ReceiveByte(Udp udp, CancellationToken ct = default)
{
// UDP 接收通常不需要预先分配固定大小的 buffer直接返回整个数据报
if (udp.UdpClient == null) throw new InvalidOperationException("UDP Client 未初始化。");
var timeoutMs = udp.ReceiveTimeout;
if (timeoutMs <= 0)
{
var res = await udp.UdpClient.ReceiveAsync();
return res.Buffer;
}
var readTask = udp.UdpClient.ReceiveAsync();
var timeoutTask = Task.Delay(timeoutMs, ct);
var completedTask = await Task.WhenAny(readTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"UDP通讯异常读取操作在 {timeoutMs} ms内未完成");
}
var result = await readTask;
return result.Buffer;
}
/// <summary>
/// 接收字符串功能80
/// </summary>
/// <param name="udp">UDP设备对象</param>
/// <param name="ct">取消令牌</param>
/// <returns>接收到的字符串</returns>
public static async Task<string> ReceiveString(Udp udp, CancellationToken ct = default)
{
var bytes = await ReceiveByte(udp, ct);
return Encoding.UTF8.GetString(bytes);
}
// ————————————————————————
// 内部辅助方法 (与 Tcp 风格一致)
// ————————————————————————
/// <summary>
/// 关闭UDP连接
/// </summary>
/// <param name="udp">UDP设备对象</param>
private static void Close(Udp udp)
{
udp.UdpClient?.Close();
// udp.UdpClient?.Dispose(); // 通常 Close 就足够了
}
/// <summary>
/// 发送字节数组到UDP设备 (内部核心方法)
/// </summary>
/// <param name="udp">UDP设备对象</param>
/// <param name="bytes">要发送的字节数组</param>
/// <param name="ct">取消令牌</param>
public static async Task SendAsync(Udp udp, byte[] bytes, CancellationToken ct = default)
{
if (udp.UdpClient == null) throw new InvalidOperationException("UDP Client 未初始化。");
var timeoutMs = udp.SendTimeout;
if (timeoutMs <= 0)
{
await udp.UdpClient.SendAsync(bytes, bytes.Length);
return;
}
var sendTask = udp.UdpClient.SendAsync(bytes, bytes.Length);
var timeoutTask = Task.Delay(timeoutMs, ct);
var completedTask = await Task.WhenAny(sendTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"UDP通讯异常写入操作在 {timeoutMs} ms内未完成");
}
await sendTask;
}
/// <summary>
/// 发送字符串到UDP设备 (内部核心方法)
/// </summary>
/// <param name="udp">UDP设备对象</param>
/// <param name="str">要发送的字符串</param>
/// <param name="ct">取消令牌</param>
private static async Task SendAsync(Udp udp, string str, CancellationToken ct = default)
{
await SendAsync(udp, Encoding.UTF8.GetBytes(str), ct);
}
// ————————————————————————
// 实例方法 (可选,便于链式调用)
// ————————————————————————
/// <summary>
/// 连接UDP设备实例方法
/// </summary>
/// <param name="ct">取消令牌</param>
/// <returns>连接结果</returns>
public async Task<bool> ConnectAsync(CancellationToken ct = default)
{
return await ConnectAsync(this, ct);
}
}
}

View File

@ -0,0 +1,256 @@
using Common.Attributes;
using DeviceCommand.Base;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Device
{
/// <summary>
/// 直流电源——阿美泰克直流电源
/// </summary>
[ATSCommand]
[DeviceCategory("阿美泰克直流电源\r\n")]
public class AMETEKSGX : Tcp
{
public SemaphoreSlim semaphoreSlimLock { get; set; } = new(1, 1);
/// <summary>
/// 切换为远程模式 阿美泰克直流电源
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct"></param>
/// <returns></returns>
public async Task Set_RemoteMode(Tcp client, CancellationToken ct = default)
{
try
{
await SendAsync(client, $"SYSTem:LOCAL? OFF\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 切换为本地模式
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct"></param>
/// <returns></returns>
public async Task Set_LocalMode(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"SYSTem:LOCAL? ON\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设定输出电压值
/// </summary>
/// <param name="client">设备</param>
/// <param name="voltage">电压</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_General_OutputVoltage(Tcp client, int voltage, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"SOURce:VOLTage {voltage}\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置最大电压
/// </summary>
/// <param name="client">设备</param>
/// <param name="voltage">输出频率</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_MaxVoltage(Tcp client, float voltage, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"SOURce:VOLTage:LIMit {voltage}\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 返回AMETEK自身的电压值
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct">取消令牌</param>
/// <returns>返回值是AMETEK自身的电流值 (Amps)</returns>
public async Task<string> QueryVoltage(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
//查询语法与命令语法相同,只需在命令后添加“?”即可
return await WriteRead(client, $"SOURce:VOLTage?\r\n", "\n", ct: ct);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置输出电流
/// </summary>
/// <param name="client">设备</param>
/// <param name="current">输出频率</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_OutputCurrent(Tcp client, float current, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"SOURce:CURRent{current}\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置最大电流
/// </summary>
/// <param name="client">设备</param>
/// <param name="current">输出频率</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_MaxCurrent(Tcp client, float current, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"SOURce:CURRent:LIMit {current}\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 返回AMETEK自身的电流值
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct">取消令牌</param>
/// <returns>返回值是AMETEK自身的电流值 (Amps)</returns>
public async Task<string> QueryCurrent(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
//查询语法与命令语法相同,只需在命令后添加“?”即可
return await WriteRead(client, $"SOURce:CURRent?\r\n", "\n", ct: ct);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置电源的开启
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_Power_ON(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
//1/0 或 ON/OFF都可以
await SendAsync(client, $"OUTPut:STATe ON\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置电源的关闭
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_Power_OFF(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"OUTPut:STATe OFF\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
}
}

View File

@ -0,0 +1,326 @@
using Common.Attributes;
using DeviceCommand.Base;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Device
{
/// <summary>
/// 直流电源——三通道可编程直流电源设备型号IT-N6322B
/// </summary>
[ATSCommand]
[DeviceCategory("三通道可编程直流电源")]
public class IT_N6322B : Tcp
{
public SemaphoreSlim semaphoreSlimLock { get; set; } = new(1, 1);
public enum ChannelList_Enum
{
CH1 = 1, CH2 = 2, CH3 = 3
}
/// <summary>
/// 切换为远程模式 三通道可编程直流电源
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct"></param>
/// <returns></returns>
public async Task Set_RemoteMode(Tcp client, CancellationToken ct = default)
{
try
{
await SendAsync(client, $"SYST:REM\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 切换为本地模式
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct"></param>
/// <returns></returns>
public async Task Set_LocalMode(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"SYST:LOC\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置源指定通道对应通道输出电压
/// </summary>
/// <param name="client">设备</param>
/// <param name="channel">通道</param>
/// <param name="voltage">电压</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_OutputVoltage(Tcp client, ChannelList_Enum channel, float voltage, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"VOLT {voltage},(@{channel} )\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置源指定通道过电压保护电平
/// </summary>
/// <param name="client">设备</param>
/// <param name="channel">通道</param>
/// <param name="voltage">输出频率</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_MaxVoltage(Tcp client, ChannelList_Enum channel, float voltage, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"VOLT:OVER:PROT {voltage},(@{channel})\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置源指定通道的输出电流
/// </summary>
/// <param name="client">设备</param>
/// <param name="channel">通道</param>
/// <param name="current">输出频率</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_OutputCurrent(Tcp client, ChannelList_Enum channel, float current, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"CURR {current},(@{channel})\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置源指定通道输出过电流保护电平
/// </summary>
/// <param name="client">设备</param>
/// <param name="channel">通道</param>
/// <param name="current">电压</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_MaxCurrent(Tcp client, ChannelList_Enum channel, float current, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"CURR:OVER:PROT {current},(@{channel})\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置源指定通道输出的电压和电流值
/// </summary>
/// <param name="client">设备</param>
/// <param name="channel">通道</param>
/// <param name="voltage">电压</param>
/// <param name="current">电流</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_MaxCurrent(Tcp client, ChannelList_Enum channel, float voltage, float current, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"APPL {voltage},{current},(@{channel})\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 返回指定通道电源输出的实际电压值
/// </summary>
/// <param name="client">设备</param>
/// <param name="channel">通道</param>
/// <param name="ct">取消令牌</param>
/// <returns>返回值是:定通道电源输出的实际电压值 (Amps)</returns>
public async Task<string> QueryVoltage(Tcp client, ChannelList_Enum channel, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
return await WriteRead(client, $"MEAS:VOLT?,(@{channel})\r\n", "\n", ct: ct);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 返回指定通道电源输出的实际电流值
/// </summary>
/// <param name="client">设备</param>
/// <param name="channel">通道</param>
/// <param name="ct">取消令牌</param>
/// <returns>返回值是:设置的电流值 (Amps)</returns>
public async Task<string> QueryCurrent(Tcp client, ChannelList_Enum channel, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
return await WriteRead(client, $"MEAS:CURR?,(@{channel})\r\n", "\n", ct: ct); // 使用缩写
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 返回指定通道电源输出的实际功率值
/// </summary>
/// <param name="client">设备</param>
/// <param name="channel">通道</param>
/// <param name="ct">取消令牌</param>
/// <returns>返回值是:指定通道电源输出的实际功率值 (Amps)</returns>
public async Task<string> QueryPower(Tcp client, ChannelList_Enum channel, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
return await WriteRead(client, $"MEAS:POW?,(@{channel})\r\n", "\n", ct: ct);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置源指定通道的开启
/// </summary>
/// <param name="client">设备</param>
/// <param name="channel">通道</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_Power_ON(Tcp client, ChannelList_Enum channel, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
//1/0 或 ON/OFF都可以
await SendAsync(client, $"OUTP ON,(@{channel})\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置源指定通道的关闭
/// </summary>
/// <param name="client">设备</param>
/// <param name="channel">通道</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_Power_OFF(Tcp client, ChannelList_Enum channel, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"OUTP OFF,(@{channel})\n", ct);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
semaphoreSlimLock.Release();
}
}
}
}

View File

@ -0,0 +1,231 @@
using Common.Attributes;
using DeviceCommand.Base;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Device
{
/// <summary>
/// 万用表型号Truevolt 系列)
/// </summary>
[ATSCommand]
[DeviceCategory("万用表")] // 添加分类属性
public class KeySight_Truevolt : Tcp
{
public SemaphoreSlim semaphoreSlimLock { get; set; } = new(1, 1);
/// <summary>
/// 耦合模式AC DC
/// </summary>
public enum Coupling_Mode
{
AC,
DC,
}
#region
/// <summary>
/// 复位仪器
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task SYSTem_PRESet(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
await SendAsync(client, $"SYSTem:PRESet\n", ct);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 设置电压量程直流电压DC
/// </summary>
/// <param name="client">设备</param>
/// <param name="range">量程</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Set_VoltageDCRange(Tcp client, float range, CancellationToken ct = default)
{
if (range > 1000)
{
throw new ArgumentNullException($"{range}超出规定的最大量程");
}
await semaphoreSlimLock.WaitAsync(ct);
try
{
//CONFigure[:VOLTage]:{AC|DC}[{<range>|AUTO|MIN|MAX|DEF}[, {<resolution>|MIN|MAX|DEF}]]
await SendAsync(client, $"CONFigure:VOLTage:DC:RANGe {range}\n", ct); // 使用 \n 作为 SCPI 命令结束符
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 读取默认配置电压测量结果
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct">取消令牌</param>
/// <returns>返回值是:定通道电源输出的实际电压值 (Amps)</returns>
public async Task<string> Default_QueryVoltage(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
return await WriteRead(client, $"MEAS:VOLT:DC? AUTO\r\n", "\n", ct: ct);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 读取自定义量程电压测量结果
/// </summary>
/// <param name="client">设备</param>
/// <param name="range">量程</param>
/// <param name="ct">取消令牌</param>
/// <returns>返回值是:定通道电源输出的实际电压值 (Amps)</returns>
public async Task<string> Range_QueryVoltage(Tcp client, float range, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
//MEASure[:VOLTage]:{AC|DC}? [{<range>|AUTO|MIN|MAX|DEF} MIN/MAX仪器默认最小和最大值
return await WriteRead(client, $"MEAS:VOLT:DC?{range}\r\n", "\n", ct: ct);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 触发并读取结果
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct">取消令牌</param>
/// <returns>返回值是:定通道电源输出的实际电压值 (Amps)</returns>
public async Task<string> Read_Voltage(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
//MEASure[:VOLTage]:{AC|DC}? [{<range>|AUTO|MIN|MAX|DEF} MIN/MAX仪器默认最小和最大值
return await WriteRead(client, $"READ?\r\n", "\n", ct: ct);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 启用自动输入阻抗模式
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task StartIMPAuto_VoltageDC(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
//CONFigure[:VOLTage]:{AC|DC}[{<range>|AUTO|MIN|MAX|DEF}[, {<resolution>|MIN|MAX|DEF}]]
await SendAsync(client, $"VOLTage:DC:IMPedance:AUTO ON\n", ct);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 关闭自动输入阻抗模式
/// </summary>
/// <param name="client">设备</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task StopIMPAuto_VoltageDC(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
//CONFigure[:VOLTage]:{AC|DC}[{<range>|AUTO|MIN|MAX|DEF}[, {<resolution>|MIN|MAX|DEF}]]
await SendAsync(client, $"VOLTage:DC:IMPedance:AUTO OFF\n", ct);
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 启动DC电压和比例测量禁用或启用自动归零模式
/// </summary>
/// <param name="client">设备</param>
/// <param name="range">量程</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Start_VoltageDCAUTO_ZERO(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
//CONFigure[:VOLTage]:{AC|DC}[{<range>|AUTO|MIN|MAX|DEF}[, {<resolution>|MIN|MAX|DEF}]]
await SendAsync(client, $"VOLTage:DC:ZERO:AUTO ON\n", ct); // 使用 \n 作为 SCPI 命令结束符
}
finally
{
semaphoreSlimLock.Release();
}
}
/// <summary>
/// 关闭DC电压和比例测量禁用或启用自动归零模式
/// </summary>
/// <param name="client">设备</param>
/// <param name="range">量程</param>
/// <param name="ct">支持中途取消发送指令</param>
/// <returns></returns>
public async Task Stop_VoltageDCAUTO_ZERO(Tcp client, CancellationToken ct = default)
{
await semaphoreSlimLock.WaitAsync(ct);
try
{
//CONFigure[:VOLTage]:{AC|DC}[{<range>|AUTO|MIN|MAX|DEF}[, {<resolution>|MIN|MAX|DEF}]]
await SendAsync(client, $"VOLTage:DC:ZERO:AUTO OFF\n", ct); // 使用 \n 作为 SCPI 命令结束符
}
finally
{
semaphoreSlimLock.Release();
}
}
#endregion
}
}

Some files were not shown because too many files have changed in this diff Show More